Skip to content
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

Expose getter for nameAttributeKey in OAuth2AuthenticatedPrincipal #16003

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

andreblanke
Copy link

Given a DefaultOidcUser or DefaultOAuth2User, it is currently not possible to create a faithful copy of the instance since the constructors require a nameAttributeKey for the all-arg constructor:

public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken,
		String nameAttributeKey)
public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes,
		String nameAttributeKey)

which is not accessible.

This issue has been mentioned before in #14461 (comment) where a custom ExtendedOidcUser decorator class is used to work around it. While that works, I feel that this should be possible without introducing a separate class using just the public API.

The PR aims to change this by adding the OAuth2AuthenticatedPrincipal.getNameAttributeKey method in 464b078. This is a breaking change for classes implementing the interface.

With the nameAttributeKey now available from within the interface, I figured it also makes sense to provide a default implementation for OAuth2AuthenticatedPrincipal.getName in c7bcf86.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Oct 26, 2024
Copy link
Contributor

@jzheaux jzheaux left a comment

Choose a reason for hiding this comment

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

Thanks, @andreblanke! I've left some feedback inline.

@jzheaux jzheaux self-assigned this Nov 1, 2024
@jzheaux jzheaux added type: enhancement A general enhancement in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 1, 2024
@jzheaux
Copy link
Contributor

jzheaux commented Nov 21, 2024

Hi, @andreblanke! Are you able to make the requested changes? No rush, if I don't hear from you in about a week, I'm happy to make the changes myself.

@jzheaux jzheaux added this to the 6.5.x milestone Nov 21, 2024
@andreblanke andreblanke force-pushed the oauth2-name-attribute-key branch from c7bcf86 to c92a4a0 Compare November 24, 2024 16:38
@andreblanke andreblanke force-pushed the oauth2-name-attribute-key branch from c92a4a0 to 25f8bec Compare November 24, 2024 16:39
@andreblanke
Copy link
Author

Hi there @jzheaux. First of all thank you for your feedback. Sorry, I've been postponing this. I'd like to finish the changes in the next few days once we've decided on an implementation (getter for nameAttributeKey vs. new constructors).

@andreblanke andreblanke force-pushed the oauth2-name-attribute-key branch from 4848c6b to 6b815d2 Compare December 8, 2024 11:19
Copy link
Contributor

@jzheaux jzheaux left a comment

Choose a reason for hiding this comment

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

Thanks for the updates, @andreblanke! I've left some feedback inilne.

@@ -96,7 +96,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
OAuth2AccessToken token = userRequest.getAccessToken();
Map<String, Object> attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
Collection<GrantedAuthority> authorities = getAuthorities(token, attributes, userNameAttributeName);
return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
return new DefaultOAuth2User(attributes.get(userNameAttributeName).toString(), attributes, authorities);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it guaranteed at this point that attribute.get(userNameAttributeName) is non-null? If not, please do a null check so that the code can offer a more informative error message.

Copy link
Author

Choose a reason for hiding this comment

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

You're right, attributes is nullable and an assertion is appropriate here, basically what my newly introduced DefaultOAuth2User.getNameFromAttributes is doing.

However, I can't help but feel that "get name from attributes using nameAttributeKey with assertions" is a frequent enough operation (4 mostly similar occurrences have been touched by this PR) to maybe warrant a separate method.

What would you think about changing the constructors to use name instead of nameAttributeKey (as done by the PR) but introducing static factory methods for DefaultOidcUser and DefaultOAuth2User taking nameAttributeKey as parameter instead of name?
That would get rid of the old, inconsistent constructors (in terms of both the parameter order and the usage of nameAttributeKey instead of name) while giving a clear upgrade path. Additionally, OidcUserAuthority.collectClaims mentioned above would not need to be duplicated or made accessible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think that could work. Perhaps a builder in that case, though, as that's more typical of Spring Security classes. It's also a bit more resilient against member additional member variables getting added over time.

Perhaps:

public static DefaultOAuth2User.Builder withNameAttributeKey(String nameAttributeKey);

Copy link
Author

Choose a reason for hiding this comment

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

I've added builders for DefaultOAuth2User and DefaultOidcUser. Right now the new "canonical constructor" is still public, but it might be better to make it private to be in line with, e.g., OAuth2AuthorizeRequest?

* @param authorities the authorities granted to the user
*/
public DefaultOidcUser(OidcIdToken idToken, Collection<? extends GrantedAuthority> authorities) {
this(null, idToken, authorities);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this correct? It seems like it should be idToken.getAttribute(IdTokenClaimNames.SUB).

Same question for DefaultOidcUser(OidcIdToken, OidcUserInfo, Collection<? extends GrantedAuthority>)

Copy link
Author

@andreblanke andreblanke Dec 9, 2024

Choose a reason for hiding this comment

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

Both constructors ultimately delegate to

public DefaultOAuth2User(String name, Map<String, Object> attributes,
			Collection<? extends GrantedAuthority> authorities) {
    // ...
    this.name = (name != null) ? name : (String) this.attributes.get("sub");
}

which is consistent with the constructor behavior of DefaultOAuth2AuthenticatedPrincipal, although using IdTokenClaimNames.SUB instead of "sub" would be more appropriate (nevermind, OAuth2 core classes should likely not access OpenId Connect-related classes).

Copy link
Author

Choose a reason for hiding this comment

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

However, for that to succeed I'd still need to remove the Assert.notNull(name, "name cannot be null"); from the constructor which is still present at the moment.

@JsonCreator
DefaultOidcUserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("idToken") OidcIdToken idToken, @JsonProperty("userInfo") OidcUserInfo userInfo,
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
}

@JsonCreator
DefaultOidcUserMixin(@JsonProperty("name") String name,
Copy link
Contributor

Choose a reason for hiding this comment

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

Will you please add DefaultOAuth2UserMixinTests and DefaultOidcUserMixinTests to ensure that serializations of either arrangement can correctly deserialize?

Copy link
Author

Choose a reason for hiding this comment

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

It seems that Jackson only supports a single @JsonCreator for each class, meaning the backwards compatibility would be a bit more involved (perhaps using a custom deserializer?).

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, the next evolution of that mixin would be to use a deserializer.

Accepts null name for consistency with DefaultOAuth2AuthenticatedPrincipal.

The private static method DefaultOAuth2User.getNameFromAttributes was added in order to keep assertions about nameAttributeKey and the looked up name for the deprecated constructor.
@andreblanke andreblanke force-pushed the oauth2-name-attribute-key branch from 6b815d2 to ad945b6 Compare December 15, 2024 22:48
@andreblanke andreblanke force-pushed the oauth2-name-attribute-key branch from ad945b6 to ced9228 Compare December 15, 2024 22:58
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) type: enhancement A general enhancement
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

3 participants