elif_auth/
error.rs

1//! Authentication and authorization error types
2
3use thiserror::Error;
4use serde::{Deserialize, Serialize};
5
6/// Authentication and authorization errors
7#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum AuthError {
9    /// Invalid credentials provided
10    #[error("Invalid credentials")]
11    InvalidCredentials,
12
13    /// Token-related errors
14    #[error("Token error: {message}")]
15    TokenError { message: String },
16
17    /// Session-related errors  
18    #[error("Session error: {message}")]
19    SessionError { message: String },
20
21    /// User not found
22    #[error("User not found")]
23    UserNotFound,
24
25    /// User account is disabled
26    #[error("User account is disabled")]
27    UserDisabled,
28
29    /// Account is locked due to failed attempts
30    #[error("Account locked due to failed login attempts")]
31    AccountLocked,
32
33    /// Multi-factor authentication required
34    #[error("Multi-factor authentication required")]
35    MfaRequired,
36
37    /// Invalid MFA code
38    #[error("Invalid multi-factor authentication code")]
39    InvalidMfaCode,
40
41    /// Authorization/permission errors
42    #[error("Access denied: {message}")]
43    AccessDenied { message: String },
44
45    /// Role not found
46    #[error("Role not found: {role}")]
47    RoleNotFound { role: String },
48
49    /// Permission not found
50    #[error("Permission not found: {permission}")]
51    PermissionNotFound { permission: String },
52
53    /// Configuration errors
54    #[error("Authentication configuration error: {message}")]
55    ConfigurationError { message: String },
56
57    /// Cryptographic errors
58    #[error("Cryptographic error: {message}")]
59    CryptographicError { message: String },
60
61    /// Database errors
62    #[error("Database error during authentication: {message}")]
63    DatabaseError { message: String },
64
65    /// Generic authentication error
66    #[error("Authentication error: {message}")]
67    Generic { message: String },
68}
69
70impl AuthError {
71    /// Get the error code for API responses
72    pub fn error_code(&self) -> &'static str {
73        match self {
74            AuthError::InvalidCredentials => "INVALID_CREDENTIALS",
75            AuthError::TokenError { .. } => "TOKEN_ERROR",
76            AuthError::SessionError { .. } => "SESSION_ERROR", 
77            AuthError::UserNotFound => "USER_NOT_FOUND",
78            AuthError::UserDisabled => "USER_DISABLED",
79            AuthError::AccountLocked => "ACCOUNT_LOCKED",
80            AuthError::MfaRequired => "MFA_REQUIRED",
81            AuthError::InvalidMfaCode => "INVALID_MFA_CODE",
82            AuthError::AccessDenied { .. } => "ACCESS_DENIED",
83            AuthError::RoleNotFound { .. } => "ROLE_NOT_FOUND",
84            AuthError::PermissionNotFound { .. } => "PERMISSION_NOT_FOUND",
85            AuthError::ConfigurationError { .. } => "CONFIGURATION_ERROR",
86            AuthError::CryptographicError { .. } => "CRYPTOGRAPHIC_ERROR",
87            AuthError::DatabaseError { .. } => "DATABASE_ERROR",
88            AuthError::Generic { .. } => "AUTHENTICATION_ERROR",
89        }
90    }
91
92    /// Get HTTP status code for the error
93    pub fn status_code(&self) -> u16 {
94        match self {
95            AuthError::InvalidCredentials => 401,
96            AuthError::TokenError { .. } => 401,
97            AuthError::SessionError { .. } => 401,
98            AuthError::UserNotFound => 401, // Don't reveal user existence
99            AuthError::UserDisabled => 401,
100            AuthError::AccountLocked => 429, // Too Many Requests
101            AuthError::MfaRequired => 202,   // Accepted, but MFA needed
102            AuthError::InvalidMfaCode => 401,
103            AuthError::AccessDenied { .. } => 403,
104            AuthError::RoleNotFound { .. } => 403,
105            AuthError::PermissionNotFound { .. } => 403,
106            AuthError::ConfigurationError { .. } => 500,
107            AuthError::CryptographicError { .. } => 500,
108            AuthError::DatabaseError { .. } => 500,
109            AuthError::Generic { .. } => 500,
110        }
111    }
112
113    /// Create a token error
114    pub fn token_error(message: impl Into<String>) -> Self {
115        Self::TokenError { message: message.into() }
116    }
117
118    /// Create a session error
119    pub fn session_error(message: impl Into<String>) -> Self {
120        Self::SessionError { message: message.into() }
121    }
122
123    /// Create an access denied error
124    pub fn access_denied(message: impl Into<String>) -> Self {
125        Self::AccessDenied { message: message.into() }
126    }
127
128    /// Create a configuration error
129    pub fn config_error(message: impl Into<String>) -> Self {
130        Self::ConfigurationError { message: message.into() }
131    }
132
133    /// Create a cryptographic error
134    pub fn crypto_error(message: impl Into<String>) -> Self {
135        Self::CryptographicError { message: message.into() }
136    }
137
138    /// Create a database error
139    pub fn database_error(message: impl Into<String>) -> Self {
140        Self::DatabaseError { message: message.into() }
141    }
142
143    /// Create a generic error
144    pub fn generic_error(message: impl Into<String>) -> Self {
145        Self::Generic { message: message.into() }
146    }
147    
148    /// Create an authentication failed error (alias for InvalidCredentials with message)
149    pub fn authentication_failed(message: impl Into<String>) -> Self {
150        Self::Generic { message: format!("Authentication failed: {}", message.into()) }
151    }
152    
153    /// Create a configuration error (alias for config_error)
154    pub fn configuration_error(message: impl Into<String>) -> Self {
155        Self::config_error(message)
156    }
157    
158    /// Create an insufficient permissions error (alias for access_denied)
159    pub fn insufficient_permissions(message: impl Into<String>) -> Self {
160        Self::access_denied(message)
161    }
162    
163    /// Create an unauthorized error (alias for InvalidCredentials with message)
164    pub fn unauthorized(message: impl Into<String>) -> Self {
165        Self::Generic { message: format!("Unauthorized: {}", message.into()) }
166    }
167}
168
169// Conversion from common error types
170#[cfg(feature = "jwt")]
171impl From<jsonwebtoken::errors::Error> for AuthError {
172    fn from(err: jsonwebtoken::errors::Error) -> Self {
173        Self::token_error(err.to_string())
174    }
175}
176
177#[cfg(feature = "argon2")]
178impl From<argon2::Error> for AuthError {
179    fn from(err: argon2::Error) -> Self {
180        Self::crypto_error(err.to_string())
181    }
182}
183
184#[cfg(feature = "bcrypt")]
185impl From<bcrypt::BcryptError> for AuthError {
186    fn from(err: bcrypt::BcryptError) -> Self {
187        Self::crypto_error(err.to_string())
188    }
189}
190
191impl From<sqlx::Error> for AuthError {
192    fn from(err: sqlx::Error) -> Self {
193        Self::database_error(err.to_string())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_error_codes() {
203        assert_eq!(AuthError::InvalidCredentials.error_code(), "INVALID_CREDENTIALS");
204        assert_eq!(AuthError::token_error("test").error_code(), "TOKEN_ERROR");
205        assert_eq!(AuthError::access_denied("test").error_code(), "ACCESS_DENIED");
206    }
207
208    #[test]
209    fn test_status_codes() {
210        assert_eq!(AuthError::InvalidCredentials.status_code(), 401);
211        assert_eq!(AuthError::access_denied("test").status_code(), 403);
212        assert_eq!(AuthError::AccountLocked.status_code(), 429);
213        assert_eq!(AuthError::MfaRequired.status_code(), 202);
214        assert_eq!(AuthError::config_error("test").status_code(), 500);
215    }
216
217    #[test]
218    fn test_error_creation_helpers() {
219        let token_err = AuthError::token_error("Invalid token");
220        assert_eq!(token_err, AuthError::TokenError { message: "Invalid token".to_string() });
221
222        let access_err = AuthError::access_denied("No permission");
223        assert_eq!(access_err, AuthError::AccessDenied { message: "No permission".to_string() });
224    }
225
226    #[test]
227    fn test_error_display() {
228        let err = AuthError::token_error("JWT expired");
229        assert_eq!(err.to_string(), "Token error: JWT expired");
230
231        let err = AuthError::access_denied("Insufficient privileges");
232        assert_eq!(err.to_string(), "Access denied: Insufficient privileges");
233    }
234}