Skip to content

Commit 2894826

Browse files
committed
Add OIDC authentication and observability support
Introduce OIDC authentication flows and observability into the codebase. Added OpenID Connect support (openidconnect crate and related kalamdb-auth oidc client/device/http modules, models, and services), CLI and UI helpers/tests for OIDC flows (browser/device), and server-side login flow checks (disable local login when configured). Added a new kalamdb-observability crate with query/storage/system metrics and integrated it into the API and server. Updated Cargo.toml and Cargo.lock to include new dependencies and added tests, docs, and helper scripts for the new auth and observability functionality.
1 parent 4c1f075 commit 2894826

182 files changed

Lines changed: 11676 additions & 3948 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 422 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ ulid = "1.1"
135135

136136
# JWT authentication
137137
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["aws_lc_rs"] }
138+
openidconnect = { version = "4.0.1", default-features = false, features = ["reqwest", "rustls-tls"] }
138139

139140
# Configuration
140141
toml = "1.1.2"

backend/crates/kalamdb-api/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ kalamdb-configs = { path = "../kalamdb-configs" }
1515
kalamdb-core = { path = "../kalamdb-core" }
1616
kalamdb-jobs = { path = "../kalamdb-jobs" }
1717
kalamdb-live = { path = "../kalamdb-live" }
18+
kalamdb-observability = { path = "../kalamdb-observability" }
1819
kalamdb-raft = { path = "../kalamdb-raft" }
1920
kalamdb-sql = { package = "kalamdb-dialect", path = "../kalamdb-dialect" }
2021
kalamdb-auth = { path = "../kalamdb-auth" }
@@ -56,6 +57,7 @@ tracing = { workspace = true }
5657
tokio = { workspace = true }
5758
futures-util = { workspace = true }
5859
reqwest = { workspace = true }
60+
openidconnect = { workspace = true }
5961

6062
# Base64 encoding/decoding
6163
base64 = { workspace = true }

backend/crates/kalamdb-api/src/http/auth/login.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ pub async fn login_handler(
4848
));
4949
}
5050

