Skip to content

Commit 51255ff

Browse files
committed
Add SpEL support for nested username extraction in OAuth2
- Add usernameExpression property to ClientRegistration - Deprecate userNameAttributeName in favor of usernameExpression - Support SpEL expressions for nested property access (e.g., "data.username") - Update DefaultOAuth2UserService to evaluate SpEL expressions - Update DefaultReactiveOAuth2UserService with same SpEL support - Maintain backward compatibility by falling back to userNameAttributeName - Use DefaultOAuth2User.withUsername() for direct username injection Fixes gh-16390 Signed-off-by: yybmion <yunyubin54@gmail.com>
1 parent 972afda commit 51255ff

File tree

9 files changed

+506
-89
lines changed

9 files changed

+506
-89
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
*
4848
* @author Joe Grandja
4949
* @author Michael Sosa
50+
* @author Yoobin Yoon
5051
* @since 5.0
5152
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2">Section 2
5253
* Client Registration</a>
@@ -299,8 +300,11 @@ public class UserInfoEndpoint implements Serializable {
299300

300301
private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;
301302

303+
@Deprecated
302304
private String userNameAttributeName;
303305

306+
private String usernameExpression;
307+
304308
UserInfoEndpoint() {
305309
}
306310

@@ -322,15 +326,27 @@ public AuthenticationMethod getAuthenticationMethod() {
322326
}
323327

324328
/**
325-
* Returns the attribute name used to access the user's name from the user
326-
* info response.
327-
* @return the attribute name used to access the user's name from the user
328-
* info response
329+
* @deprecated Use {@link #getUsernameExpression()} instead
329330
*/
331+
@Deprecated
330332
public String getUserNameAttributeName() {
331333
return this.userNameAttributeName;
332334
}
333335

336+
/**
337+
* Returns the SpEL expression used to extract the username from user info
338+
* response.
339+
* @return the SpEL expression for username extraction
340+
* @since 6.5
341+
*/
342+
public String getUsernameExpression() {
343+
if (this.usernameExpression != null) {
344+
return this.usernameExpression;
345+
}
346+
347+
return this.userNameAttributeName;
348+
}
349+
334350
}
335351

