Private
Public Access
1
0

feat: OIDC SSO provider support (Keycloak, Azure AD, custom)
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 4s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m11s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped

- Refactored azure_sso.rs to sso.rs with generic OIDC provider support
- Added OIDC discovery URL lookup with 1hr TTL caching
- Added PKCE for all providers, client_secret optional for public clients
- Added /api/v1/auth/sso/login and /api/v1/auth/sso/callback routes
- Added /api/v1/auth/azure/* backward-compatible routes
- Added POST /settings/sso/discover and POST /settings/sso/test endpoints
- Frontend: Provider dropdown (Keycloak/Azure AD/Custom OIDC)
- Frontend: Auto-fill discovery URL for Keycloak
- Frontend: Discover Endpoints and Test Connection buttons
- Frontend: Dynamic SSO button based on provider display name
- Made migration 014 idempotent with DO blocks and IF NOT EXISTS
- Fixed debian/install to use /usr/local/bin/ for binaries
- Fixed frontend file path in .deb package
- Reset admin password on dev server
- Fixed database permissions for oidc_config table
This commit is contained in:
2026-05-13 13:32:24 +00:00
parent e3d8569b05
commit 69d2e88bbd
14 changed files with 883 additions and 496 deletions

View File

@ -10,7 +10,7 @@ use pm_auth::{
rbac::{require_auth, AuthConfig},
};
use pm_core::{config::AppConfig, db, logging, request_id::request_id_middleware};
use routes::azure_sso::{JwksCache, SsoSession};
use routes::sso::{OidcCache, SsoSession};
use routes::ws::WsTicket;
use serde_json::{json, Value};
use std::{net::SocketAddr, sync::Arc, time::Duration};
@ -31,8 +31,8 @@ pub struct AppState {
pub ws_tickets: Arc<DashMap<String, WsTicket>>,
/// In-memory store for SSO PKCE sessions (state → code_verifier).
pub sso_sessions: Arc<DashMap<String, SsoSession>>,
/// Cached Azure AD JWKS for id_token signature verification.
pub jwks_cache: Arc<Mutex<JwksCache>>,
/// Cached OIDC discovery document and JWKS for SSO id_token verification.
pub oidc_cache: Arc<Mutex<OidcCache>>,
/// Internal certificate authority for mTLS client cert issuance.
pub ca: Arc<pm_ca::CertAuthority>,
}
@ -90,7 +90,7 @@ async fn main() -> anyhow::Result<()> {
let ws_tickets: Arc<DashMap<String, WsTicket>> = Arc::new(DashMap::new());
let sso_sessions: Arc<DashMap<String, SsoSession>> = Arc::new(DashMap::new());
let jwks_cache: Arc<Mutex<JwksCache>> = Arc::new(Mutex::new(JwksCache::default()));
let oidc_cache: Arc<Mutex<OidcCache>> = Arc::new(Mutex::new(OidcCache::default()));
// Background task: purge expired WS tickets every 30 seconds.
{
@ -137,7 +137,7 @@ async fn main() -> anyhow::Result<()> {
ws_tickets,
sso_sessions,
ca: Arc::new(ca),
jwks_cache,
oidc_cache,
};
let app = build_router(state);
@ -234,7 +234,7 @@ pub fn build_router(state: AppState) -> Router {
// Public auth routes (no JWT needed)
.nest("/api/v1/auth", routes::auth::public_router())
// Public Azure SSO routes (no JWT needed)
.nest("/api/v1/auth/azure", routes::azure_sso::public_router())
.nest("/api/v1/auth/azure", routes::sso::azure_compat_router())
// Protected API routes (JWT required)
.nest("/api/v1", protected_api)
// WebSocket browser endpoint — ticket-authenticated, outside JWT middleware