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 2 commits into
base: main
Choose a base branch
from

Conversation

yybmion
Copy link
Contributor

@yybmion yybmion commented Apr 1, 2025

This PR adds support for extracting usernames from nested properties in OAuth2 user info responses using SpEL expressions, addressing the limitation where providers wrap user data in nested objects.

Fixes #16390

Problem

OAuth2 providers (like X/Twitter) wrap user data in nested objects, requiring complex workarounds to extract usernames.

Solution

Commit 1: Allow injecting principal name into DefaultOAuth2User

  • Add username field to DefaultOAuth2User
  • Add static factory method withUsername() for direct username injection
  • Deprecate constructor that uses nameAttributeKey lookup
  • Maintain backward compatibility and serialization format

Commit 2: Add SpEL support for nested username extraction

  • Add usernameExpression property to ClientRegistration
  • Add username field to OAuth2UserAuthority
  • Support SpEL expressions for nested property access (e.g., "data.username")
  • Use SimpleEvaluationContext for secure expression evaluation
  • Update both DefaultOAuth2UserService and DefaultReactiveOAuth2UserService
  • Pass evaluated username directly to OAuth2UserAuthority for Expose user name attribute name in OAuth2UserAuthority #15012 compatibility

Backward Compatibility

  • No breaking changes - all existing APIs continue to work
  • Deprecated APIs remain functional with clear migration path

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 1, 2025
@rwinch rwinch self-assigned this May 7, 2025
@rwinch rwinch added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels May 7, 2025
@rwinch
Copy link
Member

rwinch commented May 7, 2025

Thanks for the PR @yybmion! I wonder if this might be better implemented using SpEL to provide more powerful options for resolving the username. What are your thoughts?

@rwinch rwinch added the status: waiting-for-feedback We need additional information before we can continue label May 7, 2025
@yybmion
Copy link
Contributor Author

yybmion commented May 7, 2025

Hi @rwinch , Thank you for your guidance on this.

I initially chose the dot notation approach because it offers a simple and intuitive solution specifically for the nested user-name-attribute issue.

However, I can see the value in using SpEL as you suggested. While I think it may be slightly more complex, SpEL provides much greater extensibility for future use cases beyond simple nested structures. The consistency with other parts of the Spring Security framework is also a advantage.

If you confirm that SpEL is the preferred direction, I'd be happy to update the PR accordingly.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 7, 2025
@rwinch
Copy link
Member

rwinch commented May 13, 2025

Yes. Please provide an implementation that uses SpEL.

@rwinch rwinch added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 13, 2025
@yybmion
Copy link
Contributor Author

yybmion commented May 18, 2025

Hello @rwinch, I'd like to clarify your feedback on my PR about supporting nested properties in the user-name-attribute.

Did you mean that I should implement support for expressions like #{data.username} in the properties and yml configuration files to handle nested structures? (Instead using data.username)

Thank you for your guidance!

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 18, 2025
@rwinch
Copy link
Member

rwinch commented May 21, 2025

I think an outline would be:

Allow Injecting the Principal Name into DefaultOAuth2User

Right now OAuth2UserService requires defining the name property as an attribute name. This limits the flexibility of resolving the name.

Update DefaultOAuth2User to allow injecting the name (rather than a property for obtaining the name). You would add a new member variable of username. This would likely require using a static factory method to distinguish from the existing constructor since the types of the nameAttributeKey and username are the same.

You can remove the nameAttributeKey member variable and instead populate the username using the attributes[nameAttributeKey] in the constructor if nameAttributeKey was specified.

Deprecate the old DefaultOAuth2User constructor in favor of injecting the name directly and update the usage of DefaultOAuth2User within Spring Security to no longer use the deprecated constructor.

Add SpEL Support

Update ClientRegistration to have a property named usernameExpression and remove the userNameAttributeName property but preserving and deprecating the methods which instead populate the usernameExpression member variable. This should be passive since the usernameAttributeName is a valid SpEL expression.

Update OAuth2UserService to extract the username using the usernameExpression property from the ClientRegistration as a SpEL expression with the attributes as the root object. Create the DefaultOAuth2User with the username injected into it rather than using the nameAttributeKey.

I think creating these as two separate commits is valuable since they are useful on their own. Let me know your thoughts.

@rwinch rwinch added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels May 21, 2025
@yybmion

This comment was marked as resolved.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels May 25, 2025
@yybmion yybmion force-pushed the gh-16390 branch 7 times, most recently from c5aa6d9 to 972afda Compare June 12, 2025 15:30
@yybmion yybmion changed the title Add support for nested user-name-attribute using dot notation Add SpEL support for nested username extraction in OAuth2 user info Jun 12, 2025
@yybmion
Copy link
Contributor Author

