Skip to main content

dbrest_core/auth/
error.rs

1//! JWT-specific error types
2//!
3//! Maps to DBRST300-303 error codes. Each variant carries enough detail
4//! to produce the correct HTTP status code, `WWW-Authenticate` header,
5//! and JSON error body.
6
7use std::fmt;
8
9/// Top-level JWT authentication error.
10///
11/// Each variant maps to one DBRST3xx error code:
12///
13/// | Variant | Code | HTTP |
14/// |---------|------|------|
15/// | `SecretMissing` | DBRST300 | 500 |
16/// | `Decode(_)` | DBRST301 | 401 |
17/// | `TokenRequired` | DBRST302 | 401 |
18/// | `Claims(_)` | DBRST303 | 401 |
19#[derive(Debug, Clone)]
20pub enum JwtError {
21    /// DBRST300 — no JWT secret or JWKS is configured on the server.
22    SecretMissing,
23
24    /// DBRST301 — the token could not be decoded (structural or crypto error).
25    Decode(JwtDecodeError),
26
27    /// DBRST302 — no token was provided and anonymous access is disabled.
28    TokenRequired,
29
30    /// DBRST303 — the token was decoded but a claims check failed.
31    Claims(JwtClaimsError),
32}
33
34/// Token decode errors (DBRST301).
35#[derive(Debug, Clone)]
36pub enum JwtDecodeError {
37    /// `Authorization: Bearer ` with an empty token string.
38    EmptyAuthHeader,
39    /// Token does not have exactly 3 dot-separated parts.
40    UnexpectedParts(usize),
41    /// No suitable key found, or key type mismatch.
42    KeyError(String),
43    /// The `alg` header specifies an unsupported algorithm.
44    BadAlgorithm(String),
45    /// Cryptographic signature verification failed.
46    BadCrypto,
47    /// The decoded token type (e.g. JWE) is not supported.
48    UnsupportedTokenType,
49}
50
51/// Claims validation errors (DBRST303).
52#[derive(Debug, Clone)]
53pub enum JwtClaimsError {
54    /// `exp` claim is in the past (beyond the 30-second skew window).
55    Expired,
56    /// `nbf` claim is in the future (beyond the 30-second skew window).
57    NotYetValid,
58    /// `iat` claim is in the future (beyond the 30-second skew window).
59    IssuedAtFuture,
60    /// `aud` claim does not match the configured audience.
61    NotInAudience,
62    /// Claims JSON could not be parsed into the expected structure.
63    ParsingFailed,
64}
65
66// ---------------------------------------------------------------------------
67// Display impls
68// ---------------------------------------------------------------------------
69
70impl fmt::Display for JwtError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            JwtError::SecretMissing => write!(f, "Server lacks JWT secret"),
74            JwtError::Decode(e) => write!(f, "{e}"),
75            JwtError::TokenRequired => write!(f, "Anonymous access is disabled"),
76            JwtError::Claims(e) => write!(f, "{e}"),
77        }
78    }
79}
80
81impl fmt::Display for JwtDecodeError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            JwtDecodeError::EmptyAuthHeader => {
85                write!(f, "Empty JWT is sent in Authorization header")
86            }
87            JwtDecodeError::UnexpectedParts(n) => {
88                write!(f, "Expected 3 parts in JWT; got {n}")
89            }
90            JwtDecodeError::KeyError(_) => {
91                write!(f, "No suitable key or wrong key type")
92            }
93            JwtDecodeError::BadAlgorithm(_) => {
94                write!(f, "Wrong or unsupported encoding algorithm")
95            }
96            JwtDecodeError::BadCrypto => {
97                write!(f, "JWT cryptographic operation failed")
98            }
99            JwtDecodeError::UnsupportedTokenType => {
100                write!(f, "Unsupported token type")
101            }
102        }
103    }
104}
105
106impl fmt::Display for JwtClaimsError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            JwtClaimsError::Expired => write!(f, "JWT expired"),
110            JwtClaimsError::NotYetValid => write!(f, "JWT not yet valid"),
111            JwtClaimsError::IssuedAtFuture => write!(f, "JWT issued at future"),
112            JwtClaimsError::NotInAudience => write!(f, "JWT not in audience"),
113            JwtClaimsError::ParsingFailed => write!(f, "Parsing claims failed"),
114        }
115    }
116}
117
118impl std::error::Error for JwtError {}
119impl std::error::Error for JwtDecodeError {}
120impl std::error::Error for JwtClaimsError {}
121
122// ---------------------------------------------------------------------------
123// Convenience conversions
124// ---------------------------------------------------------------------------
125
126impl From<JwtDecodeError> for JwtError {
127    fn from(e: JwtDecodeError) -> Self {
128        JwtError::Decode(e)
129    }
130}
131
132impl From<JwtClaimsError> for JwtError {
133    fn from(e: JwtClaimsError) -> Self {
134        JwtError::Claims(e)
135    }
136}
137
138// ---------------------------------------------------------------------------
139// Error metadata helpers
140// ---------------------------------------------------------------------------
141
142impl JwtError {
143    /// DBRST error code string.
144    pub fn code(&self) -> &'static str {
145        match self {
146            JwtError::SecretMissing => "DBRST300",
147            JwtError::Decode(_) => "DBRST301",
148            JwtError::TokenRequired => "DBRST302",
149            JwtError::Claims(_) => "DBRST303",
150        }
151    }
152
153    /// HTTP status code.
154    pub fn status(&self) -> http::StatusCode {
155        match self {
156            JwtError::SecretMissing => http::StatusCode::INTERNAL_SERVER_ERROR,
157            JwtError::Decode(_) => http::StatusCode::UNAUTHORIZED,
158            JwtError::TokenRequired => http::StatusCode::UNAUTHORIZED,
159            JwtError::Claims(_) => http::StatusCode::UNAUTHORIZED,
160        }
161    }
162
163    /// Optional detail string for the error JSON body.
164    pub fn details(&self) -> Option<String> {
165        match self {
166            JwtError::Decode(JwtDecodeError::KeyError(d)) => Some(d.clone()),
167            JwtError::Decode(JwtDecodeError::BadAlgorithm(d)) => Some(d.clone()),
168            _ => None,
169        }
170    }
171
172    /// `WWW-Authenticate` header value, if applicable.
173    pub fn www_authenticate(&self) -> Option<String> {
174        match self {
175            JwtError::TokenRequired => Some("Bearer".to_string()),
176            JwtError::Decode(e) => {
177                let msg = e.to_string();
178                Some(format!(
179                    "Bearer error=\"invalid_token\", error_description=\"{msg}\""
180                ))
181            }
182            JwtError::Claims(e) => {
183                let msg = e.to_string();
184                Some(format!(
185                    "Bearer error=\"invalid_token\", error_description=\"{msg}\""
186                ))
187            }
188            JwtError::SecretMissing => None,
189        }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Tests
195// ---------------------------------------------------------------------------
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_error_codes() {
203        assert_eq!(JwtError::SecretMissing.code(), "DBRST300");
204        assert_eq!(
205            JwtError::Decode(JwtDecodeError::EmptyAuthHeader).code(),
206            "DBRST301"
207        );
208        assert_eq!(JwtError::TokenRequired.code(), "DBRST302");
209        assert_eq!(JwtError::Claims(JwtClaimsError::Expired).code(), "DBRST303");
210    }
211
212    #[test]
213    fn test_error_status() {
214        assert_eq!(
215            JwtError::SecretMissing.status(),
216            http::StatusCode::INTERNAL_SERVER_ERROR
217        );
218        assert_eq!(
219            JwtError::Decode(JwtDecodeError::BadCrypto).status(),
220            http::StatusCode::UNAUTHORIZED
221        );
222        assert_eq!(
223            JwtError::TokenRequired.status(),
224            http::StatusCode::UNAUTHORIZED
225        );
226        assert_eq!(
227            JwtError::Claims(JwtClaimsError::NotInAudience).status(),
228            http::StatusCode::UNAUTHORIZED
229        );
230    }
231
232    #[test]
233    fn test_www_authenticate_headers() {
234        // TokenRequired → plain Bearer
235        let hdr = JwtError::TokenRequired.www_authenticate().unwrap();
236        assert_eq!(hdr, "Bearer");
237
238        // Decode error → Bearer with error_description
239        let hdr = JwtError::Decode(JwtDecodeError::BadCrypto)
240            .www_authenticate()
241            .unwrap();
242        assert!(hdr.contains("invalid_token"));
243        assert!(hdr.contains("cryptographic"));
244
245        // Claims error → Bearer with error_description
246        let hdr = JwtError::Claims(JwtClaimsError::Expired)
247            .www_authenticate()
248            .unwrap();
249        assert!(hdr.contains("expired"));
250
251        // SecretMissing → no header
252        assert!(JwtError::SecretMissing.www_authenticate().is_none());
253    }
254
255    #[test]
256    fn test_display_messages() {
257        assert_eq!(
258            JwtError::SecretMissing.to_string(),
259            "Server lacks JWT secret"
260        );
261        assert_eq!(
262            JwtError::TokenRequired.to_string(),
263            "Anonymous access is disabled"
264        );
265        assert_eq!(
266            JwtDecodeError::UnexpectedParts(2).to_string(),
267            "Expected 3 parts in JWT; got 2"
268        );
269        assert_eq!(JwtClaimsError::Expired.to_string(), "JWT expired");
270    }
271
272    #[test]
273    fn test_details() {
274        let err = JwtError::Decode(JwtDecodeError::KeyError(
275            "None of the keys was able to decode the JWT".to_string(),
276        ));
277        assert!(err.details().unwrap().contains("keys"));
278
279        assert!(
280            JwtError::Claims(JwtClaimsError::Expired)
281                .details()
282                .is_none()
283        );
284    }
285}