Skip to content

Commit

Permalink
spring-projectsgh-16231 add JwtPrincipalConverter.java support
Browse files Browse the repository at this point in the history
  • Loading branch information
ovlasovmag committed Dec 17, 2024
1 parent ff7dbb4 commit 7835460
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
Expand Down Expand Up @@ -103,6 +104,8 @@
* <li>customizing the conversion from a {@link Jwt} to an
* {@link org.springframework.security.core.Authentication} with
* {@link JwtConfigurer#jwtAuthenticationConverter(Converter)}</li>
* <li>customizing the conversion from a {@link Jwt} to a principal {@link Object} with
* {@link JwtConfigurer#jwtPrincipalConverter(JwtPrincipalConverter)}</li>
* </ul>
*
* <p>
Expand Down Expand Up @@ -382,6 +385,8 @@ public class JwtConfigurer {

private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter;

private JwtPrincipalConverter jwtPrincipalConverter;

JwtConfigurer(ApplicationContext context) {
this.context = context;
}
Expand All @@ -408,6 +413,11 @@ public JwtConfigurer jwtAuthenticationConverter(
return this;
}

public JwtConfigurer jwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) {
this.jwtPrincipalConverter = jwtPrincipalConverter;
return this;
}

/**
* @deprecated For removal in 7.0. Use {@link #jwt(Customizer)} or
* {@code jwt(Customizer.withDefaults())} to stick with defaults. See the <a href=
Expand All @@ -421,16 +431,33 @@ public OAuth2ResourceServerConfigurer<H> and() {

Converter<Jwt, ? extends AbstractAuthenticationToken> getJwtAuthenticationConverter() {
if (this.jwtAuthenticationConverter == null) {
if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) {
this.jwtAuthenticationConverter = this.context.getBean(JwtAuthenticationConverter.class);
}
else {
this.jwtAuthenticationConverter = new JwtAuthenticationConverter();
}
final var authenticationConverter = getOrCreateJwtAuthenticationConverter();
authenticationConverter.setJwtPrincipalConverter(getJwtPrincipalConverter());
this.jwtAuthenticationConverter = authenticationConverter;
}
return this.jwtAuthenticationConverter;
}

JwtPrincipalConverter getJwtPrincipalConverter() {
if (this.jwtPrincipalConverter == null) {
if (this.context.getBeanNamesForType(JwtPrincipalConverter.class).length > 0) {
return this.context.getBean(JwtPrincipalConverter.class);
} else {
return (jwt, principalName) -> jwt;
}
} else {
return this.jwtPrincipalConverter;
}
}

private JwtAuthenticationConverter getOrCreateJwtAuthenticationConverter() {
if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) {
return this.context.getBean(JwtAuthenticationConverter.class);
} else {
return new JwtAuthenticationConverter();
}
}

JwtDecoder getJwtDecoder() {
if (this.decoder == null) {
return this.context.getBean(JwtDecoder.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
Expand Down Expand Up @@ -1398,6 +1399,60 @@ public void getJwtAuthenticationConverterWhenDuplicateConverterBeansThenThrowsEx
.isThrownBy(jwtConfigurer::getJwtAuthenticationConverter);
}

@Test
public void getJwtPrincipalConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() {
ApplicationContext context = this.spring.context(new GenericWebApplicationContext()).getContext();
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isInstanceOf(JwtPrincipalConverter.class);
}

@Test
public void getJwtPrincipalConverterWhenConverterBeanSpecified() {
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean(JwtPrincipalConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converterBean);
}

@Test
public void getJwtPrincipalConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() {
JwtPrincipalConverter converter = mock(JwtPrincipalConverter.class);
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean(JwtPrincipalConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
jwtConfigurer.jwtPrincipalConverter(converter);
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converter);
}

@Test
public void getJwtPrincipalConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() {
JwtPrincipalConverter converter = mock(JwtPrincipalConverter.class);
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean("converterOne", JwtPrincipalConverter.class, () -> converterBean);
context.registerBean("converterTwo", JwtPrincipalConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
jwtConfigurer.jwtPrincipalConverter(converter);
assertThat(jwtConfigurer.getJwtPrincipalConverter()).isEqualTo(converter);
}

@Test
public void getJwtPrincipalConverterWhenDuplicateConverterBeansThenThrowsException() {
JwtPrincipalConverter converterBean = mock(JwtPrincipalConverter.class);
GenericWebApplicationContext context = new GenericWebApplicationContext();
context.registerBean("converterOne", JwtPrincipalConverter.class, () -> converterBean);
context.registerBean("converterTwo", JwtPrincipalConverter.class, () -> converterBean);
this.spring.context(context).autowire();
OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = new OAuth2ResourceServerConfigurer(context).jwt();
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
.isThrownBy(jwtConfigurer::getJwtPrincipalConverter);
}

@Test
public void getWhenCustomAuthenticationConverterThenUsed() throws Exception {
this.spring
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,6 +23,7 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
import org.springframework.util.Assert;

/**
Expand All @@ -35,15 +36,15 @@
public class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {

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

private JwtPrincipalConverter principalConverter = (jwt, principalClaimValue) -> jwt;
private String principalClaimName = JwtClaimNames.SUB;

@Override
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);
Object principal = principalConverter.convert(jwt, principalClaimValue);
return new JwtAuthenticationToken(jwt, principal, authorities, principalClaimValue);
}

/**
Expand All @@ -69,4 +70,13 @@ public void setPrincipalClaimName(String principalClaimName) {
this.principalClaimName = principalClaimName;
}

/**
* Sets the principal converter. Defaults to {@link Jwt}.
* @param jwtPrincipalConverter The principal converter
*/
public void setJwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) {
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
this.principalConverter = jwtPrincipalConverter;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -72,6 +72,19 @@ 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 converted from JWT
* @param authorities the authorities assigned to the JWT
* @param name the principal name
*/
public JwtAuthenticationToken(Jwt jwt, Object principal, Collection<? extends GrantedAuthority> authorities, String name) {
super(jwt, principal, jwt, authorities);
this.setAuthenticated(true);
this.name = name;
}

@Override
public Map<String, Object> getTokenAttributes() {
return this.getToken().getClaims();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,6 +16,7 @@

package org.springframework.security.oauth2.server.resource.authentication;

import org.springframework.security.oauth2.server.resource.introspection.JwtPrincipalConverter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

Expand All @@ -38,7 +39,7 @@ public final class ReactiveJwtAuthenticationConverter implements Converter<Jwt,

private Converter<Jwt, Flux<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new ReactiveJwtGrantedAuthoritiesConverterAdapter(
new JwtGrantedAuthoritiesConverter());

private JwtPrincipalConverter principalConverter = (jwt, principalName) -> jwt;
private String principalClaimName = JwtClaimNames.SUB;

@Override
Expand All @@ -48,7 +49,8 @@ public Mono<AbstractAuthenticationToken> convert(Jwt jwt) {
.collectList()
.map((authorities) -> {
String principalName = jwt.getClaimAsString(this.principalClaimName);
return new JwtAuthenticationToken(jwt, authorities, principalName);
Object principal = principalConverter.convert(jwt, principalName);
return new JwtAuthenticationToken(jwt, principal, authorities, principalName);
});
// @formatter:on
}
Expand All @@ -75,4 +77,13 @@ public void setPrincipalClaimName(String principalClaimName) {
this.principalClaimName = principalClaimName;
}

/**
* Sets the principal converter. Defaults to {@link Jwt}.
* @param jwtPrincipalConverter The principal converter
*/
public void setJwtPrincipalConverter(JwtPrincipalConverter jwtPrincipalConverter) {
Assert.notNull(jwtPrincipalConverter, "jwtPrincipalConverter cannot be null");
this.principalConverter = jwtPrincipalConverter;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.oauth2.server.resource.introspection;

import org.springframework.security.oauth2.jwt.Jwt;

/**
* @author Alex Vlasov
*/
public interface JwtPrincipalConverter {

Object convert(Jwt jwt, String principalName);

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,7 @@
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.TestJwts;

Expand Down Expand Up @@ -112,4 +113,19 @@ public void convertWhenPrincipalClaimNameSetAndClaimValueIsNotString() {
assertThat(authentication.getName()).isEqualTo("100");
}

@Test
public void convertWithCustomJwtPrincipalConverter() {
this.jwtAuthenticationConverter.setJwtPrincipalConverter((jwt, name) -> User.withUsername(name).password("").build());
Jwt jwt = TestJwts.user();
AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
assertThat(authentication.getPrincipal()).isInstanceOf(User.class).hasFieldOrPropertyWithValue("username", "mock-test-subject");
}

@Test
public void convertWithDefaultJwtPrincipalConverter() {
Jwt jwt = TestJwts.user();
AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt);
assertThat(authentication.getPrincipal()).isInstanceOf(Jwt.class);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,11 +24,14 @@

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.TestJwts;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;

/**
* Tests for {@link JwtAuthenticationToken}
Expand Down Expand Up @@ -115,6 +118,20 @@ public void getNameWhenConstructedWithNoSubjectThenReturnsNull() {
assertThat(new JwtAuthenticationToken(jwt).getName()).isNull();
}

@Test
public void testConstructorWithPrincipal() {
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("test");
User principal = mock(User.class);
Jwt jwt = TestJwts.user();
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, principal, authorities, "Hayden");
assertThat(token.getToken()).isSameAs(jwt);
assertThat(token.getCredentials()).isSameAs(jwt);
assertThat(token.getPrincipal()).isSameAs(principal);
assertThat(token.getAuthorities()).isEqualTo(authorities);
assertThat(token.isAuthenticated()).isTrue();
assertThat(token.getName()).isEqualTo("Hayden");
}

private Jwt.Builder builder() {
return Jwt.withTokenValue("token").header("alg", JwsAlgorithms.RS256);
}
Expand Down
Loading

0 comments on commit 7835460

Please sign in to comment.