yybmion commented Jun 13, 2025

Hi @rwinch thank you for the detailed guide.

I've implemented the SpEL-based approach as outlined in the original issue. The solution provides

  • SpEL expressions for nested property access (e.g., "data.username")
  • Full backward compatibility with existing userNameAttributeName
  • Support for both sync and reactive implementations

Please see the PR description for detailed changes.

Copy link
Member

@rwinch rwinch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the updates. This is taking shape. I've provided feedback inline.

}
else {
try {
ExpressionParser parser = new SpelExpressionParser();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExpressionParser should be reused as it is thread safe. You can safely make this a final member variable.

else {
try {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use SimpleEvaluationContext instead since StandardEvaluationContext is less safe and unnecessary.

private String evaluateUsername(Map<String, Object> attributes, String usernameExpression) {
Object value = null;

if (attributes.containsKey(usernameExpression)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to remove this if statement and always use the expression if it is set as described on the builder.

return this.usernameExpression;
}

return this.userNameAttributeName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please just return the usernameExpression without defaulting here. Instead ensure that the userNameAttributeName setter sets the usernameExpression.

@@ -672,7 +700,14 @@ private ProviderDetails createProviderDetails(ClientRegistration clientRegistrat
providerDetails.tokenUri = this.tokenUri;
providerDetails.userInfoEndpoint.uri = this.userInfoUri;
providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod;
if (this.usernameExpression != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you update the builder to set the expression when setting the userNameAttributeName, then the if statement can be removed

return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);

String evaluatedUsername = evaluateUsername(attributes, usernameExpression);
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, usernameExpression);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now passing in usernameExpression to getAuthorities which means that the usernameExpression is now passed into OAuth2UserAuthority in place of userNameAttributeName.

I think when users use more complex expressions it will break functionality introduced gh-15012 because the OAuth2UserAuthority only has an attribute name and not a property. We will need to figure out how to integrate cleanly with gh-15012 From the PR it sounds like we could just expose the username rather than the attribute, but I'm not certain how the integration is being used cc @filiphr

Copy link
Contributor Author

@yybmion yybmion Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for highlighting this important integration issue with gh-15012!

I've addressed this by following your suggestion to expose the username directly rather than the attribute name.

  • Added username field to OAuth2UserAuthority with getUsername() method
  • Builder pattern: OAuth2UserAuthority.withUsername(evaluatedUsername)
  • Pass the evaluated username result (not the SpEL expression) to OAuth2UserAuthority

I think this way, OAuth2UserAuthority always receives the final evaluated username string, preserving compatibility with gh-15012 regardless of expression complexity.

*/
@Deprecated
public Builder userNameAttributeName(String userNameAttributeName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the place where I think you should default the usernameExpression to "['" + userNameAttributeName + "']"`.

*/
@Deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that you can remove the Deprecated as this simplifies setting the expression. Just update the Javadoc.

public Builder userNameAttributeName(String userNameAttributeName) {
this.userNameAttributeName = userNameAttributeName;
return this;
}

/**
* Sets the SpEL expression used to extract the username from user info response.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please provide a few examples for the value.

* @param username the user's name
* @return a new {@code DefaultOAuth2User}
*/
public static DefaultOAuth2User withUsername(Collection<? extends GrantedAuthority> authorities,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think mentioned a factory method previously, but I do not like this with multiple arguments. Please create a public static Builder withUsername(String) method instead.

@rwinch rwinch added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jun 23, 2025
- Add username field to DefaultOAuth2User for direct name injection
- Add Builder pattern with DefaultOAuth2User.withUsername(String) static factory method
- Deprecate constructor that uses nameAttributeKey lookup in favor of Builder pattern
- Update Jackson mixins to support username field serialization/deserialization

This change prepares for SpEL support in the next commit.

Signed-off-by: yybmion <yunyubin54@gmail.com>
@yybmion yybmion force-pushed the gh-16390 branch 3 times, most recently from 0f6d3ce to 5798e87 Compare June 25, 2025 14:27
@yybmion
Copy link
Contributor Author

yybmion commented Jun 25, 2025

Thanks for the feedback @rwinch ! I've addressed all the review comments and updated the code accordingly.

But the build failure appears to be related to Kotlin version. What would you recommend I do?

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 25, 2025
- 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 spring-projectsgh-15012 compatibility
- Add Builder pattern to DefaultOAuth2User
- Support nested property access (e.g., "data.username")

Fixes spring-projectsgh-16390

Signed-off-by: yybmion <yunyubin54@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: feedback-provided Feedback has been provided type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify support of username as a nested property
3 participants