use axum::extract::FromRequestParts; use axum::http::request::Parts; use sea_orm::DatabaseConnection; use std::sync::Arc; use crate::claims::AccessClaims; use crate::config::JwtSettings; use crate::error::JwtError; use crate::keys::SigningKeyCache; use crate::rotation::{RotationContext, validate_access}; #[derive(Debug, Clone)] pub struct AuthUser { pub claims: AccessClaims, } /// Extract the bearer access token from the Authorization header. pub fn bearer_token(parts: &Parts) -> Result<&str, JwtError> { let header = parts .headers .get(axum::http::header::AUTHORIZATION) .ok_or(JwtError::MissingHeader)?; let value = header.to_str().map_err(|_| JwtError::InvalidHeader)?; let mut it = value.split_whitespace(); let scheme = it.next().unwrap_or(""); if !scheme.eq_ignore_ascii_case("bearer") { return Err(JwtError::InvalidHeader); } it.next().ok_or(JwtError::InvalidHeader) } async fn validate_ctx(parts: &Parts, token: &str) -> Result { let cache = parts .extensions .get::>>() .ok_or(JwtError::Config("signing key cache missing".into()))?; let db = parts .extensions .get::() .ok_or(JwtError::Config("db missing".into()))?; let settings = parts .extensions .get::() .ok_or(JwtError::Config("jwt settings missing".into()))?; let cache = cache.read().await; let ctx = RotationContext { db, settings, cache: &cache, }; let claims = validate_access(&ctx, token).await?; // VULN-005: access tokens are stateless, so honour the Redis deny-list // injected by `inject_jwt_ctx`. If the token (or its user) was revoked // after issuance, reject it regardless of signature/expiry. if let Some(blacklist) = parts .extensions .get::>>() .cloned() .flatten() { let revoked = blacklist .is_revoked(claims.jti, claims.sub) .await .map_err(|e| JwtError::Other(format!("access token blacklist check failed: {e}")))?; if revoked { return Err(JwtError::SessionRevoked); } } Ok(claims) } impl FromRequestParts for AuthUser { type Rejection = JwtError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let token = bearer_token(parts)?; let claims = validate_ctx(parts, token).await?; Ok(AuthUser { claims }) } } /// Optional auth: returns None when no/invalid bearer token is present. pub struct AuthOptional(pub Option); impl FromRequestParts for AuthOptional { type Rejection = JwtError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { match bearer_token(parts) { Ok(token) => match validate_ctx(parts, token).await { Ok(claims) => Ok(AuthOptional(Some(AuthUser { claims }))), Err(_) => Ok(AuthOptional(None)), }, Err(_) => Ok(AuthOptional(None)), } } } /// Admin-only auth: rejects unless the user is an admin. pub struct AuthAdmin(pub AuthUser); impl FromRequestParts for AuthAdmin { type Rejection = JwtError; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let user = AuthUser::from_request_parts(parts, _state).await?; if !user.claims.is_admin { return Err(JwtError::Other("admin required".into())); } Ok(AuthAdmin(user)) } }