Polish Jwt Authentication Converter

- Replace conditional logic with adapter class
- Added tests

Issue gh-6237

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings 2026-02-26 06:18:55 -07:00
parent aabc9fc1cc
commit c208410a91
6 changed files with 104 additions and 33 deletions

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
@ -40,34 +42,26 @@ public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthen
private static final String AUTHORITY = FactorGrantedAuthority.BEARER_AUTHORITY;
private Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter;
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter = JwtAuthenticatedPrincipal::new;
private String principalClaimName = JwtClaimNames.SUB;
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
@Override
public final AbstractAuthenticationToken convert(Jwt jwt) {
Collection<GrantedAuthority> authorities = new HashSet<>(this.jwtGrantedAuthoritiesConverter.convert(jwt));
authorities.add(FactorGrantedAuthority.fromAuthority(AUTHORITY));
if (this.jwtPrincipalConverter == null) {
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);
}
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.
* Sets the {@link Converter Converter&lt;Jwt, OAuth2AuthenticatedPrincipal&gt;} to
* use.
* @param jwtPrincipalConverter The converter
* @since 6.5.0
* @since 7.1
*/
public void setJwtPrincipalConverter(
Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter) {
public void setJwtPrincipalConverter(Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter) {
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
this.jwtPrincipalConverter = jwtPrincipalConverter;
}
@ -92,7 +86,37 @@ public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthen
*/
public void setPrincipalClaimName(String principalClaimName) {
Assert.hasText(principalClaimName, "principalClaimName cannot be empty");
this.principalClaimName = principalClaimName;
this.jwtPrincipalConverter = (jwt) -> new JwtAuthenticatedPrincipal(jwt, principalClaimName);
}
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);
}
}
}

View File

@ -21,8 +21,8 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Transient;
import org.springframework.security.oauth2.jwt.Jwt;
@ -87,13 +87,15 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
* @param jwt the JWT
* @param principal the principal
* @param authorities the authorities assigned to the JWT
* @since 7.1
*/
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 {
}
else {
this.name = jwt.getSubject();
}
}

View File

@ -17,7 +17,6 @@
package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Collection;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
@ -47,29 +46,33 @@ import org.springframework.util.Assert;
*/
public final class JwtBearerTokenAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
private Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter = (
jwt) -> new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(),
this.jwtGrantedAuthoritiesConverter.convert(jwt));
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwt.getTokenValue(),
jwt.getIssuedAt(), jwt.getExpiresAt());
Map<String, Object> attributes = jwt.getClaims();
AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
Collection<GrantedAuthority> authorities = token.getAuthorities();
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(attributes, authorities);
Collection<GrantedAuthority> authorities = this.jwtGrantedAuthoritiesConverter.convert(jwt);
OAuth2AuthenticatedPrincipal principal = this.jwtPrincipalConverter.convert(jwt);
return new BearerTokenAuthentication(principal, accessToken, authorities);
}
/**
* Sets the {@link Converter Converter&lt;Jwt, Collection&lt;OAuth2AuthenticatedPrincipal&gt;&gt;}
* to use.
* Sets the {@link Converter Converter&lt;Jwt, OAuth2AuthenticatedPrincipal&gt;} to
* use.
* <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
* @since 7.1
*/
public void setJwtPrincipalConverter(
Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter) {
public void setJwtPrincipalConverter(Converter<Jwt, OAuth2AuthenticatedPrincipal> jwtPrincipalConverter) {
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
this.jwtAuthenticationConverter.setJwtPrincipalConverter(jwtPrincipalConverter);
this.jwtPrincipalConverter = jwtPrincipalConverter;
}
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@ -28,6 +30,8 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.TestJwts;
@ -119,4 +123,21 @@ public class JwtAuthenticationConverterTests {
SecurityAssertions.assertThat(result).hasAuthority(FactorGrantedAuthority.BEARER_AUTHORITY);
}
@Test
public void whenSettingNullJwtPrincipalConverter() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.jwtAuthenticationConverter.setJwtPrincipalConverter(null))
.withMessage("jwtPrincipalConverter cannot be null");
}
@Test
public void convertWhenJwtPrincipalConverterSetThenCustomPrincipalUsed() {
OAuth2AuthenticatedPrincipal customPrincipal = new DefaultOAuth2AuthenticatedPrincipal("custom-name",
Map.of("sub", "custom-name"), List.of());
this.jwtAuthenticationConverter.setJwtPrincipalConverter((jwt) -> customPrincipal);
Jwt jwt = TestJwts.jwt().build();
AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
assertThat(authentication.getName()).isEqualTo("custom-name");
}
}

View File

@ -111,7 +111,7 @@ public class JwtAuthenticationTokenTests {
public void getNameWhenConstructedWithNoSubjectThenReturnsNull() {
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("test");
Jwt jwt = builder().claim("claim", "value").build();
assertThat(new JwtAuthenticationToken(jwt, authorities, null).getName()).isNull();
assertThat(new JwtAuthenticationToken(jwt, authorities, (String) null).getName()).isNull();
assertThat(new JwtAuthenticationToken(jwt, authorities).getName()).isNull();
assertThat(new JwtAuthenticationToken(jwt).getName()).isNull();
}

View File

@ -17,6 +17,8 @@
package org.springframework.security.oauth2.server.resource.authentication;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
@ -24,6 +26,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.SecurityAssertions;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import static org.assertj.core.api.Assertions.assertThat;
@ -81,6 +85,23 @@ public class JwtBearerTokenAuthenticationConverterTests {
SecurityAssertions.assertThat(bearerToken).hasAuthorities("SCOPE_message:read", "SCOPE_message:write");
}
@Test
public void convertWhenJwtPrincipalConverterSetThenCustomPrincipalUsed() {
OAuth2AuthenticatedPrincipal customPrincipal = new DefaultOAuth2AuthenticatedPrincipal("custom-name",
Map.of("claim", "value"), List.of());
this.converter.setJwtPrincipalConverter((jwt) -> customPrincipal);
// @formatter:off
Jwt jwt = Jwt.withTokenValue("token-value")
.claim("claim", "value")
.header("header", "value")
.build();
// @formatter:on
AbstractAuthenticationToken token = this.converter.convert(jwt);
assertThat(token).isInstanceOf(BearerTokenAuthentication.class);
BearerTokenAuthentication bearerToken = (BearerTokenAuthentication) token;
assertThat(bearerToken.getName()).isEqualTo("custom-name");
}
static Predicate<GrantedAuthority> isScope() {
return (a) -> a.getAuthority().startsWith("SCOPE_");
}