Skip to main content

securitydept_oidc_client/
error.rs

1use http::StatusCode;
2use securitydept_oauth_provider::OAuthProviderError;
3use securitydept_utils::{
4    error::{ErrorPresentation, ToErrorPresentation, UserRecovery},
5    http::ToHttpStatus,
6};
7use snafu::Snafu;
8
9#[derive(Debug, Snafu)]
10#[snafu(visibility(pub))]
11pub enum OidcError {
12    #[snafu(display("OIDC metadata error: {message}"))]
13    Metadata { message: String },
14
15    #[snafu(display("OIDC token exchange error: {message}"))]
16    TokenExchange { message: String },
17
18    #[snafu(display("OIDC device authorization error: {message}"))]
19    DeviceAuthorization { message: String },
20
21    #[snafu(display("OIDC device token poll error: {message}"))]
22    DeviceTokenPoll { message: String },
23
24    #[snafu(display("OIDC redirect URL error: {source}"))]
25    RedirectUrl { source: url::ParseError },
26
27    #[snafu(display("OIDC claims error: {message}"))]
28    Claims { message: String },
29
30    #[snafu(display("OIDC claims check script compile error: {message}"))]
31    ClaimsCheckScriptCompile { message: String },
32
33    #[snafu(display("OIDC claims check reject error: {message}"))]
34    ClaimsCheckReject { message: String },
35
36    #[snafu(display("OIDC invalid configuration: {message}"))]
37    InvalidConfig { message: String },
38
39    #[snafu(display("OIDC CSRF validation error: {message}"))]
40    CSRFValidation { message: String },
41
42    #[snafu(display("OIDC pending OAuth error: {source}"))]
43    PendingOauth {
44        source: Box<dyn std::error::Error + Send + Sync>,
45    },
46
47    #[snafu(display("OIDC token refresh error: {message}"))]
48    TokenRefresh { message: String },
49
50    #[snafu(display("OIDC token revocation error: {message}"))]
51    TokenRevocation { message: String },
52
53    /// Token endpoint returned a scope set that does not satisfy
54    /// `required_scopes`. `missing` lists the absent scopes.
55    #[snafu(display("OIDC scope validation error: token is missing required scopes: {missing:?}"))]
56    ScopeValidation { missing: Vec<String> },
57}
58
59impl ToHttpStatus for OidcError {
60    fn to_http_status(&self) -> StatusCode {
61        match self {
62            OidcError::ClaimsCheckReject { .. }
63            | OidcError::CSRFValidation { .. }
64            | OidcError::Claims { .. }
65            | OidcError::PendingOauth { .. } => StatusCode::UNAUTHORIZED,
66            OidcError::InvalidConfig { .. }
67            | OidcError::Metadata { .. }
68            | OidcError::TokenExchange { .. }
69            | OidcError::DeviceAuthorization { .. }
70            | OidcError::DeviceTokenPoll { .. }
71            | OidcError::RedirectUrl { .. }
72            | OidcError::TokenRefresh { .. }
73            | OidcError::TokenRevocation { .. }
74            | OidcError::ClaimsCheckScriptCompile { .. } => StatusCode::INTERNAL_SERVER_ERROR,
75            OidcError::ScopeValidation { .. } => StatusCode::FORBIDDEN,
76        }
77    }
78}
79
80pub type OidcResult<T> = std::result::Result<T, OidcError>;
81
82impl From<OAuthProviderError> for OidcError {
83    fn from(value: OAuthProviderError) -> Self {
84        match value {
85            OAuthProviderError::InvalidConfig { message } => Self::InvalidConfig { message },
86            OAuthProviderError::Metadata { message } => Self::Metadata { message },
87            OAuthProviderError::HttpClient { message } => Self::Metadata { message },
88            OAuthProviderError::Introspection { message } => Self::TokenExchange { message },
89        }
90    }
91}
92
93impl ToErrorPresentation for OidcError {
94    fn to_error_presentation(&self) -> ErrorPresentation {
95        match self {
96            OidcError::RedirectUrl { .. } => ErrorPresentation::new(
97                "oidc_redirect_url_invalid",
98                "The login redirect URL is invalid.",
99                UserRecovery::ContactSupport,
100            ),
101            OidcError::CSRFValidation { .. } => ErrorPresentation::new(
102                "oidc_request_invalid",
103                "The sign-in request is no longer valid. Start again.",
104                UserRecovery::RestartFlow,
105            ),
106            OidcError::PendingOauth { .. } => ErrorPresentation::new(
107                "oidc_request_expired",
108                "The sign-in request expired or was already used. Start again.",
109                UserRecovery::RestartFlow,
110            ),
111            OidcError::ClaimsCheckReject { .. } => ErrorPresentation::new(
112                "oidc_access_denied",
113                "Your account is not allowed to sign in.",
114                UserRecovery::ContactSupport,
115            ),
116            OidcError::DeviceAuthorization { .. } => ErrorPresentation::new(
117                "oidc_device_authorization_failed",
118                "The device sign-in could not be started. Try again.",
119                UserRecovery::Retry,
120            ),
121            OidcError::DeviceTokenPoll { .. } => ErrorPresentation::new(
122                "oidc_device_sign_in_failed",
123                "The device sign-in could not be completed. Start again.",
124                UserRecovery::RestartFlow,
125            ),
126            OidcError::TokenRefresh { .. } => ErrorPresentation::new(
127                "oidc_reauthentication_required",
128                "Your sign-in session expired. Sign in again.",
129                UserRecovery::Reauthenticate,
130            ),
131            OidcError::TokenRevocation { .. } => ErrorPresentation::new(
132                "oidc_token_revocation_failed",
133                "The token could not be revoked. Try again.",
134                UserRecovery::Retry,
135            ),
136            OidcError::Metadata { .. } | OidcError::TokenExchange { .. } => ErrorPresentation::new(
137                "oidc_sign_in_failed",
138                "The sign-in could not be completed. Start again.",
139                UserRecovery::RestartFlow,
140            ),
141            OidcError::Claims { .. } => ErrorPresentation::new(
142                "oidc_invalid_response",
143                "The sign-in response was invalid. Start again.",
144                UserRecovery::RestartFlow,
145            ),
146            OidcError::ClaimsCheckScriptCompile { .. } | OidcError::InvalidConfig { .. } => {
147                ErrorPresentation::new(
148                    "oidc_temporarily_unavailable",
149                    "Authentication is temporarily unavailable.",
150                    UserRecovery::ContactSupport,
151                )
152            }
153            OidcError::ScopeValidation { .. } => ErrorPresentation::new(
154                "oidc_insufficient_scope",
155                "The issued token does not grant the required permissions.",
156                UserRecovery::ContactSupport,
157            ),
158        }
159    }
160}