From 448a0c1b08d6d27660805ca23191e10d7ccba033 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 5 Jul 2026 11:02:35 +0200 Subject: [PATCH] OIDC directory: Fetch `name` and `group` claims from userinfo endpoint when missing from the JWT token --- CHANGELOG.md | 6 ++-- crates/directory/src/backend/oidc/lookup.rs | 39 +++++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 308d9bf6..1dac9690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,15 +13,17 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Fixed - DANE: Treat DNSSEC `bogus` as a temporary failures to prevent downgrade attacks. -- OIDC: +- OIDC provider: - `ECDSA` private key support for `SEC1` format. - Allow ports in `redirect_uri` for loopback addresses. +- OIDC directory: + - Removing a user from all groups does not sync the changes correctly. + - Fetch `name` and `group` claims from userinfo endpoint when missing from the JWT token. - PostgreSQL: Include error chain in error messages. - Prometheus: event counters are exported with incorrect metric names. - Registry: Changing the type of an existing account from `user` to `group` panics. - Masked emails: Return `UnknownRecipient` only for disabled or expired masked emails. - IDN: `sanitize_email` rejects valid Punycode domains. -- Directory: Removing a user from all groups does not sync the changes correctly. - Auto-ban: IP block expiration ignores per-reason ban durations. ## [0.16.11] - 2026-06-25 diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index f221f586..211e6f10 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -14,6 +14,7 @@ use jsonwebtoken::{ jwk::{self, JwkSet}, }; use reqwest::Client; +use serde_json::Value; use std::time::Instant; use std::{sync::Arc, time::Duration}; use trc::AuthEvent; @@ -77,12 +78,38 @@ impl OpenIdDirectory { self.validate_scopes(&token_data.claims)?; - let (email, claims) = if let Ok(email) = self.resolve_email(&token_data.claims) - { - (email, token_data.claims) - } else { - let claims = self.fetch_userinfo(token).await?; - (self.resolve_email(&claims)?, claims) + let mut claims = token_data.claims; + let jwt_email = self.resolve_email(&claims).ok(); + let missing_profile = self + .config + .claim_name + .as_ref() + .is_some_and(|claim| claims.get(claim).is_none()) + || self + .config + .claim_groups + .as_ref() + .is_some_and(|claim| claims.get(claim).is_none()); + + if jwt_email.is_none() || missing_profile { + match self.fetch_userinfo(token).await { + Ok(userinfo) => { + if let (Some(base), Value::Object(extra)) = + (claims.as_object_mut(), userinfo) + { + for (key, value) in extra { + base.entry(key).or_insert_with(|| value); + } + } + } + Err(err) if jwt_email.is_none() => return Err(err), + Err(_) => {} + } + } + + let email = match jwt_email { + Some(email) => email, + None => self.resolve_email(&claims)?, }; return self.build_account(email, &claims); }