Skip to content

Commit bf877a9

Browse files
committed
Add OAuth2User to OidcUser Conversion Params
Previously the Oidc(Reactive)OAuth2UserService APIs allowed a strategy for converting to the OidcUser with the OidcUserRequest and OidcUserInfo. The input should also include the OAuth2User to make it simple to use the OAuth2User as a part of the conversion. This commit introduces OidcUserSource as a POJO containing OidcUserRequest, OidcUserInfo, and OAuth2User. It then updates the OidcUser conversion strategy in OidcUserService and OidcReactiveOAuth2UserService to accept OidcUserSource as the source for the Converter used to create OidUser. Closes gh-17626
1 parent 34742c9 commit bf877a9

File tree

6 files changed

+184
-32
lines changed

6 files changed

+184
-32
lines changed

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

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public class OidcReactiveOAuth2UserService implements ReactiveOAuth2UserService<
7373

7474
private Predicate<OidcUserRequest> retrieveUserInfo = OidcUserRequestUtils::shouldRetrieveUserInfo;
7575

76-
private BiFunction<OidcUserRequest, OidcUserInfo, Mono<OidcUser>> oidcUserMapper = this::getUser;
76+
private Converter<OidcUserSource, Mono<OidcUser>> oidcUserConverter = (source) -> Mono
77+
.just(OidcUserRequestUtils.getUser(source));
7778

7879
/**
7980
* Returns the default {@link Converter}'s used for type conversion of claim values
@@ -102,34 +103,26 @@ public class OidcReactiveOAuth2UserService implements ReactiveOAuth2UserService<
102103
public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
103104
Assert.notNull(userRequest, "userRequest cannot be null");
104105
// @formatter:off
105-
return getUserInfo(userRequest)
106-
.flatMap((userInfo) -> this.oidcUserMapper.apply(userRequest, userInfo))
107-
.switchIfEmpty(Mono.defer(() -> this.oidcUserMapper.apply(userRequest, null)));
106+
return Mono.just(userRequest)
107+
.filter(this.retrieveUserInfo::test)
108+
.flatMap(this.oauth2UserService::loadUser)
109+
.flatMap((oauth2User) -> toOidcUser(userRequest, oauth2User))
110+
.switchIfEmpty(Mono.defer(() -> this.oidcUserConverter.convert(new OidcUserSource(userRequest))));
108111
// @formatter:on
109112
}
110113

111-
private Mono<OidcUser> getUser(OidcUserRequest userRequest, OidcUserInfo userInfo) {
112-
return Mono.just(OidcUserRequestUtils.getUser(userRequest, userInfo));
113-
}
114-
115-
private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) {
116-
if (!this.retrieveUserInfo.test(userRequest)) {
117-
return Mono.empty();
118-
}
119-
// @formatter:off
120-
return this.oauth2UserService
121-
.loadUser(userRequest)
122-
.map(OAuth2User::getAttributes)
123-
.map((claims) -> convertClaims(claims, userRequest.getClientRegistration()))
124-
.map(OidcUserInfo::new)
125-
.doOnNext((userInfo) -> {
126-
String subject = userInfo.getSubject();
127-
if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) {
128-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
129-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
130-
}
131-
});
132-
// @formatter:on
114+
private Mono<OidcUser> toOidcUser(OidcUserRequest userRequest, OAuth2User oauth2User) {
115+
return Mono.defer(() -> {
116+
Map<String, Object> claims = convertClaims(oauth2User.getAttributes(), userRequest.getClientRegistration());
117+
OidcUserInfo userInfo = new OidcUserInfo(claims);
118+
String subject = userInfo.getSubject();
119+
if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) {
120+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
121+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
122+
}
123+
OidcUserSource source = new OidcUserSource(userRequest, userInfo, oauth2User);
124+
return this.oidcUserConverter.convert(source);
125+
});
133126
}
134127

135128
private Map<String, Object> convertClaims(Map<String, Object> claims, ClientRegistration clientRegistration) {
@@ -229,10 +222,21 @@ public final void setRetrieveUserInfo(Predicate<OidcUserRequest> retrieveUserInf
229222
* @param oidcUserMapper the function used to map the {@link OidcUser} from the
230223
* {@link OidcUserRequest} and {@link OidcUserInfo}
231224
* @since 6.3
225+
* @deprecated Use {@link #setOidcUserConverter(Converter)} instead
232226
*/
227+
@Deprecated(since = "7.0", forRemoval = true)
233228
public final void setOidcUserMapper(BiFunction<OidcUserRequest, OidcUserInfo, Mono<OidcUser>> oidcUserMapper) {
234229
Assert.notNull(oidcUserMapper, "oidcUserMapper cannot be null");
235-
this.oidcUserMapper = oidcUserMapper;
230+
this.oidcUserConverter = (source) -> oidcUserMapper.apply(source.getUserRequest(), source.getUserInfo());
231+
}
232+
233+
/**
234+
* Allows converting from the {@link OidcUserSource} to and {@link OidcUser}.
235+
* @param oidcUserConverter the {@link Converter} to use. Cannot be null.
236+
*/
237+
public void setOidcUserConverter(Converter<OidcUserSource, Mono<OidcUser>> oidcUserConverter) {
238+
Assert.notNull(oidcUserConverter, "oidcUserConverter cannot be null");
239+
this.oidcUserConverter = oidcUserConverter;
236240
}
237241

