Make Keycloak authentication optional for local development
Some checks failed
CI / Format (push) Failing after 2s
CI / Clippy (push) Successful in 2m54s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Clippy (pull_request) Successful in 3m4s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Format (pull_request) Failing after 2s
Some checks failed
CI / Format (push) Failing after 2s
CI / Clippy (push) Successful in 2m54s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Clippy (pull_request) Successful in 3m4s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Format (pull_request) Failing after 2s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,9 @@ pub async fn auth_login(
|
||||
Extension(pending): Extension<PendingOAuthStore>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<impl IntoResponse, DashboardError> {
|
||||
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<PendingOAuthStore>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<impl IntoResponse, DashboardError> {
|
||||
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<ServerState>,
|
||||
) -> Result<impl IntoResponse, DashboardError> {
|
||||
let kc = state.keycloak;
|
||||
let kc = state.keycloak.ok_or(DashboardError::Other(
|
||||
"Keycloak not configured".into(),
|
||||
))?;
|
||||
|
||||
session
|
||||
.flush()
|
||||
|
||||
@@ -9,9 +9,21 @@ use dioxus::prelude::*;
|
||||
#[server(endpoint = "check-auth")]
|
||||
pub async fn check_auth() -> Result<AuthInfo, ServerFnError> {
|
||||
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<UserStateInner> = session
|
||||
|
||||
@@ -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<ServerState>,
|
||||
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) {
|
||||
|
||||
@@ -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<Self, DashboardError> {
|
||||
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<Self> {
|
||||
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<String, DashboardError> {
|
||||
std::env::var(name)
|
||||
.map_err(|_| DashboardError::Config(format!("{name} is required but not set")))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<ServerStateInner> for ServerState {
|
||||
|
||||
Reference in New Issue
Block a user