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

@@ -0,0 +1,56 @@
use super::error::DashboardError;
/// Keycloak OpenID Connect settings.
#[derive(Debug)]
pub struct KeycloakConfig {
pub url: String,
pub realm: String,
pub client_id: String,
pub redirect_uri: String,
pub app_url: String,
}
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 auth_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/auth",
self.url, self.realm
)
}
pub fn token_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/token",
self.url, self.realm
)
}
pub fn userinfo_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/userinfo",
self.url, self.realm
)
}
pub fn logout_endpoint(&self) -> String {
format!(
"{}/realms/{}/protocol/openid-connect/logout",
self.url, self.realm
)
}
}
fn required_env(name: &str) -> Result<String, DashboardError> {
std::env::var(name)
.map_err(|_| DashboardError::Config(format!("{name} is required but not set")))
}