From 99aee99b3457a1ecebc05d543fb417a0b0008d11 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 6 May 2024 12:32:04 +0200 Subject: [PATCH] Expose user name attribute name in `OAuth2UserAuthority` --- .../oidc/userinfo/OidcUserRequestUtils.java | 11 ++++-- .../userinfo/DefaultOAuth2UserService.java | 7 ++-- .../DefaultReactiveOAuth2UserService.java | 2 +- .../OAuth2AuthenticationTokenMixinTests.java | 2 ++ .../OidcReactiveOAuth2UserServiceTests.java | 2 ++ .../oidc/userinfo/OidcUserServiceTests.java | 1 + .../DefaultOAuth2UserServiceTests.java | 2 ++ ...DefaultReactiveOAuth2UserServiceTests.java | 2 ++ .../core/oidc/user/OidcUserAuthority.java | 30 +++++++++++++++- .../oauth2/core/user/OAuth2UserAuthority.java | 35 +++++++++++++++++++ .../oauth2/core/user/TestOAuth2Users.java | 7 ++-- .../server/SecurityMockServerConfigurers.java | 2 +- .../SecurityMockMvcRequestPostProcessors.java | 2 +- 13 files changed, 92 insertions(+), 13 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java index 5f39b8d961..01c73f9449 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserRequestUtils.java @@ -78,13 +78,18 @@ final class OidcUserRequestUtils { static OidcUser getUser(OidcUserRequest userRequest, OidcUserInfo userInfo) { Set authorities = new LinkedHashSet<>(); - authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo)); + ClientRegistration.ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails(); + String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName(); + if (StringUtils.hasLength(userNameAttributeName)) { + authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo, userNameAttributeName)); + } + else { + authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo)); + } OAuth2AccessToken token = userRequest.getAccessToken(); for (String scope : token.getScopes()) { authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope)); } - ClientRegistration.ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails(); - String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName(); if (StringUtils.hasText(userNameAttributeName)) { return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java index 95336084b7..3045d22914 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java @@ -95,7 +95,7 @@ public class DefaultOAuth2UserService implements OAuth2UserService> response = getResponse(userRequest, request); OAuth2AccessToken token = userRequest.getAccessToken(); Map attributes = this.attributesConverter.convert(userRequest).convert(response.getBody()); - Collection authorities = getAuthorities(token, attributes); + Collection authorities = getAuthorities(token, attributes, userNameAttributeName); return new DefaultOAuth2User(authorities, attributes, userNameAttributeName); } @@ -187,9 +187,10 @@ public class DefaultOAuth2UserService implements OAuth2UserService getAuthorities(OAuth2AccessToken token, Map attributes) { + private Collection getAuthorities(OAuth2AccessToken token, Map attributes, + String userNameAttributeName) { Collection authorities = new LinkedHashSet<>(); - authorities.add(new OAuth2UserAuthority(attributes)); + authorities.add(new OAuth2UserAuthority(attributes, userNameAttributeName)); for (String authority : token.getScopes()) { authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority)); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java index 920119baab..54a39a9a26 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserService.java @@ -130,7 +130,7 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi .bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP) .mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes)); return userAttributes.map((attrs) -> { - GrantedAuthority authority = new OAuth2UserAuthority(attrs); + GrantedAuthority authority = new OAuth2UserAuthority(attrs, userNameAttributeName); Set authorities = new HashSet<>(); authorities.add(authority); OAuth2AccessToken token = userRequest.getAccessToken(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java index e4c76b69db..aa14f020d1 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java @@ -247,6 +247,7 @@ public class OAuth2AuthenticationTokenMixinTests { return "{\n" + " \"@class\": \"org.springframework.security.oauth2.core.user.OAuth2UserAuthority\",\n" + " \"authority\": \"" + oauth2UserAuthority.getAuthority() + "\",\n" + + " \"userNameAttributeName\": \"username\",\n" + " \"attributes\": {\n" + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + " \"username\": \"user\"\n" + @@ -260,6 +261,7 @@ public class OAuth2AuthenticationTokenMixinTests { return "{\n" + " \"@class\": \"org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority\",\n" + " \"authority\": \"" + oidcUserAuthority.getAuthority() + "\",\n" + + " \"userNameAttributeName\": \"" + oidcUserAuthority.getUserNameAttributeName() + "\",\n" + " \"idToken\": " + asJson(oidcUserAuthority.getIdToken()) + ",\n" + " \"userInfo\": " + asJson(oidcUserAuthority.getUserInfo()) + "\n" + " }"; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java index ade191c6da..172a7b3bac 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcReactiveOAuth2UserServiceTests.java @@ -313,6 +313,7 @@ public class OidcReactiveOAuth2UserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("id"); } @Test @@ -361,6 +362,7 @@ public class OidcReactiveOAuth2UserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name"); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java index f9982c8226..baa574fa1e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java @@ -616,6 +616,7 @@ public class OidcUserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name"); } private MockResponse jsonResponse(String json) { diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java index 99457d4e3e..e210019f48 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java @@ -156,6 +156,7 @@ public class DefaultOAuth2UserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name"); } @Test @@ -196,6 +197,7 @@ public class DefaultOAuth2UserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name"); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java index 68aa1a31d3..a8614842c6 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultReactiveOAuth2UserServiceTests.java @@ -144,6 +144,7 @@ public class DefaultReactiveOAuth2UserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("id"); } // gh-9336 @@ -203,6 +204,7 @@ public class DefaultReactiveOAuth2UserServiceTests { OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER"); assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("user-name"); } // gh-5500 diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java index 31920772c7..84aeb34d08 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/OidcUserAuthority.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; @@ -57,6 +58,19 @@ public class OidcUserAuthority extends OAuth2UserAuthority { this("OIDC_USER", idToken, userInfo); } + /** + * Constructs a {@code OidcUserAuthority} using the provided parameters and defaults + * {@link #getAuthority()} to {@code OIDC_USER}. + * @param idToken the {@link OidcIdToken ID Token} containing claims about the user + * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, + * may be {@code null} + * @param userNameAttributeName the attribute name used to access the user's name from + * the attributes + */ + public OidcUserAuthority(OidcIdToken idToken, OidcUserInfo userInfo, String userNameAttributeName) { + this("OIDC_USER", idToken, userInfo, userNameAttributeName); + } + /** * Constructs a {@code OidcUserAuthority} using the provided parameters. * @param authority the authority granted to the user @@ -65,7 +79,21 @@ public class OidcUserAuthority extends OAuth2UserAuthority { * may be {@code null} */ public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo) { - super(authority, collectClaims(idToken, userInfo)); + this(authority, idToken, userInfo, IdTokenClaimNames.SUB); + } + + /** + * Constructs a {@code OidcUserAuthority} using the provided parameters. + * @param authority the authority granted to the user + * @param idToken the {@link OidcIdToken ID Token} containing claims about the user + * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, + * may be {@code null} + * @param userNameAttributeName the attribute name used to access the user's name from + * the attributes + */ + public OidcUserAuthority(String authority, OidcIdToken idToken, OidcUserInfo userInfo, + String userNameAttributeName) { + super(authority, collectClaims(idToken, userInfo), userNameAttributeName); this.idToken = idToken; this.userInfo = userInfo; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java index 0182a72e31..1624978838 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/OAuth2UserAuthority.java @@ -22,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import org.springframework.lang.Nullable; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.Assert; @@ -41,6 +42,8 @@ public class OAuth2UserAuthority implements GrantedAuthority { private final Map attributes; + private final String userNameAttributeName; + /** * Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults * {@link #getAuthority()} to {@code OAUTH2_USER}. @@ -50,16 +53,39 @@ public class OAuth2UserAuthority implements GrantedAuthority { this("OAUTH2_USER", attributes); } + /** + * Constructs a {@code OAuth2UserAuthority} using the provided parameters and defaults + * {@link #getAuthority()} to {@code OAUTH2_USER}. + * @param attributes the attributes about the user + * @param userNameAttributeName the attribute name used to access the user's name from + * the attributes + */ + public OAuth2UserAuthority(Map attributes, @Nullable String userNameAttributeName) { + this("OAUTH2_USER", attributes, userNameAttributeName); + } + /** * Constructs a {@code OAuth2UserAuthority} using the provided parameters. * @param authority the authority granted to the user * @param attributes the attributes about the user */ public OAuth2UserAuthority(String authority, Map attributes) { + this(authority, attributes, null); + } + + /** + * Constructs a {@code OAuth2UserAuthority} using the provided parameters. + * @param authority the authority granted to the user + * @param attributes the attributes about the user + * @param userNameAttributeName the attribute name used to access the user's name from + * the attributes + */ + public OAuth2UserAuthority(String authority, Map attributes, String userNameAttributeName) { Assert.hasText(authority, "authority cannot be empty"); Assert.notEmpty(attributes, "attributes cannot be empty"); this.authority = authority; this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); + this.userNameAttributeName = userNameAttributeName; } @Override @@ -75,6 +101,15 @@ public class OAuth2UserAuthority implements GrantedAuthority { return this.attributes; } + /** + * Returns the attribute name used to access the user's name from the attributes. + * @return the attribute name used to access the user's name from the attributes + */ + @Nullable + public String getUserNameAttributeName() { + return this.userNameAttributeName; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java index 8c50d677b5..f55e78ef27 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/user/TestOAuth2Users.java @@ -37,12 +37,13 @@ public final class TestOAuth2Users { String nameAttributeKey = "username"; Map attributes = new HashMap<>(); attributes.put(nameAttributeKey, "user"); - Collection authorities = authorities(attributes); + Collection authorities = authorities(attributes, nameAttributeKey); return new DefaultOAuth2User(authorities, attributes, nameAttributeKey); } - private static Collection authorities(Map attributes) { - return new LinkedHashSet<>(Arrays.asList(new OAuth2UserAuthority(attributes), + private static Collection authorities(Map attributes, + String userNameAttributeName) { + return new LinkedHashSet<>(Arrays.asList(new OAuth2UserAuthority(attributes, userNameAttributeName), new SimpleGrantedAuthority("SCOPE_read"), new SimpleGrantedAuthority("SCOPE_write"))); } diff --git a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java index 433a769aef..ee3a28ea4c 100644 --- a/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java +++ b/test/src/main/java/org/springframework/security/test/web/reactive/server/SecurityMockServerConfigurers.java @@ -834,7 +834,7 @@ public final class SecurityMockServerConfigurers { private Collection defaultAuthorities() { Set authorities = new LinkedHashSet<>(); - authorities.add(new OAuth2UserAuthority(this.attributes.get())); + authorities.add(new OAuth2UserAuthority(this.attributes.get(), this.nameAttributeKey)); for (String authority : this.accessToken.getScopes()) { authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority)); } diff --git a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java index 7361045902..aa18388113 100644 --- a/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java +++ b/test/src/main/java/org/springframework/security/test/web/servlet/request/SecurityMockMvcRequestPostProcessors.java @@ -1376,7 +1376,7 @@ public final class SecurityMockMvcRequestPostProcessors { private Collection defaultAuthorities() { Set authorities = new LinkedHashSet<>(); - authorities.add(new OAuth2UserAuthority(this.attributes.get())); + authorities.add(new OAuth2UserAuthority(this.attributes.get(), this.nameAttributeKey)); for (String authority : this.accessToken.getScopes()) { authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority)); }