feat: add Keycloak authentication for dashboard and API endpoints (#2)
Some checks failed
CI / Clippy (push) Has been cancelled
CI / Format (push) Successful in 2s
CI / Security Audit (push) Has been cancelled
CI / Tests (push) Has been cancelled

Dashboard: OAuth2/OIDC login flow with PKCE, session-based auth middleware
protecting all server function endpoints, check-auth server function for
frontend auth state, login page gate in AppShell, user info in sidebar.

Agent API: JWT validation middleware using Keycloak JWKS endpoint,
conditionally enabled when KEYCLOAK_URL and KEYCLOAK_REALM are set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-03-07 23:50:56 +00:00
parent 42cabf0582
commit 0cb06d3d6d
21 changed files with 741 additions and 13 deletions

View File

@@ -1,19 +1,37 @@
use std::sync::Arc;
use axum::Extension;
use axum::{middleware, Extension};
use tokio::sync::RwLock;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use crate::agent::ComplianceAgent;
use crate::api::auth_middleware::{require_jwt_auth, JwksState};
use crate::api::routes;
use crate::error::AgentError;
pub async fn start_api_server(agent: ComplianceAgent, port: u16) -> Result<(), AgentError> {
let app = routes::build_router()
.layer(Extension(Arc::new(agent)))
let mut app = routes::build_router()
.layer(Extension(Arc::new(agent.clone())))
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http());
if let (Some(kc_url), Some(kc_realm)) =
(&agent.config.keycloak_url, &agent.config.keycloak_realm)
{
let jwks_url = format!("{kc_url}/realms/{kc_realm}/protocol/openid-connect/certs");
let jwks_state = JwksState {
jwks: Arc::new(RwLock::new(None)),
jwks_url,
};
tracing::info!("Keycloak JWT auth enabled for realm '{kc_realm}'");
app = app
.layer(Extension(jwks_state))
.layer(middleware::from_fn(require_jwt_auth));
} else {
tracing::warn!("Keycloak not configured - API endpoints are unprotected");
}
let addr = format!("0.0.0.0:{port}");
let listener = tokio::net::TcpListener::bind(&addr)
.await