refactor(oidc): migrate provider into issuer

this make OIDC compliant with the invariant binding (issuer and subject)
admin can now rename their provider without breaking

clarifing federation_kind: report the kind of federation wired not the allowed login method
hybryd login method are still allowed
This commit is contained in:
Edouard Vanbelle
2026-08-08 15:24:06 +02:00
parent 10a8dd7d8b
commit d8b3f2e026
15 changed files with 390 additions and 61 deletions
@@ -1294,6 +1294,7 @@ pub async fn oidc_providers(
if !auth_app.oidc_enabled() {
return Ok(Json(OidcProviderInfoDto {
enabled: false,
issuer: String::new(),
provider_name: String::new(),
authorize_endpoint: String::new(),
password_login_enabled,
@@ -1305,8 +1306,31 @@ pub async fn oidc_providers(
let config = auth_app.oidc_config().unwrap();
// Prefer the DISCOVERY document's issuer — that's what
// OidcService uses to validate id_tokens AND what lands on
// `auth.users.federation_issuer` at JIT provisioning / lazy
// rebind. The config's `issuer_url` is only what the operator
// typed to point at discovery; the discovery document publishes
// the authoritative value (may differ by trailing slash, host
// casing, etc). **Cache-only lookup on purpose** — this
// endpoint is PUBLIC + UNAUTHENTICATED, so triggering an IdP
// HTTP fetch per request is a DoS amplifier (attacker at N req/s
// → we hit the IdP at N req/s, and the cache only stores on
// success so a degraded IdP means every call retries). On cold
// cache (before the first real OIDC flow warms it), fall back to
// the operator-typed `config.issuer_url`. In practice the cache
// is warm within seconds of the first login; the fallback only
// shows during that window and is only wrong if the IdP
// publishes an issuer that differs from the URL used to fetch
// discovery (rare in normal deployments).
let issuer = auth_app
.oidc_service()
.and_then(|svc| svc.cached_issuer())
.unwrap_or_else(|| config.issuer_url.clone());
Ok(Json(OidcProviderInfoDto {
enabled: true,
issuer,
provider_name: config.provider_name.clone(),
authorize_endpoint: "/api/auth/oidc/authorize".to_string(),
password_login_enabled,
+8 -2
View File
@@ -213,8 +213,14 @@ async fn user_provisioning_response(
vec!["users"]
};
// Determine backend based on auth provider
let backend = if user_dto.auth_provider.to_lowercase().contains("oidc") {
// Determine backend based on federation kind. Historically checked
// `auth_provider.to_lowercase().contains("oidc")` which happened to
// work when the DTO field held a display label containing "oidc"
// (e.g. "OIDC-Google") — but broke silently when the label was
// "MockSSO" or, post Phase B of the federation-identity rename, when
// the field became an issuer URL that doesn't contain "oidc". The
// kind field is the load-bearing signal.
let backend = if user_dto.federation_kind.as_deref() == Some("oidc") {
"OIDC"
} else {
"Database"