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

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 force-pushed the gh-16390 branch 2 times, most recently from 6511e92 to 7e98a40 Compare June 30, 2025 10:33
@yybmion
Copy link
Contributor Author

yybmion commented Jun 30, 2025

Hi @rwinch. I've addressed all the review feedback and updated the documentation as requested.
Please let me know if there are any areas in the documentation that need further improvement or clarification.

@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 30, 2025
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 changes.

I've included some comments inline around Spring Boot not supporting the new property yet. We will need to create a ticket with them once we merge here. Once they add the support we can add the documentation back.

Please go through and find/remove any usage (within main) for any of the constructors/methods that you deprecated. For example, I see that OidcUserRequestUtils still references UserInfoEndpoint.getUserNameAttributeName() We should update it to use non-deprecated methods.

@@ -36,12 +36,12 @@ public final class ClientRegistration {
private String uri; <14>
private AuthenticationMethod authenticationMethod; <15>
private String userNameAttributeName; <16>
private String usernameExpression; <17>
Copy link
Member

Choose a reason for hiding this comment

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

Can you please fix this to use tabs?

@@ -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`
Copy link
Member

Choose a reason for hiding this comment

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

This won't work yet because Spring Boot has not added support. Please remove it for now. If you like, you can save it in another branch in its own commit and we can use it when Spring Boot adds the support.

@@ -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`
Copy link
Member

Choose a reason for hiding this comment

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

This won't work yet because Spring Boot has not added support. Please remove it for now. If you like, you can save it in another branch in its own commit and we can use it when Spring Boot adds the support.


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]
Copy link
Member

Choose a reason for hiding this comment

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

The yaml won't work yet because Spring Boot has not added support. Please remove it for now. If you like, you can save it in another branch in its own commit and we can use it when Spring Boot adds the support.

@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 Jul 1, 2025
@yybmion yybmion force-pushed the gh-16390 branch 2 times, most recently from 5bbd69c to 0ca4bfd Compare July 2, 2025 05:52
@yybmion
Copy link
Contributor Author

yybmion commented Jul 2, 2025

Thank you for the review @rwinch !
I'm planning to create a separate commit to address the deprecated constructor/method usage you mentioned. However, I have a few questions that came up during the modifications.

I'm wondering about your thoughts on :

  1. Deprecating the nameAttributeKey field in DefaultOAuth2User constructors for consistency with the new usernameExpression approach
  2. Adding builder pattern to DefaultOidcUser since it extends DefaultOAuth2User and currently calls the deprecated parent constructor via super()

Should I include these changes in this PR?

@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 Jul 2, 2025
@rwinch
Copy link
Member

rwinch commented Jul 2, 2025

Thanks for reaching out and your patience as we put the final touches on this @yybmion

Deprecating the nameAttributeKey field in DefaultOAuth2User constructors for consistency with the new usernameExpression approach

Yes. I think that this is good. Generally, we want people to move to the builder instead.

Adding builder pattern to DefaultOidcUser since it extends DefaultOAuth2User and currently calls the deprecated parent constructor via super().

Yes I think that is good too. You will likely need to ensure the non-deprecated constructor in DefaultOAuth2User is protected so that it can be used by DefaultOidcUser.

Should I include these changes in this PR?

Yes please. It is important that we push people towards the new, more powerful, approach.

@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 Jul 2, 2025
@yybmion yybmion force-pushed the gh-16390 branch 3 times, most recently from e576f13 to 9cd831b Compare July 7, 2025 05:19
@yybmion
Copy link
Contributor Author

yybmion commented Jul 7, 2025

Thank you for the guidance, @rwinch !
I've updated the deprecated constructor/method usage in main based on your review feedback. After searching through the codebase, I found that most of the deprecated usage was in OidcUserRequestUtils using deprecated constructors/methods. The remaining usage was in test code and documentation, which I left unchanged as requested.

Additionally, since the username SpEL evaluation logic was being used in multiple places, I created a utility class OAuth2UsernameExpressionUtils to centralize this functionality and eliminate code duplication.

@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 Jul 7, 2025
@yybmion
Copy link
Contributor Author

yybmion commented Jul 23, 2025

Hi @rwinch,
I noticed that the "What's New" documentation for Spring Security 7 has recently been updated, which caused a conflict with the changes I previously added. I've resolved the conflict accordingly.

yybmion added 3 commits July 23, 2025 14:31
- 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 <[email protected]>
- 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
- Add Builder pattern to OAuth2UserAuthority
- Add Builder pattern to OidcUserAuthority with inherance support
- Add Builder pattern to DefaultOidcUser with inherance support
- Support nested property access (e.g., "data.username")
- Add usernameExpression property to ClientRegistration documentation
- Update What's New section

Fixes spring-projectsgh-16390

Signed-off-by: yybmion <[email protected]>
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 spring-projectsgh-16390

Signed-off-by: yybmion <[email protected]>
@rwinch
Copy link
Member

rwinch commented Jul 25, 2025

Thanks again for all of your work on this PR!

I'd like to encapsulate the logic (keep private) in OAuth2UsernameExpressionUtils so that we can refactor without breaking anything later. One example of such a refactoring is that at some point the ExpressionParser might need to be injected to allow resolving Bean references in the expressions. Even if we allow injecting the ExpressionParser, I'd still like to keep this logic encapsulated in case of other refactorings.

To work toward that end I created and resolved gh-17626. In the case where we invoke the OAuth2UserService (or the reactive equivalent), we could use the result to obtain the username. The problem is that OidcUserService (and the reactive equivalent) leverage userNameAttributeName even when the OAuth2Service is not invoked and it does not use the result from the delegate service even when it does invoke the user info endpoint.

To address those concerns I created gh-17627 gh-17628

I believe we could extract the logic for injecting the username rather than the attribute into a separate PR and merge that without being blocked since that behavior would be passive and not be impacted by the decisions of the issues above. Feel free to do this if you like. However, as of now we are blocked on merging resolving the username using an expression until those two issues are resolved.

cc @jgrandja

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