Skip to content

Add SpEL support for nested username extraction in OAuth2 user info #16857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ public final class ClientRegistration {
private String uri; <14>
private AuthenticationMethod authenticationMethod; <15>
private String userNameAttributeName; <16>
private String usernameExpression; <17>
}
}
public static final class ClientSettings {
private boolean requireProofKey; // <17>
private boolean requireProofKey; // <18>
}
}
----
Expand All @@ -67,8 +67,9 @@ The name may be used in certain scenarios, such as when displaying the name of t
<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user.
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
The supported values are *header*, *form* and *query*.
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
<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.
<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"`).
<18> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.

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].

Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ The following table outlines the mapping of the Spring Boot OAuth Client propert

|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute`
|`providerDetails.userInfoEndpoint.userNameAttributeName`

|`spring.security.oauth2.client.provider._[providerId]_.username-expression`
|`providerDetails.userInfoEndpoint.usernameExpression`
|===

[TIP]
Expand Down
8 changes: 5 additions & 3 deletions docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ public final class ClientRegistration {
private String uri; <14>
private AuthenticationMethod authenticationMethod; <15>
private String userNameAttributeName; <16>
private String usernameExpression; <17>

}
}

public static final class ClientSettings {
private boolean requireProofKey; // <17>
private boolean requireProofKey; // <18>
}
}
----
Expand All @@ -68,8 +69,9 @@ This information is available only if the Spring Boot property `spring.security.
<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims and attributes of the authenticated end-user.
<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint.
The supported values are *header*, *form*, and *query*.
<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
<17> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.
<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.
<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").
<18> [[oauth2Client-client-registration-requireProofKey]]`requireProofKey`: If `true` or if `authorizationGrantType` is `none`, then PKCE will be enabled by default.

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].

Expand Down
3 changes: 3 additions & 0 deletions docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ The following table outlines the mapping of the Spring Boot OAuth Client propert

|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute`
|`providerDetails.userInfoEndpoint.userNameAttributeName`

|`spring.security.oauth2.client.provider._[providerId]_.username-expression`
|`providerDetails.userInfoEndpoint.usernameExpression`
|===

[TIP]
Expand Down
26 changes: 26 additions & 0 deletions docs/modules/ROOT/pages/whats-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,29 @@ Below are the highlights of the release, or you can view https://github.com/spri

* Added javadoc:org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor[]
* Added OAuth2 Support for xref:features/integrations/rest/http-interface.adoc[HTTP Interface Integration]

== OAuth 2.0

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

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:

[source,yaml]
----
spring:
security:
oauth2:
client:
provider:
twitter:
user-info-uri: https://api.twitter.com/2/users/me
username-expression: "data.username" # Access nested username
----

The `usernameExpression` property supports various SpEL expressions:

- Simple attributes: `"username"` or `"['username']"`
- Nested attributes: `"data.username"` or `"profile.personal_info.display_name"`
- Conditional expressions: `"preferred_username != null ? preferred_username : email"`
- Array access: `"users[0].name"`
- Safe navigation: `"user_info?.name ?: 'anonymous'"`
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -32,6 +32,7 @@
* This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}.
*
* @author Joe Grandja
* @author YooBin Yoon
* @since 5.3
* @see DefaultOAuth2User
* @see OAuth2ClientJackson2Module
Expand All @@ -45,7 +46,7 @@ abstract class DefaultOAuth2UserMixin {
@JsonCreator
DefaultOAuth2UserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("attributes") Map<String, Object> attributes,
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
@JsonProperty("nameAttributeKey") String nameAttributeKey, @JsonProperty("username") String username) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true)
@JsonIgnoreProperties(value = { "attributes", "username" }, ignoreUnknown = true)
abstract class DefaultOidcUserMixin {

@JsonCreator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
*
* @author Joe Grandja
* @author Michael Sosa
* @author Yoobin Yoon
* @since 5.0
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2">Section 2
* Client Registration</a>
Expand Down Expand Up @@ -299,8 +300,11 @@ public class UserInfoEndpoint implements Serializable {

private AuthenticationMethod authenticationMethod = AuthenticationMethod.HEADER;

@Deprecated
private String userNameAttributeName;

private String usernameExpression;

UserInfoEndpoint() {
}

Expand All @@ -322,15 +326,23 @@ public AuthenticationMethod getAuthenticationMethod() {
}

/**
* Returns the attribute name used to access the user's name from the user
* info response.
* @return the attribute name used to access the user's name from the user
* info response
* @deprecated Use {@link #getUsernameExpression()} instead
*/
@Deprecated
public String getUserNameAttributeName() {
return this.userNameAttributeName;
}

/**
* Returns the SpEL expression used to extract the username from user info
* response.
* @return the SpEL expression for username extraction
* @since 7.0
*/
public String getUsernameExpression() {
return this.usernameExpression;
}

}

}
Expand Down Expand Up @@ -370,8 +382,11 @@ public static final class Builder implements Serializable {

private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER;

@Deprecated
private String userNameAttributeName;

private String usernameExpression;

private String jwkSetUri;

private String issuerUri;
Expand Down Expand Up @@ -399,6 +414,7 @@ private Builder(ClientRegistration clientRegistration) {
this.userInfoUri = clientRegistration.providerDetails.userInfoEndpoint.uri;
this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod;
this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName;
this.usernameExpression = clientRegistration.providerDetails.userInfoEndpoint.usernameExpression;
this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri;
this.issuerUri = clientRegistration.providerDetails.issuerUri;
Map<String, Object> configurationMetadata = clientRegistration.providerDetails.configurationMetadata;
Expand Down Expand Up @@ -552,14 +568,43 @@ public Builder userInfoAuthenticationMethod(AuthenticationMethod userInfoAuthent
}

/**
* Sets the attribute name used to access the user's name from the user info
* response.
* @param userNameAttributeName the attribute name used to access the user's name
* from the user info response
* Sets the username attribute name. This method automatically converts the
* attribute name to a SpEL expression for backward compatibility.
*
* <p>
* This is a convenience method that internally calls
* {@link #usernameExpression(String)} with the attribute name wrapped in bracket
* notation.
* @param userNameAttributeName the username attribute name
* @return the {@link Builder}
*/
public Builder userNameAttributeName(String userNameAttributeName) {
this.userNameAttributeName = userNameAttributeName;
if (userNameAttributeName != null) {
this.usernameExpression = "['" + userNameAttributeName + "']";
}
return this;
}

/**
* Sets the SpEL expression used to extract the username from user info response.
*
* <p>
* Examples:
* <ul>
* <li>Simple attribute: {@code "['username']"} or {@code "username"}</li>
* <li>Nested attribute: {@code "data.username"}</li>
* <li>Complex expression: {@code "user_info?.name ?: 'anonymous'"}</li>
* <li>Array access: {@code "users[0].name"}</li>
* <li>Conditional:
* {@code "preferred_username != null ? preferred_username : email"}</li>
* </ul>
* @param usernameExpression the SpEL expression for username extraction
* @return the {@link Builder}
* @since 7.0
*/
public Builder usernameExpression(String usernameExpression) {
this.usernameExpression = usernameExpression;
return this;
}

Expand Down Expand Up @@ -672,7 +717,10 @@ private ProviderDetails createProviderDetails(ClientRegistration clientRegistrat
providerDetails.tokenUri = this.tokenUri;
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;

providerDetails.userInfoEndpoint.usernameExpression = this.usernameExpression;
providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName;

providerDetails.jwkSetUri = this.jwkSetUri;
providerDetails.issuerUri = this.issuerUri;
providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata);
Expand Down
Loading