Skip to content

Commit 91e2203

Browse files
committed
Add support for nested username attribute in DefaultOAuth2User
Closes gh-14186 Signed-off-by: ahmd-nabil <[email protected]>
1 parent 63e726e commit 91e2203

File tree

10 files changed

+301
-53
lines changed

10 files changed

+301
-53
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.client.jackson2;
18+
19+
import java.io.IOException;
20+
import java.util.Collection;
21+
import java.util.Map;
22+
23+
import com.fasterxml.jackson.core.JacksonException;
24+
import com.fasterxml.jackson.core.JsonParser;
25+
import com.fasterxml.jackson.databind.DeserializationContext;
26+
import com.fasterxml.jackson.databind.JsonDeserializer;
27+
import com.fasterxml.jackson.databind.JsonNode;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
30+
import org.springframework.security.core.GrantedAuthority;
31+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
32+
33+
/**
34+
* A JsonDeserializer for {@link DefaultOAuth2User}.
35+
*
36+
* @author Ahmed Nabil
37+
* @since 6.3
38+
* @see DefaultOAuth2User
39+
* @see DefaultOAuth2UserMixin
40+
*/
41+
public class DefaultOAuth2UserDeserializer extends JsonDeserializer<DefaultOAuth2User> {
42+
43+
@Override
44+
public DefaultOAuth2User deserialize(JsonParser parser, DeserializationContext context)
45+
throws IOException, JacksonException {
46+
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
47+
JsonNode defaultOAuth2UserNode = mapper.readTree(parser);
48+
Collection<? extends GrantedAuthority> authorities = JsonNodeUtils.findValue(defaultOAuth2UserNode,
49+
"authorities", JsonNodeUtils.GRANTED_AUTHORITY_COLLECTION, mapper);
50+
Map<String, Object> attributes = JsonNodeUtils.findValue(defaultOAuth2UserNode, "attributes",
51+
JsonNodeUtils.STRING_OBJECT_MAP, mapper);
52+
String name = JsonNodeUtils.findStringValue(defaultOAuth2UserNode, "name");
53+
if (name != null) {
54+
return new DefaultOAuth2User(attributes, authorities, name);
55+
}
56+
String nameAttributeKey = JsonNodeUtils.findStringValue(defaultOAuth2UserNode, "nameAttributeKey");
57+
return new DefaultOAuth2User(authorities, attributes, nameAttributeKey);
58+
}
59+
60+
}

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOAuth2UserMixin.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@
2424
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2525
import com.fasterxml.jackson.annotation.JsonProperty;
2626
import com.fasterxml.jackson.annotation.JsonTypeInfo;
27+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2728

2829
import org.springframework.security.core.GrantedAuthority;
2930
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
@@ -37,6 +38,7 @@
3738
* @see OAuth2ClientJackson2Module
3839
*/
3940
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
41+
@JsonDeserialize(using = DefaultOAuth2UserDeserializer.class)
4042
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
4143
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
4244
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -48,4 +50,10 @@ abstract class DefaultOAuth2UserMixin {
4850
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
4951
}
5052

53+
@JsonCreator
54+
DefaultOAuth2UserMixin(@JsonProperty("attributes") Map<String, Object> attributes,
55+
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
56+
@JsonProperty("name") String name) {
57+
}
58+
5159
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.client.jackson2;
18+
19+
import java.io.IOException;
20+
import java.util.Collection;
21+
22+
import com.fasterxml.jackson.core.JacksonException;
23+
import com.fasterxml.jackson.core.JsonParser;
24+
import com.fasterxml.jackson.databind.DeserializationContext;
25+
import com.fasterxml.jackson.databind.JsonDeserializer;
26+
import com.fasterxml.jackson.databind.JsonNode;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
29+
import org.springframework.security.core.GrantedAuthority;
30+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
31+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
32+
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
33+
34+
/**
35+
* A JsonDeserializer for {@link DefaultOidcUser}.
36+
*
37+
* @author Ahmed Nabil
38+
* @since 6.3
39+
* @see DefaultOidcUser
40+
* @see DefaultOidcUserMixin
41+
*/
42+
public class DefaultOidcUserDeserializer extends JsonDeserializer<DefaultOidcUser> {
43+
44+
@Override
45+
public DefaultOidcUser deserialize(JsonParser parser, DeserializationContext context)
46+
throws IOException, JacksonException {
47+
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
48+
JsonNode defaultOidcUserNode = mapper.readTree(parser);
49+
Collection<? extends GrantedAuthority> authorities = JsonNodeUtils.findValue(defaultOidcUserNode, "authorities",
50+
JsonNodeUtils.GRANTED_AUTHORITY_COLLECTION, mapper);
51+
OidcIdToken idToken = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "idToken", OidcIdToken.class, mapper);
52+
OidcUserInfo userInfo = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "userInfo", OidcUserInfo.class,
53+
mapper);
54+
String nameAttributeKey = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "nameAttributeKey", String.class,
55+
mapper);
56+
String name = JsonNodeUtils.findValueByPath(defaultOidcUserNode, "name", String.class, mapper);
57+
return (name != null) ? new DefaultOidcUser(idToken, userInfo, authorities, name)
58+
: new DefaultOidcUser(authorities, idToken, userInfo, nameAttributeKey);
59+
}
60+
61+
}

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/DefaultOidcUserMixin.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2424
import com.fasterxml.jackson.annotation.JsonProperty;
2525
import com.fasterxml.jackson.annotation.JsonTypeInfo;
26+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2627