238242
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ static boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
7676
return false;
7777
}
7878

79-
static OidcUser getUser(OidcUserRequest userRequest, OidcUserInfo userInfo) {
79+
static OidcUser getUser(OidcUserSource userMetadata) {
80+
OidcUserRequest userRequest = userMetadata.getUserRequest();
81+
OidcUserInfo userInfo = userMetadata.getUserInfo();
8082
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
8183
ClientRegistration.ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
8284
String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName();

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
8282

8383
private Predicate<OidcUserRequest> retrieveUserInfo = this::shouldRetrieveUserInfo;
8484

85-
private BiFunction<OidcUserRequest, OidcUserInfo, OidcUser> oidcUserMapper = OidcUserRequestUtils::getUser;
85+
private Converter<OidcUserSource, OidcUser> oidcUserConverter = OidcUserRequestUtils::getUser;
8686

8787
/**
8888
* Returns the default {@link Converter}'s used for type conversion of claim values
@@ -111,8 +111,9 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
111111
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
112112
Assert.notNull(userRequest, "userRequest cannot be null");
113113
OidcUserInfo userInfo = null;
114+
OAuth2User oauth2User = null;
114115
if (this.retrieveUserInfo.test(userRequest)) {
115-
OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest);
116+
oauth2User = this.oauth2UserService.loadUser(userRequest);
116117
Map<String, Object> claims = getClaims(userRequest, oauth2User);
117118
userInfo = new OidcUserInfo(claims);
118119
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
@@ -133,7 +134,8 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio
133134
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
134135
}
135136
}
136-
return this.oidcUserMapper.apply(userRequest, userInfo);
137+
OidcUserSource source = new OidcUserSource(userRequest, userInfo, oauth2User);
138+
return this.oidcUserConverter.convert(source);
137139
}
138140

139141
private Map<String, Object> getClaims(OidcUserRequest userRequest, OAuth2User oauth2User) {
@@ -293,10 +295,21 @@ public final void setRetrieveUserInfo(Predicate<OidcUserRequest> retrieveUserInf
293295
* @param oidcUserMapper the function used to map the {@link OidcUser} from the
294296
* {@link OidcUserRequest} and {@link OidcUserInfo}
295297
* @since 6.3
298+
* @deprecated Use {@link #setOidcUserConverter(Converter)} instead
296299
*/
300+
@Deprecated(since = "7.0", forRemoval = true)
297301
public final void setOidcUserMapper(BiFunction<OidcUserRequest, OidcUserInfo, OidcUser> oidcUserMapper) {
298302
Assert.notNull(oidcUserMapper, "oidcUserMapper cannot be null");
299-
this.oidcUserMapper = oidcUserMapper;
303+
this.oidcUserConverter = (source) -> oidcUserMapper.apply(source.getUserRequest(), source.getUserInfo());
304+
}
305+
306+
/**
307+
* Allows converting from the {@link OidcUserSource} to and {@link OidcUser}.
308+
* @param oidcUserConverter the {@link Converter} to use. Cannot be null.
309+
*/
310+
public void setOidcUserConverter(Converter<OidcUserSource, OidcUser> oidcUserConverter) {
311+
Assert.notNull(oidcUserConverter, "oidcUserConverter cannot be null");
312+
this.oidcUserConverter = oidcUserConverter;
300313
}
301314

302315
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2002-2025 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.oidc.userinfo;
18+
19+
import org.jspecify.annotations.Nullable;
20+
21+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
22+
import org.springframework.security.oauth2.core.user.OAuth2User;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* The source for the converter to
27+
* {@link org.springframework.security.oauth2.core.oidc.user.OidcUser}.
28+
*
29+
* @author Rob Winch
30+
* @since 7.0
31+
*/
32+
public class OidcUserSource {
33+
34+
private final OidcUserRequest userRequest;
35+
36+
private final @Nullable OidcUserInfo userInfo;
37+
38+
private final @Nullable OAuth2User oauth2User;
39+
40+
public OidcUserSource(OidcUserRequest userRequest) {
41+
this(userRequest, null, null);
42+
}
43+
44+
public OidcUserSource(OidcUserRequest userRequest, @Nullable OidcUserInfo userInfo,
45+
@Nullable OAuth2User oauth2User) {
46+
Assert.notNull(userRequest, "userRequest cannot be null");
47+
this.userRequest = userRequest;
48+
this.userInfo = userInfo;
49+
this.oauth2User = oauth2User;
50+
}
51+
52+
public OidcUserRequest getUserRequest() {
53+
return this.userRequest;
54+
}
55+
56+
public @Nullable OidcUserInfo getUserInfo() {
57+
return this.userInfo;
58+
}
59+
60+
public @Nullable OAuth2User getOauth2User() {
61+
return this.oauth2User;
62+
}
63+
64+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,36 @@ public void loadUserWhenTokenDoesNotContainScopesThenNoScopeAuthorities() {
316316
assertThat(userAuthority.getUserNameAttributeName()).isEqualTo("id");
317317
}
318318

319+
@Test
320+
public void loadUserWhenCustomOidcUserConverterSetThenUsed() {
321+
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
322+
.userInfoUri("https://example.com/user")
323+
.userInfoAuthenticationMethod(AuthenticationMethod.HEADER)
324+
.userNameAttributeName(StandardClaimNames.SUB)
325+
.build();
326+
this.accessToken = TestOAuth2AccessTokens.scopes(clientRegistration.getScopes().toArray(new String[0]));
327+
Converter<OidcUserSource, Mono<OidcUser>> oidcUserConverter = mock(Converter.class);
328+
String nameAttributeKey = IdTokenClaimNames.SUB;
329+
OidcUser actualUser = new DefaultOidcUser(AuthorityUtils.createAuthorityList("a", "b"), this.idToken,
330+
nameAttributeKey);
331+
OAuth2User oauth2User = new DefaultOAuth2User(actualUser.getAuthorities(), actualUser.getClaims(),
332+
nameAttributeKey);
333+
ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2 = mock(ReactiveOAuth2UserService.class);
334+
given(oauth2.loadUser(any())).willReturn(Mono.just(oauth2User));
335+
given(oidcUserConverter.convert(any())).willReturn(Mono.just(actualUser));
336+
this.userService.setOauth2UserService(oauth2);
337+
this.userService.setOidcUserConverter(oidcUserConverter);
338+
OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, this.accessToken, this.idToken);
339+
OidcUser user = this.userService.loadUser(userRequest).block();
340+
assertThat(user).isEqualTo(actualUser);
341+
ArgumentCaptor<OidcUserSource> metadataCptr = ArgumentCaptor.forClass(OidcUserSource.class);
342+
verify(oidcUserConverter).convert(metadataCptr.capture());
343+
OidcUserSource metadata = metadataCptr.getValue();
344+
assertThat(metadata.getUserRequest()).isEqualTo(userRequest);
345+
assertThat(metadata.getOauth2User()).isEqualTo(oauth2User);
346+
assertThat(metadata.getUserInfo()).isNotNull();
347+
}
348+
319349
@Test
320350
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() throws IOException {
321351
// @formatter:off

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.springframework.security.oauth2.client.registration.ClientRegistration;
4545
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
4646
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
47+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
48+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
4749
import org.springframework.security.oauth2.core.AuthenticationMethod;
4850
import org.springframework.security.oauth2.core.OAuth2AccessToken;
4951
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -58,6 +60,7 @@
5860
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
5961
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
6062
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
63+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
6164
import org.springframework.security.oauth2.core.user.OAuth2User;
6265
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
6366

@@ -155,6 +158,15 @@ public void setOidcUserMapperWhenNullThenThrowIllegalArgumentException() {
155158
// @formatter:on
156159
}
157160

161+
@Test
162+
public void setOidcUserConverterWhenNullThenThrowIllegalArgumentException() {
163+
// @formatter:off
164+
assertThatIllegalArgumentException()
165+
.isThrownBy(() -> this.userService.setOidcUserConverter(null))
166+
.withMessage("oidcUserConverter cannot be null");
167+
// @formatter:on
168+
}
169+
158170
@Test
159171
public void loadUserWhenUserRequestIsNullThenThrowIllegalArgumentException() {
160172
assertThatIllegalArgumentException().isThrownBy(() -> this.userService.loadUser(null));
@@ -299,6 +311,33 @@ public void loadUserWhenCustomOidcUserMapperSetThenUsed() {
299311
assertThat(userInfo.getClaimAsString("preferred_username")).isEqualTo("user1");
300312
}
301313

314+
@Test
315+
public void loadUserWhenCustomOidcUserConverterSetThenUsed() {
316+
ClientRegistration clientRegistration = this.clientRegistrationBuilder.userInfoUri("https://example.com/user")
317+
.build();
318+
this.accessToken = TestOAuth2AccessTokens.noScopes();
319+
Converter<OidcUserSource, OidcUser> oidcUserConverter = mock(Converter.class);
320+
String nameAttributeKey = IdTokenClaimNames.SUB;
321+
OidcUser actualUser = new DefaultOidcUser(AuthorityUtils.createAuthorityList("a", "b"), this.idToken,
322+
nameAttributeKey);
323+
OAuth2User oauth2User = new DefaultOAuth2User(actualUser.getAuthorities(), actualUser.getClaims(),
324+
nameAttributeKey);
325+
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2 = mock(OAuth2UserService.class);
326+
given(oauth2.loadUser(any())).willReturn(oauth2User);
327+
given(oidcUserConverter.convert(any())).willReturn(actualUser);
328+
this.userService.setOauth2UserService(oauth2);
329+
this.userService.setOidcUserConverter(oidcUserConverter);
330+
OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, this.accessToken, this.idToken);
331+
OidcUser user = this.userService.loadUser(userRequest);
332+
assertThat(user).isEqualTo(actualUser);
333+
ArgumentCaptor<OidcUserSource> metadataCptr = ArgumentCaptor.forClass(OidcUserSource.class);
334+
verify(oidcUserConverter).convert(metadataCptr.capture());
335+
OidcUserSource metadata = metadataCptr.getValue();
336+
assertThat(metadata.getUserRequest()).isEqualTo(userRequest);
337+
assertThat(metadata.getOauth2User()).isEqualTo(oauth2User);
338+
assertThat(metadata.getUserInfo()).isNotNull();
339+
}
340+
302341
@Test
303342
public void loadUserWhenUserInfoSuccessResponseThenReturnUser() {
304343
// @formatter:off

0 commit comments

Comments
 (0)