feat: add configurable userIdClaim for OIDC user identification
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m12s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled

The OIDC user login ID is now configurable via the admin OIDC setup
dialog (userIdClaim field). Supports dot-separated claim paths (e.g.
'email', 'preferred_username', 'custom.user_id'). Defaults to 'sub'
for backwards compatibility. Throws if the configured claim is missing
from the id_token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-06 10:18:03 +02:00
parent 549dbaa322
commit a96cf2afed
5 changed files with 22 additions and 9 deletions

View File

@@ -101,7 +101,8 @@ public class OidcConfigAdminController {
request.rolesClaim() != null ? request.rolesClaim() : "roles", request.rolesClaim() != null ? request.rolesClaim() : "roles",
request.defaultRoles() != null ? request.defaultRoles() : List.of("VIEWER"), request.defaultRoles() != null ? request.defaultRoles() : List.of("VIEWER"),
request.autoSignup(), request.autoSignup(),
request.displayNameClaim() != null ? request.displayNameClaim() : "name" request.displayNameClaim() != null ? request.displayNameClaim() : "name",
request.userIdClaim() != null ? request.userIdClaim() : "sub"
); );
configRepository.save(config); configRepository.save(config);

View File

@@ -13,5 +13,6 @@ public record OidcAdminConfigRequest(
String rolesClaim, String rolesClaim,
List<String> defaultRoles, List<String> defaultRoles,
boolean autoSignup, boolean autoSignup,
String displayNameClaim String displayNameClaim,
String userIdClaim
) {} ) {}

View File

@@ -16,17 +16,19 @@ public record OidcAdminConfigResponse(
String rolesClaim, String rolesClaim,
List<String> defaultRoles, List<String> defaultRoles,
boolean autoSignup, boolean autoSignup,
String displayNameClaim String displayNameClaim,
String userIdClaim
) { ) {
public static OidcAdminConfigResponse unconfigured() { public static OidcAdminConfigResponse unconfigured() {
return new OidcAdminConfigResponse(false, false, null, null, false, null, null, false, null); return new OidcAdminConfigResponse(false, false, null, null, false, null, null, false, null, null);
} }
public static OidcAdminConfigResponse from(OidcConfig config) { public static OidcAdminConfigResponse from(OidcConfig config) {
return new OidcAdminConfigResponse( return new OidcAdminConfigResponse(
true, config.enabled(), config.issuerUri(), config.clientId(), true, config.enabled(), config.issuerUri(), config.clientId(),
!config.clientSecret().isBlank(), config.rolesClaim(), !config.clientSecret().isBlank(), config.rolesClaim(),
config.defaultRoles(), config.autoSignup(), config.displayNameClaim() config.defaultRoles(), config.autoSignup(), config.displayNameClaim(),
config.userIdClaim()
); );
} }
} }

View File

@@ -104,13 +104,20 @@ public class OidcTokenExchanger {
JWTClaimsSet claims = getJwtProcessor(config.issuerUri()).process(idTokenStr, null); JWTClaimsSet claims = getJwtProcessor(config.issuerUri()).process(idTokenStr, null);
String subject = claims.getSubject(); String userIdClaim = config.userIdClaim() != null && !config.userIdClaim().isBlank()
? config.userIdClaim() : "sub";
String subject = "sub".equals(userIdClaim)
? claims.getSubject()
: extractStringClaim(claims, userIdClaim);
if (subject == null || subject.isBlank()) {
throw new IllegalStateException("OIDC id_token missing user ID claim: " + userIdClaim);
}
String email = claims.getStringClaim("email"); String email = claims.getStringClaim("email");
String name = extractStringClaim(claims, config.displayNameClaim()); String name = extractStringClaim(claims, config.displayNameClaim());
List<String> roles = extractRoles(claims, config.rolesClaim()); List<String> roles = extractRoles(claims, config.rolesClaim());
log.info("OIDC user authenticated: sub={}, email={}", subject, email); log.info("OIDC user authenticated: id={}, email={}", subject, email);
return new OidcUserInfo(subject, email != null ? email : "", name != null ? name : "", roles, idTokenStr); return new OidcUserInfo(subject, email != null ? email : "", name != null ? name : "", roles, idTokenStr);
} }

View File

@@ -13,6 +13,7 @@ import java.util.List;
* @param defaultRoles fallback roles for new users with no OIDC role claim * @param defaultRoles fallback roles for new users with no OIDC role claim
* @param autoSignup whether new OIDC users are automatically created on first login * @param autoSignup whether new OIDC users are automatically created on first login
* @param displayNameClaim dot-separated path to display name in the id_token (e.g. {@code name}, {@code preferred_username}) * @param displayNameClaim dot-separated path to display name in the id_token (e.g. {@code name}, {@code preferred_username})
* @param userIdClaim dot-separated path to user identifier in the id_token (default {@code sub}); e.g. {@code email}, {@code preferred_username}
*/ */
public record OidcConfig( public record OidcConfig(
boolean enabled, boolean enabled,
@@ -22,9 +23,10 @@ public record OidcConfig(
String rolesClaim, String rolesClaim,
List<String> defaultRoles, List<String> defaultRoles,
boolean autoSignup, boolean autoSignup,
String displayNameClaim String displayNameClaim,
String userIdClaim
) { ) {
public static OidcConfig disabled() { public static OidcConfig disabled() {
return new OidcConfig(false, "", "", "", "roles", List.of("VIEWER"), true, "name"); return new OidcConfig(false, "", "", "", "roles", List.of("VIEWER"), true, "name", "sub");
} }
} }