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    /// RFC 8707: the requested resource does not match the grant.
52    #[error("Invalid target: {0}")]
53    InvalidTarget(String),
54
55    /// Server error (database or other internal error)
56    #[error("Server error: {0}")]
57    ServerError(String),
58}
59
60impl From<TokenGrantError> for ControllerError {
61    fn from(err: TokenGrantError) -> Self {
62        let data = match &err {
63            TokenGrantError::InvalidGrant(msg) => {
64                OAuthErrorData::new(OAuthErrorCode::InvalidGrant, msg)
65            }
66            TokenGrantError::InvalidClient(msg) => {
67                OAuthErrorData::new(OAuthErrorCode::InvalidClient, msg)
68            }
69            TokenGrantError::InvalidScope(msg) => {
70                OAuthErrorData::new(OAuthErrorCode::InvalidScope, msg)
71            }
72            TokenGrantError::PkceVerificationFailed => {
73                OAuthErrorData::new(OAuthErrorCode::InvalidGrant, "PKCE verification failed")
74            }
75            TokenGrantError::UnsupportedGrantType => OAuthErrorData::new(
76                OAuthErrorCode::UnsupportedGrantType,
77                "unsupported grant type",
78            ),
79            TokenGrantError::DpopMismatch => {
80                OAuthErrorData::new(OAuthErrorCode::InvalidToken, "DPoP JKT mismatch")
81            }
82            TokenGrantError::AuthorizationPending => OAuthErrorData::new(
83                OAuthErrorCode::AuthorizationPending,
84                "authorization request is still pending",
85            ),
86            TokenGrantError::SlowDown => OAuthErrorData::new(
87                OAuthErrorCode::SlowDown,
88                "polling too frequently; slow down",
89            ),
90            TokenGrantError::ExpiredToken => {
91                OAuthErrorData::new(OAuthErrorCode::ExpiredToken, "device code has expired")
92            }
93            TokenGrantError::AccessDenied => OAuthErrorData::new(
94                OAuthErrorCode::AccessDenied,
95                "authorization request was denied",
96            ),
97            TokenGrantError::InvalidTarget(msg) => {
98                OAuthErrorData::new(OAuthErrorCode::InvalidTarget, msg)
99            }
100            TokenGrantError::ServerError(msg) => {
101                OAuthErrorData::new(OAuthErrorCode::ServerError, msg)
102            }
103        };
104
105        ControllerError::new(
106            ControllerErrorType::OAuthError(Box::new(data)),
107            err.to_string(),
108            Some(anyhow::anyhow!(err)),
109        )
110    }
111}
112
113impl From<DpopError> for TokenGrantError {
114    fn from(err: DpopError) -> Self {
115        match err {
116            DpopError::AthMismatch => TokenGrantError::DpopMismatch,
117            _ => TokenGrantError::ServerError(format!("DPoP error: {}", err)),
118        }
119    }
120}