Skip to content

Commit d7599ab

Browse files
committed
Polish setAttributesConverter
- Add Tests - Add Reactive Support Issue gh-14186
1 parent 04f0f25 commit d7599ab

File tree

4 files changed

+148
-4
lines changed

4 files changed

+148
-4
lines changed

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

+33-2
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.
@@ -26,6 +26,7 @@
2626
import reactor.core.publisher.Mono;
2727

2828
import org.springframework.core.ParameterizedTypeReference;
29+
import org.springframework.core.convert.converter.Converter;
2930
import org.springframework.http.HttpHeaders;
3031
import org.springframework.http.HttpStatusCode;
3132
import org.springframework.http.MediaType;
@@ -78,6 +79,9 @@ public class DefaultReactiveOAuth2UserService implements ReactiveOAuth2UserServi
7879
private static final ParameterizedTypeReference<Map<String, String>> STRING_STRING_MAP = new ParameterizedTypeReference<Map<String, String>>() {
7980
};
8081

82+
private Converter<OAuth2UserRequest, Converter<Map<String, Object>, Map<String, Object>>> attributesConverter = (
83+
request) -> (attributes) -> attributes;
84+
8185
private WebClient webClient = WebClient.create();
8286

8387
@Override
@@ -123,7 +127,8 @@ public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2Aut
123127
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
124128
})
125129
)
126-
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP);
130+
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP)
131+
.mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes));
127132
return userAttributes.map((attrs) -> {
128133
GrantedAuthority authority = new OAuth2UserAuthority(attrs);
129134
Set<GrantedAuthority> authorities = new HashSet<>();
@@ -184,6 +189,32 @@ private WebClient.RequestHeadersSpec<?> getRequestHeaderSpec(OAuth2UserRequest u
184189
// @formatter:on
185190
}
186191

192+
/**
193+
* Use this strategy to adapt user attributes into a format understood by Spring
194+
* Security; by default, the original attributes are preserved.
195+
*
196+
* <p>
197+
* This can be helpful, for example, if the user attribute is nested. Since Spring
198+
* Security needs the username attribute to be at the top level, you can use this
199+
* method to do:
200+
*
201+
* <pre>
202+
* DefaultReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService();
203+
* userService.setAttributesConverter((userRequest) -> (attributes) ->
204+
* Map&lt;String, Object&gt; userObject = (Map&lt;String, Object&gt;) attributes.get("user");
205+
* attributes.put("user-name", userObject.get("user-name"));
206+
* return attributes;
207+
* });
208+
* </pre>
209+
* @param attributesConverter the attribute adaptation strategy to use
210+
* @since 6.3
211+
*/
212+
public void setAttributesConverter(
213+
Converter<OAuth2UserRequest, Converter<Map<String, Object>, Map<String, Object>>> attributesConverter) {
214+
Assert.notNull(attributesConverter, "attributesConverter cannot be null");
215+
this.attributesConverter = attributesConverter;
216+
}
217+
187218
/**
188219
* Sets the {@link WebClient} used for retrieving the user endpoint
189220
* @param webClient the client to use

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

+62-1
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.
@@ -16,6 +16,7 @@
1616

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

19+
import java.io.IOException;
1920
import java.time.Duration;
2021
import java.time.Instant;
2122
import java.util.Collections;
@@ -24,6 +25,8 @@
2425
import java.util.Map;
2526
import java.util.function.Function;
2627

28+
import okhttp3.mockwebserver.MockResponse;
29+
import okhttp3.mockwebserver.MockWebServer;
2730
import org.junit.jupiter.api.BeforeEach;
2831
import org.junit.jupiter.api.Test;
2932
import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,13 +35,17 @@
3235
import reactor.core.publisher.Mono;
3336

3437
import org.springframework.core.convert.converter.Converter;
38+
import org.springframework.http.HttpHeaders;
39+
import org.springframework.http.MediaType;
3540
import org.springframework.security.core.GrantedAuthority;
3641
import org.springframework.security.core.authority.AuthorityUtils;
3742
import org.springframework.security.core.authority.SimpleGrantedAuthority;
3843
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3944
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
45+
import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
4046
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
4147
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
48+
import org.springframework.security.oauth2.core.AuthenticationMethod;
4249
import org.springframework.security.oauth2.core.OAuth2AccessToken;
4350
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
4451
import org.springframework.security.oauth2.core.TestOAuth2AccessTokens;
@@ -203,8 +210,62 @@ public void loadUserWhenTokenDoesNotContainScopesThenNoScopeAuthorities() {
203210
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
204211
}
205212

213+
@Test
214+
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() throws IOException {
215+
// @formatter:off
216+
String userInfoResponse = "{\n"
217+
+ " \"user\": {\"user-name\": \"user1\"},\n"
218+
+ " \"sub\" : \"" + this.idToken.getSubject() + "\",\n"
219+
+ " \"first-name\": \"first\",\n"
220+
+ " \"last-name\": \"last\",\n"
221+
+ " \"middle-name\": \"middle\",\n"
222+
+ " \"address\": \"address\",\n"
223+
+ " \"email\": \"[email protected]\"\n"
224+
+ "}\n";
225+
// @formatter:on
226+
try (MockWebServer server = new MockWebServer()) {
227+
server.start();
228+
enqueueApplicationJsonBody(server, userInfoResponse);
229+
String userInfoUri = server.url("/user").toString();
230+
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
231+
.userInfoUri(userInfoUri)
232+
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
233+
.userNameAttributeName("user-name")
234+
.build();
235+
OidcReactiveOAuth2UserService userService = new OidcReactiveOAuth2UserService();
236+
DefaultReactiveOAuth2UserService oAuth2UserService = new DefaultReactiveOAuth2UserService();
237+
oAuth2UserService.setAttributesConverter((request) -> (attributes) -> {
238+
Map<String, Object> user = (Map<String, Object>) attributes.get("user");
239+
attributes.put("user-name", user.get("user-name"));
240+
return attributes;
241+
});
242+
userService.setOauth2UserService(oAuth2UserService);
243+
OAuth2User user = userService
244+
.loadUser(new OidcUserRequest(clientRegistration, this.accessToken, this.idToken))
245+
.block();
246+
assertThat(user.getName()).isEqualTo("user1");
247+
assertThat(user.getAttributes()).hasSize(13);
248+
assertThat(((Map<?, ?>) user.getAttribute("user")).get("user-name")).isEqualTo("user1");
249+
assertThat((String) user.getAttribute("first-name")).isEqualTo("first");
250+
assertThat((String) user.getAttribute("last-name")).isEqualTo("last");
251+
assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle");
252+
assertThat((String) user.getAttribute("address")).isEqualTo("address");
253+
assertThat((String) user.getAttribute("email")).isEqualTo("[email protected]");
254+
assertThat(user.getAuthorities()).hasSize(2);
255+
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
256+
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
257+
assertThat(userAuthority.getAuthority()).isEqualTo("OIDC_USER");
258+
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
259+
}
260+
}
261+
206262
private OidcUserRequest userRequest() {
207263
return new OidcUserRequest(this.registration.build(), this.accessToken, this.idToken);
208264
}
209265

266+
private void enqueueApplicationJsonBody(MockWebServer server, String json) {
267+
server.enqueue(
268+
new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(json));
269+
}
270+
210271
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,12 @@ public void loadUserWhenUserInfoSuccessResponseInvalidContentTypeThenThrowOAuth2
413413
+ "from '" + userInfoUri + "': response contains invalid content type 'text/plain'.");
414414
}
415415

416+
@Test
417+
public void setAttributesConverterWhenNullThenException() {
418+
assertThatExceptionOfType(IllegalArgumentException.class)
419+
.isThrownBy(() -> this.userService.setAttributesConverter(null));
420+
}
421+
416422
private DefaultOAuth2UserService withMockResponse(Map<String, Object> response) {
417423
ResponseEntity<Map<String, Object>> responseEntity = new ResponseEntity<>(response, HttpStatus.OK);
418424
Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = mock(Converter.class);

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

+47-1
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.
@@ -165,6 +165,46 @@ public void loadUserWhenUserInfo201CreatedResponseThenReturnUser() {
165165
assertThatNoException().isThrownBy(() -> this.userService.loadUser(oauth2UserRequest()).block());
166166
}
167167

168+
@Test
169+
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() {
170+
// @formatter:off
171+
String userInfoResponse = "{\n"
172+
+ " \"user\": {\"user-name\": \"user1\"},\n"
173+
+ " \"first-name\": \"first\",\n"
174+
+ " \"last-name\": \"last\",\n"
175+
+ " \"middle-name\": \"middle\",\n"
176+
+ " \"address\": \"address\",\n"
177+
+ " \"email\": \"[email protected]\"\n"
178+
+ "}\n";
179+
// @formatter:on
180+
enqueueApplicationJsonBody(userInfoResponse);
181+
String userInfoUri = this.server.url("/user").toString();
182+
ClientRegistration clientRegistration = this.clientRegistration.userInfoUri(userInfoUri)
183+
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
184+
.userNameAttributeName("user-name")
185+
.build();
186+
DefaultReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService();
187+
userService.setAttributesConverter((request) -> (attributes) -> {
188+
Map<String, Object> user = (Map<String, Object>) attributes.get("user");
189+
attributes.put("user-name", user.get("user-name"));
190+
return attributes;
191+
});
192+
OAuth2User user = userService.loadUser(new OAuth2UserRequest(clientRegistration, this.accessToken)).block();
193+
assertThat(user.getName()).isEqualTo("user1");
194+
assertThat(user.getAttributes()).hasSize(7);
195+
assertThat(((Map<?, ?>) user.getAttribute("user")).get("user-name")).isEqualTo("user1");
196+
assertThat((String) user.getAttribute("first-name")).isEqualTo("first");
197+
assertThat((String) user.getAttribute("last-name")).isEqualTo("last");
198+
assertThat((String) user.getAttribute("middle-name")).isEqualTo("middle");
199+
assertThat((String) user.getAttribute("address")).isEqualTo("address");
200+
assertThat((String) user.getAttribute("email")).isEqualTo("[email protected]");
201+
assertThat(user.getAuthorities()).hasSize(1);
202+
assertThat(user.getAuthorities().iterator().next()).isInstanceOf(OAuth2UserAuthority.class);
203+
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) user.getAuthorities().iterator().next();
204+
assertThat(userAuthority.getAuthority()).isEqualTo("OAUTH2_USER");
205+
assertThat(userAuthority.getAttributes()).isEqualTo(user.getAttributes());
206+
}
207+
168208
// gh-5500
169209
@Test
170210
public void loadUserWhenAuthenticationMethodHeaderSuccessResponseThenHttpMethodGet() throws Exception {
@@ -290,6 +330,12 @@ public void loadUserWhenUserInfoSuccessResponseInvalidContentTypeThenThrowOAuth2
290330
+ "response contains invalid content type 'text/plain'");
291331
}
292332

333+
@Test
334+
public void setAttributesConverterWhenNullThenException() {
335+
assertThatExceptionOfType(IllegalArgumentException.class)
336+
.isThrownBy(() -> this.userService.setAttributesConverter(null));
337+
}
338+
293339
private DefaultReactiveOAuth2UserService withMockResponse(Map<String, Object> body) {
294340
WebClient real = WebClient.builder().build();
295341
WebClient.RequestHeadersUriSpec spec = spy(real.post());

0 commit comments

Comments
 (0)