From b8b0f13d8d8b53cc612613073fb3e2aaca8db846 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Sun, 8 Mar 2026 14:32:29 +0100 Subject: [PATCH] Make Keycloak authentication optional for local development When KEYCLOAK_URL is not set, the dashboard runs without auth, treating all users as authenticated "Local User". Auth middleware and check-auth endpoint gracefully skip when Keycloak is unconfigured. Co-Authored-By: Claude Opus 4.6 --- .../src/infrastructure/auth.rs | 12 ++++++--- .../src/infrastructure/auth_check.rs | 12 +++++++++ .../src/infrastructure/auth_middleware.rs | 16 ++++++++++-- .../src/infrastructure/keycloak_config.rs | 25 +++++++++---------- .../src/infrastructure/server.rs | 12 ++++++--- .../src/infrastructure/server_state.rs | 2 +- 6 files changed, 56 insertions(+), 23 deletions(-) diff --git a/compliance-dashboard/src/infrastructure/auth.rs b/compliance-dashboard/src/infrastructure/auth.rs index eac180a..d37b584 100644 --- a/compliance-dashboard/src/infrastructure/auth.rs +++ b/compliance-dashboard/src/infrastructure/auth.rs @@ -75,7 +75,9 @@ pub async fn auth_login( Extension(pending): Extension, Query(params): Query>, ) -> Result { - let kc = state.keycloak; + let kc = state.keycloak.ok_or(DashboardError::Other( + "Keycloak not configured".into(), + ))?; let csrf_state = generate_state(); let code_verifier = generate_code_verifier(); let code_challenge = derive_code_challenge(&code_verifier); @@ -126,7 +128,9 @@ pub async fn auth_callback( Extension(pending): Extension, Query(params): Query>, ) -> Result { - let kc = state.keycloak; + let kc = state.keycloak.ok_or(DashboardError::Other( + "Keycloak not configured".into(), + ))?; let returned_state = params .get("state") @@ -210,7 +214,9 @@ pub async fn logout( session: Session, Extension(state): Extension, ) -> Result { - let kc = state.keycloak; + let kc = state.keycloak.ok_or(DashboardError::Other( + "Keycloak not configured".into(), + ))?; session .flush() diff --git a/compliance-dashboard/src/infrastructure/auth_check.rs b/compliance-dashboard/src/infrastructure/auth_check.rs index 52ea5d3..067f68c 100644 --- a/compliance-dashboard/src/infrastructure/auth_check.rs +++ b/compliance-dashboard/src/infrastructure/auth_check.rs @@ -9,9 +9,21 @@ use dioxus::prelude::*; #[server(endpoint = "check-auth")] pub async fn check_auth() -> Result { use super::auth::LOGGED_IN_USER_SESS_KEY; + use super::server_state::ServerState; use super::user_state::UserStateInner; use dioxus_fullstack::FullstackContext; + let state: ServerState = FullstackContext::extract().await?; + + // When Keycloak is not configured, treat as always authenticated + if state.keycloak.is_none() { + return Ok(AuthInfo { + authenticated: true, + name: "Local User".into(), + ..Default::default() + }); + } + let session: tower_sessions::Session = FullstackContext::extract().await?; let user_state: Option = session diff --git a/compliance-dashboard/src/infrastructure/auth_middleware.rs b/compliance-dashboard/src/infrastructure/auth_middleware.rs index 01a1cb6..bb6646a 100644 --- a/compliance-dashboard/src/infrastructure/auth_middleware.rs +++ b/compliance-dashboard/src/infrastructure/auth_middleware.rs @@ -2,18 +2,30 @@ use axum::{ extract::Request, middleware::Next, response::{IntoResponse, Response}, + Extension, }; use reqwest::StatusCode; use tower_sessions::Session; use super::auth::LOGGED_IN_USER_SESS_KEY; +use super::server_state::ServerState; use super::user_state::UserStateInner; const PUBLIC_API_ENDPOINTS: &[&str] = &["/api/check-auth"]; /// Axum middleware that enforces authentication on `/api/` server -/// function endpoints. -pub async fn require_auth(session: Session, request: Request, next: Next) -> Response { +/// function endpoints. Skips auth entirely when Keycloak is not configured. +pub async fn require_auth( + Extension(state): Extension, + session: Session, + request: Request, + next: Next, +) -> Response { + // Skip auth when Keycloak is not configured + if state.keycloak.is_none() { + return next.run(request).await; + } + let path = request.uri().path(); if path.starts_with("/api/") && !PUBLIC_API_ENDPOINTS.contains(&path) { diff --git a/compliance-dashboard/src/infrastructure/keycloak_config.rs b/compliance-dashboard/src/infrastructure/keycloak_config.rs index 561d681..3fc870b 100644 --- a/compliance-dashboard/src/infrastructure/keycloak_config.rs +++ b/compliance-dashboard/src/infrastructure/keycloak_config.rs @@ -1,5 +1,3 @@ -use super::error::DashboardError; - /// Keycloak OpenID Connect settings. #[derive(Debug)] pub struct KeycloakConfig { @@ -11,13 +9,18 @@ pub struct KeycloakConfig { } impl KeycloakConfig { - pub fn from_env() -> Result { - Ok(Self { - url: required_env("KEYCLOAK_URL")?, - realm: required_env("KEYCLOAK_REALM")?, - client_id: required_env("KEYCLOAK_CLIENT_ID")?, - redirect_uri: required_env("REDIRECT_URI")?, - app_url: required_env("APP_URL")?, + pub fn from_env() -> Option { + let url = std::env::var("KEYCLOAK_URL").ok()?; + let realm = std::env::var("KEYCLOAK_REALM").ok()?; + let client_id = std::env::var("KEYCLOAK_CLIENT_ID").ok()?; + let redirect_uri = std::env::var("REDIRECT_URI").ok()?; + let app_url = std::env::var("APP_URL").ok()?; + Some(Self { + url, + realm, + client_id, + redirect_uri, + app_url, }) } @@ -50,7 +53,3 @@ impl KeycloakConfig { } } -fn required_env(name: &str) -> Result { - std::env::var(name) - .map_err(|_| DashboardError::Config(format!("{name} is required but not set"))) -} diff --git a/compliance-dashboard/src/infrastructure/server.rs b/compliance-dashboard/src/infrastructure/server.rs index 0fa6e52..a48e978 100644 --- a/compliance-dashboard/src/infrastructure/server.rs +++ b/compliance-dashboard/src/infrastructure/server.rs @@ -18,11 +18,15 @@ pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> { dotenvy::dotenv().ok(); let config = config::load_config()?; - let keycloak: &'static KeycloakConfig = - Box::leak(Box::new(KeycloakConfig::from_env()?)); + let keycloak: Option<&'static KeycloakConfig> = + KeycloakConfig::from_env().map(|kc| &*Box::leak(Box::new(kc))); let db = Database::connect(&config.mongodb_uri, &config.mongodb_database).await?; - tracing::info!("Keycloak configured for realm '{}'", keycloak.realm); + if let Some(kc) = keycloak { + tracing::info!("Keycloak configured for realm '{}'", kc.realm); + } else { + tracing::warn!("Keycloak not configured - dashboard is unprotected"); + } let server_state: ServerState = ServerStateInner { agent_api_url: config.agent_api_url.clone(), @@ -54,8 +58,8 @@ pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> { .route("/logout", get(logout)) .serve_dioxus_application(ServeConfig::new(), app) .layer(Extension(PendingOAuthStore::default())) - .layer(Extension(server_state)) .layer(middleware::from_fn(require_auth)) + .layer(Extension(server_state)) .layer(session); axum::serve(listener, router.into_make_service()) diff --git a/compliance-dashboard/src/infrastructure/server_state.rs b/compliance-dashboard/src/infrastructure/server_state.rs index 2130784..221de22 100644 --- a/compliance-dashboard/src/infrastructure/server_state.rs +++ b/compliance-dashboard/src/infrastructure/server_state.rs @@ -20,7 +20,7 @@ pub struct ServerStateInner { pub db: Database, pub config: DashboardConfig, pub agent_api_url: String, - pub keycloak: &'static KeycloakConfig, + pub keycloak: Option<&'static KeycloakConfig>, } impl From for ServerState {