1use std::sync::Arc;
3
4use axum::{
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 error::{AuthError, Result},
12 jwt::{Claims, JwtValidator},
13};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AuthenticatedUser {
18 pub user_id: String,
20 pub claims: Claims,
22}
23
24impl AuthenticatedUser {
25 #[must_use]
27 pub fn get_custom_claim(&self, key: &str) -> Option<&serde_json::Value> {
28 self.claims.get_custom(key)
29 }
30
31 #[must_use]
33 pub fn has_role(&self, role: &str) -> bool {
34 if let Some(serde_json::Value::String(user_role)) = self.claims.get_custom("role") {
35 user_role == role
36 } else if let Some(serde_json::Value::Array(roles)) = self.claims.get_custom("roles") {
37 roles.iter().any(|r| {
38 if let serde_json::Value::String(r_str) = r {
39 r_str == role
40 } else {
41 false
42 }
43 })
44 } else {
45 false
46 }
47 }
48}
49
50pub struct AuthMiddleware {
52 validator: Arc<JwtValidator>,
53 public_key: Vec<u8>,
54}
55
56impl AuthMiddleware {
57 #[must_use]
70 pub const fn new(validator: Arc<JwtValidator>, public_key: Vec<u8>) -> Self {
71 Self {
72 validator,
73 public_key,
74 }
75 }
76
77 pub async fn validate_token(&self, token: &str) -> Result<Claims> {
86 self.validator.validate(token, &self.public_key)
87 }
88}
89
90impl AuthError {
91 #[allow(clippy::cognitive_complexity)] fn response_parts(&self) -> (StatusCode, &'static str, String) {
96 match self {
97 Self::TokenExpired => {
98 (StatusCode::UNAUTHORIZED, "token_expired", "Authentication failed".to_string())
99 },
100 Self::InvalidSignature => (
101 StatusCode::UNAUTHORIZED,
102 "invalid_signature",
103 "Authentication failed".to_string(),
104 ),
105 Self::InvalidToken { .. }
106 | Self::MissingClaim { .. }
107 | Self::InvalidClaimValue { .. }
108 | Self::MissingNonce
111 | Self::NonceMismatch
112 | Self::MissingAuthTime
113 | Self::SessionTooOld { .. }
114 | Self::TokenIssuedInFuture
115 | Self::TokenTooOld
116 | Self::TokenNotYetValid
117 | Self::ForbiddenAlgorithm { .. } => {
120 (StatusCode::UNAUTHORIZED, "invalid_token", "Authentication failed".to_string())
121 },
122 Self::TokenNotFound => {
123 (StatusCode::UNAUTHORIZED, "token_not_found", "Authentication failed".to_string())
124 },
125 Self::SessionRevoked => {
126 (StatusCode::UNAUTHORIZED, "session_revoked", "Authentication failed".to_string())
127 },
128 Self::InvalidState => {
129 (StatusCode::BAD_REQUEST, "invalid_state", "Authentication failed".to_string())
130 },
131 Self::Forbidden { .. } => {
132 (StatusCode::FORBIDDEN, "forbidden", "Permission denied".to_string())
133 },
134 Self::OAuthError { .. } => {
135 (StatusCode::UNAUTHORIZED, "oauth_error", "Authentication failed".to_string())
136 },
137 Self::SessionError { .. } => {
138 (StatusCode::UNAUTHORIZED, "session_error", "Authentication failed".to_string())
139 },
140 Self::DatabaseError { .. }
141 | Self::ConfigError { .. }
142 | Self::OidcMetadataError { .. }
143 | Self::Internal { .. }
144 | Self::SystemTimeError { .. } => (
145 StatusCode::INTERNAL_SERVER_ERROR,
146 "server_error",
147 "Service temporarily unavailable".to_string(),
148 ),
149 Self::PkceError { .. } => {
150 (StatusCode::BAD_REQUEST, "pkce_error", "Authentication failed".to_string())
151 },
152 Self::RateLimited { retry_after_secs } => (
153 StatusCode::TOO_MANY_REQUESTS,
154 "rate_limited",
155 format!("Too many requests. Retry after {retry_after_secs} seconds"),
156 ),
157 Self::InvalidCredentials => (
160 StatusCode::UNAUTHORIZED,
161 "invalid_credentials",
162 "Invalid email or password".to_string(),
163 ),
164 Self::AccountDisabled => {
165 (StatusCode::FORBIDDEN, "account_disabled", "This account is disabled".to_string())
166 },
167 Self::EmailAlreadyRegistered => (
168 StatusCode::CONFLICT,
169 "email_already_registered",
170 "An account already exists for this email".to_string(),
171 ),
172 Self::InvalidRegistration { .. } => (
174 StatusCode::BAD_REQUEST,
175 "invalid_registration",
176 "Invalid registration details".to_string(),
177 ),
178 }
179 }
180
181 #[allow(clippy::cognitive_complexity)] fn log_security_details(&self) {
184 use tracing::warn;
185
186 match self {
187 Self::InvalidToken { reason } => warn!("Invalid token error: {reason}"),
188 Self::MissingClaim { claim } => warn!("Missing required claim: {claim}"),
189 Self::InvalidClaimValue { claim, reason } => {
190 warn!("Invalid claim value for '{claim}': {reason}");
191 },
192 Self::Forbidden { message } => warn!("Authorization denied: {message}"),
193 Self::OAuthError { message } => warn!("OAuth provider error: {message}"),
194 Self::SessionError { message } => warn!("Session error: {message}"),
195 Self::DatabaseError { message } => {
196 warn!("Database error (should not reach client): {message}");
197 },
198 Self::ConfigError { message } => {
199 warn!("Configuration error (should not reach client): {message}");
200 },
201 Self::OidcMetadataError { message } => warn!("OIDC metadata error: {message}"),
202 Self::PkceError { message } => warn!("PKCE error: {message}"),
203 Self::Internal { message } => {
204 warn!("Internal error (should not reach client): {message}");
205 },
206 Self::SystemTimeError { message } => {
207 warn!("System time error (should not reach client): {message}");
208 },
209 Self::MissingNonce | Self::NonceMismatch => {
210 warn!("OIDC nonce validation failed: {self}");
211 },
212 Self::MissingAuthTime | Self::SessionTooOld { .. } => {
213 warn!("OIDC auth_time validation failed: {self}");
214 },
215 Self::TokenIssuedInFuture | Self::TokenTooOld | Self::TokenNotYetValid => {
216 warn!("JWT temporal claim validation failed: {self}");
217 },
218 Self::ForbiddenAlgorithm { alg } => {
219 warn!("OIDC algorithm-substitution attack rejected: forbidden algorithm '{alg}'");
220 },
221 Self::InvalidRegistration { reason } => warn!("Local signup rejected: {reason}"),
222 Self::TokenExpired
225 | Self::InvalidSignature
226 | Self::TokenNotFound
227 | Self::SessionRevoked
228 | Self::InvalidState
229 | Self::RateLimited { .. }
230 | Self::InvalidCredentials
231 | Self::AccountDisabled
232 | Self::EmailAlreadyRegistered => {},
233 }
234 }
235}
236
237impl IntoResponse for AuthError {
238 fn into_response(self) -> Response {
239 self.log_security_details();
240 let (status, error_code, sanitized_message) = self.response_parts();
241
242 let body = serde_json::json!({
243 "errors": [{
244 "message": sanitized_message,
245 "extensions": {
246 "code": error_code
247 }
248 }]
249 });
250
251 (status, axum::Json(body)).into_response()
252 }
253}