Polish "Add support for aud claim in resource server"

See gh-29084
pull/30774/head
Madhura Bhave 3 years ago committed by Stephane Nicoll
parent ee65627f4c
commit 7a659e4e12

@ -19,6 +19,8 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; 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.ConfigurationProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
@ -75,7 +77,7 @@ public class OAuth2ResourceServerProperties {
/** /**
* Identifies the recipients that the JWT is intended for. * Identifies the recipients that the JWT is intended for.
*/ */
private String audience; private List<String> audiences = new ArrayList<>();
public String getJwkSetUri() { public String getJwkSetUri() {
return this.jwkSetUri; return this.jwkSetUri;
@ -109,12 +111,12 @@ public class OAuth2ResourceServerProperties {
this.publicKeyLocation = publicKeyLocation; this.publicKeyLocation = publicKeyLocation;
} }
public String getAudience() { public List<String> getAudiences() {
return this.audience; return this.audiences;
} }
public void setAudience(String audience) { public void setAudiences(List<String> audiences) {
this.audience = audience; this.audiences = audiences;
} }
public String readPublicKey() throws IOException { public String readPublicKey() throws IOException {

@ -21,7 +21,9 @@ import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator; 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.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
import org.springframework.security.web.server.SecurityWebFilterChain; 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 * Configures a {@link ReactiveJwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI
@ -78,28 +80,35 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder
.withJwkSetUri(this.properties.getJwkSetUri()) .withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator());
String issuerUri = this.properties.getIssuerUri(); String issuerUri = this.properties.getIssuerUri();
if (issuerUri != null) { Supplier<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
validators.add(new JwtIssuerValidator(issuerUri)); ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
} nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator));
String audience = this.properties.getAudience();
if (audience != null) {
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
(aud) -> aud != null && aud.contains(audience)));
}
nimbusReactiveJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
return nimbusReactiveJwtDecoder; return nimbusReactiveJwtDecoder;
} }
private OAuth2TokenValidator<Jwt> getValidators(Supplier<OAuth2TokenValidator<Jwt>> defaultValidator) {
OAuth2TokenValidator<Jwt> defaultValidators = defaultValidator.get();
List<String> audiences = this.properties.getAudiences();
if (CollectionUtils.isEmpty(audiences)) {
return defaultValidators;
}
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(defaultValidators);
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
return new DelegatingOAuth2TokenValidator<>(validators);
}
@Bean @Bean
@Conditional(KeyValueCondition.class) @Conditional(KeyValueCondition.class)
NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
return NimbusReactiveJwtDecoder.withPublicKey(publicKey) NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault));
return jwtDecoder;
} }
private byte[] getKeySpec(String keyValue) { private byte[] getKeySpec(String keyValue) {
@ -110,8 +119,13 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
@Bean @Bean
@Conditional(IssuerUriCondition.class) @Conditional(IssuerUriCondition.class)
SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() { SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() {
return new SupplierReactiveJwtDecoder( return new SupplierReactiveJwtDecoder(() -> {
() -> ReactiveJwtDecoders.fromIssuerLocation(this.properties.getIssuerUri())); NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) ReactiveJwtDecoders
.fromIssuerLocation(this.properties.getIssuerUri());
jwtDecoder.setJwtValidator(
getValidators(() -> JwtValidators.createDefaultWithIssuer(this.properties.getIssuerUri())));
return jwtDecoder;
});
} }
} }

@ -21,7 +21,9 @@ import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
import org.springframework.security.web.SecurityFilterChain; 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 * Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public
@ -77,28 +79,35 @@ class OAuth2ResourceServerJwtConfiguration {
JwtDecoder jwtDecoderByJwkKeySetUri() { JwtDecoder jwtDecoderByJwkKeySetUri() {
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator());
String issuerUri = this.properties.getIssuerUri(); String issuerUri = this.properties.getIssuerUri();
if (issuerUri != null) { Supplier<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
validators.add(new JwtIssuerValidator(issuerUri)); ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
} nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
String audience = this.properties.getAudience();
if (audience != null) {
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
(aud) -> aud != null && aud.contains(audience)));
}
nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
return nimbusJwtDecoder; return nimbusJwtDecoder;
} }
private OAuth2TokenValidator<Jwt> getValidators(Supplier<OAuth2TokenValidator<Jwt>> defaultValidator) {
OAuth2TokenValidator<Jwt> defaultValidators = defaultValidator.get();
List<String> audiences = this.properties.getAudiences();
if (CollectionUtils.isEmpty(audiences)) {
return defaultValidators;
}
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(defaultValidators);
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
return new DelegatingOAuth2TokenValidator<>(validators);
}
@Bean @Bean
@Conditional(KeyValueCondition.class) @Conditional(KeyValueCondition.class)
JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { JwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
return NimbusJwtDecoder.withPublicKey(publicKey) NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
jwtDecoder.setJwtValidator(getValidators(JwtValidators::createDefault));
return jwtDecoder;
} }
private byte[] getKeySpec(String keyValue) { private byte[] getKeySpec(String keyValue) {
@ -109,7 +118,12 @@ class OAuth2ResourceServerJwtConfiguration {
@Bean @Bean
@Conditional(IssuerUriCondition.class) @Conditional(IssuerUriCondition.class)
SupplierJwtDecoder jwtDecoderByIssuerUri() { 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;
});
} }
} }

