fraiseql_auth/error.rs
1//! Internal OIDC/JWT/session errors — NOT the same as `fraiseql_error::AuthError`.
2//!
3//! This `AuthError` carries diagnostic detail for the middleware/handler layer
4//! (JWT parse reasons, OIDC metadata failures, PKCE state errors). It is
5//! never exposed directly to clients — it gets mapped to the domain-level
6//! `fraiseql_error::AuthError` before reaching the HTTP response.
7use thiserror::Error;
8
9/// All errors that can arise in the authentication and authorization layer.
10///
11/// Each variant maps to an appropriate HTTP status code via the [`axum::response::IntoResponse`]
12/// implementation in `middleware.rs`. Internal details are never forwarded to API clients —
13/// the `IntoResponse` impl always returns a generic user-facing message and logs the
14/// internal reason via `tracing::warn!`.
15#[derive(Debug, Error, Clone)]
16#[non_exhaustive]
17pub enum AuthError {
18 /// A supplied token could not be parsed or validated.
19 /// The `reason` field contains internal diagnostic detail and must not be
20 /// sent to API clients.
21 #[error("Invalid token: {reason}")]
22 InvalidToken {
23 /// Internal description of why the token is invalid (not forwarded to callers).
24 reason: String,
25 },
26
27 /// The token's `exp` claim is in the past.
28 #[error("Token expired. Obtain a new token by re-authenticating.")]
29 TokenExpired,
30
31 /// The token's cryptographic signature did not verify against the expected key.
32 #[error("Token signature is invalid. Ensure the token was issued by the expected provider.")]
33 InvalidSignature,
34
35 /// A required JWT claim (`sub`, `iss`, `aud`, etc.) was absent from the token.
36 #[error("Missing required claim: {claim}")]
37 MissingClaim {
38 /// Name of the missing claim (e.g., `"sub"`, `"aud"`).
39 claim: String,
40 },
41
42 /// A claim was present but its value did not satisfy the validator's constraints.
43 #[error("Invalid claim: {claim} - {reason}")]
44 InvalidClaimValue {
45 /// Name of the claim that failed validation.
46 claim: String,
47 /// Internal description of the validation failure (not forwarded to callers).
48 reason: String,
49 },
50
51 /// An error was returned by the upstream OAuth provider (e.g., during code exchange).
52 /// The `message` field must not be forwarded to API clients — it may contain
53 /// provider-internal URLs, error codes, or rate-limit state.
54 #[error("OAuth error: {message}")]
55 OAuthError {
56 /// Provider-internal error message (not forwarded to callers).
57 message: String,
58 },
59
60 /// A session-store operation failed (creation, lookup, or revocation).
61 #[error("Session error: {message}")]
62 SessionError {
63 /// Internal session error details (not forwarded to callers).
64 message: String,
65 },
66
67 /// A database operation within the auth layer failed.
68 /// Must never be forwarded to API clients — the message may reveal
69 /// connection strings, query structure, or infrastructure topology.
70 #[error("Database error: {message}")]
71 DatabaseError {
72 /// Internal database error message (not forwarded to callers).
73 message: String,
74 },
75
76 /// The auth subsystem was misconfigured or a required configuration value was missing.
77 /// Must never be forwarded to API clients — the message may reveal file paths,
78 /// environment variable names, or key material.
79 #[error("Configuration error: {message}")]
80 ConfigError {
81 /// Internal configuration error details (not forwarded to callers).
82 message: String,
83 },
84
85 /// Fetching or parsing the OIDC discovery document failed.
86 #[error("OIDC metadata error: {message}")]
87 OidcMetadataError {
88 /// Internal metadata fetch error details (not forwarded to callers).
89 message: String,
90 },
91
92 /// A PKCE (Proof Key for Code Exchange, RFC 7636) operation failed.
93 #[error("PKCE error: {message}")]
94 PkceError {
95 /// Internal PKCE error details (not forwarded to callers).
96 message: String,
97 },
98
99 /// The OAuth `state` parameter did not match any stored CSRF token.
100 /// This may indicate a replay attack or an expired authorization flow.
101 #[error("State validation failed")]
102 InvalidState,
103
104 /// No `Authorization: Bearer <token>` header was present in the request.
105 #[error(
106 "No authentication token provided. Include a Bearer token in the Authorization header."
107 )]
108 TokenNotFound,
109
110 /// The session associated with a refresh token has been explicitly revoked.
111 #[error("Session revoked")]
112 SessionRevoked,
113
114 /// The authenticated user lacks the required permission for the requested operation.
115 /// The `message` field contains the specific permission check detail and must not
116 /// be forwarded to API clients in full (it reveals internal role/permission names).
117 #[error("Forbidden: {message}")]
118 Forbidden {
119 /// Internal permission check details (not forwarded to callers).
120 message: String,
121 },
122
123 /// An unexpected internal error occurred. Must never be forwarded to API clients.
124 #[error("Internal error: {message}")]
125 Internal {
126 /// Internal error details (not forwarded to callers).
127 message: String,
128 },
129
130 /// The system clock returned an unexpected value during a time-sensitive operation.
131 /// This typically indicates a misconfigured system clock or clock rollback.
132 #[error("System time error: {message}")]
133 SystemTimeError {
134 /// Internal system time error details (not forwarded to callers).
135 message: String,
136 },
137
138 /// The client exceeded the configured rate limit for this endpoint.
139 /// Unlike most other variants, the retry window is safe to forward to clients.
140 #[error("Rate limited: retry after {retry_after_secs} seconds")]
141 RateLimited {
142 /// How many seconds the client must wait before retrying.
143 retry_after_secs: u64,
144 },
145
146 /// The OIDC ID token is missing the required `nonce` claim.
147 ///
148 /// Returned when an expected nonce was provided for comparison but the token
149 /// does not carry a `nonce` claim. May indicate a misconfigured provider or
150 /// a token replay attempt using a stripped token.
151 /// See RFC 6749 §10.12 / OpenID Connect Core §3.1.3.7.
152 #[error("ID token is missing the required nonce claim")]
153 MissingNonce,
154
155 /// The `nonce` claim in the ID token does not match the expected value.
156 ///
157 /// Indicates a possible token replay or session fixation attack.
158 /// See RFC 6749 §10.12 / OpenID Connect Core §3.1.3.7.
159 #[error("ID token nonce mismatch — possible replay attack")]
160 NonceMismatch,
161
162 /// The OIDC ID token is missing the `auth_time` claim when `max_age` was requested.
163 ///
164 /// When `max_age` is sent in the authorization request, the provider MUST include
165 /// `auth_time` in the ID token. Its absence indicates a non-conformant provider.
166 /// See OpenID Connect Core §3.1.3.7.
167 #[error("ID token is missing auth_time claim (required when max_age is used)")]
168 MissingAuthTime,
169
170 /// The session authentication time exceeds the allowed `max_age`.
171 ///
172 /// The provider authenticated the user too long ago for this request's `max_age`
173 /// constraint. The user must re-authenticate to obtain a fresh session.
174 /// See OpenID Connect Core §3.1.3.7.
175 #[error("Session is too old: authenticated {age}s ago, max_age is {max_age_secs}s")]
176 SessionTooOld {
177 /// How many seconds ago the session was authenticated.
178 age: i64,
179 /// Maximum allowed authentication age in seconds (from the authorization request).
180 max_age_secs: u64,
181 },
182}
183
184/// Convenience alias for `Result<T, AuthError>`.
185pub type Result<T> = std::result::Result<T, AuthError>;