jwt_verify/common/
error.rs

1use std::fmt;
2use anyhow::{Error as AnyhowError, Result as AnyhowResult};
3
4/// Detailed error types for JWT verification failures
5#[derive(Debug)]
6pub enum JwtError {
7    /// Token has expired
8    ExpiredToken {
9        /// When the token expired (Unix timestamp)
10        exp: Option<u64>,
11        /// Current time when validation was performed (Unix timestamp)
12        current_time: Option<u64>,
13    },
14    /// Token is not yet valid (nbf claim)
15    TokenNotYetValid {
16        /// When the token becomes valid (Unix timestamp)
17        nbf: Option<u64>,
18        /// Current time when validation was performed (Unix timestamp)
19        current_time: Option<u64>,
20    },
21    /// Token signature is invalid
22    InvalidSignature,
23    /// A specific claim is invalid
24    InvalidClaim {
25        /// The name of the claim that is invalid
26        claim: String,
27        /// The reason why the claim is invalid
28        reason: String,
29        /// The value of the claim, if available
30        value: Option<String>,
31    },
32    /// Token issuer is invalid
33    InvalidIssuer {
34        /// The expected issuer
35        expected: String,
36        /// The actual issuer in the token
37        actual: String,
38    },
39    /// Token client ID is invalid
40    InvalidClientId {
41        /// The expected client ID(s)
42        expected: Vec<String>,
43        /// The actual client ID in the token
44        actual: String,
45    },
46    /// Token use is invalid
47    InvalidTokenUse {
48        /// The expected token use
49        expected: String,
50        /// The actual token use in the token
51        actual: String,
52    },
53    /// JWK key not found
54    KeyNotFound(String),
55    /// Error fetching JWKs
56    JwksFetchError {
57        /// The URL that was being fetched
58        url: Option<String>,
59        /// The error message
60        error: String,
61    },
62    /// Error parsing token
63    ParseError {
64        /// The part of the token that failed to parse (header, payload, signature)
65        part: Option<String>,
66        /// The error message
67        error: String,
68    },
69    /// Configuration error
70    ConfigurationError {
71        /// The parameter that has an invalid configuration
72        parameter: Option<String>,
73        /// The error message
74        error: String,
75    },
76    /// Missing token
77    MissingToken,
78    /// Unsupported token type
79    UnsupportedTokenType {
80        /// The token type that was provided
81        token_type: String,
82    },
83    /// Generic token error
84    InvalidToken(String),
85    /// Unexpected error
86    UnexpectedError(String),
87}
88
89impl fmt::Display for JwtError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            JwtError::ExpiredToken { exp, current_time } => {
93                if let (Some(exp), Some(current_time)) = (exp, current_time) {
94                    write!(f, "Token has expired at {} (current time: {})", exp, current_time)
95                } else {
96                    write!(f, "Token has expired")
97                }
98            },
99            JwtError::TokenNotYetValid { nbf, current_time } => {
100                if let (Some(nbf), Some(current_time)) = (nbf, current_time) {
101                    write!(f, "Token is not yet valid until {} (current time: {})", nbf, current_time)
102                } else {
103                    write!(f, "Token is not yet valid")
104                }
105            },
106            JwtError::InvalidSignature => write!(f, "Token signature is invalid"),
107            JwtError::InvalidClaim { claim, reason, value } => {
108                if let Some(value) = value {
109                    write!(f, "Invalid claim '{}': {} (value: '{}')", claim, reason, value)
110                } else {
111                    write!(f, "Invalid claim '{}': {}", claim, reason)
112                }
113            },
114            JwtError::InvalidIssuer { expected, actual } => {
115                write!(f, "Invalid issuer: expected '{}', got '{}'", expected, actual)
116            },
117            JwtError::InvalidClientId { expected, actual } => {
118                write!(f, "Invalid client ID: expected one of {:?}, got '{}'", expected, actual)
119            },
120            JwtError::InvalidTokenUse { expected, actual } => {
121                write!(f, "Invalid token use: expected '{}', got '{}'", expected, actual)
122            },
123            JwtError::KeyNotFound(kid) => write!(f, "JWK not found for key ID: {}", kid),
124            JwtError::JwksFetchError { url, error } => {
125                if let Some(url) = url {
126                    write!(f, "Failed to fetch JWKs from {}: {}", url, error)
127                } else {
128                    write!(f, "Failed to fetch JWKs: {}", error)
129                }
130            },
131            JwtError::ParseError { part, error } => {
132                if let Some(part) = part {
133                    write!(f, "Failed to parse token {}: {}", part, error)
134                } else {
135                    write!(f, "Failed to parse token: {}", error)
136                }
137            },
138            JwtError::ConfigurationError { parameter, error } => {
139                if let Some(parameter) = parameter {
140                    write!(f, "Configuration error for '{}': {}", parameter, error)
141                } else {
142                    write!(f, "Configuration error: {}", error)
143                }
144            },
145            JwtError::MissingToken => write!(f, "Token is missing"),
146            JwtError::UnsupportedTokenType { token_type } => {
147                write!(f, "Unsupported token type: {}", token_type)
148            },
149            JwtError::InvalidToken(err) => write!(f, "Invalid token: {}", err),
150            JwtError::UnexpectedError(err) => write!(f, "Unexpected error: {}", err),
151        }
152    }
153}
154
155impl std::error::Error for JwtError {}
156
157impl From<jsonwebtoken::errors::Error> for JwtError {
158    fn from(err: jsonwebtoken::errors::Error) -> Self {
159        use jsonwebtoken::errors::ErrorKind;
160        match err.kind() {
161            ErrorKind::ExpiredSignature => JwtError::ExpiredToken {
162                exp: None,
163                current_time: None,
164            },
165            ErrorKind::InvalidSignature => JwtError::InvalidSignature,
166            ErrorKind::InvalidToken => JwtError::InvalidToken("Token format is invalid".to_string()),
167            ErrorKind::InvalidIssuer => JwtError::InvalidIssuer {
168                expected: "unknown".to_string(),
169                actual: "unknown".to_string(),
170            },
171            ErrorKind::InvalidAudience => JwtError::InvalidClaim {
172                claim: "aud".to_string(),
173                reason: "Audience is invalid".to_string(),
174                value: None,
175            },
176            ErrorKind::InvalidSubject => JwtError::InvalidClaim {
177                claim: "sub".to_string(),
178                reason: "Subject is invalid".to_string(),
179                value: None,
180            },
181            ErrorKind::ImmatureSignature => JwtError::TokenNotYetValid {
182                nbf: None,
183                current_time: None,
184            },
185            ErrorKind::InvalidAlgorithm => JwtError::InvalidClaim {
186                claim: "alg".to_string(),
187                reason: "Algorithm is invalid".to_string(),
188                value: None,
189            },
190            _ => JwtError::ParseError {
191                part: None,
192                error: format!("{}", err),
193            },
194        }
195    }
196}
197
198impl From<reqwest::Error> for JwtError {
199    fn from(err: reqwest::Error) -> Self {
200        let url = err.url().map(|u| u.to_string());
201        JwtError::JwksFetchError {
202            url,
203            error: format!("{}", err),
204        }
205    }
206}
207
208impl From<serde_json::Error> for JwtError {
209    fn from(err: serde_json::Error) -> Self {
210        JwtError::ParseError {
211            part: Some("payload".to_string()),
212            error: format!("JSON error: {}", err),
213        }
214    }
215}
216
217impl From<AnyhowError> for JwtError {
218    fn from(err: AnyhowError) -> Self {
219        JwtError::UnexpectedError(format!("{}", err))
220    }
221}
222
223impl From<std::io::Error> for JwtError {
224    fn from(err: std::io::Error) -> Self {
225        JwtError::UnexpectedError(format!("IO error: {}", err))
226    }
227}
228
229impl From<base64::DecodeError> for JwtError {
230    fn from(err: base64::DecodeError) -> Self {
231        JwtError::ParseError {
232            part: Some("base64".to_string()),
233            error: format!("Base64 decode error: {}", err),
234        }
235    }
236}
237
238impl From<std::str::Utf8Error> for JwtError {
239    fn from(err: std::str::Utf8Error) -> Self {
240        JwtError::ParseError {
241            part: Some("utf8".to_string()),
242            error: format!("UTF-8 decode error: {}", err),
243        }
244    }
245}
246
247/// Public error type that sanitizes error details for client responses
248#[derive(Debug)]
249pub enum PublicJwtError {
250    /// Generic invalid token error
251    InvalidToken,
252}
253
254impl fmt::Display for PublicJwtError {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        match self {
257            PublicJwtError::InvalidToken => write!(f, "Invalid authentication token"),
258        }
259    }
260}
261
262impl std::error::Error for PublicJwtError {}
263
264impl From<JwtError> for PublicJwtError {
265    fn from(_err: JwtError) -> Self {
266        // All errors map to a single generic error to avoid leaking sensitive information
267        PublicJwtError::InvalidToken
268    }
269}
270
271/// Error logger with configurable verbosity
272#[derive(Debug)]
273pub struct ErrorLogger {
274    verbosity: ErrorVerbosity,
275}
276
277/// Error verbosity levels
278#[derive(Debug, Clone, Copy)]
279pub enum ErrorVerbosity {
280    /// Only log error type
281    Minimal,
282    /// Log error type and general context
283    Standard,
284    /// Log all error details including specific claims
285    Detailed,
286}
287
288impl Default for ErrorVerbosity {
289    fn default() -> Self {
290        Self::Standard
291    }
292}
293
294impl ErrorLogger {
295    /// Create a new error logger with the specified verbosity
296    pub fn new(verbosity: ErrorVerbosity) -> Self {
297        Self { verbosity }
298    }
299
300    /// Create a new error logger with default verbosity (Standard)
301    pub fn default() -> Self {
302        Self {
303            verbosity: ErrorVerbosity::default(),
304        }
305    }
306
307    /// Log an error with the configured verbosity
308    pub fn log(&self, error: &JwtError) {
309        match self.verbosity {
310            ErrorVerbosity::Minimal => {
311                // Minimal logging just shows the error type without details
312                let error_type = match error {
313                    JwtError::ExpiredToken { .. } => "ExpiredToken",
314                    JwtError::TokenNotYetValid { .. } => "TokenNotYetValid",
315                    JwtError::InvalidSignature => "InvalidSignature",
316                    JwtError::InvalidClaim { .. } => "InvalidClaim",
317                    JwtError::InvalidIssuer { .. } => "InvalidIssuer",
318                    JwtError::InvalidClientId { .. } => "InvalidClientId",
319                    JwtError::InvalidTokenUse { .. } => "InvalidTokenUse",
320                    JwtError::KeyNotFound(_) => "KeyNotFound",
321                    JwtError::JwksFetchError { .. } => "JwksFetchError",
322                    JwtError::ParseError { .. } => "ParseError",
323                    JwtError::ConfigurationError { .. } => "ConfigurationError",
324                    JwtError::MissingToken => "MissingToken",
325                    JwtError::UnsupportedTokenType { .. } => "UnsupportedTokenType",
326                    JwtError::InvalidToken(_) => "InvalidToken",
327                    JwtError::UnexpectedError(_) => "UnexpectedError",
328                };
329                tracing::error!("JWT verification error: {}", error_type);
330            }
331            ErrorVerbosity::Standard => {
332                // Standard logging includes more context but omits sensitive values
333                match error {
334                    JwtError::ExpiredToken { exp, current_time } => {
335                        if let (Some(exp), Some(current_time)) = (exp, current_time) {
336                            tracing::error!("JWT verification error: Token expired at {} (current time: {})", exp, current_time);
337                        } else {
338                            tracing::error!("JWT verification error: Token has expired");
339                        }
340                    }
341                    JwtError::TokenNotYetValid { nbf, current_time } => {
342                        if let (Some(nbf), Some(current_time)) = (nbf, current_time) {
343                            tracing::error!("JWT verification error: Token not valid until {} (current time: {})", nbf, current_time);
344                        } else {
345                            tracing::error!("JWT verification error: Token is not yet valid");
346                        }
347                    }
348                    JwtError::InvalidSignature => {
349                        tracing::error!("JWT verification error: Invalid signature");
350                    }
351                    JwtError::InvalidClaim { claim, reason, .. } => {
352                        tracing::error!("JWT verification error: Invalid claim '{}': {}", claim, reason);
353                    }
354                    JwtError::InvalidIssuer { expected, actual } => {
355                        tracing::error!("JWT verification error: Invalid issuer, expected '{}', got '{}'", expected, actual);
356                    }
357                    JwtError::InvalidClientId { expected, actual } => {
358                        tracing::error!("JWT verification error: Invalid client ID, expected one of {:?}, got '{}'", expected, actual);
359                    }
360                    JwtError::InvalidTokenUse { expected, actual } => {
361                        tracing::error!(
362                            "JWT verification error: Invalid token use, expected '{}', got '{}'",
363                            expected, actual
364                        );
365                    }
366                    JwtError::KeyNotFound(kid) => {
367                        tracing::error!("JWT verification error: Key not found for ID '{}'", kid);
368                    }
369                    JwtError::JwksFetchError { url, error } => {
370                        if let Some(url) = url {
371                            tracing::error!("JWT verification error: Failed to fetch JWKs from {}: {}", url, error);
372                        } else {
373                            tracing::error!("JWT verification error: Failed to fetch JWKs: {}", error);
374                        }
375                    }
376                    JwtError::ParseError { part, error } => {
377                        if let Some(part) = part {
378                            tracing::error!("JWT verification error: Failed to parse token {}: {}", part, error);
379                        } else {
380                            tracing::error!("JWT verification error: Failed to parse token: {}", error);
381                        }
382                    }
383                    JwtError::ConfigurationError { parameter, error } => {
384                        if let Some(parameter) = parameter {
385                            tracing::error!("JWT verification error: Configuration error for '{}': {}", parameter, error);
386                        } else {
387                            tracing::error!("JWT verification error: Configuration error: {}", error);
388                        }
389                    }
390                    JwtError::MissingToken => {
391                        tracing::error!("JWT verification error: Token is missing");
392                    }
393                    JwtError::UnsupportedTokenType { token_type } => {
394                        tracing::error!("JWT verification error: Unsupported token type: {}", token_type);
395                    }
396                    JwtError::InvalidToken(err) => {
397                        tracing::error!("JWT verification error: Invalid token: {}", err);
398                    }
399                    JwtError::UnexpectedError(err) => {
400                        tracing::error!("JWT verification error: Unexpected error: {}", err);
401                    }
402                }
403            }
404            ErrorVerbosity::Detailed => {
405                // Detailed logging includes all available information
406                // This should only be used in development environments
407                tracing::error!("JWT verification error: {:?}", error);
408                
409                // Add additional context based on error type
410                match error {
411                    JwtError::ExpiredToken { exp, current_time } => {
412                        tracing::debug!("Token expiration details - exp: {:?}, current_time: {:?}", exp, current_time);
413                    }
414                    JwtError::InvalidClaim { claim, reason, value } => {
415                        tracing::debug!("Invalid claim details - claim: {}, reason: {}, value: {:?}", claim, reason, value);
416                    }
417                    JwtError::JwksFetchError { url, error } => {
418                        tracing::debug!("JWK fetch error details - url: {:?}, error: {}", url, error);
419                    }
420                    _ => {}
421                }
422            }
423        }
424    }
425
426    /// Convert an internal error to a public error
427    pub fn to_public_error(&self, error: JwtError) -> PublicJwtError {
428        // Log the error with the configured verbosity
429        self.log(&error);
430        
431        // All errors map to a single generic error to avoid leaking sensitive information
432        PublicJwtError::InvalidToken
433    }
434    
435    /// Sanitize an error message for public consumption
436    pub fn sanitize_error_message(&self, _error: &JwtError) -> String {
437        // All errors map to a single generic message to avoid leaking sensitive information
438        "Invalid authentication token".to_string()
439    }
440    
441    /// Get the appropriate HTTP status code for an error
442    pub fn status_code_for_error(&self, error: &JwtError) -> u16 {
443        match error {
444            JwtError::KeyNotFound(_) => 500, // Internal Server Error
445            JwtError::JwksFetchError { .. } => 500, // Internal Server Error
446            JwtError::ConfigurationError { .. } => 500, // Internal Server Error
447            JwtError::UnexpectedError(_) => 500, // Internal Server Error
448            _ => 401, // Unauthorized
449        }
450    }
451}
452
453/// Result type for JWT verification operations
454pub type Result<T> = AnyhowResult<T>;