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}