2728
import org.springframework.security.core.GrantedAuthority;
2829
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
@@ -38,6 +39,7 @@
3839
* @see OAuth2ClientJackson2Module
3940
*/
4041
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
42+
@JsonDeserialize(using = DefaultOidcUserDeserializer.class)
4143
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
4244
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
4345
@JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true)
@@ -49,4 +51,10 @@ abstract class DefaultOidcUserMixin {
4951
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
5052
}
5153

54+
@JsonCreator
55+
DefaultOidcUserMixin(@JsonProperty("idToken") OidcIdToken idToken, @JsonProperty("userInfo") OidcUserInfo userInfo,
56+
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
57+
@JsonProperty("name") String name) {
58+
}
59+
5260
}

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/JsonNodeUtils.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,16 @@
1616

1717
package org.springframework.security.oauth2.client.jackson2;
1818

19+
import java.util.Collection;
1920
import java.util.Map;
2021
import java.util.Set;
2122

2223
import com.fasterxml.jackson.core.type.TypeReference;
2324
import com.fasterxml.jackson.databind.JsonNode;
2425
import com.fasterxml.jackson.databind.ObjectMapper;
2526

27+
import org.springframework.security.core.GrantedAuthority;
28+
2629
/**
2730
* Utility class for {@code JsonNode}.
2831
*
@@ -37,6 +40,9 @@ abstract class JsonNodeUtils {
3740
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
3841
};
3942

43+
static final TypeReference<Collection<? extends GrantedAuthority>> GRANTED_AUTHORITY_COLLECTION = new TypeReference<Collection<? extends GrantedAuthority>>() {
44+
};
45+
4046
static String findStringValue(JsonNode jsonNode, String fieldName) {
4147
if (jsonNode == null) {
4248
return null;
@@ -62,4 +68,12 @@ static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
6268
return (value != null && value.isObject()) ? value : null;
6369
}
6470

71+
static <T> T findValueByPath(JsonNode jsonNode, String path, Class<T> type, ObjectMapper mapper) {
72+
if (jsonNode == null) {
73+
return null;
74+
}
75+
JsonNode value = jsonNode.path(path);
76+
return (value != null && !value.isMissingNode()) ? mapper.convertValue(value, type) : null;
77+
}
78+
6579
}

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java

+55-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,12 +16,16 @@
1616

1717
package org.springframework.security.oauth2.client.userinfo;
1818

19+
import java.util.Collection;
1920
import java.util.LinkedHashSet;
2021
import java.util.Map;
21-
import java.util.Set;
2222

23+
import org.springframework.context.expression.MapAccessor;
2324
import org.springframework.core.ParameterizedTypeReference;
2425
import org.springframework.core.convert.converter.Converter;
26+
import org.springframework.expression.Expression;
27+
import org.springframework.expression.spel.standard.SpelExpressionParser;
28+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
2529
import org.springframework.http.RequestEntity;
2630
import org.springframework.http.ResponseEntity;
2731
import org.springframework.security.core.GrantedAuthority;
@@ -76,6 +80,8 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
7680

7781
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
7882

83+
private final SpelExpressionParser parser = new SpelExpressionParser();
84+
7985
private RestOperations restOperations;
8086

8187
public DefaultOAuth2UserService() {
@@ -87,35 +93,14 @@ public DefaultOAuth2UserService() {
8793
@Override
8894
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
8995
Assert.notNull(userRequest, "userRequest cannot be null");
90-
if (!StringUtils
91-
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
92-
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
93-
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
94-
+ userRequest.getClientRegistration().getRegistrationId(),
95-
null);
96-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
97-
}
98-
String userNameAttributeName = userRequest.getClientRegistration()
99-
.getProviderDetails()
100-
.getUserInfoEndpoint()
101-
.getUserNameAttributeName();
102-
if (!StringUtils.hasText(userNameAttributeName)) {
103-
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
104-
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
105-
+ userRequest.getClientRegistration().getRegistrationId(),
106-
null);
107-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
108-
}
96+
String userNameAttributeName = getUserNameAttributeName(userRequest);
10997
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
11098
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
111-
Map<String, Object> userAttributes = response.getBody();
112-
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
113-
authorities.add(new OAuth2UserAuthority(userAttributes));
11499
OAuth2AccessToken token = userRequest.getAccessToken();
115-
for (String authority : token.getScopes()) {
116-
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
117-
}
118-
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
100+
Map<String, Object> attributes = response.getBody();
101+
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes);
102+
String name = getName(attributes, userNameAttributeName);
103+
return new DefaultOAuth2User(attributes, authorities, name);
119104
}
120105

121106
private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
@@ -157,6 +142,48 @@ private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRe
157142
}
158143
}
159144

145+
private String getUserNameAttributeName(OAuth2UserRequest userRequest) {
146+
if (!StringUtils
147+
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
148+
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
149+
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
150+
+ userRequest.getClientRegistration().getRegistrationId(),
151+
null);
152+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
153+
}
154+
String userNameAttributeName = userRequest.getClientRegistration()
155+
.getProviderDetails()
156+
.getUserInfoEndpoint()
157+
.getUserNameAttributeName();
158+
if (!StringUtils.hasText(userNameAttributeName)) {
159+
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
160+
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
161+
+ userRequest.getClientRegistration().getRegistrationId(),
162+
null);
163+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
164+
}
165+
return userNameAttributeName;
166+
}
167+
168+
private Collection<GrantedAuthority> getAuthorities(OAuth2AccessToken token, Map<String, Object> attributes) {
169+
Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
170+
authorities.add(new OAuth2UserAuthority(attributes));
171+
for (String authority : token.getScopes()) {
172+
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
173+
}
174+
return authorities;
175+
}
176+
177+
private String getName(Map<String, Object> attributes, String userNameAttributeName) {
178+
Assert.notEmpty(attributes, "attributes cannot be empty");
179+
Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty");
180+
SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
181+
.withRootObject(attributes)
182+
.build();
183+
Expression expression = this.parser.parseExpression(userNameAttributeName);
184+
return expression.getValue(context, String.class);
185+
}
186+
160187
/**
161188
* Sets the {@link Converter} used for converting the {@link OAuth2UserRequest} to a
162189
* {@link RequestEntity} representation of the UserInfo Request.

Diff for: oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthenticationTokenMixinTests.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,7 +36,6 @@
3636
import org.springframework.security.jackson2.SecurityJackson2Modules;
3737
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
3838
import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens;
39-
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
4039
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
4140
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
4241
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
@@ -194,7 +193,7 @@ private static String asJson(DefaultOAuth2User oauth2User) {
194193
" \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" +
195194
" \"username\": \"user\"\n" +
196195
" },\n" +
197-
" \"nameAttributeKey\": \"username\"\n" +
196+
" \"name\": \"user\"\n" +
198197
" }";
199198
// @formatter:on
200199
}
@@ -206,7 +205,7 @@ private static String asJson(DefaultOidcUser oidcUser) {
206205
" \"authorities\": " + asJson(oidcUser.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" +
207206
" \"idToken\": " + asJson(oidcUser.getIdToken()) + ",\n" +
208207
" \"userInfo\": " + asJson(oidcUser.getUserInfo()) + ",\n" +
209-
" \"nameAttributeKey\": \"" + IdTokenClaimNames.SUB + "\"\n" +
208+
" \"name\": \"" + oidcUser.getName() + "\"\n" +
210209
" }";
211210
// @formatter:on
212211
}

0 commit comments

Comments
 (0)