Skip to content

Commit d9d22c7

Browse files
ahmd-nabiljzheaux
authored andcommitted
Add support for nested username attribute in DefaultOAuth2User
Closes gh-14186 Signed-off-by: ahmd-nabil <ahm3dnabil99@gmail.com>
1 parent 93c2d1c commit d9d22c7

File tree

3 files changed

+118
-4
lines changed

3 files changed

+118
-4
lines changed

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

Lines changed: 31 additions & 2 deletions
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-2024 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.
@@ -76,6 +76,9 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
7676

7777
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
7878

79+
private Converter<OAuth2UserRequest, Converter<Map<String, Object>, Map<String, Object>>> attributesConverter = (
80+
request) -> (attributes) -> attributes;
81+
7982
private RestOperations restOperations;
8083

8184
public DefaultOAuth2UserService() {
@@ -108,7 +111,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
108111
}
109112
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
110113
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
111-
Map<String, Object> userAttributes = response.getBody();
114+
Map<String, Object> userAttributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
112115
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
113116
authorities.add(new OAuth2UserAuthority(userAttributes));
114117
OAuth2AccessToken token = userRequest.getAccessToken();
@@ -118,6 +121,32 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
118121
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
119122
}
120123

124+
/**
125+
* Use this strategy to adapt user attributes into a format understood by Spring
126+
* Security; by default, the original attributes are preserved.
127+
*
128+
* <p>
129+
* This can be helpful, for example, if the user attribute is nested. Since Spring
130+
* Security needs the username attribute to be at the top level, you can use this
131+
* method to do:
132+
*
133+
* <pre>
134+
* DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
135+
* userService.setAttributesConverter((userRequest) -> (attributes) ->
136+
* Map&lt;String, Object&gt; userObject = (Map&lt;String, Object&gt;) attributes.get("user");
137+
* attributes.put("user-name", userObject.get("user-name"));
138+
* return attributes;
139+
* });
140+
* </pre>
141+
* @param attributesConverter the attribute adaptation strategy to use
142+
* @since 6.3
143+
*/
144+
public void setAttributesConverter(
145+
Converter<OAuth2UserRequest, Converter<Map<String, Object>, Map<String, Object>>> attributesConverter) {
146+
Assert.notNull(attributesConverter, "attributesConverter cannot be null");
147+
this.attributesConverter = attributesConverter;
148+
}
149+
121150
private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRequest, RequestEntity<?> request) {
122151
try {
123152
return this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserServiceTests.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -52,6 +52,8 @@
5252
import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
5353
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
5454
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
55+
import org.springframework.security.oauth2.core.user.OAuth2User;
56+
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
5557

5658
import static org.assertj.core.api.Assertions.assertThat;
5759
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -492,6 +494,49 @@ public void loadUserWhenTokenDoesNotContainScopesAndUserInfoUriThenUserInfoReque
492494
assertThat(user.getUserInfo()).isNotNull();
493495
}
494496

497+
@Test
498+
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() {
499+
// @formatter:off
500+
String userInfoResponse = "{\n"
501+
+ " \"user\": {\"user-name\": \"user1\"},\n"
502+
+ " \"sub\" : \"subject1\",\n"
503+
+ " \"first-name\": \"first\",\n"
504+
+ " \"last-name\": \"last\",\n"
505+
+ " \"middle-name\": \"middle\",\n"
506+
+ " \"address\": \"address\",\n"
507+
+ " \"email\": \"user1@example.com\"\n"
508+
+ "}\n";
509+
// @formatter:on
510+
this.server.enqueue(jsonResponse(userInfoResponse));
511+
String userInfoUri = this.server.url("/user").toString();
512+
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
513+
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
514+
.userNameAttributeName("user-name")
515+
.build();
516+
OidcUserService userService = new OidcUserService();
517+
DefaultOAuth2UserService oAuth2UserService = new DefaultOAuth2UserService();
518+
oAuth2UserService.setAttributesConverter((request) -> (attributes) -> {
519+
Map<String, Object> user = (Map<String, Object>) attributes.get("user");
520+
attributes.put("user-name", user.get("user-name"));
521+
return attributes;
522+
});
523+
userService.setOauth2UserService(oAuth2UserService);
524+
OAuth2User user = userService.loadUser(new OidcUserRequest(clientRegistration, this.accessToken, this.idToken));
525+
assertThat(user.getName()).isEqualTo("user1");
526+
assertThat(user.getAttributes()).hasSize(9);
527+
assertThat(((Map<?, ?>) user.getAttribute("user")).get("user-name")).isEqualTo("user1");
528+
assertThat((String) user.getAttribute("first-name")).isEqualTo("first");
529+
assertThat((String) user.getAttribute("last-name")).isEqualTo("last");
530+
assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle");
531+
assertThat((String) user.getAttribute("address")).isEqualTo("address");
532+
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
533+
assertThat(user.getAuthorities()).hasSize(3);
534+
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
535+
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
536+
assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER");
537+
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
538+
}
539+
495540
private MockResponse jsonResponse(String json) {
496541
// @formatter:off
497542
return new MockResponse()

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserServiceTests.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -158,6 +158,46 @@ public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
158158
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
159159
}
160160

161+
@Test
162+
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() {
163+
// @formatter:off
164+
String userInfoResponse = "{\n"
165+
+ " \"user\": {\"user-name\": \"user1\"},\n"
166+
+ " \"first-name\": \"first\",\n"
167+
+ " \"last-name\": \"last\",\n"
168+
+ " \"middle-name\": \"middle\",\n"
169+
+ " \"address\": \"address\",\n"
170+
+ " \"email\": \"user1@example.com\"\n"
171+
+ "}\n";
172+
// @formatter:on
173+
this.server.enqueue(jsonResponse(userInfoResponse));
174+
String userInfoUri = this.server.url("/user").toString();
175+
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri(userInfoUri)
176+
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
177+
.userNameAttributeName("user-name")
178+
.build();
179+
DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
180+
userService.setAttributesConverter((request) -> (attributes) -> {
181+
Map<String, Object> user = (Map<String, Object>) attributes.get("user");
182+
attributes.put("user-name", user.get("user-name"));
183+
return attributes;
184+
});
185+
OAuth2User user = userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken));
186+
assertThat(user.getName()).isEqualTo("user1");
187+
assertThat(user.getAttributes()).hasSize(7);
188+
assertThat(((Map<?, ?>) user.getAttribute("user")).get("user-name")).isEqualTo("user1");
189+
assertThat((String) user.getAttribute("first-name")).isEqualTo("first");
190+
assertThat((String) user.getAttribute("last-name")).isEqualTo("last");
191+
assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle");
192+
assertThat((String) user.getAttribute("address")).isEqualTo("address");
193+
assertThat((String) user.getAttribute("email")).isEqualTo("user1@example.com");
194+
assertThat(user.getAuthorities()).hasSize(1);
195+
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
196+
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
197+
assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER");
198+
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
199+
}
200+
161201
@Test
162202
public void loadUserWhenUserInfoSuccessResponseInvalidThenThrowOAuth2AuthenticationException() {
163203
// @formatter:off

0 commit comments

Comments
 (0)