extractors.rs
342 lignes · 10938 octets
//! Axum extractors for authenticated user information //! //! This module provides the AuthUser extractor that automatically extracts //! and validates JWT tokens, making authenticated user information available //! to request handlers. use axum::{ extract::FromRequestParts, http::{StatusCode, request::Parts}, }; use sea_orm::DatabaseConnection; use std::sync::Arc; use uuid::Uuid; use crate::auth::jwt::JwtService; use crate::config::auth::AuthConfig; use crate::error::AppError; use crate::services::jwt_blacklist_service::JwtBlacklistService; /// Authenticated user extractor /// /// This extractor automatically validates JWT tokens from the Authorization header /// and makes user information available to request handlers. /// /// # Usage /// /// Add `AuthUser` as a parameter to any handler that requires authentication: /// /// ```rust,no_run /// use axum::Json; /// use rustwarden_core::auth::extractors::AuthUser; /// /// async fn protected_handler(user: AuthUser) -> Json<String> { /// Json(format!("Hello, {}!", user.username)) /// } /// ``` /// /// # Automatic Validation /// /// The extractor performs the following checks: /// 1. Extracts Authorization header /// 2. Validates Bearer token format /// 3. Verifies JWT signature /// 4. Checks token expiration /// 5. Validates issuer /// /// If any check fails, the extractor returns a 401 Unauthorized response /// automatically, preventing the handler from executing. /// /// # Examples /// /// ```rust,no_run /// use axum::{routing::get, Router, Json}; /// use rustwarden_core::auth::extractors::AuthUser; /// /// async fn get_profile(user: AuthUser) -> Json<serde_json::Value> { /// Json(serde_json::json!({ /// "user_id": user.user_id, /// "username": user.username, /// "roles": user.roles /// })) /// } /// /// let app: Router = Router::new().route("/profile", get(get_profile)); /// ``` /// /// # Security /// /// - Token validation happens on every request /// - Invalid or expired tokens are rejected immediately /// - User information is extracted from verified claims only /// - No database queries are performed (stateless authentication) #[derive(Debug, Clone)] pub struct AuthUser { /// User unique identifier (from JWT sub claim) pub user_id: Uuid, /// Username (from JWT username claim) pub username: String, /// User roles (from JWT roles claim) pub roles: Vec<String>, /// User permissions (from JWT permissions claim) pub permissions: Vec<String>, } impl AuthUser { /// Check if user has a specific role /// /// # Examples /// /// ```rust,no_run /// use rustwarden_core::auth::extractors::AuthUser; /// /// fn check_admin(user: &AuthUser) -> bool { /// user.has_role("admin") #[allow(dead_code)] /// } /// ``` pub fn has_role(&self, role: &str) -> bool { self.roles.iter().any(|r| r == role) } /// Check if user has any of the specified roles /// /// # Examples /// /// ```rust,no_run /// use rustwarden_core::auth::extractors::AuthUser; /// /// fn check_access(user: &AuthUser) -> bool { /// user.has_any_role(&["admin", "moderator"]) /// } /// ``` #[allow(dead_code)] pub fn has_any_role(&self, roles: &[&str]) -> bool { roles.iter().any(|role| self.has_role(role)) } /// Check if user has all of the specified roles /// /// # Examples /// /// ```rust,no_run /// use rustwarden_core::auth::extractors::AuthUser; /// /// fn check_permissions(user: &AuthUser) -> bool { /// user.has_all_roles(&["user", "verified"]) /// } /// ``` #[allow(dead_code)] pub fn has_all_roles(&self, roles: &[&str]) -> bool { roles.iter().all(|role| self.has_role(role)) } /// Check if user has a specific permission. /// Admin role is a superuser and has all permissions implicitly. pub fn has_permission(&self, permission: &str) -> bool { if self.has_role("admin") { return true; } self.permissions.iter().any(|p| p == permission) } } impl<S> FromRequestParts<S> for AuthUser where S: Send + Sync, { type Rejection = (StatusCode, String); async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { // Extract JWT token from Authorization header or cookie // Priority: Authorization header > Cookie (for API clients to override) let token: String = if let Some(auth_header) = parts .headers .get(axum::http::header::AUTHORIZATION) .and_then(|value| value.to_str().ok()) { // Try Authorization header first (for API clients) JwtService::extract_token_from_header(auth_header) .map(|t| t.to_string()) .map_err(|e| { tracing::debug!("Invalid Authorization header format: {}", e); ( StatusCode::UNAUTHORIZED, "Invalid Authorization header format".to_string(), ) })? } else { // Fallback to cookie (for web browsers with HttpOnly cookies) use axum_extra::extract::CookieJar; let cookie_jar = CookieJar::from_headers(&parts.headers); cookie_jar .get("jwt_token") .and_then(|cookie| { let value = cookie.value(); if value.is_empty() { None } else { Some(value.to_string()) } }) .ok_or_else(|| { tracing::debug!("Missing Authorization header and jwt_token cookie"); ( StatusCode::UNAUTHORIZED, "Missing Authorization header or jwt_token cookie".to_string(), ) })? }; // SEC-M1: Read AuthConfig from Extension (loaded once at startup) with fallback let auth_config_ext = parts.extensions.get::<Arc<AuthConfig>>().cloned(); let auth_config_owned; let auth_config: &AuthConfig = if let Some(ref arc) = auth_config_ext { arc.as_ref() } else { auth_config_owned = AuthConfig::from_env().map_err(|e| { tracing::error!("Failed to load auth config: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, "Server configuration error".to_string(), ) })?; &auth_config_owned }; // SEC-C1: Check JWT blacklist if DatabaseConnection is available in extensions if let Some(db) = parts.extensions.get::<DatabaseConnection>() { if JwtBlacklistService::is_blacklisted(db, &token) .await .unwrap_or(false) { tracing::warn!("AuthUser: JWT token is blacklisted"); return Err(( StatusCode::UNAUTHORIZED, "Token has been revoked".to_string(), )); } } // Validate token and extract claims let claims = JwtService::validate_token(auth_config, &token).map_err(|e| { let (status, message) = match e { AppError::TokenExpired => { tracing::debug!("Token expired"); (StatusCode::UNAUTHORIZED, "Token expired".to_string()) } AppError::InvalidToken => { tracing::debug!("Invalid token"); (StatusCode::UNAUTHORIZED, "Invalid token".to_string()) } _ => { tracing::warn!("Token validation failed: {}", e); ( StatusCode::UNAUTHORIZED, "Token validation failed".to_string(), ) } }; (status, message) })?; tracing::debug!("User {} authenticated via JWT", claims.username); Ok(AuthUser { user_id: claims.sub, username: claims.username, roles: claims.roles, permissions: claims.permissions, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_auth_user_creation() { let user = AuthUser { user_id: Uuid::new_v4(), username: "test_user".to_string(), roles: vec!["user".to_string(), "admin".to_string()], permissions: vec![], }; assert_eq!(user.username, "test_user"); assert_eq!(user.roles.len(), 2); } #[test] fn test_has_role() { let user = AuthUser { user_id: Uuid::new_v4(), username: "test_user".to_string(), roles: vec!["user".to_string(), "admin".to_string()], permissions: vec![], }; assert!(user.has_role("user")); assert!(user.has_role("admin")); assert!(!user.has_role("moderator")); } #[test] fn test_has_any_role() { let user = AuthUser { user_id: Uuid::new_v4(), username: "test_user".to_string(), roles: vec!["user".to_string()], permissions: vec![], }; assert!(user.has_any_role(&["admin", "user"])); assert!(user.has_any_role(&["user"])); assert!(!user.has_any_role(&["admin", "moderator"])); } #[test] fn test_has_all_roles() { let user = AuthUser { user_id: Uuid::new_v4(), username: "test_user".to_string(), roles: vec!["user".to_string(), "admin".to_string()], permissions: vec![], }; assert!(user.has_all_roles(&["user", "admin"])); assert!(user.has_all_roles(&["user"])); assert!(!user.has_all_roles(&["user", "admin", "moderator"])); } #[test] fn test_has_permission() { let user = AuthUser { user_id: Uuid::new_v4(), username: "test_user".to_string(), roles: vec!["user".to_string()], permissions: vec!["users.list".to_string(), "settings.view".to_string()], }; assert!(user.has_permission("users.list")); assert!(user.has_permission("settings.view")); assert!(!user.has_permission("users.delete")); } #[test] fn test_admin_has_all_permissions() { let user = AuthUser { user_id: Uuid::new_v4(), username: "admin_user".to_string(), roles: vec!["admin".to_string()], permissions: vec![], }; assert!(user.has_permission("users.delete")); assert!(user.has_permission("any.permission")); } }
GitRust