Skip to content

Support custom OAuth2AuthenticatedPrincipal in Jwt-based authentication flow #17191

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

Conversation

therepanic
Copy link
Contributor

This PR implements a simpler approach, as suggested by @jzheaux, to support OAuth2AuthenticatedPrincipal injection into JwtAuthenticationToken

Resolves: #6237

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 1, 2025
@therepanic
Copy link
Contributor Author

therepanic commented Jun 1, 2025

I think the current solution can't be merged now since at the very least we don't have tests. I would like to hear feedback if I have understood the solution to the current problem correctly.

Also I guess we can't add a constructor to (Jwt jwt, Object principal, Collection<GrantedAuthority> authorities) as written in #32 because we already have a constructor with 3 arguments (Jwt jwt, Collection<? extends GrantedAuthority> authorities, String name). Because of this we have a build failing now.

@jgrandja jgrandja 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 Jun 11, 2025
@jzheaux jzheaux self-assigned this Jun 17, 2025
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 PR, @therepanic! I've left some feedback inline that I hope will also address your question about the new JwtAuthenticationToken constructor.

After you review my comments, if we are agreed, will you also please add tests that confirm the new setter methods work?

* Sets the {@link Converter Converter&lt;Jwt, Collection&lt;OAuth2AuthenticatedPrincipal&gt;&gt;}
* to use.
* @param jwtPrincipalConverter The converter
* @since 6.5.0
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be @since 7.0 as that's the current feature release (now that 6.5 is released, it will only take bug fixes).

@@ -42,8 +45,26 @@ public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthen
public final AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);

String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
if (this.jwtPrincipalConverter == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

With the help of a private inner class, I believe we can adapt Jwt into an OAuth2AuthenticatedPrincipal so that this if statement is not necessary. Consider adding a class like this:

private static final class JwtAuthenticatedPrincipal extends Jwt implements OAuth2AuthenticatedPrincipal {

	private final String principalClaimName;

	JwtAuthenticatedPrincipal(Jwt jwt) {
		this(jwt, JwtClaimNames.SUB);
	}

	JwtAuthenticatedPrincipal(Jwt jwt, String principalClaimName) {
		super(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getHeaders(), jwt.getClaims());
		this.principalClaimName = principalClaimName;
	}

	@Override
	public Map<String, Object> getAttributes() {
		return getClaims();
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return List.of();
	}

	@Override
	public String getName() {
		return getClaimAsString(this.principalClaimName);
	}

}

Then, I believe jwtPrincipalConverter can default to JwtAuthenticatedPrincipal::new and principalClaimName can be removed by having setPrincipalClaimName use JwtAuthenticatedPrincipal as well.

@@ -58,4 +60,16 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
return new BearerTokenAuthentication(principal, accessToken, authorities);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should use the principal provided by the principal conversion instead of constructing a new DefaultOAuth2AuthenticatedPrincipal.

It might be a little simpler at this point to change this class to have its own Converter<Jwt, OAuth2AuthenticatedPrincipal> and Converter<Jwt, Collection<GrantedAuthority>> references.

Copy link
Contributor Author

@therepanic therepanic Jul 31, 2025

Choose a reason for hiding this comment

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

Adding your own links to converters will indeed be easier, but I have a question. Do we want to create the same jwtPrincipalConverter in JwtBearerTokenAuthenticationConverter as in JwtAuthenticationConverter? That is, by default with the type JwtAuthenticatedPrincipal::new? If so, then in the version you suggested, this class is private, and therefore we will not be able to create a converter by default. Should we make JwtAuthenticatedPrincipal package-private? Or we can just create getters for converters in JwtAuthenticationConverter and use them?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend that the principal converter in JwtBearerTokenAuthenticationConverter be one that creates a DefaultOAuth2AuthenticatedPrincipal in order to preserve existing behavior.

And I'm not sure, but I believe that may require calling the authorities converter twice, once to populate DefaultOAuth2AuthenticatedPrincipal and once to populate BearerTokenAuthentication; however, I don't think that's a big issue as the point is to allow the strategy to be overridden.

* Sets the {@link Converter Converter&lt;Jwt, Collection&lt;OAuth2AuthenticatedPrincipal&gt;&gt;}
* to use.
* @param jwtPrincipalConverter The converter
* @since 6.5.0
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 update this to @since 7.0?

@@ -58,4 +60,16 @@ public AbstractAuthenticationToken convert(Jwt jwt) {
return new BearerTokenAuthentication(principal, accessToken, authorities);
}

/**
* Sets the {@link Converter Converter&lt;Jwt, Collection&lt;OAuth2AuthenticatedPrincipal&gt;&gt;}
* to use.
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 state the default value as well? For example:

* <p>By default, constructs a {@link DefaultOAuth2AuthenticatedPrincipal} based on the claims and authorities derived from the {@link Jwt}.

Closes spring-projectsgh-6237

Signed-off-by: Andrey Litvitski <andrey1010102008@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) type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support UserDetailsService components in OAuth2 Resource Server flows
4 participants