mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
Auto-configure a JwtAuthenticationConverter
See gh-38105
This commit is contained in:
parent
17e9f0cb8e
commit
e9bce315ae
@ -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");
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user