diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 2614260fb4c..717c9d7e4a8 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -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 defaultValidator = (issuerUri != null) - ? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault(); - nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator)); + List> 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 getValidators(OAuth2TokenValidator defaultValidator) { + private List> getValidators() { List audiences = this.properties.getAudiences(); if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { - return defaultValidator; + return Collections.emptyList(); } List> 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> audValidator(List audiences) { @@ -141,7 +144,9 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey) .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) .build(); - jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault())); + List> 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> validators = new ArrayList<>(); + validators.add(new JwtIssuerValidator(issuerUri)); + validators.addAll(getValidators()); + jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators)); return jwtDecoder; }); } diff --git a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java index 735409d206a..573c4a9a823 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/module/spring-boot-security-oauth2-resource-server/src/main/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -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 defaultValidator = (issuerUri != null) - ? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault(); - nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator)); + List> 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 getValidators(OAuth2TokenValidator defaultValidator) { + private List> getValidators() { List audiences = this.properties.getAudiences(); if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { - return defaultValidator; + return Collections.emptyList(); } List> 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> audValidator(List audiences) { @@ -139,7 +142,9 @@ class OAuth2ResourceServerJwtConfiguration { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) .build(); - jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault())); + List> 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> validators = new ArrayList<>(); + validators.add(new JwtIssuerValidator(issuerUri)); + validators.addAll(getValidators()); + jwtDecoder.setJwtValidator(JwtValidators.createDefaultWithValidators(validators)); return jwtDecoder; }); } diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 6fd94d6b666..824656ae063 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -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 jwtDecoderSupplier = (Mono) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "jwtDecoderMono"); + assertThat(jwtDecoderSupplier).isNotNull(); + ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block(); + assertThat(jwtDecoder).isNotNull(); + assertThat(context).hasBean("customJwtTypeValidator"); + OAuth2TokenValidator customValidator = (OAuth2TokenValidator) 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 customValidator = (OAuth2TokenValidator) 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 jwtValidator = (DelegatingOAuth2TokenValidator) 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 = """ diff --git a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java index 166b90ad610..31beee47841 100644 --- a/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/module/spring-boot-security-oauth2-resource-server/src/test/java/org/springframework/boot/security/oauth2/server/resource/autoconfigure/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -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 jwtDecoderSupplier = (Supplier) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "delegate"); + assertThat(jwtDecoderSupplier).isNotNull(); + JwtDecoder jwtDecoder = jwtDecoderSupplier.get(); + assertThat(context).hasBean("customJwtTypeValidator"); + OAuth2TokenValidator customValidator = (OAuth2TokenValidator) 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 customValidator = (OAuth2TokenValidator) 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 filterChains = filterChain.getFilterChains(); @@ -814,7 +869,7 @@ class OAuth2ResourceServerAutoConfigurationTests { DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) 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 {