diff --git a/src/directory/auth_routes.rs b/src/directory/auth_routes.rs index 99992f80a..4970465fa 100644 --- a/src/directory/auth_routes.rs +++ b/src/directory/auth_routes.rs @@ -7,10 +7,29 @@ use axum::{ }; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::sync::Arc; +use tokio::sync::RwLock; +use once_cell::sync::Lazy; use crate::shared::state::AppState; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionUserData { + pub user_id: String, + pub email: String, + pub username: String, + pub first_name: Option, + pub last_name: Option, + pub display_name: Option, + pub organization_id: Option, + pub roles: Vec, + pub created_at: i64, +} + +static SESSION_CACHE: Lazy>> = + Lazy::new(|| RwLock::new(HashMap::new())); + const BOOTSTRAP_SECRET_ENV: &str = "GB_BOOTSTRAP_SECRET"; #[derive(Debug, Deserialize)] @@ -288,13 +307,33 @@ pub async fn login( .and_then(|s| s.as_str()) .map(String::from); + let access_token = session_id.clone().unwrap_or_else(|| user_id.clone()); + + let session_user = SessionUserData { + user_id: user_id.clone(), + email: req.email.clone(), + username: req.email.split('@').next().unwrap_or("user").to_string(), + first_name: None, + last_name: None, + display_name: Some(req.email.split('@').next().unwrap_or("User").to_string()), + organization_id: None, + roles: vec!["admin".to_string()], + created_at: chrono::Utc::now().timestamp(), + }; + + { + let mut cache = SESSION_CACHE.write().await; + cache.insert(access_token.clone(), session_user); + info!("Session cached for user: {} with token: {}...", req.email, &access_token[..std::cmp::min(20, access_token.len())]); + } + info!("Login successful for: {} (user_id: {})", req.email, user_id); Ok(Json(LoginResponse { success: true, user_id: Some(user_id), session_id: session_id.clone(), - access_token: session_id, + access_token: Some(access_token), refresh_token: None, expires_in: Some(3600), requires_2fa: false, @@ -314,8 +353,13 @@ pub async fn logout( .and_then(|auth| auth.strip_prefix("Bearer ")) .map(String::from); - if let Some(ref _token) = token { - info!("User logged out"); + if let Some(ref token_str) = token { + let mut cache = SESSION_CACHE.write().await; + if cache.remove(token_str).is_some() { + info!("User logged out, session removed from cache"); + } else { + info!("User logged out (session was not in cache)"); + } } Ok(Json(LogoutResponse { @@ -325,7 +369,7 @@ pub async fn logout( } pub async fn get_current_user( - State(state): State>, + State(_state): State>, headers: axum::http::HeaderMap, ) -> Result, (StatusCode, Json)> { let session_token = headers @@ -333,6 +377,7 @@ pub async fn get_current_user( .and_then(|v| v.to_str().ok()) .and_then(|auth| auth.strip_prefix("Bearer ")) .ok_or_else(|| { + warn!("get_current_user: Missing authorization header"); ( StatusCode::UNAUTHORIZED, Json(ErrorResponse { @@ -342,192 +387,49 @@ pub async fn get_current_user( ) })?; - let client = { - let auth_service = state.auth_service.lock().await; - auth_service.client().clone() - }; - - let pat_path = std::path::Path::new("./botserver-stack/conf/directory/admin-pat.txt"); - let admin_token = std::fs::read_to_string(pat_path) - .map(|s| s.trim().to_string()) - .unwrap_or_default(); - - if admin_token.is_empty() { - error!("Admin PAT token not found for user lookup"); - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "Authentication service not configured".to_string(), - details: None, - }), - )); - } - - let http_client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .map_err(|e| { - error!("Failed to create HTTP client: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "Internal server error".to_string(), - details: None, - }), - ) - })?; - - let session_url = format!("{}/v2/sessions/{}", client.api_url(), session_token); - let session_response = http_client - .get(&session_url) - .bearer_auth(&admin_token) - .send() - .await - .map_err(|e| { - error!("Failed to get session: {}", e); - ( - StatusCode::UNAUTHORIZED, - Json(ErrorResponse { - error: "Session validation failed".to_string(), - details: None, - }), - ) - })?; - - if !session_response.status().is_success() { - let error_text = session_response.text().await.unwrap_or_default(); - error!("Session lookup failed: {}", error_text); + if session_token.is_empty() { + warn!("get_current_user: Empty authorization token"); return Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { - error: "Invalid or expired session".to_string(), + error: "Invalid authorization token".to_string(), details: None, }), )); } - let session_data: serde_json::Value = session_response.json().await.map_err(|e| { - error!("Failed to parse session response: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "Failed to parse session data".to_string(), - details: None, - }), - ) - })?; + info!("get_current_user: looking up session token (len={}, prefix={}...)", + session_token.len(), + &session_token[..std::cmp::min(20, session_token.len())]); - let user_id = session_data - .get("session") - .and_then(|s| s.get("factors")) - .and_then(|f| f.get("user")) - .and_then(|u| u.get("id")) - .and_then(|id| id.as_str()) - .unwrap_or_default() - .to_string(); + let cache = SESSION_CACHE.read().await; - if user_id.is_empty() { - return Err(( - StatusCode::UNAUTHORIZED, - Json(ErrorResponse { - error: "Invalid session - no user found".to_string(), - details: None, - }), - )); + if let Some(user_data) = cache.get(session_token) { + info!("get_current_user: found cached session for user: {}", user_data.email); + return Ok(Json(CurrentUserResponse { + id: user_data.user_id.clone(), + username: user_data.username.clone(), + email: Some(user_data.email.clone()), + first_name: user_data.first_name.clone(), + last_name: user_data.last_name.clone(), + display_name: user_data.display_name.clone(), + roles: user_data.roles.clone(), + organization_id: user_data.organization_id.clone(), + avatar_url: None, + })); } - let user_url = format!("{}/v2/users/{}", client.api_url(), user_id); - let user_response = http_client - .get(&user_url) - .bearer_auth(&admin_token) - .send() - .await - .map_err(|e| { - error!("Failed to get user: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "Failed to fetch user data".to_string(), - details: None, - }), - ) - })?; + drop(cache); - if !user_response.status().is_success() { - let error_text = user_response.text().await.unwrap_or_default(); - error!("User lookup failed: {}", error_text); - return Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "Failed to fetch user data".to_string(), - details: None, - }), - )); - } + warn!("get_current_user: session not found in cache, token may be from previous server run"); - let user_data: serde_json::Value = user_response.json().await.map_err(|e| { - error!("Failed to parse user response: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "Failed to parse user data".to_string(), - details: None, - }), - ) - })?; - - let user = user_data.get("user").unwrap_or(&user_data); - let human = user.get("human"); - - let username = user - .get("userName") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); - - let email = human - .and_then(|h| h.get("email")) - .and_then(|e| e.get("email")) - .and_then(|v| v.as_str()) - .map(String::from); - - let first_name = human - .and_then(|h| h.get("profile")) - .and_then(|p| p.get("givenName")) - .and_then(|v| v.as_str()) - .map(String::from); - - let last_name = human - .and_then(|h| h.get("profile")) - .and_then(|p| p.get("familyName")) - .and_then(|v| v.as_str()) - .map(String::from); - - let display_name = human - .and_then(|h| h.get("profile")) - .and_then(|p| p.get("displayName")) - .and_then(|v| v.as_str()) - .map(String::from); - - let organization_id = user - .get("details") - .and_then(|d| d.get("resourceOwner")) - .and_then(|v| v.as_str()) - .map(String::from); - - info!("User profile loaded for: {} ({})", username, user_id); - - Ok(Json(CurrentUserResponse { - id: user_id, - username, - email, - first_name, - last_name, - display_name, - roles: vec!["admin".to_string()], - organization_id, - avatar_url: None, - })) + Err(( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse { + error: "Session expired or invalid. Please log in again.".to_string(), + details: None, + }), + )) } pub async fn refresh_token( diff --git a/src/main.rs b/src/main.rs index 0161e1a65..6a011d57b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -235,7 +235,9 @@ async fn run_axum_server( .add_anonymous_path("/api/health") .add_anonymous_path("/api/product") .add_anonymous_path("/api/i18n") - .add_anonymous_path("/api/auth") + .add_anonymous_path("/api/auth/login") + .add_anonymous_path("/api/auth/refresh") + .add_anonymous_path("/api/auth/bootstrap") .add_anonymous_path("/ws") .add_anonymous_path("/auth") .add_public_path("/static") @@ -494,27 +496,28 @@ async fn run_axum_server( let rbac_manager_for_middleware = Arc::clone(&rbac_manager); let app = app_with_ui - // Security middleware stack (order matters - first added is outermost) + // Security middleware stack (order matters - last added is outermost/runs first) .layer(middleware::from_fn(security_headers_middleware)) .layer(security_headers_extension) .layer(rate_limit_extension) // Request ID tracking for all requests .layer(middleware::from_fn(request_id_middleware)) - // Authentication middleware using provider registry - // NOTE: In Axum, layers are applied bottom-to-top, so this runs BEFORE RBAC - .layer(middleware::from_fn(move |req: axum::http::Request, next: axum::middleware::Next| { - let state = auth_middleware_state.clone(); - async move { - botserver::security::auth_middleware_with_providers(req, next, state).await - } - })) // RBAC middleware - checks permissions AFTER authentication + // NOTE: In Axum, layers run in reverse order (last added = first to run) + // So RBAC is added BEFORE auth, meaning auth runs first, then RBAC .layer(middleware::from_fn(move |req: axum::http::Request, next: axum::middleware::Next| { let rbac = Arc::clone(&rbac_manager_for_middleware); async move { botserver::security::rbac_middleware_fn(req, next, rbac).await } })) + // Authentication middleware - MUST run before RBAC (so added after) + .layer(middleware::from_fn(move |req: axum::http::Request, next: axum::middleware::Next| { + let state = auth_middleware_state.clone(); + async move { + botserver::security::auth_middleware_with_providers(req, next, state).await + } + })) // Panic handler catches panics and returns safe 500 responses .layer(middleware::from_fn(move |req, next| { let config = panic_config.clone(); diff --git a/src/security/auth.rs b/src/security/auth.rs index fb8af15ea..0ff4fc054 100644 --- a/src/security/auth.rs +++ b/src/security/auth.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use uuid::Uuid; use crate::security::auth_provider::AuthProviderRegistry; @@ -530,11 +530,8 @@ impl Default for AuthConfig { "/.well-known".to_string(), "/metrics".to_string(), "/api/auth/login".to_string(), - "/api/auth/logout".to_string(), - "/api/auth/refresh".to_string(), "/api/auth/bootstrap".to_string(), - "/api/auth/2fa/verify".to_string(), - "/api/auth/2fa/resend".to_string(), + "/api/auth/refresh".to_string(), "/oauth".to_string(), "/auth/callback".to_string(), ], @@ -873,32 +870,46 @@ pub async fn auth_middleware_with_providers( ) -> Response { let path = request.uri().path().to_string(); + let method = request.method().to_string(); + + info!("[AUTH] Processing {} {}", method, path); if state.config.is_public_path(&path) || state.config.is_anonymous_allowed(&path) { + info!("[AUTH] Path is public/anonymous, skipping auth"); request .extensions_mut() .insert(AuthenticatedUser::anonymous()); return next.run(request).await; } + let auth_header = request + .headers() + .get(header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + info!("[AUTH] Authorization header: {:?}", auth_header.as_ref().map(|h| { + if h.len() > 30 { format!("{}...", &h[..30]) } else { h.clone() } + })); + let extracted = ExtractedAuthData::from_request(&request, &state.config); let user = authenticate_with_extracted_data(extracted, &state.config, &state.provider_registry).await; match user { Ok(authenticated_user) => { - debug!("Authenticated user: {} ({})", authenticated_user.username, authenticated_user.user_id); + info!("[AUTH] Success: user={} roles={:?}", authenticated_user.username, authenticated_user.roles); request.extensions_mut().insert(authenticated_user); next.run(request).await } Err(e) => { if !state.config.require_auth { - warn!("Authentication failed but not required, allowing anonymous: {:?}", e); + warn!("[AUTH] Failed but not required, allowing anonymous: {:?}", e); request .extensions_mut() .insert(AuthenticatedUser::anonymous()); return next.run(request).await; } - debug!("Authentication failed: {:?}", e); + info!("[AUTH] Failed: {:?}", e); e.into_response() } }