Skip to main content

headless_lms_server/domain/oauth/
errors.rs

1use crate::domain::error::{ControllerError, ControllerErrorType, OAuthErrorCode, OAuthErrorData};
2use crate::prelude::BackendError;
3use dpop_verifier::DpopError;
4use thiserror::Error;
5
6/// Domain-specific errors for OAuth token grant processing.
7#[derive(Debug, Error)]
8pub enum TokenGrantError {
9    /// Invalid grant (e.g., invalid authorization code or refresh token)
10    #[error("Invalid grant: {0}")]
11    InvalidGrant(String),
12
13    /// Invalid client (e.g., client ID mismatch or missing DPoP header)
14    #[error("Invalid client: {0}")]
15    InvalidClient(String),
16
17    /// RFC 6749 ยง6: a refresh-token grant requested a scope that is not a
18    /// subset of the scope originally granted to the token being refreshed.
19    #[error("Invalid scope: {0}")]
20    InvalidScope(String),
21
22    /// PKCE verification failed
23    #[error("PKCE verification failed")]
24    PkceVerificationFailed,
25
26    /// Unsupported grant type
27    #[error("Unsupported grant type")]
28    UnsupportedGrantType,
29
30    /// DPoP JKT mismatch
31    #[error("DPoP JKT mismatch")]
32    DpopMismatch,
33
34    /// RFC 8628: the authorization request is still pending (user has not yet
35    /// approved or denied the device code).
36    #[error("Authorization pending")]
37    AuthorizationPending,
38
39    /// RFC 8628: the client is polling faster than the permitted interval.
40    #[error("Slow down")]
41    SlowDown,
42
43    /// RFC 8628: the device code has expired.
44    #[error("Device code expired")]
45    ExpiredToken,
46
47    /// RFC 8628: the user denied the authorization request.
48    #[error("Access denied")]
49    AccessDenied,
50
51    /// Server error (database or other internal error)
52    #[error("Server error: {0}")]
53    ServerError(String),
54}
55
56impl From<TokenGrantError> for ControllerError {
57    fn from(err: TokenGrantError) -> Self {
58        let data = match &err {
59            TokenGrantError::InvalidGrant(msg) => OAuthErrorData {
60                error: OAuthErrorCode::InvalidGrant.as_str().into(),
61                error_description: msg.clone(),
62                redirect_uri: None,
63                state: None,
64                nonce: None,
65            },
66            TokenGrantError::InvalidClient(msg) => OAuthErrorData {
67                error: OAuthErrorCode::InvalidClient.as_str().into(),
68                error_description: msg.clone(),
69                redirect_uri: None,
70                state: None,
71                nonce: None,
72            },
73            TokenGrantError::InvalidScope(msg) => OAuthErrorData {
74                error: OAuthErrorCode::InvalidScope.as_str().into(),
75                error_description: msg.clone(),
76                redirect_uri: None,
77                state: None,
78                nonce: None,
79            },
80            TokenGrantError::PkceVerificationFailed => OAuthErrorData {
81                error: OAuthErrorCode::InvalidGrant.as_str().into(),
82                error_description: "PKCE verification failed".into(),
83                redirect_uri: None,
84                state: None,
85                nonce: None,
86            },
87            TokenGrantError::UnsupportedGrantType => OAuthErrorData {
88                error: OAuthErrorCode::UnsupportedGrantType.as_str().into(),
89                error_description: "unsupported grant type".into(),
90                redirect_uri: None,
91                state: None,
92                nonce: None,
93            },
94            TokenGrantError::DpopMismatch => OAuthErrorData {
95                error: OAuthErrorCode::InvalidToken.as_str().into(),
96                error_description: "DPoP JKT mismatch".into(),
97                redirect_uri: None,
98                state: None,
99                nonce: None,
100            },
101            TokenGrantError::AuthorizationPending => OAuthErrorData {
102                error: OAuthErrorCode::AuthorizationPending.as_str().into(),
103                error_description: "authorization request is still pending".into(),
104                redirect_uri: None,
105                state: None,
106                nonce: None,
107            },
108            TokenGrantError::SlowDown => OAuthErrorData {
109                error: OAuthErrorCode::SlowDown.as_str().into(),
110                error_description: "polling too frequently; slow down".into(),
111                redirect_uri: None,
112                state: None,
113                nonce: None,
114            },
115            TokenGrantError::ExpiredToken => OAuthErrorData {
116                error: OAuthErrorCode::ExpiredToken.as_str().into(),
117                error_description: "device code has expired".into(),
118                redirect_uri: None,
119                state: None,
120                nonce: None,
121            },
122            TokenGrantError::AccessDenied => OAuthErrorData {
123                error: OAuthErrorCode::AccessDenied.as_str().into(),
124                error_description: "authorization request was denied".into(),
125                redirect_uri: None,
126                state: None,
127                nonce: None,
128            },
129            TokenGrantError::ServerError(msg) => OAuthErrorData {
130                error: OAuthErrorCode::ServerError.as_str().into(),
131                error_description: msg.clone(),
132                redirect_uri: None,
133                state: None,
134                nonce: None,
135            },
136        };
137
138        ControllerError::new(
139            ControllerErrorType::OAuthError(Box::new(data)),
140            err.to_string(),
141            Some(anyhow::anyhow!(err)),
142        )
143    }
144}
145
146impl From<DpopError> for TokenGrantError {
147    fn from(err: DpopError) -> Self {
148        match err {
149            DpopError::AthMismatch => TokenGrantError::DpopMismatch,
150            _ => TokenGrantError::ServerError(format!("DPoP error: {}", err)),
151        }
152    }
153}