Skip to content

Commit af30db9

Browse files
yybmionrwinch
authored andcommitted
Remove deprecated constructor/method usage in OAuth2 user services
Replace deprecated constructor and method calls with new builder pattern and updated APIs to eliminate deprecated code usage - Update OidcUserRequestUtils to use non-deprecated APIs - Extract OAuth2UsernameExpressionUtils for SpEL evaluation logic - minor fixes for clarity closes gh-16390 Signed-off-by: yybmion <[email protected]>
1 parent beb5a44 commit af30db9

File tree

10 files changed

+191
-120
lines changed

10 files changed

+191
-120
lines changed

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -191,23 +191,22 @@ public final void setRetrieveUserInfo(Predicate<OidcUserRequest> retrieveUserInf
191191
* var accessToken = userRequest.getAccessToken();
192192
* var grantedAuthorities = new HashSet&lt;GrantedAuthority&gt;();
193193
* // TODO: Map authorities from the access token
194-
* var userNameAttributeName = "preferred_username";
195-
* return Mono.just(new DefaultOidcUser(
196-
* grantedAuthorities,
197-
* userRequest.getIdToken(),
198-
* userInfo,
199-
* userNameAttributeName
200-
* ));
194+
* var username = "preferred_username";
195+
* return Mono.just(DefaultOidcUser.withUsername(username)
196+
* .authorities(grantedAuthorities)
197+
* .idToken(userRequest.getIdToken())
198+
* .userInfo(userInfo)
199+
* .build());
201200
* };
202201
* }
203202
* </pre>
204203
* <p>
205-
* Note that you can access the {@code userNameAttributeName} via the
206-
* {@link ClientRegistration} as follows: <pre>
207-
* var userNameAttributeName = userRequest.getClientRegistration()
208-
* .getProviderDetails()
209-
* .getUserInfoEndpoint()
210-
* .getUserNameAttributeName();
204+
* Note that you can access the username expression via the {@link ClientRegistration}
205+
* as follows: <pre>
206+
* var usernameExpression = userRequest.getClientRegistration()
207+
* .getProviderDetails()
208+
* .getUserInfoEndpoint()
209+
* .getUsernameExpression();
211210
* </pre>
212211
* <p>
213212
* By default, a {@link DefaultOidcUser} is created with authorities mapped as

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

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,31 @@
1616

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

19+
import java.util.HashMap;
1920
import java.util.LinkedHashSet;
21+
import java.util.Map;
2022
import java.util.Set;
2123

2224
import org.springframework.security.core.GrantedAuthority;
2325
import org.springframework.security.core.authority.SimpleGrantedAuthority;
2426
import org.springframework.security.oauth2.client.registration.ClientRegistration;
27+
import org.springframework.security.oauth2.client.userinfo.OAuth2UsernameExpressionUtils;
2528
import org.springframework.security.oauth2.core.AuthorizationGrantType;
2629
import org.springframework.security.oauth2.core.OAuth2AccessToken;
30+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
2731
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
2832
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
2933
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
3034
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
35+
import org.springframework.util.Assert;
3136
import org.springframework.util.CollectionUtils;
3237
import org.springframework.util.StringUtils;
3338

3439
/**
3540
* Utilities for working with the {@link OidcUserRequest}
3641
*
3742
* @author Rob Winch
43+
* @author Yoobin Yoon
3844
* @since 5.1
3945
*/
4046
final class OidcUserRequestUtils {
@@ -81,21 +87,40 @@ static OidcUser getUser(OidcUserSource userMetadata) {
8187
OidcUserInfo userInfo = userMetadata.getUserInfo();
8288
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
8389
ClientRegistration.ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
84-
String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName();
85-
if (StringUtils.hasText(userNameAttributeName)) {
86-
authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo, userNameAttributeName));
90+
String usernameExpression = providerDetails.getUserInfoEndpoint().getUsernameExpression();
91+
92+
String username;
93+
if (StringUtils.hasText(usernameExpression)) {
94+
Map<String, Object> claims = collectClaims(userRequest.getIdToken(), userInfo);
95+
username = OAuth2UsernameExpressionUtils.evaluateUsername(claims, usernameExpression);
8796
}
8897
else {
89-
authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo));
98+
username = userRequest.getIdToken().getSubject();
9099
}
100+
101+
authorities
102+
.add(OidcUserAuthority.withUsername(username).idToken(userRequest.getIdToken()).userInfo(userInfo).build());
103+
91104
OAuth2AccessToken token = userRequest.getAccessToken();
92105
for (String scope : token.getScopes()) {
93106
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
94107
}
95-
if (StringUtils.hasText(userNameAttributeName)) {
96-
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
108+
109+
return DefaultOidcUser.withUsername(username)
110+
.authorities(authorities)
111+
.idToken(userRequest.getIdToken())
112+
.userInfo(userInfo)
113+
.build();
114+
}
115+
116+
private static Map<String, Object> collectClaims(OidcIdToken idToken, OidcUserInfo userInfo) {
117+
Assert.notNull(idToken, "idToken cannot be null");
118+
Map<String, Object> claims = new HashMap<>();
119+
if (userInfo != null) {
120+
claims.putAll(userInfo.getClaims());
97121
}
98-
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
122+
claims.putAll(idToken.getClaims());
123+
return claims;
99124
}
100125

