Skip to content

Commit 965333a

Browse files
committed
Add SpEL support for nested username extraction in OAuth2
- Add usernameExpression property with SpEL evaluation support - Auto-convert userNameAttributeName to SpEL for backward compatibility - Use SimpleEvaluationContext for secure expression evaluation - Pass evaluated username to OAuth2UserAuthority for gh-15012 compatibility - Add Builder pattern to DefaultOAuth2User - Add Builder pattern to OAuth2UserAuthority - Add Builder pattern to OidcUserAuthority with inherance support - Support nested property access (e.g., "data.username") Fixes gh-16390 Signed-off-by: yybmion <yunyubin54@gmail.com>
1 parent 0dc9709 commit 965333a

File tree

19 files changed

+938
-198
lines changed

19 files changed

+938
-198
lines changed

docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ public final class ClientRegistration {
3636
private String uri; <14>
3737
private AuthenticationMethod authenticationMethod; <15>
3838
private String userNameAttributeName; <16>
39-
39+
private String usernameExpression; <17>
4040
}
4141
}
4242
4343
public static final class ClientSettings {
44-
private boolean requireProofKey; // <17>
44+
private boolean requireProofKey; // <18>
4545
}
4646
}
4747
----
@@ -67,8 +67,9 @@ The name may be used in certain scenarios, such as when displaying the name of t
6767
<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user.
6868
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
6969
The supported values are *header*, *form* and *query*.
70-
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
71-
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
70+
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. *Deprecated* - use `usernameExpression` instead.
71+
<17> `usernameExpression`: A SpEL expression used to extract the username from the UserInfo Response. Supports accessing nested attributes (e.g., `"data.username"`) and complex expressions (e.g., `"preferred_username ?: email"`).
72+
<18> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
7273

7374
A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint].
7475

docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ The following table outlines the mapping of the Spring Boot OAuth Client propert
144144

145145
|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute`
146146
|`providerDetails.userInfoEndpoint.userNameAttributeName`
147+
148+
|`spring.security.oauth2.client.provider._[providerId]_.username-expression`
149+
|`providerDetails.userInfoEndpoint.usernameExpression`
147150
|===
148151

149152
[TIP]
@@ -153,7 +156,7 @@ A `ClientRegistration` can be initially configured using discovery of an OpenID
153156
[[webflux-oauth2-login-common-oauth2-provider]]
154157
== CommonOAuth2Provider
155158

156-
`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta.
159+
`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, X, and Okta.
157160

158161
For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider.
159162
Therefore, it makes sense to provide default values in order to reduce the required configuration.
@@ -337,7 +340,7 @@ public class OAuth2LoginSecurityConfig {
337340
@Bean
338341
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
339342
http
340-
.authorizeExchange(authorize -> authorize
343+
.authorizeExchange((authorize) -> authorize
341344
.anyExchange().authenticated()
342345
)
343346
.oauth2Login(withDefaults());
@@ -390,7 +393,7 @@ public class OAuth2LoginConfig {
390393
@Bean
391394
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
392395
http
393-
.authorizeExchange(authorize -> authorize
396+
.authorizeExchange((authorize) -> authorize
394397
.anyExchange().authenticated()
395398
)
396399
.oauth2Login(withDefaults());
@@ -487,7 +490,7 @@ public class OAuth2LoginConfig {
487490
@Bean
488491
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
489492
http
490-
.authorizeExchange(authorize -> authorize
493+
.authorizeExchange((authorize) -> authorize
491494
.anyExchange().authenticated()
492495
)
493496
.oauth2Login(withDefaults());

docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ public final class ClientRegistration {
3737
private String uri; <14>
3838
private AuthenticationMethod authenticationMethod; <15>
3939
private String userNameAttributeName; <16>
40+
private String usernameExpression; <17>
4041
4142
}
4243
}
4344
4445
public static final class ClientSettings {
45-
private boolean requireProofKey; // <17>
46+
private boolean requireProofKey; // <18>
4647
}
4748
}
4849
----
@@ -68,8 +69,9 @@ This information is available only if the Spring Boot property `spring.security.
6869
<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims and attributes of the authenticated end-user.
6970
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
7071
The supported values are *header*, *form*, and *query*.
71-
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
72-
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
72+
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. Deprecated - use usernameExpression instead.
73+
<17> `usernameExpression`: A SpEL expression used to extract the username from the UserInfo Response. Supports accessing nested attributes (e.g., "data.username") and complex expressions (e.g., "preferred_username ?: email").
74+
<18> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
7375

