Enable customization of JWK Set URI decoder builders

Closes gh-20750
pull/35090/head
Andy Wilkinson 2 years ago
parent 45068c777f
commit a03fe8befc

@ -0,0 +1,40 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
/**
* Callback interface for the customization of the
* {@link JwkSetUriReactiveJwtDecoderBuilder} used to create the auto-configured
* {@link ReactiveJwtDecoder} for a JWK set URI that has been configured directly or
* obtained through an issuer URI.
*
* @author Andy Wilkinson
* @since 3.1.0
*/
@FunctionalInterface
public interface JwkSetUriReactiveJwtDecoderBuilderCustomizer {
/**
* Customize the given {@code builder}.
* @param builder the {@code builder} to customize
*/
void customize(JwkSetUriReactiveJwtDecoderBuilder builder);
}

@ -26,6 +26,7 @@ import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -45,8 +46,8 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder;
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;
@ -77,11 +78,12 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder
ReactiveJwtDecoder jwtDecoder(ObjectProvider<JwkSetUriReactiveJwtDecoderBuilderCustomizer> customizers) {
JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder
.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithms(this::jwsAlgorithms)
.build();
.jwsAlgorithms(this::jwsAlgorithms);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = builder.build();
String issuerUri = this.properties.getIssuerUri();
Supplier<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
@ -138,10 +140,13 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
@Bean
@Conditional(IssuerUriCondition.class)
SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() {
SupplierReactiveJwtDecoder jwtDecoderByIssuerUri(
ObjectProvider<JwkSetUriReactiveJwtDecoderBuilderCustomizer> customizers) {
return new SupplierReactiveJwtDecoder(() -> {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) ReactiveJwtDecoders
.fromIssuerLocation(this.properties.getIssuerUri());
JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder
.withIssuerLocation(this.properties.getIssuerUri());
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
NimbusReactiveJwtDecoder jwtDecoder = builder.build();
jwtDecoder.setJwtValidator(
getValidators(() -> JwtValidators.createDefaultWithIssuer(this.properties.getIssuerUri())));
return jwtDecoder;

@ -0,0 +1,39 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
/**
* Callback interface for the customization of the {@link JwkSetUriJwtDecoderBuilder} used
* to create the auto-configured {@link JwtDecoder} for a JWK set URI that has been
* configured directly or obtained through an issuer URI.
*
* @author Andy Wilkinson
* @since 3.1.0
*/
@FunctionalInterface
public interface JwkSetUriJwtDecoderBuilderCustomizer {
/**
* Customize the given {@code builder}.
* @param builder the {@code builder} to customize
*/
void customize(JwkSetUriJwtDecoderBuilder builder);
}

@ -26,6 +26,7 @@ import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -44,9 +45,9 @@ import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.CollectionUtils;
@ -78,10 +79,11 @@ class OAuth2ResourceServerJwtConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
JwtDecoder jwtDecoderByJwkKeySetUri() {
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithms(this::jwsAlgorithms)
.build();
JwtDecoder jwtDecoderByJwkKeySetUri(ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> customizers) {
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithms(this::jwsAlgorithms);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
NimbusJwtDecoder nimbusJwtDecoder = builder.build();
String issuerUri = this.properties.getIssuerUri();
Supplier<OAuth2TokenValidator<Jwt>> defaultValidator = (issuerUri != null)
? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault;
@ -138,10 +140,12 @@ class OAuth2ResourceServerJwtConfiguration {
@Bean
@Conditional(IssuerUriCondition.class)
SupplierJwtDecoder jwtDecoderByIssuerUri() {
SupplierJwtDecoder jwtDecoderByIssuerUri(ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> customizers) {
return new SupplierJwtDecoder(() -> {
String issuerUri = this.properties.getIssuerUri();
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri);
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
NimbusJwtDecoder jwtDecoder = builder.build();
jwtDecoder.setJwtValidator(getValidators(() -> JwtValidators.createDefaultWithIssuer(issuerUri)));
return jwtDecoder;
});

@ -35,6 +35,7 @@ import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@ -43,6 +44,7 @@ import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplic
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -72,6 +74,8 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.WebFilter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.springframework.security.config.Customizer.withDefaults;
@ -92,7 +96,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
private MockWebServer server;
private static final Duration TIMEOUT = Duration.ofSeconds(5);
private static final Duration TIMEOUT = Duration.ofSeconds(5000000);
private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\","
+ "\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGm"
@ -127,9 +131,21 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.signatureAlgorithms")
.asInstanceOf(InstanceOfAssertFactories.collection(SignatureAlgorithm.class))
.containsExactlyInAnyOrder(SignatureAlgorithm.RS512);
assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context);
});
}
private void assertJwkSetUriReactiveJwtDecoderBuilderCustomization(
AssertableReactiveWebApplicationContext context) {
JwkSetUriReactiveJwtDecoderBuilderCustomizer customizer = context.getBean("decoderBuilderCustomizer",
JwkSetUriReactiveJwtDecoderBuilderCustomizer.class);
JwkSetUriReactiveJwtDecoderBuilderCustomizer anotherCustomizer = context
.getBean("anotherDecoderBuilderCustomizer", JwkSetUriReactiveJwtDecoderBuilderCustomizer.class);
InOrder inOrder = inOrder(customizer, anotherCustomizer);
inOrder.verify(customizer).customize(any());
inOrder.verify(anotherCustomizer).customize(any());
}
@Test
void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingMultipleJwsAlgorithms() {
this.contextRunner
@ -141,6 +157,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
.asInstanceOf(InstanceOfAssertFactories.collection(SignatureAlgorithm.class))
.containsExactlyInAnyOrder(SignatureAlgorithm.RS256, SignatureAlgorithm.RS384,
SignatureAlgorithm.RS512);
assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context);
});
}
@ -172,7 +189,6 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
}
@Test
@SuppressWarnings("unchecked")
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException {
this.server = new MockWebServer();
this.server.start();
@ -187,18 +203,32 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context
.getBean(SupplierReactiveJwtDecoder.class);
Mono<ReactiveJwtDecoder> reactiveJwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
.getField(supplierReactiveJwtDecoder, "jwtDecoderMono");
reactiveJwtDecoderSupplier.block(TIMEOUT);
// Trigger calls to the issuer by decoding a token
decodeJwt(context);
assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context);
});
// The last request is to the JWK Set endpoint to look up the algorithm
assertThat(this.server.getRequestCount()).isOne();
assertThat(this.server.getRequestCount()).isEqualTo(2);
}
@Test
@SuppressWarnings("unchecked")
private void decodeJwt(AssertableReactiveWebApplicationContext context) {
SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context.getBean(SupplierReactiveJwtDecoder.class);
Mono<ReactiveJwtDecoder> reactiveJwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
.getField(supplierReactiveJwtDecoder, "jwtDecoderMono");
try {
reactiveJwtDecoderSupplier.flatMap((decoder) -> decoder.decode("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0."
+ "NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ"))
.block(TIMEOUT);
}
catch (Exception ex) {
// This fails, but it's enough to check that the expected HTTP calls
// are made
}
}
@Test
void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
@ -212,18 +242,15 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context
.getBean(SupplierReactiveJwtDecoder.class);
Mono<ReactiveJwtDecoder> reactiveJwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
.getField(supplierReactiveJwtDecoder, "jwtDecoderMono");
reactiveJwtDecoderSupplier.block(TIMEOUT);
// Trigger calls to the issuer by decoding a token
decodeJwt(context);
// assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context);
});
// The last request is to the JWK Set endpoint to look up the algorithm
assertThat(this.server.getRequestCount()).isEqualTo(2);
assertThat(this.server.getRequestCount()).isEqualTo(3);
}
@Test
@SuppressWarnings("unchecked")
void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
@ -237,14 +264,12 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context
.getBean(SupplierReactiveJwtDecoder.class);
Mono<ReactiveJwtDecoder> reactiveJwtDecoderSupplier = (Mono<ReactiveJwtDecoder>) ReflectionTestUtils
.getField(supplierReactiveJwtDecoder, "jwtDecoderMono");
reactiveJwtDecoderSupplier.block(TIMEOUT);
// Trigger calls to the issuer by decoding a token
decodeJwt(context);
assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context);
});
// The last request is to the JWK Set endpoint to look up the algorithm
assertThat(this.server.getRequestCount()).isEqualTo(3);
assertThat(this.server.getRequestCount()).isEqualTo(4);
}
@Test
@ -666,6 +691,18 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
return mock(MapReactiveUserDetailsService.class);
}
@Bean
@Order(1)
JwkSetUriReactiveJwtDecoderBuilderCustomizer decoderBuilderCustomizer() {
return mock(JwkSetUriReactiveJwtDecoderBuilderCustomizer.class);
}
@Bean
@Order(2)
JwkSetUriReactiveJwtDecoderBuilderCustomizer anotherDecoderBuilderCustomizer() {
return mock(JwkSetUriReactiveJwtDecoderBuilderCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)

@ -35,6 +35,7 @@ import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
@ -43,6 +44,7 @@ import org.springframework.boot.test.context.assertj.AssertableWebApplicationCon
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@ -67,6 +69,8 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
@ -105,9 +109,20 @@ class OAuth2ResourceServerAutoConfigurationTests {
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
assertJwkSetUriJwtDecoderBuilderCustomization(context);
});
}
private void assertJwkSetUriJwtDecoderBuilderCustomization(AssertableWebApplicationContext context) {
JwkSetUriJwtDecoderBuilderCustomizer customizer = context.getBean("decoderBuilderCustomizer",
JwkSetUriJwtDecoderBuilderCustomizer.class);
JwkSetUriJwtDecoderBuilderCustomizer anotherCustomizer = context.getBean("anotherDecoderBuilderCustomizer",
JwkSetUriJwtDecoderBuilderCustomizer.class);
InOrder inOrder = inOrder(customizer, anotherCustomizer);
inOrder.verify(customizer).customize(any());
inOrder.verify(anotherCustomizer).customize(any());
}
@Test
void autoConfigurationShouldMatchDefaultJwsAlgorithm() {
this.contextRunner
@ -194,6 +209,7 @@ class OAuth2ResourceServerAutoConfigurationTests {
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
.getField(supplierJwtDecoderBean, "delegate");
jwtDecoderSupplier.get();
assertJwkSetUriJwtDecoderBuilderCustomization(context);
});
// The last request is to the JWK Set endpoint to look up the algorithm
assertThat(this.server.getRequestCount()).isEqualTo(2);
@ -218,6 +234,7 @@ class OAuth2ResourceServerAutoConfigurationTests {
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
.getField(supplierJwtDecoderBean, "delegate");
jwtDecoderSupplier.get();
assertJwkSetUriJwtDecoderBuilderCustomization(context);
});
// The last request is to the JWK Set endpoint to look up the algorithm
assertThat(this.server.getRequestCount()).isEqualTo(3);
@ -243,6 +260,7 @@ class OAuth2ResourceServerAutoConfigurationTests {
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
.getField(supplierJwtDecoderBean, "delegate");
jwtDecoderSupplier.get();
assertJwkSetUriJwtDecoderBuilderCustomization(context);
});
// The last request is to the JWK Set endpoint to look up the algorithm
assertThat(this.server.getRequestCount()).isEqualTo(4);
@ -678,6 +696,18 @@ class OAuth2ResourceServerAutoConfigurationTests {
@EnableWebSecurity
static class TestConfig {
@Bean
@Order(1)
JwkSetUriJwtDecoderBuilderCustomizer decoderBuilderCustomizer() {
return mock(JwkSetUriJwtDecoderBuilderCustomizer.class);
}
@Bean
@Order(2)
JwkSetUriJwtDecoderBuilderCustomizer anotherDecoderBuilderCustomizer() {
return mock(JwkSetUriJwtDecoderBuilderCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)

Loading…
Cancel
Save