Skip to main content

ferrox_security/
auth_middleware.rs

1use axum::{
2    extract::State,
3    http::{Request, StatusCode, header},
4    middleware::Next,
5    response::Response,
6};
7use std::sync::Arc;
8use crate::{PasetoAuth, AuthPayload};
9
10/// A middleware that extracts the PASETO token from the `Authorization: Bearer <token>` header,
11/// validates it, and injects the `AuthPayload` into the request extensions.
12pub async fn require_auth<B>(
13    State(auth_engine): State<Arc<PasetoAuth>>,
14    mut req: Request<B>,
15    next: Next,
16) -> Result<Response, StatusCode> {
17    let auth_header = req.headers().get(header::AUTHORIZATION)
18        .and_then(|value| value.to_str().ok())
19        .filter(|value| value.starts_with("Bearer "))
20        .map(|value| &value[7..]);
21
22    let token = match auth_header {
23        Some(token) => token,
24        None => return Err(StatusCode::UNAUTHORIZED),
25    };
26
27    match auth_engine.validate_token(token) {
28        Ok(payload) => {
29            // 1. Inject payload into local request context (for Monolith controllers)
30            req.extensions_mut().insert(payload.clone());
31            
32            // 2. Inject claims as HTTP Headers (API Gateway Pattern for Downstream Microservices)
33            // This prevents downstream microservices from having to re-validate the cryptographic signature
34            // or query the database, achieving zero-trust security without performance penalties.
35            if let Ok(user_id_val) = header::HeaderValue::from_str(&payload.user_id) {
36                req.headers_mut().insert("x-ferrox-user-id", user_id_val);
37            }
38            if let Ok(role_val) = header::HeaderValue::from_str(&payload.role) {
39                req.headers_mut().insert("x-ferrox-user-role", role_val);
40            }
41
42            Ok(next.run(req).await)
43        }
44        Err(_) => Err(StatusCode::UNAUTHORIZED),
45    }
46}
47
48/// An extractor guard for RBAC (Role-Based Access Control).
49/// Use it to protect routes, e.g., `.route_layer(axum::middleware::from_fn(|req, next| require_role(req, next, "admin")))`
50pub async fn require_role<B>(
51    req: Request<B>,
52    next: Next,
53    required_role: &str,
54) -> Result<Response, StatusCode> {
55    // Attempt to extract the AuthPayload previously injected by `require_auth`
56    let auth_payload = req.extensions().get::<AuthPayload>();
57
58    match auth_payload {
59        Some(payload) if payload.role == required_role => {
60            Ok(next.run(req).await)
61        }
62        _ => Err(StatusCode::FORBIDDEN), // User exists but lacks the required role
63    }
64}