101126
private OidcUserRequestUtils() {

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -264,23 +264,22 @@ public final void setRetrieveUserInfo(Predicate<OidcUserRequest> retrieveUserInf
264264
* var accessToken = userRequest.getAccessToken();
265265
* var grantedAuthorities = new HashSet&lt;GrantedAuthority&gt;();
266266
* // TODO: Map authorities from the access token
267-
* var userNameAttributeName = "preferred_username";
268-
* return new DefaultOidcUser(
269-
* grantedAuthorities,
270-
* userRequest.getIdToken(),
271-
* userInfo,
272-
* userNameAttributeName
273-
* );
267+
* var username = "preferred_username";
268+
* return DefaultOidcUser.withUsername(username)
269+
* .authorities(grantedAuthorities)
270+
* .idToken(userRequest.getIdToken())
271+
* .userInfo(userInfo)
272+
* .build();
274273
* };
275274
* }
276275
* </pre>
277276
* <p>
278-
* Note that you can access the {@code userNameAttributeName} via the
279-
* {@link ClientRegistration} as follows: <pre>
280-
* var userNameAttributeName = userRequest.getClientRegistration()
281-
* .getProviderDetails()
282-
* .getUserInfoEndpoint()
283-
* .getUserNameAttributeName();
277+
* Note that you can access the username expression via the {@link ClientRegistration}
278+
* as follows: <pre>
279+
* var usernameExpression = userRequest.getClientRegistration()
280+
* .getProviderDetails()
281+
* .getUserInfoEndpoint()
282+
* .getUsernameExpression();
284283
* </pre>
285284
* <p>
286285
* By default, a {@link DefaultOidcUser} is created with authorities mapped as

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

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,8 @@
2020
import java.util.LinkedHashSet;
2121
import java.util.Map;
2222

23-
import org.springframework.context.expression.MapAccessor;
2423
import org.springframework.core.ParameterizedTypeReference;
2524
import org.springframework.core.convert.converter.Converter;
26-
import org.springframework.expression.ExpressionParser;
27-
import org.springframework.expression.spel.standard.SpelExpressionParser;
28-
import org.springframework.expression.spel.support.SimpleEvaluationContext;
2925
import org.springframework.http.RequestEntity;
3026
import org.springframework.http.ResponseEntity;
3127
import org.springframework.security.core.GrantedAuthority;
@@ -76,10 +72,6 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
7672

7773
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
7874

79-
private static final String INVALID_USERNAME_EXPRESSION_ERROR_CODE = "invalid_username_expression";
80-
81-
private static final ExpressionParser expressionParser = new SpelExpressionParser();
82-
8375
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<>() {
8476
};
8577

@@ -104,15 +96,9 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
10496
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
10597
OAuth2AccessToken token = userRequest.getAccessToken();
10698
Map<String, Object> attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
107-
108-
String evaluatedUsername = evaluateUsername(attributes, usernameExpression);
109-
110-
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, evaluatedUsername);
111-
112-
return DefaultOAuth2User.withUsername(evaluatedUsername)
113-
.authorities(authorities)
114-
.attributes(attributes)
115-
.build();
99+
String username = OAuth2UsernameExpressionUtils.evaluateUsername(attributes, usernameExpression);
100+
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, username);
101+
return DefaultOAuth2User.withUsername(username).authorities(authorities).attributes(attributes).build();
116102
}
117103

