|
| 1 | +use axum::{ |
| 2 | + extract::{Request, State}, |
| 3 | + middleware::Next, |
| 4 | + response::Response, |
| 5 | +}; |
| 6 | +use axum_extra::extract::cookie::CookieJar; |
| 7 | + |
| 8 | +use crate::handlers::auth::{ApiError, AppState, SESSION_COOKIE_NAME}; |
| 9 | +use infera_management_core::error::Error as CoreError; |
| 10 | + |
| 11 | +use super::{require_server_jwt, require_session, ServerContext, SessionContext}; |
| 12 | + |
| 13 | +/// Dual authentication middleware |
| 14 | +/// |
| 15 | +/// Accepts EITHER session authentication OR server JWT authentication. |
| 16 | +/// This allows both user requests (via session cookies/tokens) and |
| 17 | +/// server-to-server requests (via JWT) to access the same endpoints. |
| 18 | +/// |
| 19 | +/// Attaches either SessionContext or ServerContext to the request extensions. |
| 20 | +pub async fn require_session_or_server_jwt( |
| 21 | + State(state): State<AppState>, |
| 22 | + jar: CookieJar, |
| 23 | + request: Request, |
| 24 | + next: Next, |
| 25 | +) -> Result<Response, ApiError> { |
| 26 | + // Check if this looks like a JWT request (Bearer token in Authorization header) |
| 27 | + let has_bearer_token = request |
| 28 | + .headers() |
| 29 | + .get("authorization") |
| 30 | + .and_then(|h| h.to_str().ok()) |
| 31 | + .map(|s| s.starts_with("Bearer ")) |
| 32 | + .unwrap_or(false); |
| 33 | + |
| 34 | + // Check if this looks like a session request (session cookie or numeric Bearer token) |
| 35 | + let has_session = jar.get(SESSION_COOKIE_NAME).is_some() |
| 36 | + || request |
| 37 | + .headers() |
| 38 | + .get("authorization") |
| 39 | + .and_then(|h| h.to_str().ok()) |
| 40 | + .and_then(|s| s.strip_prefix("Bearer ")) |
| 41 | + .and_then(|token| token.parse::<i64>().ok()) |
| 42 | + .is_some(); |
| 43 | + |
| 44 | + // If we have a Bearer token and it's NOT a session (numeric), try JWT auth first |
| 45 | + if has_bearer_token && !has_session { |
| 46 | + return require_server_jwt(State(state), request, next).await; |
| 47 | + } |
| 48 | + |
| 49 | + // Otherwise, try session auth first |
| 50 | + if has_session { |
| 51 | + return require_session(State(state), jar, request, next).await; |
| 52 | + } |
| 53 | + |
| 54 | + // No recognizable auth present |
| 55 | + Err(CoreError::Auth( |
| 56 | + "Authentication required: provide either a session token or server JWT".to_string(), |
| 57 | + ) |
| 58 | + .into()) |
| 59 | +} |
| 60 | + |
| 61 | +/// Extract either session context or server context from request |
| 62 | +/// |
| 63 | +/// Returns SessionContext if session auth was used, or converts ServerContext to a |
| 64 | +/// compatible format if server JWT was used. |
| 65 | +pub fn extract_dual_auth_context(request: &Request) -> Result<AuthContextType, ApiError> { |
| 66 | + // Try to extract session context first |
| 67 | + if let Some(session_ctx) = request.extensions().get::<SessionContext>() { |
| 68 | + return Ok(AuthContextType::Session(session_ctx.clone())); |
| 69 | + } |
| 70 | + |
| 71 | + // Try to extract server context |
| 72 | + if let Some(server_ctx) = request.extensions().get::<ServerContext>() { |
| 73 | + return Ok(AuthContextType::Server(server_ctx.clone())); |
| 74 | + } |
| 75 | + |
| 76 | + Err(CoreError::Auth("No authentication context found in request".to_string()).into()) |
| 77 | +} |
| 78 | + |
| 79 | +/// Enum representing the type of authentication used |
| 80 | +#[derive(Debug, Clone)] |
| 81 | +pub enum AuthContextType { |
| 82 | + /// Session-based authentication (user request) |
| 83 | + Session(SessionContext), |
| 84 | + /// Server JWT authentication (server-to-server) |
| 85 | + Server(ServerContext), |
| 86 | +} |
| 87 | + |
| 88 | +impl AuthContextType { |
| 89 | + /// Get user ID if this is a session context, None otherwise |
| 90 | + pub fn user_id(&self) -> Option<i64> { |
| 91 | + match self { |
| 92 | + AuthContextType::Session(ctx) => Some(ctx.user_id), |
| 93 | + AuthContextType::Server(_) => None, |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + /// Get server ID if this is a server context, None otherwise |
| 98 | + pub fn server_id(&self) -> Option<&str> { |
| 99 | + match self { |
| 100 | + AuthContextType::Session(_) => None, |
| 101 | + AuthContextType::Server(ctx) => Some(&ctx.server_id), |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + /// Check if this is a server authentication |
| 106 | + pub fn is_server_auth(&self) -> bool { |
| 107 | + matches!(self, AuthContextType::Server(_)) |
| 108 | + } |
| 109 | + |
| 110 | + /// Check if this is a session authentication |
| 111 | + pub fn is_session_auth(&self) -> bool { |
| 112 | + matches!(self, AuthContextType::Session(_)) |
| 113 | + } |
| 114 | +} |
0 commit comments