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
71            | AuthProviderError::ServiceAccountInteractiveLogin) => {
72                Self::access_denied(e.to_string())
73            },
74            other => Self::server_error("Authentication provider failed").with_source(other),
75        }
76    }
77}
78
79impl From<SecretsBootstrapError> for OAuthHttpError {
80    fn from(err: SecretsBootstrapError) -> Self {
81        Self::server_error("Secrets unavailable").with_source(err)
82    }
83}
84
85impl From<sqlx::Error> for OAuthHttpError {
86    fn from(err: sqlx::Error) -> Self {
87        Self::server_error("Database operation failed").with_source(err)
88    }
89}
90
91impl From<anyhow::Error> for OAuthHttpError {
92    fn from(err: anyhow::Error) -> Self {
93        Self::server_error("Authorization operation failed").with_source(err)
94    }
95}
96
97impl From<IssuanceError> for OAuthHttpError {
98    fn from(error: IssuanceError) -> Self {
99        match error {
100            IssuanceError::InvalidRequest { field, message } => {
101                Self::invalid_request(format!("{field}: {message}"))
102            },
103            IssuanceError::MalformedField {
104                field,
105                reason,
106                source,
107            } => internal::rejected(Self::invalid_request(format!("{field}: {reason}")), source),
108            IssuanceError::UnsupportedGrantType { grant_type } => {
109                Self::unsupported_grant_type(format!("Grant type '{grant_type}' is not supported"))
110            },
111            IssuanceError::InvalidClient => Self::invalid_client("Client authentication failed"),
112            IssuanceError::InvalidGrant { reason } => Self::invalid_grant(reason),
113            IssuanceError::RejectedGrant { reason, source } => {
114                internal::rejected(Self::invalid_grant(reason), source)
115            },
116            IssuanceError::InvalidRefreshToken { reason } => {
117                Self::invalid_grant(format!("Refresh token invalid: {reason}"))
118            },
119            IssuanceError::InvalidCredentials => Self::invalid_grant("Invalid credentials"),
120            IssuanceError::InvalidClientSecret => Self::invalid_client("Invalid client secret"),
121            IssuanceError::ExpiredCode => Self::invalid_grant("Authorization code expired"),
122            IssuanceError::ServerError { context, source } => {
123                internal::server_error(context, source)
124            },
125            IssuanceError::InvalidTarget { message } => Self::invalid_target(message),
126            IssuanceError::InvalidScope { message } => Self::invalid_scope(message),
127            IssuanceError::IdJagRejected(error) => Self::invalid_grant(error.to_string()),
128            IssuanceError::BoundResource(error) => Self::invalid_target(error.to_string()),
129            IssuanceError::Oauth(error) => Self::from(error),
130        }
131    }
132}