From 7a659e4e12f71ecb6f51c12a307a6d7a97d0560e Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Wed, 20 Apr 2022 15:04:57 -0700 Subject: [PATCH] Polish "Add support for aud claim in resource server" See gh-29084 --- .../OAuth2ResourceServerProperties.java | 12 +- ...eOAuth2ResourceServerJwkConfiguration.java | 46 ++++--- .../OAuth2ResourceServerJwtConfiguration.java | 44 ++++--- ...2ResourceServerAutoConfigurationTests.java | 124 ++++++++++++++++-- ...2ResourceServerAutoConfigurationTests.java | 121 +++++++++++++++-- 5 files changed, 292 insertions(+), 55 deletions(-) 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 d2f2a38c34..8b06de17db 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 @@ -19,6 +19,8 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; @@ -75,7 +77,7 @@ public class OAuth2ResourceServerProperties { /** * Identifies the recipients that the JWT is intended for. */ - private String audience; + private List audiences = new ArrayList<>(); public String getJwkSetUri() { return this.jwkSetUri; @@ -109,12 +111,12 @@ public class OAuth2ResourceServerProperties { this.publicKeyLocation = publicKeyLocation; } - public String getAudience() { - return this.audience; + public List getAudiences() { + return this.audiences; } - public void setAudience(String audience) { - this.audience = audience; + public void setAudiences(List audiences) { + this.audiences = audiences; } public String readPublicKey() throws IOException { 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 955594afc8..4daf3f2a53 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 @@ -21,7 +21,9 @@ import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.List; +import java.util.function.Supplier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -40,13 +42,13 @@ 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.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.util.CollectionUtils; /** * Configures a {@link ReactiveJwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI @@ -78,28 +80,35 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { 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) { - 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)); + Supplier> defaultValidator = (issuerUri != null) + ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault; + nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator)); return nimbusReactiveJwtDecoder; } + private OAuth2TokenValidator getValidators(Supplier> defaultValidator) { + OAuth2TokenValidator defaultValidators = defaultValidator.get(); + List audiences = this.properties.getAudiences(); + if (CollectionUtils.isEmpty(audiences)) { + return defaultValidators; + } + List> validators = new ArrayList<>(); + validators.add(defaultValidators); + validators.add(new JwtClaimValidator>(JwtClaimNames.AUD, + (aud) -> aud != null && !Collections.disjoint(aud, audiences))); + return new DelegatingOAuth2TokenValidator<>(validators); + } + @Bean @Conditional(KeyValueCondition.class) NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - return NimbusReactiveJwtDecoder.withPublicKey(publicKey) + NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey) .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault)); + return jwtDecoder; } private byte[] getKeySpec(String keyValue) { @@ -110,8 +119,13 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { @Bean @Conditional(IssuerUriCondition.class) SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() { - return new SupplierReactiveJwtDecoder( - () -> ReactiveJwtDecoders.fromIssuerLocation(this.properties.getIssuerUri())); + return new SupplierReactiveJwtDecoder(() -> { + NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) ReactiveJwtDecoders + .fromIssuerLocation(this.properties.getIssuerUri()); + jwtDecoder.setJwtValidator( + getValidators(() -> JwtValidators.createDefaultWithIssuer(this.properties.getIssuerUri()))); + return jwtDecoder; + }); } } 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 5202288914..14d37c2bee 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 @@ -21,7 +21,9 @@ import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.List; +import java.util.function.Supplier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -43,11 +45,11 @@ 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.JwtIssuerValidator; -import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.util.CollectionUtils; /** * Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public @@ -77,28 +79,35 @@ class OAuth2ResourceServerJwtConfiguration { 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) { - 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)); + Supplier> defaultValidator = (issuerUri != null) + ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault; + nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator)); return nimbusJwtDecoder; } + private OAuth2TokenValidator getValidators(Supplier> defaultValidator) { + OAuth2TokenValidator defaultValidators = defaultValidator.get(); + List audiences = this.properties.getAudiences(); + if (CollectionUtils.isEmpty(audiences)) { + return defaultValidators; + } + List> validators = new ArrayList<>(); + validators.add(defaultValidators); + validators.add(new JwtClaimValidator>(JwtClaimNames.AUD, + (aud) -> aud != null && !Collections.disjoint(aud, audiences))); + return new DelegatingOAuth2TokenValidator<>(validators); + } + @Bean @Conditional(KeyValueCondition.class) JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - return NimbusJwtDecoder.withPublicKey(publicKey) + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault)); + return jwtDecoder; } private byte[] getKeySpec(String keyValue) { @@ -109,7 +118,12 @@ class OAuth2ResourceServerJwtConfiguration { @Bean @Conditional(IssuerUriCondition.class) SupplierJwtDecoder jwtDecoderByIssuerUri() { - return new SupplierJwtDecoder(() -> JwtDecoders.fromIssuerLocation(this.properties.getIssuerUri())); + return new SupplierJwtDecoder(() -> { + String issuerUri = this.properties.getIssuerUri(); + NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri); + jwtDecoder.setJwtValidator(getValidators(() -> JwtValidators.createDefaultWithIssuer(issuerUri))); + return jwtDecoder; + }); } } 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 d4e1628992..f8357730bb 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 @@ -17,6 +17,9 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -423,20 +426,108 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { 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") + String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path; + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri, + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com") .run((context) -> { assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class); + validate(issuerUri, reactiveJwtDecoder); + }); + } + + @SuppressWarnings("unchecked") + private void validate(String issuerUri, ReactiveJwtDecoder jwtDecoder) throws MalformedURLException { + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + .getField(jwtDecoder, "jwtValidator"); + Jwt.Builder builder = jwt().claim("aud", Collections.singletonList("https://test-audience.com")); + if (issuerUri != null) { + builder.claim("iss", new URL(issuerUri)); + } + Jwt jwt = builder.build(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + Collection> delegates = (Collection>) ReflectionTestUtils + .getField(jwtValidator, "tokenValidators"); + validateDelegates(issuerUri, delegates); + } + + @SuppressWarnings("unchecked") + private void validateDelegates(String issuerUri, Collection> delegates) { + assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class); + OAuth2TokenValidator delegatingValidator = delegates.stream() + .filter((v) -> v instanceof DelegatingOAuth2TokenValidator).findFirst().get(); + Collection> nestedDelegates = (Collection>) ReflectionTestUtils + .getField(delegatingValidator, "tokenValidators"); + if (issuerUri != null) { + assertThat(nestedDelegates).hasAtLeastOneElementOfType(JwtIssuerValidator.class); + } + } + + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssuerUri() 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, + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com") + .run((context) -> { + SupplierReactiveJwtDecoder supplierJwtDecoderBean = context + .getBean(SupplierReactiveJwtDecoder.class); + Mono jwtDecoderSupplier = (Mono) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "jwtDecoderMono"); + ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block(); + validate(issuerUri, jwtDecoder); + }); + } + + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() 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.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class); + validate(null, jwtDecoder); + }); + } + + @SuppressWarnings("unchecked") + @Test + void audienceValidatorWhenAudienceInvalid() 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.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri, + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + ReactiveJwtDecoder jwtDecoder = 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); + .getField(jwtDecoder, "jwtValidator"); + Jwt jwt = jwt().claim("iss", new URL(issuerUri)) + .claim("aud", Collections.singletonList("https://other-audience.com")).build(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue(); }); } @@ -508,6 +599,19 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { return response; } + static Jwt.Builder jwt() { + // @formatter:off + return Jwt.withTokenValue("token") + .header("alg", "none") + .expiresAt(Instant.MAX) + .issuedAt(Instant.MIN) + .issuer("https://issuer.example.org") + .jti("jti") + .notBefore(Instant.MIN) + .subject("mock-test-subject"); + // @formatter:on + } + @EnableWebFluxSecurity static class TestConfig { 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 96c234ad0d..f292d2b700 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 @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -440,20 +443,107 @@ class OAuth2ResourceServerAutoConfigurationTests { 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") + String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path; + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri, + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com") + .run((context) -> { + assertThat(context).hasSingleBean(JwtDecoder.class); + JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + validate(issuerUri, jwtDecoder); + }); + } + + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssuerUri() 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, + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com") + .run((context) -> { + SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class); + Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "jwtDecoderSupplier"); + JwtDecoder jwtDecoder = jwtDecoderSupplier.get(); + validate(issuerUri, jwtDecoder); + }); + } + + @SuppressWarnings("unchecked") + private void validate(String issuerUri, JwtDecoder jwtDecoder) throws MalformedURLException { + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + .getField(jwtDecoder, "jwtValidator"); + Jwt.Builder builder = jwt().claim("aud", Collections.singletonList("https://test-audience.com")); + if (issuerUri != null) { + builder.claim("iss", new URL(issuerUri)); + } + Jwt jwt = builder.build(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + Collection> delegates = (Collection>) ReflectionTestUtils + .getField(jwtValidator, "tokenValidators"); + validateDelegates(issuerUri, delegates); + } + + @SuppressWarnings("unchecked") + private void validateDelegates(String issuerUri, Collection> delegates) { + assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class); + OAuth2TokenValidator delegatingValidator = delegates.stream() + .filter((v) -> v instanceof DelegatingOAuth2TokenValidator).findFirst().get(); + Collection> nestedDelegates = (Collection>) ReflectionTestUtils + .getField(delegatingValidator, "tokenValidators"); + if (issuerUri != null) { + assertThat(nestedDelegates).hasAtLeastOneElementOfType(JwtIssuerValidator.class); + } + } + + @SuppressWarnings("unchecked") + @Test + void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() 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.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,http://test-audience1.com") + .run((context) -> { + assertThat(context).hasSingleBean(JwtDecoder.class); + JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); + validate(null, jwtDecoder); + }); + } + + @SuppressWarnings("unchecked") + @Test + void audienceValidatorWhenAudienceInvalid() 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.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri, + "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.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); + Jwt jwt = jwt().claim("iss", new URL(issuerUri)) + .claim("aud", Collections.singletonList("https://other-audience.com")).build(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue(); }); } @@ -525,6 +615,19 @@ class OAuth2ResourceServerAutoConfigurationTests { return response; } + static Jwt.Builder jwt() { + // @formatter:off + return Jwt.withTokenValue("token") + .header("alg", "none") + .expiresAt(Instant.MAX) + .issuedAt(Instant.MIN) + .issuer("https://issuer.example.org") + .jti("jti") + .notBefore(Instant.MIN) + .subject("mock-test-subject"); + // @formatter:on + } + @Configuration(proxyBeanMethods = false) @EnableWebSecurity static class TestConfig {