From 1bc0ce02d4d97b331ea5bd2608459278f3dac8c3 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 17 Apr 2026 11:17:12 -0400 Subject: [PATCH 1/3] fix(oidc): skip UserInfo endpoint call for Entra ID compatibility Entra's discovered UserInfo endpoint (graph.microsoft.com/oidc/userinfo) returns HTTP 200 but omits preferred_username, causing Spring Security's DefaultOAuth2UserService to throw before the ID token claims are merged. All required claims (preferred_username, groups) are present in the Entra v2.0 ID token when the profile scope is requested. Skip the UserInfo call entirely via setRetrieveUserInfo(req -> false). Co-Authored-By: Claude Sonnet 4.6 --- .../java/org/finos/gitproxy/dashboard/SecurityConfig.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java index 0cdc389..1459c80 100644 --- a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java +++ b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java @@ -502,7 +502,7 @@ private void provisionIdpUser(Authentication auth) { * mapped group, otherwise authentication is rejected. */ private OidcUserService buildOidcUserService(Map> roleMappings, String groupsClaim) { - return new OidcUserService() { + OidcUserService service = new OidcUserService() { @Override public OidcUser loadUser(OidcUserRequest userRequest) { OidcUser oidcUser = super.loadUser(userRequest); @@ -539,6 +539,11 @@ public OidcUser loadUser(OidcUserRequest userRequest) { authorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), nameAttributeKey); } }; + // graph.microsoft.com/oidc/userinfo (Entra's discovered UserInfo endpoint) 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 the profile scope. + service.setRetrieveUserInfo(req -> false); + return service; } /** From fbd0d603d8ca3737500f11e9a81df3d2d8bc52a6 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 17 Apr 2026 11:26:57 -0400 Subject: [PATCH 2/3] fix(oidc): make UserInfo skip configurable via skip-user-info flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the unconditional setRetrieveUserInfo(req -> false) with a per-registration config flag (auth.oidc.skip-user-info, default false). When true, all claims are read from the ID token and the UserInfo endpoint is never contacted. Required for Entra ID, whose discovered UserInfo endpoint (graph.microsoft.com/oidc/userinfo) returns HTTP 200 but omits preferred_username — a Microsoft design constraint present on all Entra tenants. Standard OIDC providers (Keycloak, Okta, Dex) are unaffected by default. Co-Authored-By: Claude Sonnet 4.6 --- .../gitproxy/dashboard/SecurityConfig.java | 16 ++++++++----- .../gitproxy/jetty/config/OidcAuthConfig.java | 23 ++++++++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java index 1459c80..4c2d411 100644 --- a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java +++ b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java @@ -395,7 +395,7 @@ private void configureOidcAuth( .successHandler(successHandler) .failureUrl("/login.html?error") .userInfoEndpoint(userInfo -> - userInfo.oidcUserService(buildOidcUserService(roleMappings, groupsClaim))); + userInfo.oidcUserService(buildOidcUserService(roleMappings, groupsClaim, oidcCfg.isSkipUserInfo()))); if (usePrivateKeyJwt) { RSAKey rsaKey = @@ -501,7 +501,8 @@ private void provisionIdpUser(Authentication auth) { * {@code roleMappings} is non-empty, access is deny-by-default: the user must belong to at least one * mapped group, otherwise authentication is rejected. */ - private OidcUserService buildOidcUserService(Map> roleMappings, String groupsClaim) { + private OidcUserService buildOidcUserService( + Map> roleMappings, String groupsClaim, boolean skipUserInfo) { OidcUserService service = new OidcUserService() { @Override public OidcUser loadUser(OidcUserRequest userRequest) { @@ -539,10 +540,13 @@ public OidcUser loadUser(OidcUserRequest userRequest) { authorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), nameAttributeKey); } }; - // graph.microsoft.com/oidc/userinfo (Entra's discovered UserInfo endpoint) 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 the profile scope. - service.setRetrieveUserInfo(req -> false); + 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; } diff --git a/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java b/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java index e328416..d97c20b 100644 --- a/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java +++ b/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java @@ -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. * - *

Required for Entra ID — 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. - * - *

Example: {@code https://login.microsoftonline.com/{tenant}/v2.0/userinfo} + *

For Entra ID, prefer setting {@code skip-user-info: true} instead of overriding this URL. */ private String userInfoUri = ""; @@ -108,6 +105,20 @@ 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. + * + *

Required for Entra ID — 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}). + * + *

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 From 3277c0126086a8c480b43db468b5d79ee5a04d3b Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 17 Apr 2026 11:31:49 -0400 Subject: [PATCH 3/3] style: apply spotless formatting Co-Authored-By: Claude Sonnet 4.6 --- .../finos/gitproxy/dashboard/SecurityConfig.java | 4 ++-- .../finos/gitproxy/jetty/config/OidcAuthConfig.java | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java index 4c2d411..277974e 100644 --- a/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java +++ b/git-proxy-java-dashboard/src/main/java/org/finos/gitproxy/dashboard/SecurityConfig.java @@ -394,8 +394,8 @@ private void configureOidcAuth( .authorizedClientRepository(new HttpSessionOAuth2AuthorizedClientRepository()) .successHandler(successHandler) .failureUrl("/login.html?error") - .userInfoEndpoint(userInfo -> - userInfo.oidcUserService(buildOidcUserService(roleMappings, groupsClaim, oidcCfg.isSkipUserInfo()))); + .userInfoEndpoint(userInfo -> userInfo.oidcUserService( + buildOidcUserService(roleMappings, groupsClaim, oidcCfg.isSkipUserInfo()))); if (usePrivateKeyJwt) { RSAKey rsaKey = diff --git a/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java b/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java index d97c20b..b3eee23 100644 --- a/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java +++ b/git-proxy-java-server/src/main/java/org/finos/gitproxy/jetty/config/OidcAuthConfig.java @@ -54,8 +54,8 @@ public class OidcAuthConfig { private String tokenUri = ""; /** - * 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. + * 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. * *

For Entra ID, prefer setting {@code skip-user-info: true} instead of overriding this URL. */ @@ -109,10 +109,11 @@ public class OidcAuthConfig { * Skip the UserInfo endpoint call entirely. When {@code true}, all claims are read from the ID token and the * UserInfo endpoint is never contacted. * - *

Required for Entra ID — 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}). + *

Required for Entra ID — 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}). * *

Set to {@code false} (the default) for standard OIDC providers (Keycloak, Okta, Dex) that return all required * claims from their UserInfo endpoints.