Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ private void configureOidcAuth(
.authorizedClientRepository(new HttpSessionOAuth2AuthorizedClientRepository())
.successHandler(successHandler)
.failureUrl("/login.html?error")
.userInfoEndpoint(userInfo ->
userInfo.oidcUserService(buildOidcUserService(roleMappings, groupsClaim)));
.userInfoEndpoint(userInfo -> userInfo.oidcUserService(
buildOidcUserService(roleMappings, groupsClaim, oidcCfg.isSkipUserInfo())));

if (usePrivateKeyJwt) {
RSAKey rsaKey =
Expand Down Expand Up @@ -501,8 +501,9 @@ private void provisionIdpUser(Authentication auth) {
* {@code roleMappings} is non-empty, access is <em>deny-by-default</em>: the user must belong to at least one
* mapped group, otherwise authentication is rejected.
*/
private OidcUserService buildOidcUserService(Map<String, List<String>> roleMappings, String groupsClaim) {
return new OidcUserService() {
private OidcUserService buildOidcUserService(
Map<String, List<String>> roleMappings, String groupsClaim, boolean skipUserInfo) {
OidcUserService service = new OidcUserService() {
@Override
public OidcUser loadUser(OidcUserRequest userRequest) {
OidcUser oidcUser = super.loadUser(userRequest);
Expand Down Expand Up @@ -539,6 +540,14 @@ public OidcUser loadUser(OidcUserRequest userRequest) {
authorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), nameAttributeKey);
}
};
if (skipUserInfo) {
// skip-user-info=true: all claims are read from the ID token; the UserInfo endpoint is
// never contacted. Required for Entra ID — graph.microsoft.com/oidc/userinfo returns 200
// but omits preferred_username, causing DefaultOAuth2UserService to throw before the ID
// token merge. All required claims are present in the Entra v2.0 ID token with profile scope.
service.setRetrieveUserInfo(req -> false);
}
return service;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,10 @@ public class OidcAuthConfig {
private String tokenUri = "";

/**
* Override for the UserInfo endpoint URL. When blank, the value from OIDC discovery is used.
* Override for the UserInfo endpoint URL. When blank, the value from OIDC discovery is used. Rarely needed — only
* set this when the provider's discovered UserInfo endpoint is unreachable or must be substituted.
*
* <p><b>Required for Entra ID</b> — OIDC discovery for Entra points to {@code graph.microsoft.com/oidc/userinfo},
* which does not return {@code preferred_username}. Set this to
* {@code https://login.microsoftonline.com/{tenant}/v2.0/userinfo} to get the full claim set.
*
* <p>Example: {@code https://login.microsoftonline.com/{tenant}/v2.0/userinfo}
* <p>For Entra ID, prefer setting {@code skip-user-info: true} instead of overriding this URL.
*/
private String userInfoUri = "";

Expand Down Expand Up @@ -108,6 +105,21 @@ public class OidcAuthConfig {
*/
private String certPath = "";

/**
* Skip the UserInfo endpoint call entirely. When {@code true}, all claims are read from the ID token and the
* UserInfo endpoint is never contacted.
*
* <p><b>Required for Entra ID</b> — Entra's discovered UserInfo endpoint
* ({@code graph.microsoft.com/oidc/userinfo}) returns HTTP 200 but omits {@code preferred_username}. This is a
* Microsoft design constraint present on all Entra tenants; it is not a tenant configuration issue. With the
* {@code profile} scope, the Entra v2.0 ID token contains all required claims ({@code preferred_username},
* {@code groups}, {@code email}).
*
* <p>Set to {@code false} (the default) for standard OIDC providers (Keycloak, Okta, Dex) that return all required
* claims from their UserInfo endpoints.
*/
private boolean skipUserInfo = false;

/**
* Explicit {@code kid} (key ID) to include in the {@code private_key_jwt} assertion header. Use this when the IdP
* matches the client assertion against a registered JWKS by {@code kid} — Keycloak, Okta, Auth0, and Dex all work
Expand Down
Loading