auth_required.rs
243 lignes · 8016 octets
/// Authentication Required Middleware /// Feature 004: UI Post-Auth /// /// Middleware to protect routes that require authentication. /// Redirects to login page if user is not authenticated. use axum::{ extract::{Request, State}, http::Uri, middleware::Next, response::{IntoResponse, Response}, }; use sea_orm::DatabaseConnection; use std::sync::Arc; use crate::auth::jwt::{Claims, JwtService}; use crate::auth::jwt_extractor; use crate::config::auth::AuthConfig; use crate::error::AppError; use crate::services::jwt_blacklist_service::JwtBlacklistService; /// Extract user claims from JWT token in request /// /// Helper function to extract user claims from JWT token (cookie or header). /// Returns None if not authenticated or error. /// /// # Arguments /// /// * `db` - Database connection (for blacklist check) /// * `request` - The HTTP request /// /// # Returns /// /// * `Some(Claims)` if authenticated and token valid /// * `None` if not authenticated or error pub async fn extract_user_from_jwt(db: &DatabaseConnection, request: &Request) -> Option<Claims> { extract_user_from_jwt_impl(db, request).await } /// Internal implementation of extract_user_from_jwt async fn extract_user_from_jwt_impl(db: &DatabaseConnection, request: &Request) -> Option<Claims> { // Load auth configuration from request extensions (injected at startup) let auth_config = match request.extensions().get::<Arc<AuthConfig>>() { Some(config) => config.clone(), None => { tracing::warn!("AuthConfig extension missing, falling back to from_env"); match AuthConfig::from_env() { Ok(config) => Arc::new(config), Err(e) => { tracing::warn!("Failed to load auth config: {}", e); return None; } } } }; // Extract JWT from request let jwt_token = match jwt_extractor::extract_jwt_from_request(request) { Ok(token) => token, Err(_) => return None, }; // Check if token is blacklisted if JwtBlacklistService::is_blacklisted(db, &jwt_token) .await .is_ok_and(|b| b) { tracing::debug!("JWT token is blacklisted"); return None; } // Validate JWT token match JwtService::validate_token(&auth_config, &jwt_token) { Ok(claims) => { tracing::debug!("User extracted from JWT: {}", claims.sub); Some(claims) } Err(e) => { tracing::debug!("Failed to validate JWT: {}", e); None } } } /// Middleware that requires authentication /// /// Checks if the user has a valid JWT token (from cookie or Authorization header). /// If authenticated, allows the request to proceed. /// If not authenticated, redirects to `/login` with a redirect parameter. /// /// # Arguments /// /// * `State(db)` - Database connection (for blacklist check) /// * `uri` - Original request URI (for redirect after login) /// * `request` - The HTTP request /// * `next` - Next middleware or handler /// /// # Returns /// /// * `200 OK` if authenticated (passes to handler) /// * `302 Found` redirects to `/login` if not authenticated /// /// # Example /// /// ```rust,ignore /// use axum::Router; /// use axum::routing::get; /// use axum::middleware; /// /// let protected_routes = Router::new() /// .route("/home", get(home_handler)) /// .layer(middleware::from_fn(auth_required)); /// ``` /// Internal implementation that takes db as parameter /// Made public for use in main.rs with from_fn closure pub async fn auth_required_impl( db: &DatabaseConnection, uri: Uri, request: Request, next: Next, ) -> Result<Response, AppError> { tracing::debug!( "auth_required middleware: checking authentication for {}", uri ); // Load auth configuration from request extensions (injected at startup) let auth_config = request .extensions() .get::<Arc<AuthConfig>>() .cloned() .ok_or_else(|| { tracing::error!("AuthConfig extension missing from request"); AppError::Config("Auth configuration not available".to_string()) })?; // Extract JWT from request (cookie or header) let jwt_token = match jwt_extractor::extract_jwt_from_request(&request) { Ok(token) => token, Err(_) => { tracing::warn!("Unauthenticated access attempt to: {} (no token)", uri); return Ok(crate::middleware::auth_service::auth_failure_response( &auth_config, &uri, )); } }; // Check if token is blacklisted if JwtBlacklistService::is_blacklisted(db, &jwt_token).await? { tracing::warn!("Blacklisted JWT token used for: {}", uri); return Ok(crate::middleware::auth_service::auth_failure_response( &auth_config, &uri, )); } // Validate JWT token let claims = match JwtService::validate_token(&auth_config, &jwt_token) { Ok(claims) => claims, Err(e) => { tracing::warn!("Invalid JWT token for {}: {}", uri, e); return Ok(crate::middleware::auth_service::auth_failure_response( &auth_config, &uri, )); } }; tracing::debug!("User authenticated: {} (from JWT)", claims.sub); // Store claims in request extensions for handlers to use let (mut parts, body) = request.into_parts(); parts.extensions.insert(claims.clone()); let request = Request::from_parts(parts, body); // User is authenticated, proceed to handler Ok(next.run(request).await) } /// Middleware that requires authentication (for use with from_fn_with_state) /// /// Checks if the user has a valid JWT token (from cookie or Authorization header). /// If authenticated, allows the request to proceed. /// If not authenticated, redirects to `/login` with a redirect parameter. /// /// # Note /// /// This function signature is compatible with `axum::middleware::from_fn_with_state`. /// The URI is extracted from `request.uri()` internally. /// Returns `Response` directly (not `Result`) as required by `from_fn_with_state`. /// Errors are converted to responses using `IntoResponse`. pub async fn auth_required( State(db): State<DatabaseConnection>, request: Request, next: Next, ) -> Response { // Extract URI from request (as per Axum middleware best practices) let uri = request.uri().clone(); // Call the implementation and convert Result to Response match auth_required_impl(&db, uri, request, next).await { Ok(response) => response, Err(e) => { tracing::error!("Auth middleware error: {}", e); e.into_response() } } } /// Optional authentication middleware /// /// Similar to `auth_required` but doesn't redirect if not authenticated. /// Useful for routes that are accessible to both authenticated and /// non-authenticated users. /// /// # Returns /// /// Always allows the request to proceed, whether authenticated or not. pub async fn optional_auth( State(db): State<DatabaseConnection>, request: Request, next: Next, ) -> Response { tracing::debug!("optional_auth middleware: checking authentication"); // Try to extract and validate JWT (but don't fail if missing) if let Ok(jwt_token) = jwt_extractor::extract_jwt_from_request(&request) { if let Some(auth_config) = request.extensions().get::<Arc<AuthConfig>>().cloned() { if JwtBlacklistService::is_blacklisted(&db, &jwt_token) .await .is_ok_and(|b| !b) { if let Ok(claims) = JwtService::validate_token(&auth_config, &jwt_token) { tracing::debug!("User authenticated: {} (from JWT)", claims.sub); } } } } // Always proceed to handler next.run(request).await }
GitRust