Skip to main content

fraiseql_error/
auth.rs

1/// Domain-level auth errors for HTTP response mapping.
2///
3/// These are the **client-facing** auth error variants that get converted
4/// to HTTP status codes via `RuntimeError`. For internal OIDC/JWT processing
5/// errors, see `fraiseql_auth::AuthError`. For wire-protocol SCRAM errors,
6/// see `fraiseql_wire::auth::AuthError`.
7#[derive(Debug, thiserror::Error)]
8#[non_exhaustive]
9pub enum AuthError {
10    /// The supplied username/password (or API key) did not match any account.
11    #[error("Invalid credentials")]
12    InvalidCredentials,
13
14    /// The access token has passed its expiry time and must be refreshed.
15    #[error("Token expired")]
16    TokenExpired,
17
18    /// The access token is structurally invalid or has been tampered with.
19    #[error("Invalid token: {reason}")]
20    InvalidToken {
21        /// Reason the token was rejected (kept server-side; not forwarded to clients).
22        reason: String,
23    },
24
25    /// An upstream OAuth / OIDC provider returned an error during the flow.
26    #[error("Provider error: {provider} - {message}")]
27    ProviderError {
28        /// Name of the provider (e.g. `"google"`, `"github"`).
29        provider: String,
30        /// Provider-supplied error message (kept server-side; not forwarded to clients).
31        message:  String,
32    },
33
34    /// The OAuth `state` parameter did not match the stored value, indicating a
35    /// possible CSRF attack or a stale/replayed authorisation request.
36    #[error("Invalid OAuth state")]
37    InvalidState,
38
39    /// The resource owner explicitly declined the authorisation request at the
40    /// provider's consent screen.
41    #[error("User denied authorization")]
42    UserDenied,
43
44    /// No active session exists for the supplied session identifier.
45    #[error("Session not found")]
46    SessionNotFound,
47
48    /// The session existed but has expired and can no longer be used.
49    #[error("Session expired")]
50    SessionExpired,
51
52    /// The authenticated principal does not have the scopes or roles required
53    /// to perform the requested operation.
54    #[error("Insufficient permissions: requires {required}")]
55    InsufficientPermissions {
56        /// The permission or scope that was required but not granted.
57        required: String,
58    },
59
60    /// The refresh token has been revoked, used more than once, or has expired.
61    #[error("Refresh token invalid or expired")]
62    RefreshTokenInvalid,
63
64    /// The account has been administratively locked and cannot be used.
65    #[error("Account locked: {reason}")]
66    AccountLocked {
67        /// Reason the account was locked.
68        reason: String,
69    },
70}
71
72impl AuthError {
73    /// Returns a short, stable error code string suitable for API responses and
74    /// structured logging.
75    pub const fn error_code(&self) -> &'static str {
76        match self {
77            Self::InvalidCredentials => "invalid_credentials",
78            Self::TokenExpired => "token_expired",
79            Self::InvalidToken { .. } => "invalid_token",
80            Self::ProviderError { .. } => "auth_provider_error",
81            Self::InvalidState => "invalid_oauth_state",
82            Self::UserDenied => "user_denied",
83            Self::SessionNotFound => "session_not_found",
84            Self::SessionExpired => "session_expired",
85            Self::InsufficientPermissions { .. } => "insufficient_permissions",
86            Self::RefreshTokenInvalid => "refresh_token_invalid",
87            Self::AccountLocked { .. } => "account_locked",
88        }
89    }
90}