7476
You can initially configure a `ClientRegistration` by using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint].
7577

docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ The following table outlines the mapping of the Spring Boot OAuth Client propert
142142

143143
|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute`
144144
|`providerDetails.userInfoEndpoint.userNameAttributeName`
145+
146+
|`spring.security.oauth2.client.provider._[providerId]_.username-expression`
147+
|`providerDetails.userInfoEndpoint.usernameExpression`
145148
|===
146149

147150
[TIP]
@@ -153,7 +156,7 @@ You can initially configure a `ClientRegistration` by using discovery of an Open
153156
[[oauth2login-common-oauth2-provider]]
154157
== CommonOAuth2Provider
155158

156-
`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta.
159+
`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, X, and Okta.
157160

158161
For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a provider.
159162
Therefore, it makes sense to provide default values, to reduce the required configuration.
@@ -332,7 +335,7 @@ public class OAuth2LoginSecurityConfig {
332335
@Bean
333336
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
334337
http
335-
.authorizeHttpRequests(authorize -> authorize
338+
.authorizeHttpRequests((authorize) -> authorize
336339
.anyRequest().authenticated()
337340
)
338341
.oauth2Login(withDefaults());
@@ -381,7 +384,7 @@ public class OAuth2LoginConfig {
381384
@Bean
382385
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
383386
http
384-
.authorizeHttpRequests(authorize -> authorize
387+
.authorizeHttpRequests((authorize) -> authorize
385388
.anyRequest().authenticated()
386389
)
387390
.oauth2Login(withDefaults());
@@ -475,7 +478,7 @@ public class OAuth2LoginConfig {
475478
@Bean
476479
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
477480
http
478-
.authorizeHttpRequests(authorize -> authorize
481+
.authorizeHttpRequests((authorize) -> authorize
479482
.anyRequest().authenticated()
480483
)
481484
.oauth2Login(withDefaults());
Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
[[new]]
2-
= What's New in Spring Security 6.5
2+
= What's New in Spring Security 7.0
33

4-
Spring Security 6.5 provides a number of new features.
4+
Spring Security 7.0 provides a number of new features.
55
Below are the highlights of the release, or you can view https://github.com/spring-projects/spring-security/releases[the release notes] for a detailed listing of each feature and bug fix.
66

7-
== New Features
7+
== Web
88

9-
* Support for automatic context-propagation with Micrometer (https://github.com/spring-projects/spring-security/issues/16665[gh-16665])
9+
* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
10+
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]
1011

11-
== Breaking Changes
12+
== OAuth 2.0
1213

13-
=== Observability
14+
==== Username Expression Support for Nested Attributes - https://github.com/spring-projects/spring-security/pull/16390[gh-16390]
1415

15-
The `security.security.reached.filter.section` key name was corrected to `spring.security.reached.filter.section`.
16-
Note that this may affect reports that operate on this key name.
16+
OAuth2 Client now supports SpEL expressions for extracting usernames from nested UserInfo responses, eliminating the need for custom `OAuth2UserService` implementations in many cases. This is particularly useful for APIs like Twitter API v2 that return nested user data:
1717

18-
== OAuth
18+
[source,yaml]
19+
----
20+
spring:
21+
security:
22+
oauth2:
23+
client:
24+
provider:
25+
twitter:
26+
user-info-uri: https://api.twitter.com/2/users/me
27+
username-expression: "data.username" # Access nested username
28+
----
1929

20-
* https://github.com/spring-projects/spring-security/pull/16386[gh-16386] - Enable PKCE for confidential clients using `ClientRegistration.clientSettings.requireProofKey=true` for xref:servlet/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[servlet] and xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[reactive] applications
30+
The `usernameExpression` property supports various SpEL expressions:
2131

22-
== WebAuthn
23-
24-
* https://github.com/spring-projects/spring-security/pull/16282[gh-16282] - xref:servlet/authentication/passkeys.adoc#passkeys-configuration-persistence[JDBC Persistence] for WebAuthn/Passkeys
25-
* https://github.com/spring-projects/spring-security/pull/16397[gh-16397] - Added the ability to configure a custom `HttpMessageConverter` for Passkeys using the optional xref:servlet/authentication/passkeys.adoc#passkeys-configuration[`messageConverter` property] on the `webAuthn` DSL.
26-
* https://github.com/spring-projects/spring-security/pull/16396[gh-16396] - Added the ability to configure a custom xref:servlet/authentication/passkeys.adoc#passkeys-configuration-pkccor[`PublicKeyCredentialCreationOptionsRepository`]
27-
28-
== One-Time Token Login
29-
30-
* https://github.com/spring-projects/spring-security/issues/16291[gh-16291] - `oneTimeTokenLogin()` now supports customizing GenerateOneTimeTokenRequest xref:servlet/authentication/onetimetoken.adoc#customize-generate-token-request[via GenerateOneTimeTokenRequestResolver]
32+
- Simple attributes: `"username"` or `"['username']"`
33+
- Nested attributes: `"data.username"` or `"profile.personal_info.display_name"`
34+
- Conditional expressions: `"preferred_username != null ? preferred_username : email"`
35+
- Array access: `"users[0].name"`
36+
- Safe navigation: `"user_info?.name ?: 'anonymous'"`

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

Lines changed: 56 additions & 8 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,23 @@ 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 7.0
341+
*/
342+
public String getUsernameExpression() {
343+
return this.usernameExpression;
344+
}
345+
334346
}
335347

336348
}
@@ -370,8 +382,11 @@ public static final class Builder implements Serializable {
370382

371383
private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER;
372384

385+
@Deprecated
373386
private String userNameAttributeName;
374387

388+
private String usernameExpression;
389+
375390
private String jwkSetUri;
376391

377392
private String issuerUri;
@@ -399,6 +414,7 @@ private Builder(ClientRegistration clientRegistration) {
399414
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
400415
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
401416
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
417+
this.usernameExpression = clientRegistration.providerDetails.userInfoEndpoint.usernameExpression;
402418
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
403419
this.issuerUri = clientRegistration.providerDetails.issuerUri;
404420
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
@@ -552,14 +568,43 @@ public Builder userInfoAuthenticationMethod(AuthenticationMethod userInfoAuthent
552568
}
553569

554570
/**
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
571+
* Sets the username attribute name. This method automatically converts the
572+
* attribute name to a SpEL expression for backward compatibility.
573+
*
574+
* <p>
575+
* This is a convenience method that internally calls
576+
* {@link #usernameExpression(String)} with the attribute name wrapped in bracket
577+
* notation.
578+
* @param userNameAttributeName the username attribute name
559579
* @return the {@link Builder}
560580
*/
561581
public Builder userNameAttributeName(String userNameAttributeName) {
562582
this.userNameAttributeName = userNameAttributeName;
583+
if (userNameAttributeName != null) {
584+
this.usernameExpression = "['" + userNameAttributeName + "']";
585+
}
586+
return this;
587+
}
588+
589+
/**
590+
* Sets the SpEL expression used to extract the username from user info response.
591+
*
592+
* <p>
593+
* Examples:
594+
* <ul>
595+
* <li>Simple attribute: {@code "['username']"} or {@code "username"}</li>
596+
* <li>Nested attribute: {@code "data.username"}</li>
597+
* <li>Complex expression: {@code "user_info?.name ?: 'anonymous'"}</li>
598+
* <li>Array access: {@code "users[0].name"}</li>
599+
* <li>Conditional:
600+
* {@code "preferred_username != null ? preferred_username : email"}</li>
601+
* </ul>
602+
* @param usernameExpression the SpEL expression for username extraction
603+
* @return the {@link Builder}
604+
* @since 7.0
605+
*/
606+
public Builder usernameExpression(String usernameExpression) {
607+
this.usernameExpression = usernameExpression;
563608
return this;
564609
}
565610

@@ -672,7 +717,10 @@ private ProviderDetails createProviderDetails(ClientRegistration clientRegistrat
672717
providerDetails.tokenUri = this.tokenUri;
673718
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
674719
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
720+
721+
providerDetails.userInfoEndpoint.usernameExpression = this.usernameExpression;
675722
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;
723+
676724
providerDetails.jwkSetUri = this.jwkSetUri;
677725
providerDetails.issuerUri = this.issuerUri;
678726
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);

0 commit comments

Comments
 (0)