Polish "Support custom token validators for OAuth2"
See gh-35874
This commit is contained in:
parent
7500dab321
commit
4feaa28fd1
|
@ -72,12 +72,12 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||||
|
|
||||||
private final OAuth2ResourceServerProperties.Jwt properties;
|
private final OAuth2ResourceServerProperties.Jwt properties;
|
||||||
|
|
||||||
private final List<OAuth2TokenValidator<Jwt>> customOAuth2TokenValidators;
|
private final List<OAuth2TokenValidator<Jwt>> additionalValidators;
|
||||||
|
|
||||||
JwtConfiguration(OAuth2ResourceServerProperties properties,
|
JwtConfiguration(OAuth2ResourceServerProperties properties,
|
||||||
List<OAuth2TokenValidator<Jwt>> customOAuth2TokenValidators) {
|
ObjectProvider<OAuth2TokenValidator<Jwt>> additionalValidators) {
|
||||||
this.properties = properties.getJwt();
|
this.properties = properties.getJwt();
|
||||||
this.customOAuth2TokenValidators = customOAuth2TokenValidators;
|
this.additionalValidators = additionalValidators.orderedStream().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -102,17 +102,17 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
|
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
|
||||||
|
List<String> audiences = this.properties.getAudiences();
|
||||||
|
if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
|
||||||
|
return defaultValidator;
|
||||||
|
}
|
||||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||||
validators.add(defaultValidator);
|
validators.add(defaultValidator);
|
||||||
validators.addAll(this.customOAuth2TokenValidators);
|
|
||||||
List<String> audiences = this.properties.getAudiences();
|
|
||||||
if (!CollectionUtils.isEmpty(audiences)) {
|
if (!CollectionUtils.isEmpty(audiences)) {
|
||||||
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
|
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
|
||||||
(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
|
(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
|
||||||
}
|
}
|
||||||
if (validators.size() == 1) {
|
validators.addAll(this.additionalValidators);
|
||||||
return validators.get(0);
|
|
||||||
}
|
|
||||||
return new DelegatingOAuth2TokenValidator<>(validators);
|
return new DelegatingOAuth2TokenValidator<>(validators);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,12 +73,12 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||||
|
|
||||||
private final OAuth2ResourceServerProperties.Jwt properties;
|
private final OAuth2ResourceServerProperties.Jwt properties;
|
||||||
|
|
||||||
private final List<OAuth2TokenValidator<Jwt>> customOAuth2TokenValidators;
|
private final List<OAuth2TokenValidator<Jwt>> additionalValidators;
|
||||||
|
|
||||||
JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
|
JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
|
||||||
List<OAuth2TokenValidator<Jwt>> customOAuth2TokenValidators) {
|
ObjectProvider<OAuth2TokenValidator<Jwt>> additionalValidators) {
|
||||||
this.properties = properties.getJwt();
|
this.properties = properties.getJwt();
|
||||||
this.customOAuth2TokenValidators = customOAuth2TokenValidators;
|
this.additionalValidators = additionalValidators.orderedStream().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -102,17 +102,17 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
|
private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
|
||||||
|
List<String> audiences = this.properties.getAudiences();
|
||||||
|
if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
|
||||||
|
return defaultValidator;
|
||||||
|
}
|
||||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||||
validators.add(defaultValidator);
|
validators.add(defaultValidator);
|
||||||
validators.addAll(this.customOAuth2TokenValidators);
|
|
||||||
List<String> audiences = this.properties.getAudiences();
|
|
||||||
if (!CollectionUtils.isEmpty(audiences)) {
|
if (!CollectionUtils.isEmpty(audiences)) {
|
||||||
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
|
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
|
||||||
(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
|
(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
|
||||||
}
|
}
|
||||||
if (validators.size() == 1) {
|
validators.addAll(this.additionalValidators);
|
||||||
return validators.get(0);
|
|
||||||
}
|
|
||||||
return new DelegatingOAuth2TokenValidator<>(validators);
|
return new DelegatingOAuth2TokenValidator<>(validators);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,16 +17,17 @@
|
||||||
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.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
@ -35,6 +36,7 @@ import com.nimbusds.jose.JWSAlgorithm;
|
||||||
import okhttp3.mockwebserver.MockResponse;
|
import okhttp3.mockwebserver.MockResponse;
|
||||||
import okhttp3.mockwebserver.MockWebServer;
|
import okhttp3.mockwebserver.MockWebServer;
|
||||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||||
|
import org.assertj.core.api.ThrowingConsumer;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
|
@ -441,7 +443,6 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOpaqueTokenIntrospector.class));
|
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOpaqueTokenIntrospector.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Test
|
@Test
|
||||||
void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() throws Exception {
|
void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
|
@ -457,15 +458,11 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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);
|
||||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
validate(jwt().claim("iss", issuer), reactiveJwtDecoder,
|
||||||
.getField(reactiveJwtDecoder, "jwtValidator");
|
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class));
|
||||||
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
|
||||||
.getField(jwtValidator, "tokenValidators");
|
|
||||||
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Test
|
@Test
|
||||||
void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() throws Exception {
|
void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
|
@ -479,13 +476,8 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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);
|
||||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
validate(jwt(), reactiveJwtDecoder, (validators) -> assertThat(validators).singleElement()
|
||||||
.getField(reactiveJwtDecoder, "jwtValidator");
|
.isInstanceOf(JwtTimestampValidator.class));
|
||||||
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
|
||||||
.getField(jwtValidator, "tokenValidators");
|
|
||||||
assertThat(tokenValidators).hasExactlyElementsOfTypes(JwtTimestampValidator.class);
|
|
||||||
assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class);
|
|
||||||
assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,76 +497,15 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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, null);
|
validate(
|
||||||
|
jwt().claim("iss", URI.create(issuerUri).toURL())
|
||||||
|
.claim("aud", List.of("https://test-audience.com")),
|
||||||
|
reactiveJwtDecoder,
|
||||||
|
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
|
||||||
|
.satisfiesOnlyOnce(audClaimValidator()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Test
|
|
||||||
void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvidedAndIssuerUri() 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")
|
|
||||||
.withUserConfiguration(CustomTokenValidatorsConfig.class)
|
|
||||||
.run((context) -> {
|
|
||||||
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
|
||||||
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
|
||||||
assertThat(context).hasBean("customJwtClaimValidator");
|
|
||||||
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
|
||||||
.getBean("customJwtClaimValidator");
|
|
||||||
validate(issuerUri, reactiveJwtDecoder, customValidator);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void validate(String issuerUri, ReactiveJwtDecoder jwtDecoder, OAuth2TokenValidator<Jwt> customValidator)
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
if (customValidator != null) {
|
|
||||||
builder.claim("custom_claim", "custom_claim_value");
|
|
||||||
}
|
|
||||||
Jwt jwt = builder.build();
|
|
||||||
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
|
|
||||||
Collection<OAuth2TokenValidator<Jwt>> delegates = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
|
||||||
.getField(jwtValidator, "tokenValidators");
|
|
||||||
validateDelegates(issuerUri, delegates, customValidator);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateDelegates(String issuerUri, Collection<OAuth2TokenValidator<Jwt>> delegates,
|
|
||||||
OAuth2TokenValidator<Jwt> customValidator) {
|
|
||||||
assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
|
|
||||||
OAuth2TokenValidator<Jwt> delegatingValidator = delegates.stream()
|
|
||||||
.filter((v) -> v instanceof DelegatingOAuth2TokenValidator)
|
|
||||||
.findFirst()
|
|
||||||
.get();
|
|
||||||
if (issuerUri != null) {
|
|
||||||
assertThat(delegatingValidator).extracting("tokenValidators")
|
|
||||||
.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
|
|
||||||
.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
|
|
||||||
}
|
|
||||||
List<OAuth2TokenValidator<Jwt>> claimValidators = delegates.stream()
|
|
||||||
.filter((d) -> d instanceof JwtClaimValidator<?>)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
assertThat(claimValidators).anyMatch((v) -> "aud".equals(ReflectionTestUtils.getField(v, "claim")));
|
|
||||||
if (customValidator != null) {
|
|
||||||
assertThat(claimValidators)
|
|
||||||
.anyMatch((v) -> "custom_claim".equals(ReflectionTestUtils.getField(v, "claim")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssuerUri() throws Exception {
|
void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssuerUri() throws Exception {
|
||||||
|
@ -592,7 +523,12 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
Mono<ReactiveJwtDecoder> jwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
|
Mono<ReactiveJwtDecoder> jwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
|
||||||
.getField(supplierJwtDecoderBean, "jwtDecoderMono");
|
.getField(supplierJwtDecoderBean, "jwtDecoderMono");
|
||||||
ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
|
ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
|
||||||
validate(issuerUri, jwtDecoder, null);
|
validate(
|
||||||
|
jwt().claim("iss", URI.create(issuerUri).toURL())
|
||||||
|
.claim("aud", List.of("https://test-audience.com")),
|
||||||
|
jwtDecoder,
|
||||||
|
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
|
||||||
|
.satisfiesOnlyOnce(audClaimValidator()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -610,7 +546,33 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
.run((context) -> {
|
.run((context) -> {
|
||||||
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
||||||
ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
||||||
validate(null, jwtDecoder, null);
|
validate(jwt().claim("aud", List.of("https://test-audience.com")), jwtDecoder,
|
||||||
|
(validators) -> assertThat(validators).satisfiesOnlyOnce(audClaimValidator()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
void autoConfigurationShouldConfigureCustomValidators() 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)
|
||||||
|
.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
|
||||||
|
.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
||||||
|
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
||||||
|
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
||||||
|
.getBean("customJwtClaimValidator");
|
||||||
|
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).claim("custom_claim", "custom_claim_value"),
|
||||||
|
reactiveJwtDecoder, (validators) -> assertThat(validators).contains(customValidator)
|
||||||
|
.hasAtLeastOneElementOfType(JwtIssuerValidator.class));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,6 +602,30 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
void customValidatorWhenInvalid() 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)
|
||||||
|
.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
|
||||||
|
.run((context) -> {
|
||||||
|
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
||||||
|
ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
||||||
|
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||||
|
.getField(jwtDecoder, "jwtValidator");
|
||||||
|
Jwt jwt = jwt().claim("iss", new URL(issuerUri)).claim("custom_claim", "invalid_value").build();
|
||||||
|
assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
|
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
|
||||||
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
|
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
|
||||||
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||||
|
@ -723,6 +709,37 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
.subject("mock-test-subject");
|
.subject("mock-test-subject");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void validate(Jwt.Builder builder, ReactiveJwtDecoder jwtDecoder,
|
||||||
|
ThrowingConsumer<List<OAuth2TokenValidator<Jwt>>> validatorsConsumer) {
|
||||||
|
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||||
|
.getField(jwtDecoder, "jwtValidator");
|
||||||
|
assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
|
||||||
|
validatorsConsumer.accept(extractValidators(jwtValidator));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<OAuth2TokenValidator<Jwt>> extractValidators(DelegatingOAuth2TokenValidator<Jwt> delegatingValidator) {
|
||||||
|
Collection<OAuth2TokenValidator<Jwt>> delegates = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
||||||
|
.getField(delegatingValidator, "tokenValidators");
|
||||||
|
List<OAuth2TokenValidator<Jwt>> extracted = new ArrayList<>();
|
||||||
|
for (OAuth2TokenValidator<Jwt> delegate : delegates) {
|
||||||
|
if (delegate instanceof DelegatingOAuth2TokenValidator<Jwt> delegatingDelegate) {
|
||||||
|
extracted.addAll(extractValidators(delegatingDelegate));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
extracted.add(delegate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<OAuth2TokenValidator<Jwt>> audClaimValidator() {
|
||||||
|
return (validator) -> assertThat(validator).isInstanceOf(JwtClaimValidator.class)
|
||||||
|
.extracting("claim")
|
||||||
|
.isEqualTo("aud");
|
||||||
|
}
|
||||||
|
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
static class TestConfig {
|
static class TestConfig {
|
||||||
|
|
||||||
|
@ -781,7 +798,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class CustomTokenValidatorsConfig {
|
static class CustomJwtClaimValidatorConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
JwtClaimValidator<String> customJwtClaimValidator() {
|
JwtClaimValidator<String> customJwtClaimValidator() {
|
||||||
|
|
|
@ -16,16 +16,17 @@
|
||||||
|
|
||||||
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.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
@ -34,6 +35,7 @@ import jakarta.servlet.Filter;
|
||||||
import okhttp3.mockwebserver.MockResponse;
|
import okhttp3.mockwebserver.MockResponse;
|
||||||
import okhttp3.mockwebserver.MockWebServer;
|
import okhttp3.mockwebserver.MockWebServer;
|
||||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||||
|
import org.assertj.core.api.ThrowingConsumer;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
|
@ -192,8 +194,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception {
|
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
this.server.start();
|
this.server.start();
|
||||||
|
@ -217,8 +219,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
assertThat(this.server.getRequestCount()).isEqualTo(2);
|
assertThat(this.server.getRequestCount()).isEqualTo(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception {
|
void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
this.server.start();
|
this.server.start();
|
||||||
|
@ -242,8 +244,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
assertThat(this.server.getRequestCount()).isEqualTo(3);
|
assertThat(this.server.getRequestCount()).isEqualTo(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception {
|
void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
this.server.start();
|
this.server.start();
|
||||||
|
@ -474,9 +476,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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);
|
||||||
assertThat(jwtDecoder).extracting("jwtValidator.tokenValidators")
|
validate(jwt().claim("iss", issuer), jwtDecoder,
|
||||||
.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
|
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class));
|
||||||
.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,11 +494,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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);
|
||||||
assertThat(jwtDecoder).extracting("jwtValidator.tokenValidators")
|
validate(jwt(), jwtDecoder, (validators) -> assertThat(validators).singleElement()
|
||||||
.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
|
.isInstanceOf(JwtTimestampValidator.class));
|
||||||
.hasExactlyElementsOfTypes(JwtTimestampValidator.class)
|
|
||||||
.doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class)
|
|
||||||
.doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,7 +515,12 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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);
|
||||||
validate(issuerUri, jwtDecoder, null);
|
validate(
|
||||||
|
jwt().claim("iss", URI.create(issuerUri).toURL())
|
||||||
|
.claim("aud", List.of("https://test-audience.com")),
|
||||||
|
jwtDecoder,
|
||||||
|
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
|
||||||
|
.satisfiesOnlyOnce(audClaimValidator()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -538,13 +541,18 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
|
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
|
||||||
.getField(supplierJwtDecoderBean, "delegate");
|
.getField(supplierJwtDecoderBean, "delegate");
|
||||||
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
|
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
|
||||||
validate(issuerUri, jwtDecoder, null);
|
validate(
|
||||||
|
jwt().claim("iss", URI.create(issuerUri).toURL())
|
||||||
|
.claim("aud", List.of("https://test-audience.com")),
|
||||||
|
jwtDecoder,
|
||||||
|
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
|
||||||
|
.satisfiesOnlyOnce(audClaimValidator()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@Test
|
@Test
|
||||||
void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvidedAndIssuerUri() throws Exception {
|
void autoConfigurationShouldConfigureCustomValidators() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
this.server.start();
|
this.server.start();
|
||||||
String path = "test";
|
String path = "test";
|
||||||
|
@ -552,9 +560,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||||
setupMockResponse(cleanIssuerPath);
|
setupMockResponse(cleanIssuerPath);
|
||||||
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
|
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
|
||||||
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
|
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")
|
.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
|
||||||
.withUserConfiguration(CustomTokenValidatorsConfig.class)
|
|
||||||
.run((context) -> {
|
.run((context) -> {
|
||||||
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
|
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
|
||||||
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
|
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
|
||||||
|
@ -563,51 +570,12 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
assertThat(context).hasBean("customJwtClaimValidator");
|
assertThat(context).hasBean("customJwtClaimValidator");
|
||||||
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
|
||||||
.getBean("customJwtClaimValidator");
|
.getBean("customJwtClaimValidator");
|
||||||
validate(issuerUri, jwtDecoder, customValidator);
|
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).claim("custom_claim", "custom_claim_value"),
|
||||||
|
jwtDecoder, (validators) -> assertThat(validators).contains(customValidator)
|
||||||
|
.hasAtLeastOneElementOfType(JwtIssuerValidator.class));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void validate(String issuerUri, JwtDecoder jwtDecoder, OAuth2TokenValidator<Jwt> customValidator)
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
if (customValidator != null) {
|
|
||||||
builder.claim("custom_claim", "custom_claim_value");
|
|
||||||
}
|
|
||||||
Jwt jwt = builder.build();
|
|
||||||
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
|
|
||||||
Collection<OAuth2TokenValidator<Jwt>> delegates = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
|
||||||
.getField(jwtValidator, "tokenValidators");
|
|
||||||
validateDelegates(issuerUri, delegates, customValidator);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateDelegates(String issuerUri, Collection<OAuth2TokenValidator<Jwt>> delegates,
|
|
||||||
OAuth2TokenValidator<Jwt> customValidator) {
|
|
||||||
assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
|
|
||||||
OAuth2TokenValidator<Jwt> delegatingValidator = delegates.stream()
|
|
||||||
.filter((v) -> v instanceof DelegatingOAuth2TokenValidator)
|
|
||||||
.findFirst()
|
|
||||||
.get();
|
|
||||||
if (issuerUri != null) {
|
|
||||||
assertThat(delegatingValidator).extracting("tokenValidators")
|
|
||||||
.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
|
|
||||||
.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
|
|
||||||
}
|
|
||||||
List<OAuth2TokenValidator<Jwt>> claimValidators = delegates.stream()
|
|
||||||
.filter((d) -> d instanceof JwtClaimValidator<?>)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
assertThat(claimValidators).anyMatch((v) -> "aud".equals(ReflectionTestUtils.getField(v, "claim")));
|
|
||||||
if (customValidator != null) {
|
|
||||||
assertThat(claimValidators)
|
|
||||||
.anyMatch((v) -> "custom_claim".equals(ReflectionTestUtils.getField(v, "claim")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() throws Exception {
|
void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() throws Exception {
|
||||||
this.server = new MockWebServer();
|
this.server = new MockWebServer();
|
||||||
|
@ -622,7 +590,8 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
.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);
|
||||||
validate(null, jwtDecoder, null);
|
validate(jwt().claim("aud", List.of("https://test-audience.com")), jwtDecoder,
|
||||||
|
(validators) -> assertThat(validators).satisfiesOnlyOnce(audClaimValidator()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,6 +701,37 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
.subject("mock-test-subject");
|
.subject("mock-test-subject");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void validate(Jwt.Builder builder, JwtDecoder jwtDecoder,
|
||||||
|
ThrowingConsumer<List<OAuth2TokenValidator<Jwt>>> validatorsConsumer) {
|
||||||
|
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||||
|
.getField(jwtDecoder, "jwtValidator");
|
||||||
|
assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
|
||||||
|
validatorsConsumer.accept(extractValidators(jwtValidator));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<OAuth2TokenValidator<Jwt>> extractValidators(DelegatingOAuth2TokenValidator<Jwt> delegatingValidator) {
|
||||||
|
Collection<OAuth2TokenValidator<Jwt>> delegates = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
||||||
|
.getField(delegatingValidator, "tokenValidators");
|
||||||
|
List<OAuth2TokenValidator<Jwt>> extracted = new ArrayList<>();
|
||||||
|
for (OAuth2TokenValidator<Jwt> delegate : delegates) {
|
||||||
|
if (delegate instanceof DelegatingOAuth2TokenValidator<Jwt> delegatingDelegate) {
|
||||||
|
extracted.addAll(extractValidators(delegatingDelegate));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
extracted.add(delegate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<OAuth2TokenValidator<Jwt>> audClaimValidator() {
|
||||||
|
return (validator) -> assertThat(validator).isInstanceOf(JwtClaimValidator.class)
|
||||||
|
.extracting("claim")
|
||||||
|
.isEqualTo("aud");
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
static class TestConfig {
|
static class TestConfig {
|
||||||
|
@ -786,7 +786,7 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class CustomTokenValidatorsConfig {
|
static class CustomJwtClaimValidatorConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
JwtClaimValidator<String> customJwtClaimValidator() {
|
JwtClaimValidator<String> customJwtClaimValidator() {
|
||||||
|
|
Loading…
Reference in New Issue