Auto-configure a JwtAuthenticationConverter

See gh-38105
This commit is contained in:
Yan Kardziyaka 2023-10-30 03:26:59 +03:00 committed by Moritz Halbritter
parent 17e9f0cb8e
commit e9bce315ae
8 changed files with 348 additions and 1 deletions

View File

@ -26,6 +26,7 @@ import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.io.Resource;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
@ -35,6 +36,7 @@ import org.springframework.util.StreamUtils;
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author Mushtaq Ahmed
* @author Yan Kardziyaka
* @since 2.1.0
*/
@ConfigurationProperties(prefix = "spring.security.oauth2.resourceserver")
@ -80,6 +82,28 @@ public class OAuth2ResourceServerProperties {
*/
private List<String> audiences = new ArrayList<>();
/**
* Prefix to use for {@link GrantedAuthority authorities} mapped from JWT.
*/
private String authorityPrefix;
/**
* Regex to use for splitting the value of the authorities claim into
* {@link GrantedAuthority authorities}.
*/
private String authoritiesClaimDelimiter;
/**
* Name of token claim to use for mapping {@link GrantedAuthority authorities}
* from JWT.
*/
private String authoritiesClaimName;
/**
* JWT principal claim name.
*/
private String principalClaimName;
public String getJwkSetUri() {
return this.jwkSetUri;
}
@ -120,6 +144,38 @@ public class OAuth2ResourceServerProperties {
this.audiences = audiences;
}
public String getAuthorityPrefix() {
return this.authorityPrefix;
}
public void setAuthorityPrefix(String authorityPrefix) {
this.authorityPrefix = authorityPrefix;
}
public String getAuthoritiesClaimDelimiter() {
return this.authoritiesClaimDelimiter;
}
public void setAuthoritiesClaimDelimiter(String authoritiesClaimDelimiter) {
this.authoritiesClaimDelimiter = authoritiesClaimDelimiter;
}
public String getAuthoritiesClaimName() {
return this.authoritiesClaimName;
}
public void setAuthoritiesClaimName(String authoritiesClaimName) {
this.authoritiesClaimName = authoritiesClaimName;
}
public String getPrincipalClaimName() {
return this.principalClaimName;
}
public void setPrincipalClaimName(String principalClaimName) {
this.principalClaimName = principalClaimName;
}
public String readPublicKey() throws IOException {
String key = "spring.security.oauth2.resourceserver.public-key-location";
Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null");

View File

@ -34,6 +34,7 @@ class ReactiveOAuth2ResourceServerConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class,
ReactiveOAuth2ResourceServerJwkConfiguration.JwtConverterConfiguration.class,
ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class })
static class JwtConfiguration {

View File

@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -48,6 +49,9 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.util.CollectionUtils;
@ -62,6 +66,7 @@ import org.springframework.util.CollectionUtils;
* @author Anastasiia Losieva
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
@Configuration(proxyBeanMethods = false)
class ReactiveOAuth2ResourceServerJwkConfiguration {
@ -161,6 +166,34 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class)
static class JwtConverterConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
this.properties = properties.getJwt();
}
@Bean
ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
map.from(this.properties.getAuthoritiesClaimDelimiter())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
map.from(this.properties.getAuthoritiesClaimName())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter));
return jwtAuthenticationConverter;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
static class WebSecurityConfiguration {

View File

@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSe
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -48,6 +49,8 @@ import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.CollectionUtils;
@ -63,6 +66,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
* @author HaiTao Zhang
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
@Configuration(proxyBeanMethods = false)
class OAuth2ResourceServerJwtConfiguration {
@ -173,4 +177,31 @@ class OAuth2ResourceServerJwtConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JwtAuthenticationConverter.class)
static class JwtConverterConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
this.properties = properties.getJwt();
}
@Bean
JwtAuthenticationConverter getJwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
map.from(this.properties.getAuthoritiesClaimDelimiter())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
map.from(this.properties.getAuthoritiesClaimName())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
}

View File

@ -32,7 +32,8 @@ class Oauth2ResourceServerConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JwtDecoder.class)
@Import({ OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class,
OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class })
OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class,
OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class })
static class JwtConfiguration {
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2012-2023 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.boot.autoconfigure.security.oauth2.resource;
import java.time.Instant;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* {@link ArgumentsProvider Arguments provider} supplying different Spring Boot properties
* to customize JWT converter behavior, JWT token for conversion, expected principal name
* and expected authorities.
*
* @author Yan Kardziyaka
*/
public final class JwtConverterCustomizationsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
String customPrefix = "CUSTOM_AUTHORITY_PREFIX_";
String customDelimiter = "[~,#:]";
String customAuthoritiesClaim = "custom_authorities";
String customPrincipalClaim = "custom_principal";
String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com";
String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix;
String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter="
+ customDelimiter;
String authoritiesClaimProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-name="
+ customAuthoritiesClaim;
String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name="
+ customPrincipalClaim;
String[] noJwtConverterProps = { jwkSetUriProperty };
String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty };
String[] customDelimiterProps = { jwkSetUriProperty, authoritiesDelimiterProperty };
String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty };
String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty };
String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty,
authoritiesClaimProperty, principalClaimProperty };
String[] jwtScopes = { "custom_scope0", "custom_scope1" };
String subjectValue = UUID.randomUUID().toString();
String customPrincipalValue = UUID.randomUUID().toString();
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.expiresAt(Instant.MAX)
.issuedAt(Instant.MIN)
.issuer("https://issuer.example.org")
.jti("jti")
.notBefore(Instant.MIN)
.subject(subjectValue)
.claim(customPrincipalClaim, customPrincipalValue);
Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build();
Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build();
Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null)
.claim(customAuthoritiesClaim, jwtScopes[0] + " " + jwtScopes[1])
.build();
Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null)
.claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1])
.build();
String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] };
String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] };
return Stream.of(
Arguments.of(Named.named("No JWT converter customizations", noJwtConverterProps),
noAuthoritiesCustomizationsJwt, subjectValue, defaultPrefixAuthorities),
Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps),
noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities),
Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps),
customAuthoritiesDelimiterJwt, subjectValue, defaultPrefixAuthorities),
Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps),
customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities),
Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps),
noAuthoritiesCustomizationsJwt, customPrincipalValue, defaultPrefixAuthorities),
Arguments.of(Named.named("All JWT converter customizations", allJwtConverterProps),
customAuthoritiesClaimAndDelimiterJwt, customPrincipalValue, customPrefixAuthorities));
}
}

