Skip to main content

systemprompt_api/routes/oauth/error/
conversions.rs

1//! `From` impls mapping domain errors onto [`OAuthHttpError`], keeping the
2//! variant-to-RFC-code mapping in one place so handlers use `?`.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use systemprompt_config::SecretsBootstrapError;
8use systemprompt_identifiers::error::IdValidationError;
9use systemprompt_models::errors::GlobalConfigError;
10use systemprompt_oauth::{OauthError, OauthErrorKind};
11use systemprompt_oauth_issuance::IssuanceError;
12use systemprompt_traits::auth::AuthProviderError;
13
14use super::OAuthHttpError;
15use crate::routes::oauth::internal;
16
17impl From<GlobalConfigError> for OAuthHttpError {
18    fn from(err: GlobalConfigError) -> Self {
19        Self::server_error("Configuration unavailable").with_source(err)
20    }
21}
22
23impl From<IdValidationError> for OAuthHttpError {
24    fn from(err: IdValidationError) -> Self {
25        Self::invalid_request(err.to_string()).with_source(err)
26    }
27}
28
29impl From<OauthError> for OAuthHttpError {
30    fn from(err: OauthError) -> Self {
31        match err.kind() {
32            OauthErrorKind::InvalidClient => Self::invalid_client("Client authentication failed"),
33            OauthErrorKind::InvalidClientMetadata => Self::invalid_client_metadata(err.to_string()),
34            OauthErrorKind::InvalidGrant => Self::invalid_grant(err.to_string()),
35            OauthErrorKind::InvalidToken => Self::invalid_token(err.to_string()),
36            OauthErrorKind::InvalidRequest => Self::invalid_request(err.to_string()),
37            OauthErrorKind::AccessDenied => Self::access_denied(err.to_string()),
38            OauthErrorKind::UsernameUnavailable => Self::username_unavailable(
39                "Username is already taken. Please choose a different username.",
40            ),
41            OauthErrorKind::EmailExists => {
42                Self::email_exists("An account with this email already exists.")
43            },
44            OauthErrorKind::NotFound => Self::not_found(err.to_string()),
45            OauthErrorKind::ExpiredChallenge => Self::expired_challenge(
46                "The challenge has expired. Please start the ceremony again.",
47            ),
48            OauthErrorKind::InvalidCredential => Self::invalid_credential(
49                "WebAuthn verification failed. Please ensure your authenticator and browser are \
50                 compatible.",
51            ),
52            OauthErrorKind::AuthenticationFailed => Self::authentication_failed(
53                "Authentication failed. Check the email address or register a passkey.",
54            ),
55            OauthErrorKind::ServerError => {
56                Self::server_error("Authorization operation failed").with_source(err)
57            },
58        }
59    }
60}
61
62impl From<AuthProviderError> for OAuthHttpError {
63    fn from(err: AuthProviderError) -> Self {
64        match err {
65            e @ (AuthProviderError::InvalidCredentials | AuthProviderError::InvalidToken) => {
66                Self::invalid_client(e.to_string())
67            },
68            e @ AuthProviderError::UserNotFound => Self::not_found(e.to_string()),
69            e @ AuthProviderError::TokenExpired => Self::invalid_grant(e.to_string()),
70            e @ AuthProviderError::InsufficientPermissions => Self::access_denied(e.to_string()),
71            other => Self::server_error("Authentication provider failed").with_source(other),
72        }
73    }
74}
75
76impl From<SecretsBootstrapError> for OAuthHttpError {
77    fn from(err: SecretsBootstrapError) -> Self {
78        Self::server_error("Secrets unavailable").with_source(err)
79    }
80}
81
82impl From<sqlx::Error> for OAuthHttpError {
83    fn from(err: sqlx::Error) -> Self {
84        Self::server_error("Database operation failed").with_source(err)
85    }
86}
87
88impl From<anyhow::Error> for OAuthHttpError {
89    fn from(err: anyhow::Error) -> Self {
90        Self::server_error("Authorization operation failed").with_source(err)
91    }
92}
93
94impl From<IssuanceError> for OAuthHttpError {
95    fn from(error: IssuanceError) -> Self {
96        match error {
97            IssuanceError::InvalidRequest { field, message } => {
98                Self::invalid_request(format!("{field}: {message}"))
99            },
100            IssuanceError::MalformedField {
101                field,
102                reason,
103                source,
104            } => internal::rejected(Self::invalid_request(format!("{field}: {reason}")), source),
105            IssuanceError::UnsupportedGrantType { grant_type } => {
106                Self::unsupported_grant_type(format!("Grant type '{grant_type}' is not supported"))
107            },
108            IssuanceError::InvalidClient => Self::invalid_client("Client authentication failed"),
109            IssuanceError::InvalidGrant { reason } => Self::invalid_grant(reason),
110            IssuanceError::RejectedGrant { reason, source } => {
111                internal::rejected(Self::invalid_grant(reason), source)
112            },
113            IssuanceError::InvalidRefreshToken { reason } => {
114                Self::invalid_grant(format!("Refresh token invalid: {reason}"))
115            },
116            IssuanceError::InvalidCredentials => Self::invalid_grant("Invalid credentials"),
117            IssuanceError::InvalidClientSecret => Self::invalid_client("Invalid client secret"),
118            IssuanceError::ExpiredCode => Self::invalid_grant("Authorization code expired"),
119            IssuanceError::ServerError { context, source } => {
120                internal::server_error(context, source)
121            },
122            IssuanceError::InvalidTarget { message } => Self::invalid_target(message),
123            IssuanceError::InvalidScope { message } => Self::invalid_scope(message),
124            IssuanceError::IdJagRejected(error) => Self::invalid_grant(error.to_string()),
125            IssuanceError::BoundResource(error) => Self::invalid_target(error.to_string()),
126            IssuanceError::Oauth(error) => Self::from(error),
127        }
128    }
129}