Skip to content

Commit c3e51d3

Browse files
committed
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] spring-projects/spring-security@6d3b54d
1 parent a5a0ad2 commit c3e51d3

File tree

4 files changed

+169
-22
lines changed

4 files changed

+169
-22
lines changed

module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
import org.springframework.context.annotation.Configuration;
4242
import org.springframework.security.config.web.server.ServerHttpSecurity;
4343
import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec;
44-
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
4544
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
4645
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
4746
import org.springframework.security.oauth2.jwt.Jwt;
4847
import org.springframework.security.oauth2.jwt.JwtClaimNames;
4948
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
49+
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
5050
import org.springframework.security.oauth2.jwt.JwtValidators;
5151
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
5252
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
@@ -99,9 +99,13 @@ ReactiveJwtDecoder jwtDecoder(ObjectProvider<JwkSetUriReactiveJwtDecoderBuilderC
9999
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
100100
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = builder.build();
101101
String issuerUri = this.properties.getIssuerUri();
102-
OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
103-
? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
104-
nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator));
102+
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
103+
if (issuerUri != null) {
104+
validators.add(new JwtIssuerValidator(issuerUri));
105+
}
106+
validators.addAll(getValidators());
107+
nimbusReactiveJwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
108+
: JwtValidators.createDefaultWithValidators(validators));
105109
return nimbusReactiveJwtDecoder;
106110
}
107111

@@ -111,18 +115,17 @@ private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
111115
}
112116
}
113117

114-
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
118+
private List<OAuth2TokenValidator<Jwt>> getValidators() {
115119
List<String> audiences = this.properties.getAudiences();
116120
if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
117-
return defaultValidator;
121+
return Collections.emptyList();
118122
}
119123
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
120-
validators.add(defaultValidator);
121124
if (!CollectionUtils.isEmpty(audiences)) {
122125
validators.add(audValidator(audiences));
123126
}
124127
validators.addAll(this.additionalValidators);
125-
return new DelegatingOAuth2TokenValidator<>(validators);
128+
return validators;
126129
}
127130

128131
private JwtClaimValidator<List<String>> audValidator(List<String> audiences) {
@@ -141,7 +144,9 @@ NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
141144
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey)
142145
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm()))
143146
.build();
144-
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault()));
147+
List<OAuth2TokenValidator<Jwt>> validators = getValidators();
148+
jwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
149+
: JwtValidators.createDefaultWithValidators(validators));
145150
return jwtDecoder;
146151
}
147152

@@ -171,7 +176,10 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri(
171176
JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder.withIssuerLocation(issuerUri);
172177
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
173178
NimbusReactiveJwtDecoder jwtDecoder = builder.build();
174-
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri)));
179+
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
180+
validators.add(new JwtIssuerValidator(issuerUri));
181+
validators.addAll(getValidators());
182+
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators));
175183
return jwtDecoder;
176184
});
177185
}

module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@
4141
import org.springframework.context.annotation.Conditional;
4242
import org.springframework.context.annotation.Configuration;
4343
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
44-
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
4544
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
4645
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
4746
import org.springframework.security.oauth2.jwt.Jwt;
4847
import org.springframework.security.oauth2.jwt.JwtClaimNames;
4948
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
5049
import org.springframework.security.oauth2.jwt.JwtDecoder;
50+
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
5151
import org.springframework.security.oauth2.jwt.JwtValidators;
5252
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
5353
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
@@ -97,9 +97,13 @@ JwtDecoder jwtDecoderByJwkKeySetUri(ObjectProvider<JwkSetUriJwtDecoderBuilderCus
9797
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
9898
NimbusJwtDecoder nimbusJwtDecoder = builder.build();
9999
String issuerUri = this.properties.getIssuerUri();
100-
OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
101-
? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
102-
nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
100+
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
101+
if (issuerUri != null) {
102+
validators.add(new JwtIssuerValidator(issuerUri));
103+
}
104+
validators.addAll(getValidators());
105+
nimbusJwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
106+
: JwtValidators.createDefaultWithValidators(validators));
103107
return nimbusJwtDecoder;
104108
}
105109

@@ -109,18 +113,17 @@ private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
109113
}
110114
}
111115

112-
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
116+
private List<OAuth2TokenValidator<Jwt>> getValidators() {
113117
List<String> audiences = this.properties.getAudiences();
114118
if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
115-
return defaultValidator;
119+
return Collections.emptyList();
116120
}
117121
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
118-
validators.add(defaultValidator);
119122
if (!CollectionUtils.isEmpty(audiences)) {
120123
validators.add(audValidator(audiences));
121124
}
122125
validators.addAll(this.additionalValidators);
123-
return new DelegatingOAuth2TokenValidator<>(validators);
126+
return validators;
124127
}
125128

