mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-01 14:59:50 +00:00
Allow a JwtTypeValidator bean to override Security's default
A change in Spring Security [1] means that type validation is now performed by default by Spring Security. A breaking side-effect of this is that setting validateTypes to false no longer has an effect and the default JwtTypeValidator is still present. Its presence, wrapped in a DelegatingOAuth2TokenValidator, prevents a user's JwtTypeValidator bean from being used for type validation. This commit updates Boot's auto-configuration to change how the type validators are created. We avoid wrapping in a DelegatingOAuth2TokenValidator so that the user's custom JwtTypeValidator can be detected and used in place of the default. This requires us to create the JwtIssuerValidator rather than using the createDefaultWithIssuer method as it does not allow additional validators to be provided. Fixes gh-48301 [1] https://github.com/spring-projects/spring-security/commit/6d3b54df21ec0cffc30c8b3e0784220bd117a87d
This commit is contained in:
+18
-10
@@ -41,12 +41,12 @@ import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
|
||||
@@ -99,9 +99,13 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = builder.build();
|
||||
String issuerUri = this.properties.getIssuerUri();
|
||||
OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
|
||||
? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
|
||||
nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator));
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
if (issuerUri != null) {
|
||||
validators.add(new JwtIssuerValidator(issuerUri));
|
||||
}
|
||||
validators.addAll(getValidators());
|
||||
nimbusReactiveJwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
|
||||
: JwtValidators.createDefaultWithValidators(validators));
|
||||
return nimbusReactiveJwtDecoder;
|
||||
}
|
||||
|
||||
@@ -111,18 +115,17 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
|
||||
private List<OAuth2TokenValidator<Jwt>> getValidators() {
|
||||
List<String> audiences = this.properties.getAudiences();
|
||||
if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
|
||||
return defaultValidator;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
validators.add(defaultValidator);
|
||||
if (!CollectionUtils.isEmpty(audiences)) {
|
||||
validators.add(audValidator(audiences));
|
||||
}
|
||||
validators.addAll(this.additionalValidators);
|
||||
return new DelegatingOAuth2TokenValidator<>(validators);
|
||||
return validators;
|
||||
}
|
||||
|
||||
private JwtClaimValidator<List<String>> audValidator(List<String> audiences) {
|
||||
@@ -141,7 +144,9 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey)
|
||||
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm()))
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault()));
|
||||
List<OAuth2TokenValidator<Jwt>> validators = getValidators();
|
||||
jwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
|
||||
: JwtValidators.createDefaultWithValidators(validators));
|
||||
return jwtDecoder;
|
||||
}
|
||||
|
||||
@@ -171,7 +176,10 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri);
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
NimbusReactiveJwtDecoder jwtDecoder = builder.build();
|
||||
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri)));
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
validators.add(new JwtIssuerValidator(issuerUri));
|
||||
validators.addAll(getValidators());
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators));
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
||||
+18
-10
@@ -41,13 +41,13 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
|
||||
@@ -97,9 +97,13 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
NimbusJwtDecoder nimbusJwtDecoder = builder.build();
|
||||
String issuerUri = this.properties.getIssuerUri();
|
||||
OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
|
||||
? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
|
||||
nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
if (issuerUri != null) {
|
||||
validators.add(new JwtIssuerValidator(issuerUri));
|
||||
}
|
||||
validators.addAll(getValidators());
|
||||
nimbusJwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
|
||||
: JwtValidators.createDefaultWithValidators(validators));
|
||||
return nimbusJwtDecoder;
|
||||
}
|
||||
|
||||
@@ -109,18 +113,17 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
|
||||
private List<OAuth2TokenValidator<Jwt>> getValidators() {
|
||||
List<String> audiences = this.properties.getAudiences();
|
||||
if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
|
||||
return defaultValidator;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
validators.add(defaultValidator);
|
||||
if (!CollectionUtils.isEmpty(audiences)) {
|
||||
validators.add(audValidator(audiences));
|
||||
}
|
||||
validators.addAll(this.additionalValidators);
|
||||
return new DelegatingOAuth2TokenValidator<>(validators);
|
||||
return validators;
|
||||
}
|
||||
|
||||
private JwtClaimValidator<List<String>> audValidator(List<String> audiences) {
|
||||
@@ -139,7 +142,9 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey)
|
||||
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm()))
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault()));
|
||||
List<OAuth2TokenValidator<Jwt>> validators = getValidators();
|
||||
jwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
|
||||
: JwtValidators.createDefaultWithValidators(validators));
|
||||
return jwtDecoder;
|
||||
}
|
||||
|
||||
@@ -168,7 +173,10 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri);
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
NimbusJwtDecoder jwtDecoder = builder.build();
|
||||
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri)));
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
validators.add(new JwtIssuerValidator(issuerUri));
|
||||
validators.addAll(getValidators());
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators));
|
||||
return jwtDecoder;
|
||||
});
|
||||
}
|
||||
|
||||
+67
-1
@@ -73,9 +73,11 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtTypeValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
|
||||
@@ -727,6 +729,60 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
.doesNotHaveBean(ReactiveManagementWebSecurityAutoConfiguration.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customTypeValidatorCanReplaceDefaultWhenUsingIssuerUri() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
|
||||
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
|
||||
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
|
||||
.run((context) -> {
|
||||
SupplierReactiveJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierReactiveJwtDecoder.class);
|
||||
Mono<ReactiveJwtDecoder> jwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
|
||||
.getField(supplierJwtDecoderBean, "jwtDecoderMono");
|
||||
assertThat(jwtDecoderSupplier).isNotNull();
|
||||
ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
|
||||
assertThat(jwtDecoder).isNotNull();
|
||||
assertThat(context).hasBean("customJwtTypeValidator");
|
||||
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
||||
.getBean("customJwtTypeValidator");
|
||||
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).header(JoseHeaderNames.TYP, "custom-type"),
|
||||
jwtDecoder,
|
||||
(validators) -> assertThat(validators).contains(customValidator)
|
||||
.satisfiesOnlyOnce(
|
||||
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customTypeValidatorCanReplaceDefaultWhenUsingJwkSetUri() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
|
||||
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
|
||||
.run((context) -> {
|
||||
ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
||||
assertThat(context).hasBean("customJwtTypeValidator");
|
||||
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
||||
.getBean("customJwtTypeValidator");
|
||||
validate(jwt().header(JoseHeaderNames.TYP, "custom-type"), jwtDecoder,
|
||||
(validators) -> assertThat(validators).contains(customValidator)
|
||||
.satisfiesOnlyOnce(
|
||||
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
|
||||
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
|
||||
@@ -826,7 +882,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||
.getField(jwtDecoder, "jwtValidator");
|
||||
assertThat(jwtValidator).isNotNull();
|
||||
assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
|
||||
assertThat(jwtValidator.validate(builder.build()).getErrors()).isEmpty();
|
||||
validatorsConsumer.accept(extractValidators(jwtValidator));
|
||||
}
|
||||
|
||||
@@ -934,6 +990,16 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomJwtTypeValidatorConfig {
|
||||
|
||||
@Bean
|
||||
JwtTypeValidator customJwtTypeValidator() {
|
||||
return new JwtTypeValidator("custom-type");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@WithResource(name = "public-key-location", content = """
|
||||
|
||||
+66
-1
@@ -70,10 +70,12 @@ import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.FactorGrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtTypeValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
|
||||
@@ -745,6 +747,59 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||
.doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customTypeValidatorCanReplaceDefaultWhenUsingIssuerUri() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
|
||||
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
|
||||
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
|
||||
.run((context) -> {
|
||||
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
|
||||
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
|
||||
.getField(supplierJwtDecoderBean, "delegate");
|
||||
assertThat(jwtDecoderSupplier).isNotNull();
|
||||
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
|
||||
assertThat(context).hasBean("customJwtTypeValidator");
|
||||
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
||||
.getBean("customJwtTypeValidator");
|
||||
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).header(JoseHeaderNames.TYP, "custom-type"),
|
||||
jwtDecoder,
|
||||
(validators) -> assertThat(validators).contains(customValidator)
|
||||
.satisfiesOnlyOnce(
|
||||
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void customTypeValidatorCanReplaceDefaultWhenUsingJwkSetUri() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
|
||||
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
|
||||
.run((context) -> {
|
||||
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
|
||||
assertThat(context).hasBean("customJwtTypeValidator");
|
||||
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
||||
.getBean("customJwtTypeValidator");
|
||||
validate(jwt().header(JoseHeaderNames.TYP, "custom-type"), jwtDecoder,
|
||||
(validators) -> assertThat(validators).contains(customValidator)
|
||||
.satisfiesOnlyOnce(
|
||||
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
|
||||
});
|
||||
}
|
||||
|
||||
private @Nullable Filter getBearerTokenFilter(AssertableWebApplicationContext context) {
|
||||
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
|
||||
@@ -814,7 +869,7 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||
.getField(jwtDecoder, "jwtValidator");
|
||||
assertThat(jwtValidator).isNotNull();
|
||||
assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
|
||||
assertThat(jwtValidator.validate(builder.build()).getErrors()).isEmpty();
|
||||
validatorsConsumer.accept(extractValidators(jwtValidator));
|
||||
}
|
||||
|
||||
@@ -904,6 +959,16 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomJwtTypeValidatorConfig {
|
||||
|
||||
@Bean
|
||||
JwtTypeValidator customJwtTypeValidator() {
|
||||
return new JwtTypeValidator("custom-type");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomJwtConverterConfig {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user