Skip to main content

axum_security_oidc/
error.rs

1use std::fmt;
2
3/// A failure while verifying an ID token.
4///
5/// Every variant is a rejection reason — the token is not trusted. The
6/// distinctions exist for diagnostics/logging; a caller should treat all of
7/// them as "authentication failed".
8#[derive(Debug)]
9#[non_exhaustive]
10pub enum VerifyError {
11    /// The JWT could not be parsed (bad header, wrong number of segments,
12    /// non-base64url payload).
13    MalformedToken,
14    /// The token's `alg` header is not in the verifier's allow-list. Guards
15    /// against algorithm-confusion / key-substitution attacks.
16    AlgorithmNotAllowed,
17    /// No key in the JWKS matches the token's `kid` (or the token carried no
18    /// `kid` and the JWKS does not hold exactly one key).
19    UnknownKey,
20    /// The matched JWK could not be turned into a verification key.
21    InvalidKey,
22    /// The signature did not verify against the selected key.
23    SignatureInvalid,
24    /// The `iss` claim did not match the expected issuer.
25    IssuerMismatch,
26    /// The `aud` claim did not contain the expected audience (the client id).
27    AudienceMismatch,
28    /// The `azp` (authorized party) claim was present but did not equal the
29    /// expected audience (the client id). Per OpenID Connect Core §3.1.3.7, a
30    /// token minted for a different authorized party must not be accepted even
31    /// if it lists this client in `aud`.
32    AuthorizedPartyMismatch,
33    /// The token named more than one audience but carried no `azp` claim. Per
34    /// OpenID Connect Core §3.1.3.7 a multi-audience token must identify its
35    /// authorized party, so it cannot be attributed to this client.
36    AuthorizedPartyMissing,
37    /// The `exp` claim is in the past (outside the configured leeway).
38    Expired,
39    /// The `nbf` claim is in the future (outside the configured leeway).
40    ImmatureToken,
41    /// The `nonce` claim did not match the expected value — replay protection.
42    NonceMismatch,
43    /// The signing keys could not be fetched from the provider's `jwks_uri`
44    /// (network error, non-2xx status, or unparsable JWK set), so the token
45    /// could not be checked.
46    JwksUnavailable,
47    /// A standard-claim check failed for a reason not covered above.
48    InvalidToken,
49}
50
51impl fmt::Display for VerifyError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            VerifyError::MalformedToken => f.write_str("malformed ID token"),
55            VerifyError::AlgorithmNotAllowed => {
56                f.write_str("ID token signing algorithm is not allowed")
57            }
58            VerifyError::UnknownKey => f.write_str("no JWKS key matches the ID token"),
59            VerifyError::InvalidKey => f.write_str("JWKS key could not be used for verification"),
60            VerifyError::SignatureInvalid => f.write_str("ID token signature is invalid"),
61            VerifyError::IssuerMismatch => f.write_str("ID token issuer does not match"),
62            VerifyError::AudienceMismatch => f.write_str("ID token audience does not match"),
63            VerifyError::AuthorizedPartyMismatch => {
64                f.write_str("ID token authorized party (azp) does not match")
65            }
66            VerifyError::AuthorizedPartyMissing => {
67                f.write_str("ID token has multiple audiences but no authorized party (azp)")
68            }
69            VerifyError::Expired => f.write_str("ID token has expired"),
70            VerifyError::ImmatureToken => f.write_str("ID token is not yet valid"),
71            VerifyError::NonceMismatch => f.write_str("ID token nonce does not match"),
72            VerifyError::JwksUnavailable => f.write_str("could not fetch the provider's JWKS"),
73            VerifyError::InvalidToken => f.write_str("ID token failed verification"),
74        }
75    }
76}
77
78impl std::error::Error for VerifyError {}
79
80/// A failure while fetching OpenID Connect provider metadata from a
81/// `.well-known/openid-configuration` document.
82#[derive(Debug)]
83#[non_exhaustive]
84pub enum DiscoveryError {
85    /// The issuer URL could not be turned into a discovery URL.
86    InvalidIssuerUrl(url::ParseError),
87    /// The metadata request failed at the transport layer.
88    Http(axum_security_oauth2::HttpError),
89    /// The discovery endpoint returned a non-2xx status.
90    Status(u16),
91    /// The metadata document could not be deserialized.
92    Parse(serde_json::Error),
93    /// The `issuer` in the metadata did not match the requested issuer URL
94    /// (OpenID Connect Discovery §4.3) — a misconfigured or hostile provider.
95    IssuerMismatch,
96}
97
98impl std::fmt::Display for DiscoveryError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            DiscoveryError::InvalidIssuerUrl(e) => write!(f, "invalid issuer URL: {e}"),
102            DiscoveryError::Http(e) => write!(f, "discovery request failed: {e}"),
103            DiscoveryError::Status(status) => {
104                write!(f, "discovery endpoint returned status {status}")
105            }
106            DiscoveryError::Parse(e) => write!(f, "could not parse provider metadata: {e}"),
107            DiscoveryError::IssuerMismatch => {
108                f.write_str("provider metadata issuer does not match the requested issuer")
109            }
110        }
111    }
112}
113
114impl std::error::Error for DiscoveryError {
115    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116        match self {
117            DiscoveryError::InvalidIssuerUrl(e) => Some(e),
118            DiscoveryError::Http(e) => Some(e),
119            DiscoveryError::Parse(e) => Some(e),
120            DiscoveryError::Status(_) | DiscoveryError::IssuerMismatch => None,
121        }
122    }
123}
124
125/// A failure in the OpenID Connect login flow
126/// ([`OidcClient::finish_login`](crate::OidcClient::finish_login)).
127#[derive(Debug)]
128#[non_exhaustive]
129pub enum OidcError {
130    /// The authorization-code exchange at the token endpoint failed.
131    Exchange(axum_security_oauth2::Error),
132    /// The token response contained no `id_token` — the provider did not treat
133    /// this as an OpenID Connect request (is `openid` in the scopes?).
134    NoIdToken,
135    /// The ID token failed verification.
136    Verify(VerifyError),
137}
138
139impl fmt::Display for OidcError {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        match self {
142            OidcError::Exchange(e) => write!(f, "authorization code exchange failed: {e}"),
143            OidcError::NoIdToken => f.write_str("token response contained no id_token"),
144            OidcError::Verify(e) => write!(f, "ID token verification failed: {e}"),
145        }
146    }
147}
148
149impl std::error::Error for OidcError {
150    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
151        match self {
152            OidcError::Exchange(e) => Some(e),
153            OidcError::Verify(e) => Some(e),
154            OidcError::NoIdToken => None,
155        }
156    }
157}
158
159/// A failure while deserializing a verified ID token's payload into
160/// [`OidcClaims`](crate::OidcClaims).
161///
162/// Only reachable after verification succeeded, so the payload is already
163/// valid JSON — this signals a claim whose shape does not match the standard
164/// claim set (e.g. a missing required claim or a wrongly-typed field).
165#[derive(Debug)]
166pub enum ClaimsError {
167    /// The payload did not deserialize into the standard claim set.
168    Deserialize(serde_json::Error),
169}
170
171impl fmt::Display for ClaimsError {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            ClaimsError::Deserialize(e) => write!(f, "could not deserialize ID token claims: {e}"),
175        }
176    }
177}
178
179impl std::error::Error for ClaimsError {
180    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
181        match self {
182            ClaimsError::Deserialize(e) => Some(e),
183        }
184    }
185}