use std::fmt;
use anyhow::{Error as AnyhowError, Result as AnyhowResult};
#[derive(Debug)]
pub enum JwtError {
ExpiredToken {
exp: Option<u64>,
current_time: Option<u64>,
},
TokenNotYetValid {
nbf: Option<u64>,
current_time: Option<u64>,
},
InvalidSignature,
InvalidClaim {
claim: String,
reason: String,
value: Option<String>,
},
InvalidIssuer {
expected: String,
actual: String,
},
InvalidClientId {
expected: Vec<String>,
actual: String,
},
InvalidTokenUse {
expected: String,
actual: String,
},
KeyNotFound(String),
JwksFetchError {
url: Option<String>,
error: String,
},
ParseError {
part: Option<String>,
error: String,
},
ConfigurationError {
parameter: Option<String>,
error: String,
},
MissingToken,
UnsupportedTokenType {
token_type: String,
},
InvalidToken(String),
UnexpectedError(String),
}
impl fmt::Display for JwtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JwtError::ExpiredToken { exp, current_time } => {
if let (Some(exp), Some(current_time)) = (exp, current_time) {
write!(f, "Token has expired at {} (current time: {})", exp, current_time)
} else {
write!(f, "Token has expired")
}
},
JwtError::TokenNotYetValid { nbf, current_time } => {
if let (Some(nbf), Some(current_time)) = (nbf, current_time) {
write!(f, "Token is not yet valid until {} (current time: {})", nbf, current_time)
} else {
write!(f, "Token is not yet valid")
}
},
JwtError::InvalidSignature => write!(f, "Token signature is invalid"),
JwtError::InvalidClaim { claim, reason, value } => {
if let Some(value) = value {
write!(f, "Invalid claim '{}': {} (value: '{}')", claim, reason, value)
} else {
write!(f, "Invalid claim '{}': {}", claim, reason)
}
},
JwtError::InvalidIssuer { expected, actual } => {
write!(f, "Invalid issuer: expected '{}', got '{}'", expected, actual)
},
JwtError::InvalidClientId { expected, actual } => {
write!(f, "Invalid client ID: expected one of {:?}, got '{}'", expected, actual)
},
JwtError::InvalidTokenUse { expected, actual } => {
write!(f, "Invalid token use: expected '{}', got '{}'", expected, actual)
},
JwtError::KeyNotFound(kid) => write!(f, "JWK not found for key ID: {}", kid),
JwtError::JwksFetchError { url, error } => {
if let Some(url) = url {
write!(f, "Failed to fetch JWKs from {}: {}", url, error)
} else {
write!(f, "Failed to fetch JWKs: {}", error)
}
},
JwtError::ParseError { part, error } => {
if let Some(part) = part {
write!(f, "Failed to parse token {}: {}", part, error)
} else {
write!(f, "Failed to parse token: {}", error)
}
},
JwtError::ConfigurationError { parameter, error } => {
if let Some(parameter) = parameter {
write!(f, "Configuration error for '{}': {}", parameter, error)
} else {
write!(f, "Configuration error: {}", error)
}
},
JwtError::MissingToken => write!(f, "Token is missing"),
JwtError::UnsupportedTokenType { token_type } => {
write!(f, "Unsupported token type: {}", token_type)
},
JwtError::InvalidToken(err) => write!(f, "Invalid token: {}", err),
JwtError::UnexpectedError(err) => write!(f, "Unexpected error: {}", err),
}
}
}
impl std::error::Error for JwtError {}
impl From<jsonwebtoken::errors::Error> for JwtError {
fn from(err: jsonwebtoken::errors::Error) -> Self {
use jsonwebtoken::errors::ErrorKind;
match err.kind() {
ErrorKind::ExpiredSignature => JwtError::ExpiredToken {
exp: None,
current_time: None,
},
ErrorKind::InvalidSignature => JwtError::InvalidSignature,
ErrorKind::InvalidToken => JwtError::InvalidToken("Token format is invalid".to_string()),
ErrorKind::InvalidIssuer => JwtError::InvalidIssuer {
expected: "unknown".to_string(),
actual: "unknown".to_string(),
},
ErrorKind::InvalidAudience => JwtError::InvalidClaim {
claim: "aud".to_string(),
reason: "Audience is invalid".to_string(),
value: None,
},
ErrorKind::InvalidSubject => JwtError::InvalidClaim {
claim: "sub".to_string(),
reason: "Subject is invalid".to_string(),
value: None,
},
ErrorKind::ImmatureSignature => JwtError::TokenNotYetValid {
nbf: None,
current_time: None,
},
ErrorKind::InvalidAlgorithm => JwtError::InvalidClaim {
claim: "alg".to_string(),
reason: "Algorithm is invalid".to_string(),
value: None,
},
_ => JwtError::ParseError {
part: None,
error: format!("{}", err),
},
}
}
}
impl From<reqwest::Error> for JwtError {
fn from(err: reqwest::Error) -> Self {
let url = err.url().map(|u| u.to_string());
JwtError::JwksFetchError {
url,
error: format!("{}", err),
}
}
}
impl From<serde_json::Error> for JwtError {
fn from(err: serde_json::Error) -> Self {
JwtError::ParseError {
part: Some("payload".to_string()),
error: format!("JSON error: {}", err),
}
}
}
impl From<AnyhowError> for JwtError {
fn from(err: AnyhowError) -> Self {
JwtError::UnexpectedError(format!("{}", err))
}
}
impl From<std::io::Error> for JwtError {
fn from(err: std::io::Error) -> Self {
JwtError::UnexpectedError(format!("IO error: {}", err))
}
}
impl From<base64::DecodeError> for JwtError {
fn from(err: base64::DecodeError) -> Self {
JwtError::ParseError {
part: Some("base64".to_string()),
error: format!("Base64 decode error: {}", err),
}
}
}
impl From<std::str::Utf8Error> for JwtError {
fn from(err: std::str::Utf8Error) -> Self {
JwtError::ParseError {
part: Some("utf8".to_string()),
error: format!("UTF-8 decode error: {}", err),
}
}
}
#[derive(Debug)]
pub enum PublicJwtError {
InvalidToken,
}
impl fmt::Display for PublicJwtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PublicJwtError::InvalidToken => write!(f, "Invalid authentication token"),
}
}
}
impl std::error::Error for PublicJwtError {}
impl From<JwtError> for PublicJwtError {
fn from(_err: JwtError) -> Self {
PublicJwtError::InvalidToken
}
}
#[derive(Debug)]
pub struct ErrorLogger {
verbosity: ErrorVerbosity,
}
#[derive(Debug, Clone, Copy)]
pub enum ErrorVerbosity {
Minimal,
Standard,
Detailed,
}
impl Default for ErrorVerbosity {
fn default() -> Self {
Self::Standard
}
}
impl ErrorLogger {
pub fn new(verbosity: ErrorVerbosity) -> Self {
Self { verbosity }
}
pub fn default() -> Self {
Self {
verbosity: ErrorVerbosity::default(),
}
}
pub fn log(&self, error: &JwtError) {
match self.verbosity {
ErrorVerbosity::Minimal => {
let error_type = match error {
JwtError::ExpiredToken { .. } => "ExpiredToken",
JwtError::TokenNotYetValid { .. } => "TokenNotYetValid",
JwtError::InvalidSignature => "InvalidSignature",
JwtError::InvalidClaim { .. } => "InvalidClaim",
JwtError::InvalidIssuer { .. } => "InvalidIssuer",
JwtError::InvalidClientId { .. } => "InvalidClientId",
JwtError::InvalidTokenUse { .. } => "InvalidTokenUse",
JwtError::KeyNotFound(_) => "KeyNotFound",
JwtError::JwksFetchError { .. } => "JwksFetchError",
JwtError::ParseError { .. } => "ParseError",
JwtError::ConfigurationError { .. } => "ConfigurationError",
JwtError::MissingToken => "MissingToken",
JwtError::UnsupportedTokenType { .. } => "UnsupportedTokenType",
JwtError::InvalidToken(_) => "InvalidToken",
JwtError::UnexpectedError(_) => "UnexpectedError",
};
tracing::error!("JWT verification error: {}", error_type);
}
ErrorVerbosity::Standard => {
match error {
JwtError::ExpiredToken { exp, current_time } => {
if let (Some(exp), Some(current_time)) = (exp, current_time) {
tracing::error!("JWT verification error: Token expired at {} (current time: {})", exp, current_time);
} else {
tracing::error!("JWT verification error: Token has expired");
}
}
JwtError::TokenNotYetValid { nbf, current_time } => {
if let (Some(nbf), Some(current_time)) = (nbf, current_time) {
tracing::error!("JWT verification error: Token not valid until {} (current time: {})", nbf, current_time);
} else {
tracing::error!("JWT verification error: Token is not yet valid");
}
}
JwtError::InvalidSignature => {
tracing::error!("JWT verification error: Invalid signature");
}
JwtError::InvalidClaim { claim, reason, .. } => {
tracing::error!("JWT verification error: Invalid claim '{}': {}", claim, reason);
}
JwtError::InvalidIssuer { expected, actual } => {
tracing::error!("JWT verification error: Invalid issuer, expected '{}', got '{}'", expected, actual);
}
JwtError::InvalidClientId { expected, actual } => {
tracing::error!("JWT verification error: Invalid client ID, expected one of {:?}, got '{}'", expected, actual);
}
JwtError::InvalidTokenUse { expected, actual } => {
tracing::error!(
"JWT verification error: Invalid token use, expected '{}', got '{}'",
expected, actual
);
}
JwtError::KeyNotFound(kid) => {
tracing::error!("JWT verification error: Key not found for ID '{}'", kid);
}
JwtError::JwksFetchError { url, error } => {
if let Some(url) = url {
tracing::error!("JWT verification error: Failed to fetch JWKs from {}: {}", url, error);
} else {
tracing::error!("JWT verification error: Failed to fetch JWKs: {}", error);
}
}
JwtError::ParseError { part, error } => {
if let Some(part) = part {
tracing::error!("JWT verification error: Failed to parse token {}: {}", part, error);
} else {
tracing::error!("JWT verification error: Failed to parse token: {}", error);
}
}
JwtError::ConfigurationError { parameter, error } => {
if let Some(parameter) = parameter {
tracing::error!("JWT verification error: Configuration error for '{}': {}", parameter, error);
} else {
tracing::error!("JWT verification error: Configuration error: {}", error);
}
}
JwtError::MissingToken => {
tracing::error!("JWT verification error: Token is missing");
}
JwtError::UnsupportedTokenType { token_type } => {
tracing::error!("JWT verification error: Unsupported token type: {}", token_type);
}
JwtError::InvalidToken(err) => {
tracing::error!("JWT verification error: Invalid token: {}", err);
}
JwtError::UnexpectedError(err) => {
tracing::error!("JWT verification error: Unexpected error: {}", err);
}
}
}
ErrorVerbosity::Detailed => {
tracing::error!("JWT verification error: {:?}", error);
match error {
JwtError::ExpiredToken { exp, current_time } => {
tracing::debug!("Token expiration details - exp: {:?}, current_time: {:?}", exp, current_time);
}
JwtError::InvalidClaim { claim, reason, value } => {
tracing::debug!("Invalid claim details - claim: {}, reason: {}, value: {:?}", claim, reason, value);
}
JwtError::JwksFetchError { url, error } => {
tracing::debug!("JWK fetch error details - url: {:?}, error: {}", url, error);
}
_ => {}
}
}
}
}
pub fn to_public_error(&self, error: JwtError) -> PublicJwtError {
self.log(&error);
PublicJwtError::InvalidToken
}
pub fn sanitize_error_message(&self, _error: &JwtError) -> String {
"Invalid authentication token".to_string()
}
pub fn status_code_for_error(&self, error: &JwtError) -> u16 {
match error {
JwtError::KeyNotFound(_) => 500, JwtError::JwksFetchError { .. } => 500, JwtError::ConfigurationError { .. } => 500, JwtError::UnexpectedError(_) => 500, _ => 401, }
}
}
pub type Result<T> = AnyhowResult<T>;