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
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.util.Assert;
Expand All @@ -30,10 +31,12 @@
* @author Josh Cummings
* @author Evgeniy Cheban
* @author Olivier Antoine
* @author Andrey Litvitski
* @since 5.1
*/
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

private Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter;
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

private String principalClaimName = JwtClaimNames.SUB;
Expand All @@ -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.

String principalClaimValue = jwt.getClaimAsString(this.principalClaimName);
return new JwtAuthenticationToken(jwt, authorities, principalClaimValue);
} else {
OAuth2AuthenticatedPrincipal principal = this.jwtPrincipalConverter.convert(jwt);
authorities.addAll(principal.getAuthorities());
return new JwtAuthenticationToken(jwt, principal, authorities);
}
}

/**
* 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).

*/
public void setJwtPrincipalConverter(
Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter) {
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
this.jwtPrincipalConverter = jwtPrincipalConverter;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Collection;
import java.util.Map;

import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.Transient;
Expand All @@ -29,6 +30,7 @@
* {@link Jwt} {@code Authentication}.
*
* @author Joe Grandja
* @author Andrey Litvitski
* @since 5.1
* @see AbstractOAuth2TokenAuthenticationToken
* @see Jwt
Expand Down Expand Up @@ -72,6 +74,22 @@ public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> au
this.name = name;
}

/**
* Constructs a {@code JwtAuthenticationToken} using the provided parameters.
* @param jwt the JWT
* @param principal the principal
* @param authorities the authorities assigned to the JWT
*/
public JwtAuthenticationToken(Jwt jwt, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(jwt, principal, jwt, authorities);
this.setAuthenticated(true);
if (principal instanceof AuthenticatedPrincipal) {
this.name = ((AuthenticatedPrincipal) principal).getName();
} else {
this.name = jwt.getSubject();
}
}

@Override
public Map<String, Object> getTokenAttributes() {
return this.getToken().getClaims();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;

/**
* A {@link Converter} that takes a {@link Jwt} and converts it into a
Expand All @@ -41,6 +42,7 @@
* {@link BearerTokenAuthentication}.
*
* @author Josh Cummings
* @author Andrey Litvitski
* @since 5.2
*/
public final class JwtBearerTokenAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
Expand All @@ -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.
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}.

* @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?

*/
public void setJwtPrincipalConverter(
Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter) {
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
this.jwtAuthenticationConverter.setJwtPrincipalConverter(jwtPrincipalConverter);
}

}
Loading