View File

@ -39,10 +39,13 @@ import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.mockito.InOrder;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtConverterCustomizationsArgumentsProvider;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
@ -52,10 +55,12 @@ import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@ -70,6 +75,7 @@ import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
@ -92,6 +98,7 @@ import static org.springframework.security.config.Customizer.withDefaults;
* @author Anastasiia Losieva
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
class ReactiveOAuth2ResourceServerAutoConfigurationTests {
@ -626,6 +633,46 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
});
}
@ParameterizedTest(name = "{0}")
@ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class)
void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt,
String expectedPrincipal, String[] expectedAuthorities) {
this.contextRunner.withPropertyValues(properties).run((context) -> {
ReactiveJwtAuthenticationConverter converter = context.getBean(ReactiveJwtAuthenticationConverter.class);
AbstractAuthenticationToken token = converter.convert(jwt).block();
assertThat(token).isNotNull().extracting(AbstractAuthenticationToken::getName).isEqualTo(expectedPrincipal);
assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.containsExactlyInAnyOrder(expectedAuthorities);
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
@Test
void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() {
String propertiesPrincipalClaim = "principal_from_properties";
String propertiesPrincipalValue = "from_props";
String userConfigPrincipalValue = "from_user_config";
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + propertiesPrincipalClaim)
.withUserConfiguration(CustomJwtConverterConfig.class)
.run((context) -> {
ReactiveJwtAuthenticationConverter converter = context
.getBean(ReactiveJwtAuthenticationConverter.class);
Jwt jwt = jwt().claim(propertiesPrincipalClaim, propertiesPrincipalValue)
.claim(CustomJwtConverterConfig.PRINCIPAL_CLAIM, userConfigPrincipalValue)
.build();
AbstractAuthenticationToken token = converter.convert(jwt).block();
assertThat(token).isNotNull()
.extracting(AbstractAuthenticationToken::getName)
.isEqualTo(userConfigPrincipalValue)
.isNotEqualTo(propertiesPrincipalValue);
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
@ -807,4 +854,18 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomJwtConverterConfig {
static String PRINCIPAL_CLAIM = "principal_from_user_configuration";
@Bean
ReactiveJwtAuthenticationConverter customReactiveJwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setPrincipalClaimName(PRINCIPAL_CLAIM);
return converter;
}
}
}

View File

@ -38,9 +38,12 @@ import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.mockito.InOrder;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtConverterCustomizationsArgumentsProvider;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
@ -51,9 +54,11 @@ import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
@ -64,6 +69,7 @@ import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
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.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
@ -84,6 +90,7 @@ import static org.mockito.Mockito.mock;
* @author HaiTao Zhang
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
class OAuth2ResourceServerAutoConfigurationTests {
@ -640,6 +647,45 @@ class OAuth2ResourceServerAutoConfigurationTests {
.run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class));
}
@ParameterizedTest(name = "{0}")
@ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class)
void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt,
String expectedPrincipal, String[] expectedAuthorities) {
this.contextRunner.withPropertyValues(properties).run((context) -> {
JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class);
AbstractAuthenticationToken token = converter.convert(jwt);
assertThat(token).isNotNull().extracting(AbstractAuthenticationToken::getName).isEqualTo(expectedPrincipal);
assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.containsExactlyInAnyOrder(expectedAuthorities);
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
@Test
void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() {
String propertiesPrincipalClaim = "principal_from_properties";
String propertiesPrincipalValue = "from_props";
String userConfigPrincipalValue = "from_user_config";
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + propertiesPrincipalClaim)
.withUserConfiguration(CustomJwtConverterConfig.class)
.run((context) -> {
JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class);
Jwt jwt = jwt().claim(propertiesPrincipalClaim, propertiesPrincipalValue)
.claim(CustomJwtConverterConfig.PRINCIPAL_CLAIM, userConfigPrincipalValue)
.build();
AbstractAuthenticationToken token = converter.convert(jwt);
assertThat(token).isNotNull()
.extracting(AbstractAuthenticationToken::getName)
.isEqualTo(userConfigPrincipalValue)
.isNotEqualTo(propertiesPrincipalValue);
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
private Filter getBearerTokenFilter(AssertableWebApplicationContext context) {
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
@ -795,4 +841,18 @@ class OAuth2ResourceServerAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
static class CustomJwtConverterConfig {
static String PRINCIPAL_CLAIM = "principal_from_user_configuration";
@Bean
JwtAuthenticationConverter customJwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setPrincipalClaimName(PRINCIPAL_CLAIM);
return converter;
}
}
}