126129
private JwtClaimValidator<List<String>> audValidator(List<String> audiences) {
@@ -139,7 +142,9 @@ JwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
139142
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey)
140143
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm()))
141144
.build();
142-
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault()));
145+
List<OAuth2TokenValidator<Jwt>> validators = getValidators();
146+
jwtDecoder.setJwtValidator(validators.isEmpty() ? JwtValidators.createDefault()
147+
: JwtValidators.createDefaultWithValidators(validators));
143148
return jwtDecoder;
144149
}
145150

@@ -168,7 +173,10 @@ SupplierJwtDecoder jwtDecoderByIssuerUri(ObjectProvider<JwkSetUriJwtDecoderBuild
168173
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri);
169174
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
170175
NimbusJwtDecoder jwtDecoder = builder.build();
171-
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri)));
176+
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
177+
validators.add(new JwtIssuerValidator(issuerUri));
178+
validators.addAll(getValidators());
179+
jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators));
172180
return jwtDecoder;
173181
});
174182
}

module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@
7373
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
7474
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
7575
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
76+
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
7677
import org.springframework.security.oauth2.jwt.Jwt;
7778
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
7879
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
80+
import org.springframework.security.oauth2.jwt.JwtTypeValidator;
7981
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
8082
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
8183
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
@@ -727,6 +729,60 @@ void causesReactiveManagementWebSecurityAutoConfigurationToBackOff() {
727729
.doesNotHaveBean(ReactiveManagementWebSecurityAutoConfiguration.class));
728730
}
729731

