diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java index acf7fbc23566..d2f2a38c3474 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java @@ -31,6 +31,7 @@ * * @author Madhura Bhave * @author Artsiom Yudovin + * @author Mushtaq Ahmed * @since 2.1.0 */ @ConfigurationProperties(prefix = "spring.security.oauth2.resourceserver") @@ -71,6 +72,11 @@ public static class Jwt { */ private Resource publicKeyLocation; + /** + * Identifies the recipients that the JWT is intended for. + */ + private String audience; + public String getJwkSetUri() { return this.jwkSetUri; } @@ -103,6 +109,14 @@ public void setPublicKeyLocation(Resource publicKeyLocation) { this.publicKeyLocation = publicKeyLocation; } + public String getAudience() { + return this.audience; + } + + public void setAudience(String audience) { + this.audience = audience; + } + public String readPublicKey() throws IOException { String key = "spring.security.oauth2.resourceserver.public-key-location"; Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 20a3c3a5eabd..955594afc843 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -19,7 +19,9 @@ import java.security.KeyFactory; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -32,8 +34,14 @@ 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.JwtValidators; +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.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; @@ -49,6 +57,7 @@ * @author Artsiom Yudovin * @author HaiTao Zhang * @author Anastasiia Losieva + * @author Mushtaq Ahmed */ @Configuration(proxyBeanMethods = false) class ReactiveOAuth2ResourceServerJwkConfiguration { @@ -69,10 +78,18 @@ ReactiveJwtDecoder jwtDecoder() { NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder .withJwkSetUri(this.properties.getJwkSetUri()) .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + List> validators = new ArrayList<>(); + validators.add(new JwtTimestampValidator()); String issuerUri = this.properties.getIssuerUri(); if (issuerUri != null) { - nimbusReactiveJwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri)); + validators.add(new JwtIssuerValidator(issuerUri)); } + String audience = this.properties.getAudience(); + if (audience != null) { + validators.add(new JwtClaimValidator>(JwtClaimNames.AUD, + (aud) -> aud != null && aud.contains(audience))); + } + nimbusReactiveJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return nimbusReactiveJwtDecoder; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index 77f8faf812f5..38ddb9a423f4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -19,7 +19,9 @@ import java.security.KeyFactory; import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -34,10 +36,16 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +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.JwtDecoders; -import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; +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.web.SecurityFilterChain; @@ -50,6 +58,7 @@ * @author Madhura Bhave * @author Artsiom Yudovin * @author HaiTao Zhang + * @author Mushtaq Ahmed */ @Configuration(proxyBeanMethods = false) class OAuth2ResourceServerJwtConfiguration { @@ -69,10 +78,18 @@ static class JwtDecoderConfiguration { JwtDecoder jwtDecoderByJwkKeySetUri() { NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + List> validators = new ArrayList<>(); + validators.add(new JwtTimestampValidator()); String issuerUri = this.properties.getIssuerUri(); if (issuerUri != null) { - nimbusJwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri)); + validators.add(new JwtIssuerValidator(issuerUri)); } + String audience = this.properties.getAudience(); + if (audience != null) { + validators.add(new JwtClaimValidator>(JwtClaimNames.AUD, + (aud) -> aud != null && aud.contains(audience))); + } + nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return nimbusJwtDecoder; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index fb4f3880cf68..d4e162899261 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -50,7 +50,9 @@ import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; 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.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; @@ -74,6 +76,7 @@ * @author Artsiom Yudovin * @author HaiTao Zhang * @author Anastasiia Losieva + * @author Mushtaq Ahmed */ class ReactiveOAuth2ResourceServerAutoConfigurationTests { @@ -387,6 +390,56 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() }); } + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() 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") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class); + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + .getField(reactiveJwtDecoder, "jwtValidator"); + Collection> tokenValidators = (Collection>) ReflectionTestUtils + .getField(jwtValidator, "tokenValidators"); + assertThat(tokenValidators).hasExactlyElementsOfTypes(JwtTimestampValidator.class); + assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class); + assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class); + }); + } + + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldConfigureIssuerAndAudienceJwtValidatorIfPropertyProvided() 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", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + + this.server.getPort() + "/" + path, + "spring.security.oauth2.resourceserver.jwt.audience=http://test-audience.com") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class); + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + .getField(reactiveJwtDecoder, "jwtValidator"); + Collection> tokenValidators = (Collection>) ReflectionTestUtils + .getField(jwtValidator, "tokenValidators"); + assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class); + assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtClaimValidator.class); + }); + } + private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) { MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index 7c9ab28b4cf9..897dfd212bc0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -48,8 +48,10 @@ import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; 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.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; @@ -68,6 +70,7 @@ * @author Madhura Bhave * @author Artsiom Yudovin * @author HaiTao Zhang + * @author Mushtaq Ahmed */ class OAuth2ResourceServerAutoConfigurationTests { @@ -404,6 +407,56 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() }); } + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() 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") + .run((context) -> { + assertThat(context).hasSingleBean(JwtDecoder.class); + JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + .getField(jwtDecoder, "jwtValidator"); + Collection> tokenValidators = (Collection>) ReflectionTestUtils + .getField(jwtValidator, "tokenValidators"); + assertThat(tokenValidators).hasExactlyElementsOfTypes(JwtTimestampValidator.class); + assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class); + assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class); + }); + } + + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldConfigureAudienceAndIssuerJwtValidatorIfPropertyProvided() 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", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + + this.server.getPort() + "/" + path, + "spring.security.oauth2.resourceserver.jwt.audience=http://test-audience.com") + .run((context) -> { + assertThat(context).hasSingleBean(JwtDecoder.class); + JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + .getField(jwtDecoder, "jwtValidator"); + Collection> tokenValidators = (Collection>) ReflectionTestUtils + .getField(jwtValidator, "tokenValidators"); + assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class); + assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtClaimValidator.class); + }); + } + @Test void jwtSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() { this.contextRunner