|
|
|
|
@ -17,7 +17,10 @@
@@ -17,7 +17,10 @@
|
|
|
|
|
package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; |
|
|
|
|
|
|
|
|
|
import java.io.IOException; |
|
|
|
|
import java.net.MalformedURLException; |
|
|
|
|
import java.net.URL; |
|
|
|
|
import java.time.Duration; |
|
|
|
|
import java.time.Instant; |
|
|
|
|
import java.util.Collection; |
|
|
|
|
import java.util.Collections; |
|
|
|
|
import java.util.HashMap; |
|
|
|
|
@ -51,7 +54,9 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi
@@ -51,7 +54,9 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi
|
|
|
|
|
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; |
|
|
|
|
import org.springframework.security.oauth2.core.OAuth2TokenValidator; |
|
|
|
|
import org.springframework.security.oauth2.jwt.Jwt; |
|
|
|
|
import org.springframework.security.oauth2.jwt.JwtClaimValidator; |
|
|
|
|
import org.springframework.security.oauth2.jwt.JwtIssuerValidator; |
|
|
|
|
import org.springframework.security.oauth2.jwt.JwtTimestampValidator; |
|
|
|
|
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; |
|
|
|
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; |
|
|
|
|
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; |
|
|
|
|
@ -75,6 +80,7 @@ import static org.mockito.Mockito.mock;
@@ -75,6 +80,7 @@ import static org.mockito.Mockito.mock;
|
|
|
|
|
* @author Artsiom Yudovin |
|
|
|
|
* @author HaiTao Zhang |
|
|
|
|
* @author Anastasiia Losieva |
|
|
|
|
* @author Mushtaq Ahmed |
|
|
|
|
*/ |
|
|
|
|
class ReactiveOAuth2ResourceServerAutoConfigurationTests { |
|
|
|
|
|
|
|
|
|
@ -390,6 +396,144 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
@@ -390,6 +396,144 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked") |
|
|
|
|
@Test |
|
|
|
|
void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() throws Exception { |
|
|
|
|
this.server = new MockWebServer(); |
|
|
|
|
this.server.start(); |
|
|
|
|
String path = "test"; |
|
|
|
|
String issuer = this.server.url(path).toString(); |
|
|
|
|
String cleanIssuerPath = cleanIssuerPath(issuer); |
|
|
|
|
setupMockResponse(cleanIssuerPath); |
|
|
|
|
this.contextRunner |
|
|
|
|
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") |
|
|
|
|
.run((context) -> { |
|
|
|
|
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); |
|
|
|
|
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class); |
|
|
|
|
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils |
|
|
|
|
.getField(reactiveJwtDecoder, "jwtValidator"); |
|
|
|
|
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); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked") |
|
|
|
|
@Test |
|
|
|
|
void autoConfigurationShouldConfigureIssuerAndAudienceJwtValidatorIfPropertyProvided() throws Exception { |
|
|
|
|
this.server = new MockWebServer(); |
|
|
|
|
this.server.start(); |
|
|
|
|
String path = "test"; |
|
|
|
|
String issuer = this.server.url(path).toString(); |
|
|
|
|
String cleanIssuerPath = cleanIssuerPath(issuer); |
|
|
|
|
setupMockResponse(cleanIssuerPath); |
|
|
|
|
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<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 |
|
|
|
|
.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(); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) { |
|
|
|
|
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context |
|
|
|
|
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); |
|
|
|
|
@ -458,6 +602,19 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
@@ -458,6 +602,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 { |
|
|
|
|
|
|
|
|
|
|