732+
@Test
733+
@SuppressWarnings("unchecked")
734+
void customTypeValidatorCanReplaceDefaultWhenUsingIssuerUri() throws Exception {
735+
this.server = new MockWebServer();
736+
this.server.start();
737+
String path = "test";
738+
String issuer = this.server.url(path).toString();
739+
String cleanIssuerPath = cleanIssuerPath(issuer);
740+
setupMockResponse(cleanIssuerPath);
741+
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
742+
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
743+
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
744+
.run((context) -> {
745+
SupplierReactiveJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierReactiveJwtDecoder.class);
746+
Mono<ReactiveJwtDecoder> jwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
747+
.getField(supplierJwtDecoderBean, "jwtDecoderMono");
748+
assertThat(jwtDecoderSupplier).isNotNull();
749+
ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
750+
assertThat(jwtDecoder).isNotNull();
751+
assertThat(context).hasBean("customJwtTypeValidator");
752+
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
753+
.getBean("customJwtTypeValidator");
754+
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).header(JoseHeaderNames.TYP, "custom-type"),
755+
jwtDecoder,
756+
(validators) -> assertThat(validators).contains(customValidator)
757+
.satisfiesOnlyOnce(
758+
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
759+
});
760+
}
761+
762+
@Test
763+
@SuppressWarnings("unchecked")
764+
void customTypeValidatorCanReplaceDefaultWhenUsingJwkSetUri() throws Exception {
765+
this.server = new MockWebServer();
766+
this.server.start();
767+
String path = "test";
768+
String issuer = this.server.url(path).toString();
769+
String cleanIssuerPath = cleanIssuerPath(issuer);
770+
setupMockResponse(cleanIssuerPath);
771+
this.contextRunner
772+
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
773+
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
774+
.run((context) -> {
775+
ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
776+
assertThat(context).hasBean("customJwtTypeValidator");
777+
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
778+
.getBean("customJwtTypeValidator");
779+
validate(jwt().header(JoseHeaderNames.TYP, "custom-type"), jwtDecoder,
780+
(validators) -> assertThat(validators).contains(customValidator)
781+
.satisfiesOnlyOnce(
782+
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
783+
});
784+
}
785+
730786
@SuppressWarnings("unchecked")
731787
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
732788
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
@@ -826,7 +882,7 @@ private void validate(Jwt.Builder builder, ReactiveJwtDecoder jwtDecoder,
826882
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
827883
.getField(jwtDecoder, "jwtValidator");
828884
assertThat(jwtValidator).isNotNull();
829-
assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
885+
assertThat(jwtValidator.validate(builder.build()).getErrors()).isEmpty();
830886
validatorsConsumer.accept(extractValidators(jwtValidator));
831887
}
832888

@@ -934,6 +990,16 @@ ReactiveJwtAuthenticationConverter customReactiveJwtAuthenticationConverter() {
934990

935991
}
936992

993+
@Configuration(proxyBeanMethods = false)
994+
static class CustomJwtTypeValidatorConfig {
995+
996+
@Bean
997+
JwtTypeValidator customJwtTypeValidator() {
998+
return new JwtTypeValidator("custom-type");
999+
}
1000+
1001+
}
1002+
9371003
@Target(ElementType.METHOD)
9381004
@Retention(RetentionPolicy.RUNTIME)
9391005
@WithResource(name = "public-key-location", content = """

module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@
7070
import org.springframework.security.core.authority.FactorGrantedAuthority;
7171
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
7272
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
73+
import org.springframework.security.oauth2.jwt.JoseHeaderNames;
7374
import org.springframework.security.oauth2.jwt.Jwt;
7475
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
7576
import org.springframework.security.oauth2.jwt.JwtDecoder;
7677
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
78+
import org.springframework.security.oauth2.jwt.JwtTypeValidator;
7779
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
7880
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
7981
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
@@ -745,6 +747,59 @@ void causesManagementWebSecurityAutoConfigurationToBackOff() {
745747
.doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN));
746748
}
747749

750+
@Test
751+
@SuppressWarnings("unchecked")
752+
void customTypeValidatorCanReplaceDefaultWhenUsingIssuerUri() throws Exception {
753+
this.server = new MockWebServer();
754+
this.server.start();
755+
String path = "test";
756+
String issuer = this.server.url(path).toString();
757+
String cleanIssuerPath = cleanIssuerPath(issuer);
758+
setupMockResponse(cleanIssuerPath);
759+
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
760+
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
761+
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
762+
.run((context) -> {
763+
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
764+
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
765+
.getField(supplierJwtDecoderBean, "delegate");
766+
assertThat(jwtDecoderSupplier).isNotNull();
767+
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
768+
assertThat(context).hasBean("customJwtTypeValidator");
769+
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
770+
.getBean("customJwtTypeValidator");
771+
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).header(JoseHeaderNames.TYP, "custom-type"),
772+
jwtDecoder,
773+
(validators) -> assertThat(validators).contains(customValidator)
774+
.satisfiesOnlyOnce(
775+
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
776+
});
777+
}
778+
779+
@Test
780+
@SuppressWarnings("unchecked")
781+
void customTypeValidatorCanReplaceDefaultWhenUsingJwkSetUri() throws Exception {
782+
this.server = new MockWebServer();
783+
this.server.start();
784+
String path = "test";
785+
String issuer = this.server.url(path).toString();
786+
String cleanIssuerPath = cleanIssuerPath(issuer);
787+
setupMockResponse(cleanIssuerPath);
788+
this.contextRunner
789+
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
790+
.withUserConfiguration(CustomJwtTypeValidatorConfig.class)
791+
.run((context) -> {
792+
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
793+
assertThat(context).hasBean("customJwtTypeValidator");
794+
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
795+
.getBean("customJwtTypeValidator");
796+
validate(jwt().header(JoseHeaderNames.TYP, "custom-type"), jwtDecoder,
797+
(validators) -> assertThat(validators).contains(customValidator)
798+
.satisfiesOnlyOnce(
799+
(validator) -> assertThat(validator).isInstanceOf(JwtTypeValidator.class)));
800+
});
801+
}
802+
748803
private @Nullable Filter getBearerTokenFilter(AssertableWebApplicationContext context) {
749804
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
750805
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
@@ -814,7 +869,7 @@ private void validate(Jwt.Builder builder, JwtDecoder jwtDecoder,
814869
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
815870
.getField(jwtDecoder, "jwtValidator");
816871
assertThat(jwtValidator).isNotNull();
817-
assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
872+
assertThat(jwtValidator.validate(builder.build()).getErrors()).isEmpty();
818873
validatorsConsumer.accept(extractValidators(jwtValidator));
819874
}
820875

@@ -904,6 +959,16 @@ JwtClaimValidator<String> customJwtClaimValidator() {
904959

905960
}
906961

962+
@Configuration(proxyBeanMethods = false)
963+
static class CustomJwtTypeValidatorConfig {
964+
965+
@Bean
966+
JwtTypeValidator customJwtTypeValidator() {
967+
return new JwtTypeValidator("custom-type");
968+
}
969+
970+
}
971+
907972
@Configuration(proxyBeanMethods = false)
908973
static class CustomJwtConverterConfig {
909974

0 commit comments

Comments
 (0)