336352
}
@@ -370,8 +386,11 @@ public static final class Builder implements Serializable {
370386

371387
private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
372388

389+
@Deprecated
373390
private String userNameAttributeName;
374391

392+
private String usernameExpression;
393+
375394
private String jwkSetUri;
376395

377396
private String issuerUri;
@@ -399,6 +418,7 @@ private Builder(ClientRegistration clientRegistration) {
399418
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
400419
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
401420
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
421+
this.usernameExpression = clientRegistration.providerDetails.userInfoEndpoint.usernameExpression;
402422
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
403423
this.issuerUri = clientRegistration.providerDetails.issuerUri;
404424
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
@@ -552,17 +572,25 @@ public Builder userInfoAuthenticationMethod(AuthenticationMethod userInfoAuthent
552572
}
553573

554574
/**
555-
* Sets the attribute name used to access the user's name from the user info
556-
* response.
557-
* @param userNameAttributeName the attribute name used to access the user's name
558-
* from the user info response
559-
* @return the {@link Builder}
575+
* @deprecated Use {@link #usernameExpression(String)} instead
560576
*/
577+
@Deprecated
561578
public Builder userNameAttributeName(String userNameAttributeName) {
562579
this.userNameAttributeName = userNameAttributeName;
563580
return this;
564581
}
565582

583+
/**
584+
* Sets the SpEL expression used to extract the username from user info response.
585+
* @param usernameExpression the SpEL expression for username extraction
586+
* @return the {@link Builder}
587+
* @since 6.5
588+
*/
589+
public Builder usernameExpression(String usernameExpression) {
590+
this.usernameExpression = usernameExpression;
591+
return this;
592+
}
593+
566594
/**
567595
* Sets the uri for the JSON Web Key (JWK) Set endpoint.
568596
* @param jwkSetUri the uri for the JSON Web Key (JWK) Set endpoint
@@ -672,7 +700,14 @@ private ProviderDetails createProviderDetails(ClientRegistration clientRegistrat
672700
providerDetails.tokenUri = this.tokenUri;
673701
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
674702
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
703+
if (this.usernameExpression != null) {
704+
providerDetails.userInfoEndpoint.usernameExpression = this.usernameExpression;
705+
}
706+
else if (this.userNameAttributeName != null) {
707+
providerDetails.userInfoEndpoint.usernameExpression = this.userNameAttributeName;
708+
}
675709
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
710+
676711
providerDetails.jwkSetUri = this.jwkSetUri;
677712
providerDetails.issuerUri = this.issuerUri;
678713
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);

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

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -20,8 +20,12 @@
2020
import java.util.LinkedHashSet;
2121
import java.util.Map;
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.ExpressionParser;
27+
import org.springframework.expression.spel.standard.SpelExpressionParser;
28+
import org.springframework.expression.spel.support.StandardEvaluationContext;
2529
import org.springframework.http.RequestEntity;
2630
import org.springframework.http.ResponseEntity;
2731
import org.springframework.security.core.GrantedAuthority;
@@ -57,6 +61,7 @@
5761
* supported user attribute names.
5862
*
5963
* @author Joe Grandja
64+
* @author Yoobin Yoon
6065
* @since 5.0
6166
* @see OAuth2UserService
6267
* @see OAuth2UserRequest
@@ -71,6 +76,8 @@ public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserReq
7176

7277
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
7378

79+
private static final String INVALID_USERNAME_EXPRESSION_ERROR_CODE = "invalid_username_expression";
80+
7481
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<>() {
7582
};
7683

@@ -90,13 +97,69 @@ public DefaultOAuth2UserService() {
9097
@Override
9198
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
9299
Assert.notNull(userRequest, "userRequest cannot be null");
93-
String userNameAttributeName = getUserNameAttributeName(userRequest);
100+
String usernameExpression = getUsernameExpression(userRequest);
94101
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
95102
ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
96103
OAuth2AccessToken token = userRequest.getAccessToken();
97104
Map<String, Object> attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
98-
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, userNameAttributeName);
99-
return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
105+
106+
String evaluatedUsername = evaluateUsername(attributes, usernameExpression);
107+
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, usernameExpression);
108+
109+
return DefaultOAuth2User.withUsername(authorities, attributes, evaluatedUsername);
110+
}
111+
112+
private String getUsernameExpression(OAuth2UserRequest userRequest) {
113+
if (!StringUtils
114+
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
115+
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
116+
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
117+
+ userRequest.getClientRegistration().getRegistrationId(),
118+
null);
119+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
120+
}
121+
String usernameExpression = userRequest.getClientRegistration()
122+
.getProviderDetails()
123+
.getUserInfoEndpoint()
124+
.getUsernameExpression();
125+
if (!StringUtils.hasText(usernameExpression)) {
126+
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
127+
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
128+
+ userRequest.getClientRegistration().getRegistrationId(),
129+
null);
130+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
131+
}
132+
return usernameExpression;
133+
}
134+
135+
private String evaluateUsername(Map<String, Object> attributes, String usernameExpression) {
136+
Object value = null;
137+
138+
if (attributes.containsKey(usernameExpression)) {
139+
value = attributes.get(usernameExpression);
140+
}
141+
else {
142+
try {
143+
ExpressionParser parser = new SpelExpressionParser();
144+
StandardEvaluationContext context = new StandardEvaluationContext();
145+
context.setRootObject(attributes);
146+
context.addPropertyAccessor(new MapAccessor());
147+
value = parser.parseExpression(usernameExpression).getValue(context);
148+
}
149+
catch (Exception ex) {
150+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USERNAME_EXPRESSION_ERROR_CODE,
151+
"Invalid username expression or SPEL expression: " + usernameExpression, null);
152+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
153+
}
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();
100163
}
101164

102165
/**
@@ -164,29 +227,6 @@ private ResponseEntity<Map<String, Object>> getResponse(OAuth2UserRequest userRe
164227
}
165228
}
166229

167-
private String getUserNameAttributeName(OAuth2UserRequest userRequest) {
168-
if (!StringUtils
169-
.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
170-
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
171-
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
172-
+ userRequest.getClientRegistration().getRegistrationId(),
173-
null);
174-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
175-
}
176-
String userNameAttributeName = userRequest.getClientRegistration()
177-
.getProviderDetails()
178-
.getUserInfoEndpoint()
179-
.getUserNameAttributeName();
180-
if (!StringUtils.hasText(userNameAttributeName)) {
181-
OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
182-
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
183-
+ userRequest.getClientRegistration().getRegistrationId(),
184-
null);
185-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
186-
}
187-
return userNameAttributeName;
188-
}
189-
190230
private Collection<GrantedAuthority> getAuthorities(OAuth2AccessToken token, Map<String, Object> attributes,
191231
String userNameAttributeName) {
192232
Collection<GrantedAuthority> authorities = new LinkedHashSet<>();

0 commit comments

Comments
 (0)