51+
if !config.local.enabled {
52+
return HttpResponse::Forbidden().json(AuthErrorResponse::new(
53+
"local_auth_disabled",
54+
"Local username/password login is disabled. Use the configured OIDC login method.",
55+
));
56+
}
57+
5158
// Authenticate using unified auth flow (includes localhost/empty password rules)
5259
let auth_request = AuthRequest::Credentials {
5360
user: body.user.clone(),
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::sync::Arc;
2+
3+
use actix_web::{web, HttpResponse};
4+
use kalamdb_auth::providers::jwt_config;
5+
use kalamdb_core::app_context::AppContext;
6+
7+
use super::models::{
8+
AuthLoginOptionsResponse, LocalLoginOptions, OidcDeviceFlowOptions, OidcLoginOptions,
9+
};
10+
11+
pub async fn login_options_handler(app_context: web::Data<Arc<AppContext>>) -> HttpResponse {
12+
let config = app_context.config();
13+
let oidc = if config.auth.oidc.enabled {
14+
match (config.auth.oidc.issuer_str(), config.auth.oidc.client_id_str()) {
15+
(Some(issuer), Some(client_id)) => {
16+
let public_metadata =
17+
match jwt_config::get_jwt_config().oidc_public_metadata(issuer).await {
18+
Ok(metadata) => Some(metadata),
19+
Err(error) => {
20+
log::warn!("OIDC login-options metadata discovery failed: {}", error);
21+
None
22+
},
23+
};
24+
let device_authorization_endpoint = public_metadata
25+
.as_ref()
26+
.and_then(|metadata| metadata.device_authorization_endpoint.clone())
27+
.or_else(|| config.auth.oidc.device_authorization_endpoint.clone());
28+
29+
Some(OidcLoginOptions {
30+
enabled: true,
31+
display_name: config.auth.oidc.display_name.clone(),
32+
issuer: issuer.to_string(),
33+
client_id: client_id.to_string(),
34+
authorization_endpoint: public_metadata
35+
.as_ref()
36+
.and_then(|metadata| metadata.authorization_endpoint.clone()),
37+
token_endpoint: public_metadata
38+
.as_ref()
39+
.and_then(|metadata| metadata.token_endpoint.clone()),
40+
device_authorization_endpoint: device_authorization_endpoint.clone(),
41+
scopes: public_metadata
42+
.as_ref()
43+
.map(|metadata| metadata.scopes.clone())
44+
.unwrap_or_else(|| config.auth.oidc.scopes.clone()),
45+
device_flow: Some(OidcDeviceFlowOptions {
46+
direct_supported: device_authorization_endpoint.is_some(),
47+
broker_supported: config.auth.oidc.broker_device_flow_enabled,
48+
device_authorization_endpoint,
49+
broker_start_endpoint: Some("/v1/api/auth/oidc/device/start".to_string()),
50+
broker_poll_endpoint: Some("/v1/api/auth/oidc/device/poll".to_string()),
51+
}),
52+
})
53+
},
54+
_ => None,
55+
}
56+
} else {
57+
None
58+
};
59+
60+
HttpResponse::Ok().json(AuthLoginOptionsResponse {
61+
local: LocalLoginOptions {
62+
enabled: config.auth.local.enabled,
63+
},
64+
oidc,
65+
})
66+
}

backend/crates/kalamdb-api/src/http/auth/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,23 @@ pub mod models;
1414

1515
mod audit;
1616
mod login;
17+
mod login_options;
1718
mod logout;
1819
mod me;
19-
mod oauth;
20+
mod oidc_device;
21+
mod oidc_exchange;
2022
mod refresh;
2123
mod setup;
2224

2325
use actix_web::HttpResponse;
2426
use kalamdb_auth::AuthError;
2527
pub(crate) use login::login_handler;
28+
pub(crate) use login_options::login_options_handler;
2629
pub(crate) use logout::logout_handler;
2730
pub(crate) use me::me_handler;
2831
use models::AuthErrorResponse;
29-
pub(crate) use oauth::oauth_providers_handler;
32+
pub(crate) use oidc_device::{oidc_device_poll_handler, oidc_device_start_handler};
33+
pub(crate) use oidc_exchange::{oidc_code_exchange_handler, oidc_token_exchange_handler};
3034
pub(crate) use refresh::refresh_handler;
3135
pub(crate) use setup::{server_setup_handler, setup_status_handler};
3236

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4+
pub struct AuthLoginOptionsResponse {
5+
pub local: LocalLoginOptions,
6+
#[serde(skip_serializing_if = "Option::is_none")]
7+
pub oidc: Option<OidcLoginOptions>,
8+
}
9+
10+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11+
pub struct LocalLoginOptions {
12+
pub enabled: bool,
13+
}
14+
15+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16+
pub struct OidcLoginOptions {
17+
pub enabled: bool,
18+
pub display_name: String,
19+
pub issuer: String,
20+
pub client_id: String,
21+
#[serde(skip_serializing_if = "Option::is_none")]
22+
pub authorization_endpoint: Option<String>,
23+
#[serde(skip_serializing_if = "Option::is_none")]
24+
pub token_endpoint: Option<String>,
25+
#[serde(skip_serializing_if = "Option::is_none")]
26+
pub device_authorization_endpoint: Option<String>,
27+
pub scopes: Vec<String>,
28+
#[serde(skip_serializing_if = "Option::is_none")]
29+
pub device_flow: Option<OidcDeviceFlowOptions>,
30+
}
31+
32+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33+
pub struct OidcDeviceFlowOptions {
34+
pub direct_supported: bool,
35+
pub broker_supported: bool,
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
pub device_authorization_endpoint: Option<String>,
38+
#[serde(skip_serializing_if = "Option::is_none")]
39+
pub broker_start_endpoint: Option<String>,
40+
#[serde(skip_serializing_if = "Option::is_none")]
41+
pub broker_poll_endpoint: Option<String>,
42+
}

backend/crates/kalamdb-api/src/http/auth/models/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@
33
//! This module contains type-safe models for all authentication endpoints.
44
55
mod error_response;
6+
mod login_options;
67
mod login_response;
8+
mod oidc_device;
9+
mod oidc_exchange;
710
mod setup_request;
811
mod setup_response;
912
mod user_info;
1013

1114
pub use error_response::AuthErrorResponse;
1215
pub use kalamdb_auth::LoginRequest;
16+
pub use login_options::{
17+
AuthLoginOptionsResponse, LocalLoginOptions, OidcDeviceFlowOptions, OidcLoginOptions,
18+
};
1319
pub use login_response::LoginResponse;
20+
pub use oidc_device::{
21+
OidcDevicePollRequest, OidcDevicePollResponse, OidcDevicePollStatus, OidcDeviceStartRequest,
22+
OidcDeviceStartResponse,
23+
};
24+
pub use oidc_exchange::{OidcCodeExchangeRequest, OidcTokenExchangeRequest};
1425
pub use setup_request::ServerSetupRequest;
1526
pub use setup_response::ServerSetupResponse;
1627
pub use user_info::{CurrentUserResponse, UserInfo};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
use super::UserInfo;
4+
5+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6+
pub struct OidcDeviceStartRequest {
7+
#[serde(default)]
8+
pub scopes: Vec<String>,
9+
}
10+
11+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12+
pub struct OidcDeviceStartResponse {
13+
pub device_session_id: String,
14+
pub verification_uri: String,
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub verification_uri_complete: Option<String>,
17+
pub user_code: String,
18+
pub expires_in_seconds: u64,
19+
pub interval_seconds: u64,
20+
}
21+
22+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23+
pub struct OidcDevicePollRequest {
24+
#[serde(alias = "session_id")]
25+
pub device_session_id: String,
26+
}
27+
28+
#[derive(Debug, Serialize)]
29+
pub struct OidcDevicePollResponse {
30+
pub status: OidcDevicePollStatus,
31+
#[serde(skip_serializing_if = "Option::is_none")]
32+
pub interval_seconds: Option<u64>,
33+
#[serde(skip_serializing_if = "Option::is_none")]
34+
pub token_type: Option<String>,
35+
#[serde(skip_serializing_if = "Option::is_none")]
36+
pub access_token: Option<String>,
37+
#[serde(skip_serializing_if = "Option::is_none")]
38+
pub expires_at: Option<String>,
39+
#[serde(skip_serializing_if = "Option::is_none")]
40+
pub refresh_token: Option<String>,
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub refresh_expires_at: Option<String>,
43+
#[serde(skip_serializing_if = "Option::is_none")]
44+
pub user: Option<UserInfo>,
45+
#[serde(skip_serializing_if = "Option::is_none")]
46+
pub admin_ui_access: Option<bool>,
47+
#[serde(skip_serializing_if = "Option::is_none")]
48+
pub message: Option<String>,
49+
}
50+
51+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52+
#[serde(rename_all = "snake_case")]
53+
pub enum OidcDevicePollStatus {
54+
Pending,
55+
SlowDown,
56+
Authorized,
57+
Denied,
58+
Expired,
59+
Failed,
60+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4+
pub struct OidcCodeExchangeRequest {
5+
pub code: String,
6+
pub redirect_uri: String,
7+
pub code_verifier: String,
8+
}
9+
10+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11+
pub struct OidcTokenExchangeRequest {
12+
pub token: String,
13+
}

0 commit comments

Comments
 (0)