d5939db
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<AccessClaims, JwtError> {
let cache = parts
.extensions
.get::<Arc<tokio::sync::RwLock<SigningKeyCache>>>()
.ok_or(JwtError::Config("signing key cache missing".into()))?;
let db = parts
.extensions
.get::<DatabaseConnection>()
.ok_or(JwtError::Config("db missing".into()))?;
let settings = parts
.extensions
.get::<JwtSettings>()
.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::<Option<Arc<rate_limit::AccessTokenBlacklist>>>()
.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<S: Send + Sync> FromRequestParts<S> for AuthUser {
type Rejection = JwtError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
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<AuthUser>);
impl<S: Send + Sync> FromRequestParts<S> for AuthOptional {
type Rejection = JwtError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
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<S: Send + Sync> FromRequestParts<S> for AuthAdmin {
type Rejection = JwtError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let user = AuthUser::from_request_parts(parts, _state).await?;
if !user.claims.is_admin {
return Err(JwtError::Other("admin required".into()));
}
Ok(AuthAdmin(user))
}
}