feat: add Keycloak authentication for dashboard and API endpoints (#2)
Some checks failed
CI / Format (push) Successful in 2s
CI / Security Audit (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Clippy (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,9 +1,15 @@
use axum::routing::get;
use axum::{middleware, Extension};
use dioxus::prelude::*;
use time::Duration;
use tower_sessions::{cookie::Key, MemoryStore, SessionManagerLayer};
use super::config;
use super::database::Database;
use super::error::DashboardError;
use super::keycloak_config::KeycloakConfig;
use super::server_state::{ServerState, ServerStateInner};
use super::{auth_callback, auth_login, logout, require_auth, PendingOAuthStore};
pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> {
tokio::runtime::Runtime::new()
@@ -12,15 +18,29 @@ 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 db = Database::connect(&config.mongodb_uri, &config.mongodb_database).await?;
tracing::info!("Keycloak configured for realm '{}'", keycloak.realm);
let server_state: ServerState = ServerStateInner {
agent_api_url: config.agent_api_url.clone(),
db,
config,
keycloak,
}
.into();
// Session layer
let key = Key::generate();
let store = MemoryStore::default();
let session = SessionManagerLayer::new(store)
.with_secure(false)
.with_same_site(tower_sessions::cookie::SameSite::Lax)
.with_expiry(tower_sessions::Expiry::OnInactivity(Duration::hours(24)))
.with_signed(key);
let addr = dioxus_cli_config::fullstack_address_or_localhost();
let listener = tokio::net::TcpListener::bind(addr)
.await
@@ -29,8 +49,14 @@ pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> {
tracing::info!("Dashboard server listening on {addr}");
let router = axum::Router::new()
.route("/auth", get(auth_login))
.route("/auth/callback", get(auth_callback))
.route("/logout", get(logout))
.serve_dioxus_application(ServeConfig::new(), app)
.layer(axum::Extension(server_state));
.layer(Extension(PendingOAuthStore::default()))
.layer(Extension(server_state))
.layer(middleware::from_fn(require_auth))
.layer(session);
axum::serve(listener, router.into_make_service())
.await