@ -17,6 +17,9 @@
package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -423,20 +426,108 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
String issuer = this.server.url(path).toString(); String issuer = this.server.url(path).toString();
String cleanIssuerPath = cleanIssuerPath(issuer); String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath); setupMockResponse(cleanIssuerPath);
this.contextRunner String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", this.contextRunner.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
+ this.server.getPort() + "/" + path, "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
"spring.security.oauth2.resourceserver.jwt.audience=http://test-audience.com") "spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
.run((context) -> { .run((context) -> {
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class); ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
validate(issuerUri, reactiveJwtDecoder);
});
}
@SuppressWarnings("unchecked")
private void validate(String issuerUri, ReactiveJwtDecoder jwtDecoder) throws MalformedURLException {
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) 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<OAuth2TokenValidator<Jwt>> delegates = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
.getField(jwtValidator, "tokenValidators");
validateDelegates(issuerUri, delegates);
}
@SuppressWarnings("unchecked")
private void validateDelegates(String issuerUri, Collection<OAuth2TokenValidator<Jwt>> delegates) {
assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
OAuth2TokenValidator<Jwt> delegatingValidator = delegates.stream()
.filter((v) -> v instanceof DelegatingOAuth2TokenValidator).findFirst().get();
Collection<OAuth2TokenValidator<Jwt>> nestedDelegates = (Collection<OAuth2TokenValidator<Jwt>>) 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<ReactiveJwtDecoder> jwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) 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<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
.getField(reactiveJwtDecoder, "jwtValidator"); .getField(jwtDecoder, "jwtValidator");
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils Jwt jwt = jwt().claim("iss", new URL(issuerUri))
.getField(jwtValidator, "tokenValidators"); .claim("aud", Collections.singletonList("https://other-audience.com")).build();
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class); assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue();
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtClaimValidator.class);
}); });
} }
@ -508,6 +599,19 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
return response; 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 @EnableWebFluxSecurity
static class TestConfig { static class TestConfig {

@ -16,6 +16,9 @@
package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; 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.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -440,20 +443,107 @@ class OAuth2ResourceServerAutoConfigurationTests {
String issuer = this.server.url(path).toString(); String issuer = this.server.url(path).toString();
String cleanIssuerPath = cleanIssuerPath(issuer); String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath); setupMockResponse(cleanIssuerPath);
this.contextRunner String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", this.contextRunner.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
+ this.server.getPort() + "/" + path, "spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
"spring.security.oauth2.resourceserver.jwt.audience=http://test-audience.com") "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<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
.getField(supplierJwtDecoderBean, "jwtDecoderSupplier");
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
validate(issuerUri, jwtDecoder);
});
}
@SuppressWarnings("unchecked")
private void validate(String issuerUri, JwtDecoder jwtDecoder) throws MalformedURLException {
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) 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<OAuth2TokenValidator<Jwt>> delegates = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
.getField(jwtValidator, "tokenValidators");
validateDelegates(issuerUri, delegates);
}
@SuppressWarnings("unchecked")
private void validateDelegates(String issuerUri, Collection<OAuth2TokenValidator<Jwt>> delegates) {
assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
OAuth2TokenValidator<Jwt> delegatingValidator = delegates.stream()
.filter((v) -> v instanceof DelegatingOAuth2TokenValidator).findFirst().get();
Collection<OAuth2TokenValidator<Jwt>> nestedDelegates = (Collection<OAuth2TokenValidator<Jwt>>) 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) -> { .run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class); assertThat(context).hasSingleBean(JwtDecoder.class);
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
.getField(jwtDecoder, "jwtValidator"); .getField(jwtDecoder, "jwtValidator");
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils Jwt jwt = jwt().claim("iss", new URL(issuerUri))
.getField(jwtValidator, "tokenValidators"); .claim("aud", Collections.singletonList("https://other-audience.com")).build();
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class); assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue();
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtClaimValidator.class);
}); });
} }
@ -525,6 +615,19 @@ class OAuth2ResourceServerAutoConfigurationTests {
return response; 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) @Configuration(proxyBeanMethods = false)
@EnableWebSecurity @EnableWebSecurity
static class TestConfig { static class TestConfig {

Loading…
Cancel
Save