use thiserror::Error;
use serde::{Deserialize, Serialize};
#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Token error: {message}")]
TokenError { message: String },
#[error("Session error: {message}")]
SessionError { message: String },
#[error("User not found")]
UserNotFound,
#[error("User account is disabled")]
UserDisabled,
#[error("Account locked due to failed login attempts")]
AccountLocked,
#[error("Multi-factor authentication required")]
MfaRequired,
#[error("Invalid multi-factor authentication code")]
InvalidMfaCode,
#[error("Access denied: {message}")]
AccessDenied { message: String },
#[error("Role not found: {role}")]
RoleNotFound { role: String },
#[error("Permission not found: {permission}")]
PermissionNotFound { permission: String },
#[error("Authentication configuration error: {message}")]
ConfigurationError { message: String },
#[error("Cryptographic error: {message}")]
CryptographicError { message: String },
#[error("Database error during authentication: {message}")]
DatabaseError { message: String },
#[error("Authentication error: {message}")]
Generic { message: String },
}
impl AuthError {
pub fn error_code(&self) -> &'static str {
match self {
AuthError::InvalidCredentials => "INVALID_CREDENTIALS",
AuthError::TokenError { .. } => "TOKEN_ERROR",
AuthError::SessionError { .. } => "SESSION_ERROR",
AuthError::UserNotFound => "USER_NOT_FOUND",
AuthError::UserDisabled => "USER_DISABLED",
AuthError::AccountLocked => "ACCOUNT_LOCKED",
AuthError::MfaRequired => "MFA_REQUIRED",
AuthError::InvalidMfaCode => "INVALID_MFA_CODE",
AuthError::AccessDenied { .. } => "ACCESS_DENIED",
AuthError::RoleNotFound { .. } => "ROLE_NOT_FOUND",
AuthError::PermissionNotFound { .. } => "PERMISSION_NOT_FOUND",
AuthError::ConfigurationError { .. } => "CONFIGURATION_ERROR",
AuthError::CryptographicError { .. } => "CRYPTOGRAPHIC_ERROR",
AuthError::DatabaseError { .. } => "DATABASE_ERROR",
AuthError::Generic { .. } => "AUTHENTICATION_ERROR",
}
}
pub fn status_code(&self) -> u16 {
match self {
AuthError::InvalidCredentials => 401,
AuthError::TokenError { .. } => 401,
AuthError::SessionError { .. } => 401,
AuthError::UserNotFound => 401, AuthError::UserDisabled => 401,
AuthError::AccountLocked => 429, AuthError::MfaRequired => 202, AuthError::InvalidMfaCode => 401,
AuthError::AccessDenied { .. } => 403,
AuthError::RoleNotFound { .. } => 403,
AuthError::PermissionNotFound { .. } => 403,
AuthError::ConfigurationError { .. } => 500,
AuthError::CryptographicError { .. } => 500,
AuthError::DatabaseError { .. } => 500,
AuthError::Generic { .. } => 500,
}
}
pub fn token_error(message: impl Into<String>) -> Self {
Self::TokenError { message: message.into() }
}
pub fn session_error(message: impl Into<String>) -> Self {
Self::SessionError { message: message.into() }
}
pub fn access_denied(message: impl Into<String>) -> Self {
Self::AccessDenied { message: message.into() }
}
pub fn config_error(message: impl Into<String>) -> Self {
Self::ConfigurationError { message: message.into() }
}
pub fn crypto_error(message: impl Into<String>) -> Self {
Self::CryptographicError { message: message.into() }
}
pub fn database_error(message: impl Into<String>) -> Self {
Self::DatabaseError { message: message.into() }
}
pub fn generic_error(message: impl Into<String>) -> Self {
Self::Generic { message: message.into() }
}
pub fn authentication_failed(message: impl Into<String>) -> Self {
Self::Generic { message: format!("Authentication failed: {}", message.into()) }
}
pub fn configuration_error(message: impl Into<String>) -> Self {
Self::config_error(message)
}
pub fn insufficient_permissions(message: impl Into<String>) -> Self {
Self::access_denied(message)
}
pub fn unauthorized(message: impl Into<String>) -> Self {
Self::Generic { message: format!("Unauthorized: {}", message.into()) }
}
pub fn invalid_credentials(message: impl Into<String>) -> Self {
Self::Generic { message: format!("Invalid credentials: {}", message.into()) }
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::Generic { message: format!("Not found: {}", message.into()) }
}
}
#[cfg(feature = "jwt")]
impl From<jsonwebtoken::errors::Error> for AuthError {
fn from(err: jsonwebtoken::errors::Error) -> Self {
Self::token_error(err.to_string())
}
}
#[cfg(feature = "argon2")]
impl From<argon2::Error> for AuthError {
fn from(err: argon2::Error) -> Self {
Self::crypto_error(err.to_string())
}
}
#[cfg(feature = "bcrypt")]
impl From<bcrypt::BcryptError> for AuthError {
fn from(err: bcrypt::BcryptError) -> Self {
Self::crypto_error(err.to_string())
}
}
impl From<sqlx::Error> for AuthError {
fn from(err: sqlx::Error) -> Self {
Self::database_error(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_codes() {
assert_eq!(AuthError::InvalidCredentials.error_code(), "INVALID_CREDENTIALS");
assert_eq!(AuthError::token_error("test").error_code(), "TOKEN_ERROR");
assert_eq!(AuthError::access_denied("test").error_code(), "ACCESS_DENIED");
}
#[test]
fn test_status_codes() {
assert_eq!(AuthError::InvalidCredentials.status_code(), 401);
assert_eq!(AuthError::access_denied("test").status_code(), 403);
assert_eq!(AuthError::AccountLocked.status_code(), 429);
assert_eq!(AuthError::MfaRequired.status_code(), 202);
assert_eq!(AuthError::config_error("test").status_code(), 500);
}
#[test]
fn test_error_creation_helpers() {
let token_err = AuthError::token_error("Invalid token");
assert_eq!(token_err, AuthError::TokenError { message: "Invalid token".to_string() });
let access_err = AuthError::access_denied("No permission");
assert_eq!(access_err, AuthError::AccessDenied { message: "No permission".to_string() });
}
#[test]
fn test_error_display() {
let err = AuthError::token_error("JWT expired");
assert_eq!(err.to_string(), "Token error: JWT expired");
let err = AuthError::access_denied("Insufficient privileges");
assert_eq!(err.to_string(), "Access denied: Insufficient privileges");
}
}