118104
private String getUsernameExpression(OAuth2UserRequest userRequest) {
@@ -138,30 +124,6 @@ private String getUsernameExpression(OAuth2UserRequest userRequest) {
138124
return usernameExpression;
139125
}
140126

141-
private String evaluateUsername(Map<String, Object> attributes, String usernameExpression) {
142-
Object value = null;
143-
144-
try {
145-
SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
146-
.withRootObject(attributes)
147-
.build();
148-
value = expressionParser.parseExpression(usernameExpression).getValue(context);
149-
}
150-
catch (Exception ex) {
151-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USERNAME_EXPRESSION_ERROR_CODE,
152-
"Invalid username expression or SPEL expression: " + usernameExpression, null);
153-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
154-
}
155-
156-
if (value == null) {
157-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
158-
"An error occurred while attempting to retrieve the UserInfo Resource: username cannot be null",
159-
null);
160-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
161-
}
162-
return value.toString();
163-
}
164-
165127
/**
166128
* Use this strategy to adapt user attributes into a format understood by Spring
167129
* Security; by default, the original attributes are preserved.

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,15 @@ public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2Aut
130130
.bodyToMono(DefaultReactiveOAuth2UserService.STRING_OBJECT_MAP)
131131
.mapNotNull((attributes) -> this.attributesConverter.convert(userRequest).convert(attributes));
132132
return userAttributes.map((attrs) -> {
133-
GrantedAuthority authority = new OAuth2UserAuthority(attrs, userNameAttributeName);
134-
Set<GrantedAuthority> authorities = new HashSet<>();
135-
authorities.add(authority);
136-
OAuth2AccessToken token = userRequest.getAccessToken();
137-
for (String scope : token.getScopes()) {
138-
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
139-
}
133+
String username = OAuth2UsernameExpressionUtils.evaluateUsername(attrs, usernameExpression);
134+
Set<GrantedAuthority> authorities = new HashSet<>();
135+
authorities.add(OAuth2UserAuthority.withUsername(username)
136+
.attributes(attrs)
137+
.build());
138+
OAuth2AccessToken token = userRequest.getAccessToken();
139+
for (String scope : token.getScopes()) {
140+
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
141+
}
140142

141143
return new DefaultOAuth2User(authorities, attrs, userNameAttributeName);
142144
})
@@ -168,6 +170,21 @@ public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2Aut
168170
// @formatter:on
169171
}
170172

173+
private String getUsernameExpression(OAuth2UserRequest userRequest) {
174+
String usernameExpression = userRequest.getClientRegistration()
175+
.getProviderDetails()
176+
.getUserInfoEndpoint()
177+
.getUsernameExpression();
178+
if (!StringUtils.hasText(usernameExpression)) {
179+
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
180+
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
181+
+ userRequest.getClientRegistration().getRegistrationId(),
182+
null);
183+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
184+
}
185+
return usernameExpression;
186+
}
187+
171188
private WebClient.RequestHeadersSpec<?> getRequestHeaderSpec(OAuth2UserRequest userRequest, String userInfoUri,
172189
AuthenticationMethod authenticationMethod) {
173190
if (AuthenticationMethod.FORM.equals(authenticationMethod)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.userinfo;
18+
19+
import java.util.Map;
20+
21+
import org.springframework.context.expression.MapAccessor;
22+
import org.springframework.expression.ExpressionParser;
23+
import org.springframework.expression.spel.standard.SpelExpressionParser;
24+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
25+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
26+
import org.springframework.security.oauth2.core.OAuth2Error;
27+
28+
/**
29+
* Utility class for evaluating username expressions in OAuth2 user information.
30+
*
31+
* @author Yoobin Yoon
32+
* @since 7.0
33+
*/
34+
public final class OAuth2UsernameExpressionUtils {
35+
36+
private static final String INVALID_USERNAME_EXPRESSION_ERROR_CODE = "invalid_username_expression";
37+
38+
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
39+
40+
private static final ExpressionParser expressionParser = new SpelExpressionParser();
41+
42+
/**
43+
* Evaluates a SpEL expression to extract the username from user attributes.
44+
*
45+
* <p>
46+
* Examples:
47+
* <ul>
48+
* <li>Simple attribute: {@code "username"} or {@code "['username']"}</li>
49+
* <li>Nested attribute: {@code "data.username"}</li>
50+
* <li>Complex expression: {@code "user_info?.name ?: 'anonymous'"}</li>
51+
* </ul>
52+
* @param attributes the user attributes (used as SpEL root object)
53+
* @param usernameExpression the SpEL expression to evaluate
54+
* @return the evaluated username (never null)
55+
* @throws OAuth2AuthenticationException if expression is invalid or evaluates to null
56+
*/
57+
public static String evaluateUsername(Map<String, Object> attributes, String usernameExpression) {
58+
Object value = null;
59+
60+
try {
61+
SimpleEvaluationContext context = SimpleEvaluationContext.forPropertyAccessors(new MapAccessor())
62+
.withRootObject(attributes)
63+
.build();
64+
value = expressionParser.parseExpression(usernameExpression).getValue(context);
65+
}
66+
catch (Exception ex) {
67+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USERNAME_EXPRESSION_ERROR_CODE,
68+
"Invalid username expression or SPEL expression: " + usernameExpression, null);
69+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
70+
}
71+
72+
if (value == null) {
73+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
74+
"An error occurred while attempting to retrieve the UserInfo Resource: username cannot be null",
75+
null);
76+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
77+
}
78+
return value.toString();
79+
}
80+
81+
private OAuth2UsernameExpressionUtils() {
82+
}
83+
84+
}

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

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -316,36 +316,6 @@ 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-
349319
@Test
350320
public void loadUserWhenNestedUserInfoSuccessThenReturnUser() throws IOException {
351321
// @formatter:off

0 commit comments

Comments
 (0)