Skip to main content

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>;