1use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Debug, Error, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum AuthError {
9 #[error("Invalid credentials")]
11 InvalidCredentials,
12
13 #[error("Token error: {message}")]
15 TokenError { message: String },
16
17 #[error("Session error: {message}")]
19 SessionError { message: String },
20
21 #[error("User not found")]
23 UserNotFound,
24
25 #[error("User account is disabled")]
27 UserDisabled,
28
29 #[error("Account locked due to failed login attempts")]
31 AccountLocked,
32
33 #[error("Multi-factor authentication required")]
35 MfaRequired,
36
37 #[error("Invalid multi-factor authentication code")]
39 InvalidMfaCode,
40
41 #[error("Access denied: {message}")]
43 AccessDenied { message: String },
44
45 #[error("Role not found: {role}")]
47 RoleNotFound { role: String },
48
49 #[error("Permission not found: {permission}")]
51 PermissionNotFound { permission: String },
52
53 #[error("Authentication configuration error: {message}")]
55 ConfigurationError { message: String },
56
57 #[error("Cryptographic error: {message}")]
59 CryptographicError { message: String },
60
61 #[error("Database error during authentication: {message}")]
63 DatabaseError { message: String },
64
65 #[error("Authentication error: {message}")]
67 Generic { message: String },
68}
69
70impl AuthError {
71 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 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, AuthError::UserDisabled => 401,
100 AuthError::AccountLocked => 429, AuthError::MfaRequired => 202, 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 pub fn token_error(message: impl Into<String>) -> Self {
115 Self::TokenError {
116 message: message.into(),
117 }
118 }
119
120 pub fn session_error(message: impl Into<String>) -> Self {
122 Self::SessionError {
123 message: message.into(),
124 }
125 }
126
127 pub fn access_denied(message: impl Into<String>) -> Self {
129 Self::AccessDenied {
130 message: message.into(),
131 }
132 }
133
134 pub fn config_error(message: impl Into<String>) -> Self {
136 Self::ConfigurationError {
137 message: message.into(),
138 }
139 }
140
141 pub fn crypto_error(message: impl Into<String>) -> Self {
143 Self::CryptographicError {
144 message: message.into(),
145 }
146 }
147
148 pub fn database_error(message: impl Into<String>) -> Self {
150 Self::DatabaseError {
151 message: message.into(),
152 }
153 }
154
155 pub fn generic_error(message: impl Into<String>) -> Self {
157 Self::Generic {
158 message: message.into(),
159 }
160 }
161
162 pub fn authentication_failed(message: impl Into<String>) -> Self {
164 Self::Generic {
165 message: format!("Authentication failed: {}", message.into()),
166 }
167 }
168
169 pub fn configuration_error(message: impl Into<String>) -> Self {
171 Self::config_error(message)
172 }
173
174 pub fn insufficient_permissions(message: impl Into<String>) -> Self {
176 Self::access_denied(message)
177 }
178
179 pub fn unauthorized(message: impl Into<String>) -> Self {
181 Self::Generic {
182 message: format!("Unauthorized: {}", message.into()),
183 }
184 }
185
186 pub fn invalid_credentials(message: impl Into<String>) -> Self {
188 Self::Generic {
189 message: format!("Invalid credentials: {}", message.into()),
190 }
191 }
192
193 pub fn not_found(message: impl Into<String>) -> Self {
195 Self::Generic {
196 message: format!("Not found: {}", message.into()),
197 }
198 }
199}
200
201#[cfg(feature = "jwt")]
203impl From<jsonwebtoken::errors::Error> for AuthError {
204 fn from(err: jsonwebtoken::errors::Error) -> Self {
205 Self::token_error(err.to_string())
206 }
207}
208
209#[cfg(feature = "argon2")]
210impl From<argon2::Error> for AuthError {
211 fn from(err: argon2::Error) -> Self {
212 Self::crypto_error(err.to_string())
213 }
214}
215
216#[cfg(feature = "bcrypt")]
217impl From<bcrypt::BcryptError> for AuthError {
218 fn from(err: bcrypt::BcryptError) -> Self {
219 Self::crypto_error(err.to_string())
220 }
221}
222
223impl From<sqlx::Error> for AuthError {
224 fn from(err: sqlx::Error) -> Self {
225 Self::database_error(err.to_string())
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_error_codes() {
235 assert_eq!(
236 AuthError::InvalidCredentials.error_code(),
237 "INVALID_CREDENTIALS"
238 );
239 assert_eq!(AuthError::token_error("test").error_code(), "TOKEN_ERROR");
240 assert_eq!(
241 AuthError::access_denied("test").error_code(),
242 "ACCESS_DENIED"
243 );
244 }
245
246 #[test]
247 fn test_status_codes() {
248 assert_eq!(AuthError::InvalidCredentials.status_code(), 401);
249 assert_eq!(AuthError::access_denied("test").status_code(), 403);
250 assert_eq!(AuthError::AccountLocked.status_code(), 429);
251 assert_eq!(AuthError::MfaRequired.status_code(), 202);
252 assert_eq!(AuthError::config_error("test").status_code(), 500);
253 }
254
255 #[test]
256 fn test_error_creation_helpers() {
257 let token_err = AuthError::token_error("Invalid token");
258 assert_eq!(
259 token_err,
260 AuthError::TokenError {
261 message: "Invalid token".to_string()
262 }
263 );
264
265 let access_err = AuthError::access_denied("No permission");
266 assert_eq!(
267 access_err,
268 AuthError::AccessDenied {
269 message: "No permission".to_string()
270 }
271 );
272 }
273
274 #[test]
275 fn test_error_display() {
276 let err = AuthError::token_error("JWT expired");
277 assert_eq!(err.to_string(), "Token error: JWT expired");
278
279 let err = AuthError::access_denied("Insufficient privileges");
280 assert_eq!(err.to_string(), "Access denied: Insufficient privileges");
281 }
282}