From 00493dc4e408d7637df2f07b5449307425059b93 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 18 Jan 2024 17:02:49 -0700 Subject: [PATCH] Polish Custom Username Location - Exposes lookup strategy programmatically instead of relying on SpEL expressions. The reason for this is that it's impossible to tell what the user intends by a key like since that is also a value JSON key name by itself. --- .../DefaultOAuth2UserDeserializer.java | 60 ------------------ .../jackson2/DefaultOAuth2UserMixin.java | 10 +-- .../jackson2/DefaultOidcUserDeserializer.java | 61 ------------------- .../client/jackson2/DefaultOidcUserMixin.java | 10 +-- .../oauth2/client/jackson2/JsonNodeUtils.java | 16 +---- .../userinfo/DefaultOAuth2UserService.java | 50 +++++++++------ .../OAuth2AuthenticationTokenMixinTests.java | 7 ++- .../oidc/userinfo/OidcUserServiceTests.java | 47 +++++++++++++- .../DefaultOAuth2UserServiceTests.java | 36 ++++++----- .../core/oidc/user/DefaultOidcUser.java | 18 +----- .../oauth2/core/user/DefaultOAuth2User.java | 31 ++-------- 11 files changed, 111 insertions(+), 235 deletions(-) delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserDeserializer.java delete mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserDeserializer.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserDeserializer.java deleted file mode 100644 index 9d654c6e902..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserDeserializer.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.client.jackson2; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; - -/** - * A JsonDeserializer for {@link DefaultOAuth2User}. - * - * @author Ahmed Nabil - * @since 6.3 - * @see DefaultOAuth2User - * @see DefaultOAuth2UserMixin - */ -public class DefaultOAuth2UserDeserializer extends JsonDeserializer { - - @Override - public DefaultOAuth2User deserialize(JsonParser parser, DeserializationContext context) - throws IOException, JacksonException { - ObjectMapper mapper = (ObjectMapper) parser.getCodec(); - JsonNode defaultOAuth2UserNode = mapper.readTree(parser); - Collection authorities = JsonNodeUtils.findValue(defaultOAuth2UserNode, - "authorities", JsonNodeUtils.GRANTED_AUTHORITY_COLLECTION, mapper); - Map attributes = JsonNodeUtils.findValue(defaultOAuth2UserNode, "attributes", - JsonNodeUtils.STRING_OBJECT_MAP, mapper); - String name = JsonNodeUtils.findStringValue(defaultOAuth2UserNode, "name"); - if (name != null) { - return new DefaultOAuth2User(attributes, authorities, name); - } - String nameAttributeKey = JsonNodeUtils.findStringValue(defaultOAuth2UserNode, "nameAttributeKey"); - return new DefaultOAuth2User(authorities, attributes, nameAttributeKey); - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java index c3f852e91ba..917062c905c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; @@ -38,7 +37,6 @@ * @see OAuth2ClientJackson2Module */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) -@JsonDeserialize(using = DefaultOAuth2UserDeserializer.class) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) @JsonIgnoreProperties(ignoreUnknown = true) @@ -50,10 +48,4 @@ abstract class DefaultOAuth2UserMixin { @JsonProperty("nameAttributeKey") String nameAttributeKey) { } - @JsonCreator - DefaultOAuth2UserMixin(@JsonProperty("attributes") Map attributes, - @JsonProperty("authorities") Collection authorities, - @JsonProperty("name") String name) { - } - } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserDeserializer.java deleted file mode 100644 index c0267ae9a4f..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserDeserializer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2002-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.oauth2.client.jackson2; - -import java.io.IOException; -import java.util.Collection; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.OidcUserInfo; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; - -/** - * A JsonDeserializer for {@link DefaultOidcUser}. - * - * @author Ahmed Nabil - * @since 6.3 - * @see DefaultOidcUser - * @see DefaultOidcUserMixin - */ -public class DefaultOidcUserDeserializer extends JsonDeserializer { - - @Override - public DefaultOidcUser deserialize(JsonParser parser, DeserializationContext context) - throws IOException, JacksonException { - ObjectMapper mapper = (ObjectMapper) parser.getCodec(); - JsonNode defaultOidcUserNode = mapper.readTree(parser); - Collection authorities = JsonNodeUtils.findValue(defaultOidcUserNode, "authorities", - JsonNodeUtils.GRANTED_AUTHORITY_COLLECTION, mapper); - OidcIdToken idToken = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "idToken", OidcIdToken.class, mapper); - OidcUserInfo userInfo = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "userInfo", OidcUserInfo.class, - mapper); - String nameAttributeKey = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "nameAttributeKey", String.class, - mapper); - String name = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "name", String.class, mapper); - return (name != null) ? new DefaultOidcUser(idToken, userInfo, authorities, name) - : new DefaultOidcUser(authorities, idToken, userInfo, nameAttributeKey); - } - -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java index 796003a404f..5b46dc9396f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.oidc.OidcIdToken; @@ -39,7 +38,6 @@ * @see OAuth2ClientJackson2Module */ @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) -@JsonDeserialize(using = DefaultOidcUserDeserializer.class) @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) @JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true) @@ -51,10 +49,4 @@ abstract class DefaultOidcUserMixin { @JsonProperty("nameAttributeKey") String nameAttributeKey) { } - @JsonCreator - DefaultOidcUserMixin(@JsonProperty("idToken") OidcIdToken idToken, @JsonProperty("userInfo") OidcUserInfo userInfo, - @JsonProperty("authorities") Collection authorities, - @JsonProperty("name") String name) { - } - } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java index 0608f291be3..351295efe61 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.security.oauth2.client.jackson2; -import java.util.Collection; import java.util.Map; import java.util.Set; @@ -24,8 +23,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.security.core.GrantedAuthority; - /** * Utility class for {@code JsonNode}. * @@ -40,9 +37,6 @@ abstract class JsonNodeUtils { static final TypeReference> STRING_OBJECT_MAP = new TypeReference>() { }; - static final TypeReference> GRANTED_AUTHORITY_COLLECTION = new TypeReference>() { - }; - static String findStringValue(JsonNode jsonNode, String fieldName) { if (jsonNode == null) { return null; @@ -68,12 +62,4 @@ static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { return (value != null && value.isObject()) ? value : null; } - static T findValueByPath(JsonNode jsonNode, String path, Class type, ObjectMapper mapper) { - if (jsonNode == null) { - return null; - } - JsonNode value = jsonNode.path(path); - return (value != null && !value.isMissingNode()) ? mapper.convertValue(value, type) : null; - } - } 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 7972ac17497..95336084b7b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,8 @@ import java.util.LinkedHashSet; import java.util.Map; -import org.springframework.context.expression.MapAccessor; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.security.core.GrantedAuthority; @@ -80,7 +76,8 @@ public class DefaultOAuth2UserService implements OAuth2UserService> requestEntityConverter = new OAuth2UserRequestEntityConverter(); - private final SpelExpressionParser parser = new SpelExpressionParser(); + private Converter, Map>> attributesConverter = ( + request) -> (attributes) -> attributes; private RestOperations restOperations; @@ -97,10 +94,35 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic RequestEntity request = this.requestEntityConverter.convert(userRequest); ResponseEntity> response = getResponse(userRequest, request); OAuth2AccessToken token = userRequest.getAccessToken(); - Map attributes = response.getBody(); + Map attributes = this.attributesConverter.convert(userRequest).convert(response.getBody()); Collection authorities = getAuthorities(token, attributes); - String name = getName(attributes, userNameAttributeName); - return new DefaultOAuth2User(attributes, authorities, name); + return new DefaultOAuth2User(authorities, attributes, userNameAttributeName); + } + + /** + * Use this strategy to adapt user attributes into a format understood by Spring + * Security; by default, the original attributes are preserved. + * + *

+ * This can be helpful, for example, if the user attribute is nested. Since Spring + * Security needs the username attribute to be at the top level, you can use this + * method to do: + * + *

+	 *     DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
+	 *     userService.setAttributesConverter((userRequest) -> (attributes) ->
+	 *         Map<String, Object> userObject = (Map<String, Object>) attributes.get("user");
+	 *         attributes.put("user-name", userObject.get("user-name"));
+	 *         return attributes;
+	 *     });
+	 * 
+ * @param attributesConverter the attribute adaptation strategy to use + * @since 6.3 + */ + public void setAttributesConverter( + Converter, Map>> attributesConverter) { + Assert.notNull(attributesConverter, "attributesConverter cannot be null"); + this.attributesConverter = attributesConverter; } private ResponseEntity> getResponse(OAuth2UserRequest userRequest, RequestEntity request) { @@ -174,16 +196,6 @@ private Collection getAuthorities(OAuth2AccessToken token, Map return authorities; } - private String getName(Map attributes, String userNameAttributeName) { - Assert.notEmpty(attributes, "attributes cannot be empty"); - Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty"); - SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor()) - .withRootObject(attributes) - .build(); - Expression expression = this.parser.parseExpression(userNameAttributeName); - return expression.getValue(context, String.class); - } - /** * Sets the {@link Converter} used for converting the {@link OAuth2UserRequest} to a * {@link RequestEntity} representation of the UserInfo Request. 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 92523e6a88c..e4c76b69db8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens; +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.oidc.StandardClaimNames; @@ -193,7 +194,7 @@ private static String asJson(DefaultOAuth2User oauth2User) { " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + " \"username\": \"user\"\n" + " },\n" + - " \"name\": \"user\"\n" + + " \"nameAttributeKey\": \"username\"\n" + " }"; // @formatter:on } @@ -205,7 +206,7 @@ private static String asJson(DefaultOidcUser oidcUser) { " \"authorities\": " + asJson(oidcUser.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" + " \"idToken\": " + asJson(oidcUser.getIdToken()) + ",\n" + " \"userInfo\": " + asJson(oidcUser.getUserInfo()) + ",\n" + - " \"name\": \"" + oidcUser.getName() + "\"\n" + + " \"nameAttributeKey\": \"" + IdTokenClaimNames.SUB + "\"\n" + " }"; // @formatter:on } 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 310667e2fff..6d63e8a5c3f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,8 @@ import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -492,6 +494,49 @@ public void loadUserWhenTokenDoesNotContainScopesAndUserInfoUriThenUserInfoReque assertThat(user.getUserInfo()).isNotNull(); } + @Test + public void loadUserWhenNestedUserInfoSuccessThenReturnUser() { + // @formatter:off + String userInfoResponse = "{\n" + + " \"user\": {\"user-name\": \"user1\"},\n" + + " \"sub\" : \"subject1\",\n" + + " \"first-name\": \"first\",\n" + + " \"last-name\": \"last\",\n" + + " \"middle-name\": \"middle\",\n" + + " \"address\": \"address\",\n" + + " \"email\": \"user1@example.com\"\n" + + "}\n"; + // @formatter:on + this.server.enqueue(jsonResponse(userInfoResponse)); + String userInfoUri = this.server.url("/user").toString(); + ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) + .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) + .userNameAttributeName("user-name") + .build(); + OidcUserService userService = new OidcUserService(); + DefaultOAuth2UserService oAuth2UserService = new DefaultOAuth2UserService(); + oAuth2UserService.setAttributesConverter((request) -> (attributes) -> { + Map user = (Map) attributes.get("user"); + attributes.put("user-name", user.get("user-name")); + return attributes; + }); + userService.setOauth2UserService(oAuth2UserService); + OAuth2User user = userService.loadUser(new OidcUserRequest(clientRegistration, this.accessToken, this.idToken)); + assertThat(user.getName()).isEqualTo("user1"); + assertThat(user.getAttributes()).hasSize(9); + assertThat(((Map) user.getAttribute("user")).get("user-name")).isEqualTo("user1"); + assertThat((String) user.getAttribute("first-name")).isEqualTo("first"); + assertThat((String) user.getAttribute("last-name")).isEqualTo("last"); + assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle"); + assertThat((String) user.getAttribute("address")).isEqualTo("address"); + assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com"); + assertThat(user.getAuthorities()).hasSize(3); + assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class); + OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next(); + assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER"); + assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes()); + } + private MockResponse jsonResponse(String json) { // @formatter:off return new MockResponse() 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 12c5bd636f5..e7a04c8db9d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -140,7 +140,7 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() { String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); assertThat(user.getName()).isEqualTo("user1"); @@ -174,11 +174,17 @@ public void loadUserWhenNestedUserInfoSuccessThenReturnUser() { String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("user['user-name']") + .userNameAttributeName("user-name") .build(); - OAuth2User user = this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); + DefaultOAuth2UserService userService = new DefaultOAuth2UserService(); + userService.setAttributesConverter((request) -> (attributes) -> { + Map user = (Map) attributes.get("user"); + attributes.put("user-name", user.get("user-name")); + return attributes; + }); + OAuth2User user = userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); assertThat(user.getName()).isEqualTo("user1"); - assertThat(user.getAttributes()).hasSize(6); + assertThat(user.getAttributes()).hasSize(7); assertThat(((Map) user.getAttribute("user")).get("user-name")).isEqualTo("user1"); assertThat((String) user.getAttribute("first-name")).isEqualTo("first"); assertThat((String) user.getAttribute("last-name")).isEqualTo("last"); @@ -196,7 +202,7 @@ public void loadUserWhenNestedUserInfoSuccessThenReturnUser() { public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() { // @formatter:off String userInfoResponse = "{\n" - + " \"username\": \"user1\",\n" + + " \"user-name\": \"user1\",\n" + " \"first-name\": \"first\",\n" + " \"last-name\": \"last\",\n" + " \"middle-name\": \"middle\",\n" @@ -208,7 +214,7 @@ public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2Authenticat String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))) @@ -226,7 +232,7 @@ public void loadUserWhenUserInfoErrorResponseWwwAuthenticateHeaderThenThrowOAuth String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))) @@ -246,7 +252,7 @@ public void loadUserWhenUserInfoErrorResponseThenThrowOAuth2AuthenticationExcept String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))) @@ -261,7 +267,7 @@ public void loadUserWhenServerErrorThenThrowOAuth2AuthenticationException() { String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))) @@ -274,7 +280,7 @@ public void loadUserWhenUserInfoUriInvalidThenThrowOAuth2AuthenticationException String userInfoUri = "https://invalid-provider.com/user"; ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))) @@ -299,7 +305,7 @@ public void loadUserWhenUserInfoSuccessResponseThenAcceptHeaderJson() throws Exc String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); assertThat(this.server.takeRequest(1, TimeUnit.SECONDS).getHeader(HttpHeaders.ACCEPT)) @@ -323,7 +329,7 @@ public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodG String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); RecordedRequest request = this.server.takeRequest(); @@ -350,7 +356,7 @@ public void loadUserWhenAuthenticationMethodFormSuccessResponseThenHttpMethodPos String userInfoUri = this.server.url("/user").toString(); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.FORM) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)); RecordedRequest request = this.server.takeRequest(); @@ -398,7 +404,7 @@ public void loadUserWhenUserInfoSuccessResponseInvalidContentTypeThenThrowOAuth2 this.server.enqueue(response); ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri) .userInfoAuthenticationMethod(AuthenticationMethod.HEADER) - .userNameAttributeName("['user-name']") + .userNameAttributeName("user-name") .build(); assertThatExceptionOfType(OAuth2AuthenticationException.class) .isThrownBy(() -> this.userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken))) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java index efbaf26164b..2266fcf0e1c 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/user/DefaultOidcUser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,8 +87,6 @@ public DefaultOidcUser(Collection authorities, OidcI * may be {@code null} * @param nameAttributeKey the key used to access the user's "name" from * {@link #getAttributes()} - * @deprecated Use - * {@link #DefaultOidcUser(OidcIdToken, OidcUserInfo, Collection, String)} instead */ public DefaultOidcUser(Collection authorities, OidcIdToken idToken, OidcUserInfo userInfo, String nameAttributeKey) { @@ -97,20 +95,6 @@ public DefaultOidcUser(Collection authorities, OidcI this.userInfo = userInfo; } - /** - * Constructs a {@code DefaultOidcUser} using the provided parameters. - * @param idToken the {@link OidcIdToken ID Token} containing claims about the user - * @param userInfo the {@link OidcUserInfo UserInfo} containing claims about the user, - * @param authorities the authorities granted to the user may be {@code null} - * @param name the user's "name" from {@link #getAttributes()} - */ - public DefaultOidcUser(OidcIdToken idToken, OidcUserInfo userInfo, - Collection authorities, String name) { - super(OidcUserAuthority.collectClaims(idToken, userInfo), authorities, name); - this.idToken = idToken; - this.userInfo = userInfo; - } - @Override public Map getClaims() { return this.getAttributes(); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java index f76ea32cbe5..a8ad76cd00f 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/user/DefaultOAuth2User.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public class DefaultOAuth2User implements OAuth2User, Serializable { private final Map attributes; - private final String name; + private final String nameAttributeKey; /** * Constructs a {@code DefaultOAuth2User} using the provided parameters. @@ -63,9 +63,6 @@ public class DefaultOAuth2User implements OAuth2User, Serializable { * @param attributes the attributes about the user * @param nameAttributeKey the key used to access the user's "name" from * {@link #getAttributes()} - * @deprecated Use - * {@link #DefaultOAuth2User(Map attributes, Collection authorities, String name)} - * instead */ public DefaultOAuth2User(Collection authorities, Map attributes, String nameAttributeKey) { @@ -78,30 +75,12 @@ public DefaultOAuth2User(Collection authorities, Map ? Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities))) : Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES)); this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); - this.name = getAttribute(nameAttributeKey); - } - - /** - * Constructs a {@code DefaultOAuth2User} using the provided parameters. - * @param attributes the attributes about the user - * @param authorities the authorities granted to the user - * @param name user's "name" from {@link #getAttributes()} - * @since 6.3 - */ - public DefaultOAuth2User(Map attributes, Collection authorities, - String name) { - Assert.notEmpty(attributes, "attributes cannot be empty"); - Assert.hasText(name, "name cannot be empty"); - this.authorities = (authorities != null) - ? Collections.unmodifiableSet(new LinkedHashSet<>(this.sortAuthorities(authorities))) - : Collections.unmodifiableSet(new LinkedHashSet<>(AuthorityUtils.NO_AUTHORITIES)); - this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); - this.name = name; + this.nameAttributeKey = nameAttributeKey; } @Override public String getName() { - return this.name; + return this.getAttribute(this.nameAttributeKey).toString(); } @Override @@ -151,7 +130,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Name: ["); - sb.append(getName()); + sb.append(this.getName()); sb.append("], Granted Authorities: ["); sb.append(getAuthorities()); sb.append("], User Attributes: [");