Create spring-boot-security-oauth2-resource-server module

This commit is contained in:
Andy Wilkinson
2025-03-31 12:58:44 +01:00
committed by Phillip Webb
parent d5e84c627d
commit 0c0af48fea
42 changed files with 120 additions and 92 deletions

View File

@@ -0,0 +1,30 @@
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies"
}
description = "Spring Boot Security OAuth2 Resource Server"
dependencies {
api(project(":spring-boot-project:spring-boot"))
api("org.springframework.security:spring-security-oauth2-jose")
api("org.springframework.security:spring-security-oauth2-resource-server")
implementation(project(":spring-boot-project:spring-boot-security"))
optional(project(":spring-boot-project:spring-boot-autoconfigure"))
optional("io.projectreactor:reactor-core")
optional("jakarta.servlet:jakarta.servlet-api")
testImplementation(project(":spring-boot-project:spring-boot-test"))
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation(project(":spring-boot-project:spring-boot-webmvc"))
testImplementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("com.squareup.okhttp3:mockwebserver")
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("org.springframework:spring-webflux")
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
/**
* Condition that matches when an {@link NimbusJwtDecoder#withIssuerLocation
* issuer-location-based JWT decoder} should be used.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(IssuerUriCondition.class)
public @interface ConditionalOnIssuerLocationJwtDecoder {
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
/**
* Condition that matches when a {@link NimbusJwtDecoder#withPublicKey public-key-based
* JWT decoder} should be used.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(KeyValueCondition.class)
public @interface ConditionalOnPublicKeyJwtDecoder {
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.StringUtils;
/**
* Condition for creating {@link JwtDecoder} by oidc issuer location.
*
* @author Artsiom Yudovin
*/
class IssuerUriCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OpenID Connect Issuer URI Condition");
Environment environment = context.getEnvironment();
String issuerUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.issuer-uri");
if (!StringUtils.hasText(issuerUri)) {
return ConditionOutcome.noMatch(message.didNotFind("issuer-uri property").atAll());
}
String jwkSetUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri");
if (StringUtils.hasText(jwkSetUri)) {
return ConditionOutcome.noMatch(message.found("jwk-set-uri property").items(jwkSetUri));
}
return ConditionOutcome.match(message.foundExactly("issuer-uri property"));
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.StringUtils;
/**
* Condition for creating a jwt decoder using a public key value.
*
* @author Madhura Bhave
*/
class KeyValueCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("Public Key Value Condition");
Environment environment = context.getEnvironment();
String publicKeyLocation = environment
.getProperty("spring.security.oauth2.resourceserver.jwt.public-key-location");
if (!StringUtils.hasText(publicKeyLocation)) {
return ConditionOutcome.noMatch(message.didNotFind("public-key-location property").atAll());
}
String jwkSetUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.jwk-set-uri");
if (StringUtils.hasText(jwkSetUri)) {
return ConditionOutcome.noMatch(message.found("jwk-set-uri property").items(jwkSetUri));
}
String issuerUri = environment.getProperty("spring.security.oauth2.resourceserver.jwt.issuer-uri");
if (StringUtils.hasText(issuerUri)) {
return ConditionOutcome.noMatch(message.found("issuer-uri property").items(issuerUri));
}
return ConditionOutcome.match(message.foundExactly("public key location property"));
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;
/**
* OAuth 2.0 resource server properties.
*
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author Mushtaq Ahmed
* @author Yan Kardziyaka
* @since 4.0.0
*/
@ConfigurationProperties("spring.security.oauth2.resourceserver")
public class OAuth2ResourceServerProperties {
private final Jwt jwt = new Jwt();
public Jwt getJwt() {
return this.jwt;
}
private final Opaquetoken opaquetoken = new Opaquetoken();
public Opaquetoken getOpaquetoken() {
return this.opaquetoken;
}
public static class Jwt {
/**
* JSON Web Key URI to use to verify the JWT token.
*/
private String jwkSetUri;
/**
* JSON Web Algorithms used for verifying the digital signatures.
*/
private List<String> jwsAlgorithms = Arrays.asList("RS256");
/**
* URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0
* Authorization Server Metadata endpoint defined by RFC 8414.
*/
private String issuerUri;
/**
* Location of the file containing the public key used to verify a JWT.
*/
private Resource publicKeyLocation;
/**
* Identifies the recipients that the JWT is intended for.
*/
private List<String> audiences = new ArrayList<>();
/**
* Prefix to use for authorities mapped from JWT.
*/
private String authorityPrefix;
/**
* Regex to use for splitting the value of the authorities claim into authorities.
*/
private String authoritiesClaimDelimiter;
/**
* Name of token claim to use for mapping authorities from JWT.
*/
private String authoritiesClaimName;
/**
* JWT principal claim name.
*/
private String principalClaimName;
public String getJwkSetUri() {
return this.jwkSetUri;
}
public void setJwkSetUri(String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}
public List<String> getJwsAlgorithms() {
return this.jwsAlgorithms;
}
public void setJwsAlgorithms(List<String> jwsAlgorithms) {
this.jwsAlgorithms = jwsAlgorithms;
}
public String getIssuerUri() {
return this.issuerUri;
}
public void setIssuerUri(String issuerUri) {
this.issuerUri = issuerUri;
}
public Resource getPublicKeyLocation() {
return this.publicKeyLocation;
}
public void setPublicKeyLocation(Resource publicKeyLocation) {
this.publicKeyLocation = publicKeyLocation;
}
public List<String> getAudiences() {
return this.audiences;
}
public void setAudiences(List<String> audiences) {
this.audiences = audiences;
}
public String getAuthorityPrefix() {
return this.authorityPrefix;
}
public void setAuthorityPrefix(String authorityPrefix) {
this.authorityPrefix = authorityPrefix;
}
public String getAuthoritiesClaimDelimiter() {
return this.authoritiesClaimDelimiter;
}
public void setAuthoritiesClaimDelimiter(String authoritiesClaimDelimiter) {
this.authoritiesClaimDelimiter = authoritiesClaimDelimiter;
}
public String getAuthoritiesClaimName() {
return this.authoritiesClaimName;
}
public void setAuthoritiesClaimName(String authoritiesClaimName) {
this.authoritiesClaimName = authoritiesClaimName;
}
public String getPrincipalClaimName() {
return this.principalClaimName;
}
public void setPrincipalClaimName(String principalClaimName) {
this.principalClaimName = principalClaimName;
}
public String readPublicKey() throws IOException {
String key = "spring.security.oauth2.resourceserver.public-key-location";
if (this.publicKeyLocation == null) {
throw new InvalidConfigurationPropertyValueException(key, this.publicKeyLocation,
"No public key location specified");
}
if (!this.publicKeyLocation.exists()) {
throw new InvalidConfigurationPropertyValueException(key, this.publicKeyLocation,
"Public key location does not exist");
}
try (InputStream inputStream = this.publicKeyLocation.getInputStream()) {
return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
}
}
}
public static class Opaquetoken {
/**
* Client id used to authenticate with the token introspection endpoint.
*/
private String clientId;
/**
* Client secret used to authenticate with the token introspection endpoint.
*/
private String clientSecret;
/**
* OAuth 2.0 endpoint through which token introspection is accomplished.
*/
private String introspectionUri;
public String getClientId() {
return this.clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return this.clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getIntrospectionUri() {
return this.introspectionUri;
}
public void setIntrospectionUri(String introspectionUri) {
this.introspectionUri = introspectionUri;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Support for Spring Security's OAuth2 resource server.
*/
package org.springframework.boot.security.oauth2.server.resource.autoconfigure;

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.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 4.0.0
*/
@FunctionalInterface
public interface JwkSetUriReactiveJwtDecoderBuilderCustomizer {
/**
* Customize the given {@code builder}.
* @param builder the {@code builder} to customize
*/
void customize(JwkSetUriReactiveJwtDecoderBuilder builder);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.reactive;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.security.autoconfigure.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.security.autoconfigure.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Reactive OAuth2 resource server
* support.
*
* @author Madhura Bhave
* @since 4.0.0
*/
@AutoConfiguration(
before = { ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class })
@EnableConfigurationProperties(OAuth2ResourceServerProperties.class)
@ConditionalOnClass({ EnableWebFluxSecurity.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@Import({ ReactiveOAuth2ResourceServerConfiguration.JwtConfiguration.class,
ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class })
public class ReactiveOAuth2ResourceServerAutoConfiguration {
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.reactive;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
/**
* Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a
* regular auto-configuration class to guarantee their order of execution.
*
* @author Madhura Bhave
*/
class ReactiveOAuth2ResourceServerConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class,
ReactiveOAuth2ResourceServerJwkConfiguration.JwtConverterConfiguration.class,
ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class })
static class JwtConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class })
@Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class,
ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class })
static class OpaqueTokenConfiguration {
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.reactive;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.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.SupplierReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter;
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
* or Public Key configuration is available. Also configures a
* {@link SecurityWebFilterChain} if a {@link ReactiveJwtDecoder} bean is found.
*
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Anastasiia Losieva
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
@Configuration(proxyBeanMethods = false)
class ReactiveOAuth2ResourceServerJwkConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveJwtDecoder.class)
static class JwtConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
private final List<OAuth2TokenValidator<Jwt>> additionalValidators;
JwtConfiguration(OAuth2ResourceServerProperties properties,
ObjectProvider<OAuth2TokenValidator<Jwt>> additionalValidators) {
this.properties = properties.getJwt();
this.additionalValidators = additionalValidators.orderedStream().toList();
}
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
ReactiveJwtDecoder jwtDecoder(ObjectProvider<JwkSetUriReactiveJwtDecoderBuilderCustomizer> customizers) {
JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder
.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithms(this::jwsAlgorithms);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = builder.build();
String issuerUri = this.properties.getIssuerUri();
OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
nimbusReactiveJwtDecoder.setJwtValidator(getValidators(defaultValidator));
return nimbusReactiveJwtDecoder;
}
private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
for (String algorithm : this.properties.getJwsAlgorithms()) {
signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
}
}
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<>();
validators.add(defaultValidator);
if (!CollectionUtils.isEmpty(audiences)) {
validators.add(audValidator(audiences));
}
validators.addAll(this.additionalValidators);
return new DelegatingOAuth2TokenValidator<>(validators);
}
private JwtClaimValidator<List<String>> audValidator(List<String> audiences) {
return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences));
}
private boolean nullSafeDisjoint(List<String> c1, List<String> c2) {
return c1 != null && !Collections.disjoint(c1, c2);
}
@Bean
@ConditionalOnPublicKeyJwtDecoder
NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm()))
.build();
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault()));
return jwtDecoder;
}
private byte[] getKeySpec(String keyValue) {
keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
return Base64.getMimeDecoder().decode(keyValue);
}
private String exactlyOneAlgorithm() {
List<String> algorithms = this.properties.getJwsAlgorithms();
int count = (algorithms != null) ? algorithms.size() : 0;
if (count != 1) {
throw new IllegalStateException(
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count
+ " were configured");
}
return algorithms.get(0);
}
@Bean
@ConditionalOnIssuerLocationJwtDecoder
SupplierReactiveJwtDecoder jwtDecoderByIssuerUri(
ObjectProvider<JwkSetUriReactiveJwtDecoderBuilderCustomizer> customizers) {
return new SupplierReactiveJwtDecoder(() -> {
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;
});
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class)
@Conditional(JwtConverterPropertiesCondition.class)
static class JwtConverterConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
this.properties = properties.getJwt();
}
@Bean
ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
map.from(this.properties.getAuthoritiesClaimDelimiter())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
map.from(this.properties.getAuthoritiesClaimName())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter();
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter));
return jwtAuthenticationConverter;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
static class WebSecurityConfiguration {
@Bean
@ConditionalOnBean(ReactiveJwtDecoder.class)
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated());
http.oauth2ResourceServer((server) -> customDecoder(server, jwtDecoder));
return http.build();
}
private void customDecoder(OAuth2ResourceServerSpec server, ReactiveJwtDecoder decoder) {
server.jwt((jwt) -> jwt.jwtDecoder(decoder));
}
}
private static class JwtConverterPropertiesCondition extends AnyNestedCondition {
JwtConverterPropertiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authority-prefix")
static class OnAuthorityPrefix {
}
@ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.principal-claim-name")
static class OnPrincipalClaimName {
}
@ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authorities-claim-name")
static class OnAuthoritiesClaimName {
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.reactive;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Configures a {@link ReactiveOpaqueTokenIntrospector} when a token introspection
* endpoint is available. Also configures a {@link SecurityWebFilterChain} if a
* {@link ReactiveOpaqueTokenIntrospector} bean is found.
*
* @author Madhura Bhave
*/
class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveOpaqueTokenIntrospector.class)
static class OpaqueTokenIntrospectionClientConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri")
SpringReactiveOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) {
OAuth2ResourceServerProperties.Opaquetoken opaquetoken = properties.getOpaquetoken();
return SpringReactiveOpaqueTokenIntrospector.withIntrospectionUri(opaquetoken.getIntrospectionUri())
.clientId(opaquetoken.getClientId())
.clientSecret(opaquetoken.getClientSecret())
.build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
static class WebSecurityConfiguration {
@Bean
@ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class)
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated());
http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));
return http.build();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-configuration for Spring Security's Reactive OAuth2 resource server.
*/
package org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive;

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.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 4.0.0
*/
@FunctionalInterface
public interface JwkSetUriJwtDecoderBuilderCustomizer {
/**
* Customize the given {@code builder}.
* @param builder the {@code builder} to customize
*/
void customize(JwkSetUriJwtDecoderBuilder builder);
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.servlet;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration;
import org.springframework.boot.security.autoconfigure.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
/**
* {@link EnableAutoConfiguration Auto-configuration} for OAuth2 resource server support.
*
* @author Madhura Bhave
* @since 4.0.0
*/
@AutoConfiguration(before = { SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class })
@EnableConfigurationProperties(OAuth2ResourceServerProperties.class)
@ConditionalOnClass(BearerTokenAuthenticationToken.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@Import({ Oauth2ResourceServerConfiguration.JwtConfiguration.class,
Oauth2ResourceServerConfiguration.OpaqueTokenConfiguration.class })
public class OAuth2ResourceServerAutoConfiguration {
}

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.servlet;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.security.autoconfigure.ConditionalOnDefaultWebSecurity;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
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.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.CollectionUtils;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public
* Key configuration is available. Also configures a {@link SecurityFilterChain} if a
* {@link JwtDecoder} bean is found.
*
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
@Configuration(proxyBeanMethods = false)
class OAuth2ResourceServerJwtConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JwtDecoder.class)
static class JwtDecoderConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
private final List<OAuth2TokenValidator<Jwt>> additionalValidators;
JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
ObjectProvider<OAuth2TokenValidator<Jwt>> additionalValidators) {
this.properties = properties.getJwt();
this.additionalValidators = additionalValidators.orderedStream().toList();
}
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
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();
OAuth2TokenValidator<Jwt> defaultValidator = (issuerUri != null)
? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault();
nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
return nimbusJwtDecoder;
}
private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
for (String algorithm : this.properties.getJwsAlgorithms()) {
signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
}
}
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<>();
validators.add(defaultValidator);
if (!CollectionUtils.isEmpty(audiences)) {
validators.add(audValidator(audiences));
}
validators.addAll(this.additionalValidators);
return new DelegatingOAuth2TokenValidator<>(validators);
}
private JwtClaimValidator<List<String>> audValidator(List<String> audiences) {
return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences));
}
private boolean nullSafeDisjoint(List<String> c1, List<String> c2) {
return c1 != null && !Collections.disjoint(c1, c2);
}
@Bean
@ConditionalOnPublicKeyJwtDecoder
JwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey)
.signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm()))
.build();
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault()));
return jwtDecoder;
}
private byte[] getKeySpec(String keyValue) {
keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
return Base64.getMimeDecoder().decode(keyValue);
}
private String exactlyOneAlgorithm() {
List<String> algorithms = this.properties.getJwsAlgorithms();
int count = (algorithms != null) ? algorithms.size() : 0;
if (count != 1) {
throw new IllegalStateException(
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count
+ " were configured");
}
return algorithms.get(0);
}
@Bean
@ConditionalOnIssuerLocationJwtDecoder
SupplierJwtDecoder jwtDecoderByIssuerUri(ObjectProvider<JwkSetUriJwtDecoderBuilderCustomizer> customizers) {
return new SupplierJwtDecoder(() -> {
String issuerUri = this.properties.getIssuerUri();
JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
NimbusJwtDecoder jwtDecoder = builder.build();
jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri)));
return jwtDecoder;
});
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class OAuth2SecurityFilterChainConfiguration {
@Bean
@ConditionalOnBean(JwtDecoder.class)
SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults()));
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JwtAuthenticationConverter.class)
@Conditional(JwtConverterPropertiesCondition.class)
static class JwtConverterConfiguration {
private final OAuth2ResourceServerProperties.Jwt properties;
JwtConverterConfiguration(OAuth2ResourceServerProperties properties) {
this.properties = properties.getJwt();
}
@Bean
JwtAuthenticationConverter getJwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix);
map.from(this.properties.getAuthoritiesClaimDelimiter())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter);
map.from(this.properties.getAuthoritiesClaimName())
.to(grantedAuthoritiesConverter::setAuthoritiesClaimName);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName);
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
private static class JwtConverterPropertiesCondition extends AnyNestedCondition {
JwtConverterPropertiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authority-prefix")
static class OnAuthorityPrefix {
}
@ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.principal-claim-name")
static class OnPrincipalClaimName {
}
@ConditionalOnProperty("spring.security.oauth2.resourceserver.jwt.authorities-claim-name")
static class OnAuthoritiesClaimName {
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.security.autoconfigure.ConditionalOnDefaultWebSecurity;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Configures an {@link OpaqueTokenIntrospector} when a token introspection endpoint is
* available. Also configures a {@link SecurityFilterChain} if a
* {@link OpaqueTokenIntrospector} bean is found.
*
* @author Madhura Bhave
*/
@Configuration(proxyBeanMethods = false)
class OAuth2ResourceServerOpaqueTokenConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(OpaqueTokenIntrospector.class)
static class OpaqueTokenIntrospectionClientConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri")
SpringOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProperties properties) {
OAuth2ResourceServerProperties.Opaquetoken opaquetoken = properties.getOpaquetoken();
return SpringOpaqueTokenIntrospector.withIntrospectionUri(opaquetoken.getIntrospectionUri())
.clientId(opaquetoken.getClientId())
.clientSecret(opaquetoken.getClientSecret())
.build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class OAuth2SecurityFilterChainConfiguration {
@Bean
@ConditionalOnBean(OpaqueTokenIntrospector.class)
SecurityFilterChain opaqueTokenSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));
return http.build();
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.jwt.JwtDecoder;
/**
* Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a
* regular auto-configuration class to guarantee their order of execution.
*
* @author Madhura Bhave
*/
class Oauth2ResourceServerConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JwtDecoder.class)
@Import({ OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class,
OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class,
OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class })
static class JwtConfiguration {
}
@Configuration(proxyBeanMethods = false)
@Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class,
OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2SecurityFilterChainConfiguration.class })
static class OpaqueTokenConfiguration {
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-configuration for Spring Security's OAuth2 resource server.
*/
package org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet;

View File

@@ -0,0 +1,13 @@
{
"groups": [],
"properties": [
{
"name": "spring.security.oauth2.resourceserver.jwt.jws-algorithm",
"type": "java.lang.String",
"deprecation": {
"replacement": "spring.security.oauth2.resourceserver.jwt.jws-algorithms",
"level": "error"
}
}
]
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure;
import java.time.Instant;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* {@link ArgumentsProvider Arguments provider} supplying different Spring Boot properties
* to customize JWT converter behavior, JWT token for conversion, expected principal name
* and expected authorities.
*
* @author Yan Kardziyaka
*/
public final class JwtConverterCustomizationsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
String customPrefix = "CUSTOM_AUTHORITY_PREFIX_";
String customDelimiter = "[~,#:]";
String customAuthoritiesClaim = "custom_authorities";
String customPrincipalClaim = "custom_principal";
String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com";
String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix;
String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter="
+ customDelimiter;
String authoritiesClaimProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-name="
+ customAuthoritiesClaim;
String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name="
+ customPrincipalClaim;
String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty };
String[] customDelimiterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty };
String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty };
String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty };
String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty,
authoritiesClaimProperty, principalClaimProperty };
String[] jwtScopes = { "custom_scope0", "custom_scope1" };
String subjectValue = UUID.randomUUID().toString();
String customPrincipalValue = UUID.randomUUID().toString();
Jwt.Builder jwtBuilder = Jwt.withTokenValue("token")
.header("alg", "none")
.expiresAt(Instant.MAX)
.issuedAt(Instant.MIN)
.issuer("https://issuer.example.org")
.jti("jti")
.notBefore(Instant.MIN)
.subject(subjectValue)
.claim(customPrincipalClaim, customPrincipalValue);
Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build();
Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build();
Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null)
.claim(customAuthoritiesClaim, jwtScopes[0] + " " + jwtScopes[1])
.build();
Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null)
.claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1])
.build();
String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] };
String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] };
return Stream.of(
Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps),
noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities),
Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps),
customAuthoritiesDelimiterJwt, subjectValue, customPrefixAuthorities),
Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps),
customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities),
Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps),
noAuthoritiesCustomizationsJwt, customPrincipalValue, defaultPrefixAuthorities),
Arguments.of(Named.named("All JWT converter customizations", allJwtConverterProps),
customAuthoritiesClaimAndDelimiterJwt, customPrincipalValue, customPrefixAuthorities));
}
}

View File

@@ -0,0 +1,917 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.reactive;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JWSAlgorithm;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.mockito.InOrder;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwtConverterCustomizationsArgumentsProvider;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
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;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
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.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager;
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
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;
/**
* Tests for {@link ReactiveOAuth2ResourceServerAutoConfiguration}.
*
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Anastasiia Losieva
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
class ReactiveOAuth2ResourceServerAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ResourceServerAutoConfiguration.class))
.withUserConfiguration(TestConfig.class);
private MockWebServer server;
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"
+ "uLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtd"
+ "F4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAj"
+ "jDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
@AfterEach
void cleanup() throws Exception {
if (this.server != null) {
this.server.shutdown();
}
}
@Test
void autoConfigurationShouldConfigureResourceServer() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.run((context) -> {
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
@Test
void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingSingleJwsAlgorithm() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS512")
.run((context) -> {
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class);
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
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS256, RS384, RS512")
.run((context) -> {
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class);
assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.signatureAlgorithms")
.asInstanceOf(InstanceOfAssertFactories.collection(SignatureAlgorithm.class))
.containsExactlyInAnyOrder(SignatureAlgorithm.RS256, SignatureAlgorithm.RS384,
SignatureAlgorithm.RS512);
assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context);
});
}
@Test
@WithPublicKeyResource
void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingSingleJwsAlgorithm() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384")
.run((context) -> {
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class);
assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.jwsKeySelector.expectedJWSAlg")
.isEqualTo(JWSAlgorithm.RS384);
});
}
@Test
@WithPublicKeyResource
void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RSA256,RS384")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).hasRootCauseMessage(
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but 2 were "
+ "configured");
});
}
@Test
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException {
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.issuer-uri=http://"
+ this.server.getHostName() + ":" + this.server.getPort() + "/" + path)
.run((context) -> {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
// 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);
}
@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();
String issuer = this.server.url("").toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponsesWithErrors(cleanIssuerPath, 1);
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://"
+ this.server.getHostName() + ":" + this.server.getPort())
.run((context) -> {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
// 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);
}
@Test
void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
String issuer = this.server.url("").toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponsesWithErrors(cleanIssuerPath, 2);
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://"
+ this.server.getHostName() + ":" + this.server.getPort())
.run((context) -> {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
// 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(4);
}
@Test
@WithPublicKeyResource
void autoConfigurationShouldConfigureResourceServerUsingPublicKeyValue() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location")
.run((context) -> {
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
@Test
void autoConfigurationShouldFailIfPublicKeyLocationDoesNotExist() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:does-not-exist")
.run((context) -> assertThat(context).hasFailed()
.getFailure()
.hasMessageContaining("class path resource [does-not-exist]")
.hasMessageContaining("Public key location does not exist"));
}
@Test
void autoConfigurationWhenSetUriKeyLocationIssuerUriPresentShouldUseSetUri() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com")
.run((context) -> {
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoder")).isTrue();
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isFalse();
});
}
@Test
void autoConfigurationWhenKeyLocationAndIssuerUriPresentShouldUseIssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
String issuer = this.server.url("").toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath);
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":"
+ this.server.getPort(),
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location")
.run((context) -> {
assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
});
}
@Test
void autoConfigurationWhenJwkSetUriNullShouldNotFail() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN));
}
@Test
void jwtDecoderBeanIsConditionalOnMissingBean() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.run((this::assertFilterConfiguredWithJwtAuthenticationManager));
}
@Test
void jwtDecoderByIssuerUriBeanIsConditionalOnMissingBean() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com")
.withUserConfiguration(JwtDecoderConfig.class)
.run((this::assertFilterConfiguredWithJwtAuthenticationManager));
}
@Test
void autoConfigurationShouldBeConditionalOnBearerTokenAuthenticationTokenClass() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class))
.run((context) -> assertThat(context).doesNotHaveBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN));
}
@Test
void autoConfigurationShouldBeConditionalOnReactiveJwtDecoderClass() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.withClassLoader(new FilteredClassLoader(ReactiveJwtDecoder.class))
.run((context) -> assertThat(context).doesNotHaveBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN));
}
@Test
void autoConfigurationWhenSecurityWebFilterChainConfigPresentShouldNotAddOne() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(SecurityWebFilterChainConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(SecurityWebFilterChain.class);
assertThat(context).hasBean("testSpringSecurityFilterChain");
});
}
@Test
void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> {
assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class);
assertFilterConfiguredWithOpaqueTokenAuthenticationManager(context);
});
}
@Test
void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> {
assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class);
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
@Test
void opaqueTokenIntrospectorIsConditionalOnMissingBean() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com")
.withUserConfiguration(OpaqueTokenIntrospectorConfig.class)
.run((this::assertFilterConfiguredWithOpaqueTokenAuthenticationManager));
}
@Test
void autoConfigurationForOpaqueTokenWhenSecurityWebFilterChainConfigPresentShouldNotAddOne() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.withUserConfiguration(SecurityWebFilterChainConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(SecurityWebFilterChain.class);
assertThat(context).hasBean("testSpringSecurityFilterChain");
});
}
@Test
void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass() {
this.contextRunner.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class))
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOpaqueTokenIntrospector.class));
}
@Test
void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() 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",
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":"
+ this.server.getPort() + "/" + path)
.run((context) -> {
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
validate(jwt().claim("iss", issuer), reactiveJwtDecoder,
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class));
});
}
@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);
validate(jwt(), reactiveJwtDecoder,
(validators) -> assertThat(validators).hasSize(3).noneSatisfy(audClaimValidator()));
});
}
@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(
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 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(
jwt().claim("iss", URI.create(issuerUri).toURL())
.claim("aud", List.of("https://test-audience.com")),
jwtDecoder,
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
.satisfiesOnlyOnce(audClaimValidator()));
});
}
@Test
@WithPublicKeyResource
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(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));
});
}
@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();
});
}
@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();
});
}
@Test
void shouldNotConfigureJwtConverterIfNoPropertiesAreSet() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveJwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfPrincipalClaimNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.principal-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityPrefixIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authority-prefix=dummy")
.run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class));
}
@ParameterizedTest(name = "{0}")
@ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class)
void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt,
String expectedPrincipal, String[] expectedAuthorities) {
this.contextRunner.withPropertyValues(properties).run((context) -> {
ReactiveJwtAuthenticationConverter converter = context.getBean(ReactiveJwtAuthenticationConverter.class);
AbstractAuthenticationToken token = converter.convert(jwt).block();
assertThat(token).isNotNull().extracting(AbstractAuthenticationToken::getName).isEqualTo(expectedPrincipal);
assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.containsExactlyInAnyOrder(expectedAuthorities);
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
@Test
void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() {
String propertiesPrincipalClaim = "principal_from_properties";
String propertiesPrincipalValue = "from_props";
String userConfigPrincipalValue = "from_user_config";
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + propertiesPrincipalClaim)
.withUserConfiguration(CustomJwtConverterConfig.class)
.run((context) -> {
ReactiveJwtAuthenticationConverter converter = context
.getBean(ReactiveJwtAuthenticationConverter.class);
Jwt jwt = jwt().claim(propertiesPrincipalClaim, propertiesPrincipalValue)
.claim(CustomJwtConverterConfig.PRINCIPAL_CLAIM, userConfigPrincipalValue)
.build();
AbstractAuthenticationToken token = converter.convert(jwt).block();
assertThat(token).isNotNull()
.extracting(AbstractAuthenticationToken::getName)
.isEqualTo(userConfigPrincipalValue)
.isNotEqualTo(propertiesPrincipalValue);
assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
assertFilterConfiguredWithJwtAuthenticationManager(context);
});
}
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
Stream<WebFilter> filters = filterChain.getWebFilters().toStream();
AuthenticationWebFilter webFilter = (AuthenticationWebFilter) filters
.filter((f) -> f instanceof AuthenticationWebFilter)
.findFirst()
.orElse(null);
ReactiveAuthenticationManagerResolver<?> authenticationManagerResolver = (ReactiveAuthenticationManagerResolver<?>) ReflectionTestUtils
.getField(webFilter, "authenticationManagerResolver");
Object authenticationManager = authenticationManagerResolver.resolve(null).block(TIMEOUT);
assertThat(authenticationManager).isInstanceOf(JwtReactiveAuthenticationManager.class);
}
private void assertFilterConfiguredWithOpaqueTokenAuthenticationManager(
AssertableReactiveWebApplicationContext context) {
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
Stream<WebFilter> filters = filterChain.getWebFilters().toStream();
AuthenticationWebFilter webFilter = (AuthenticationWebFilter) filters
.filter((f) -> f instanceof AuthenticationWebFilter)
.findFirst()
.orElse(null);
ReactiveAuthenticationManagerResolver<?> authenticationManagerResolver = (ReactiveAuthenticationManagerResolver<?>) ReflectionTestUtils
.getField(webFilter, "authenticationManagerResolver");
Object authenticationManager = authenticationManagerResolver.resolve(null).block(TIMEOUT);
assertThat(authenticationManager).isInstanceOf(OpaqueTokenReactiveAuthenticationManager.class);
}
private String cleanIssuerPath(String issuer) {
if (issuer.endsWith("/")) {
return issuer.substring(0, issuer.length() - 1);
}
return issuer;
}
private void setupMockResponse(String issuer) throws JsonProcessingException {
MockResponse mockResponse = new MockResponse().setResponseCode(HttpStatus.OK.value())
.setBody(new ObjectMapper().writeValueAsString(getResponse(issuer)))
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.server.enqueue(mockResponse);
this.server.enqueue(
new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json").setBody(JWK_SET));
}
private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException {
for (int i = 0; i < errorResponseCount; i++) {
MockResponse emptyResponse = new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value());
this.server.enqueue(emptyResponse);
}
setupMockResponse(issuer);
}
private Map<String, Object> getResponse(String issuer) {
Map<String, Object> response = new HashMap<>();
response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth");
response.put("claims_supported", Collections.emptyList());
response.put("code_challenge_methods_supported", Collections.emptyList());
response.put("id_token_signing_alg_values_supported", Collections.emptyList());
response.put("issuer", issuer);
response.put("jwks_uri", issuer + "/.well-known/jwks.json");
response.put("response_types_supported", Collections.emptyList());
response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke");
response.put("scopes_supported", Collections.singletonList("openid"));
response.put("subject_types_supported", Collections.singletonList("public"));
response.put("grant_types_supported", Collections.singletonList("authorization_code"));
response.put("token_endpoint", "https://example.com/oauth2/v4/token");
response.put("token_endpoint_auth_methods_supported", Collections.singletonList("client_secret_basic"));
response.put("userinfo_endpoint", "https://example.com/oauth2/v3/userinfo");
return response;
}
static Jwt.Builder jwt() {
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");
}
@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
static class TestConfig {
@Bean
MapReactiveUserDetailsService userDetailsService() {
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)
static class JwtDecoderConfig {
@Bean
ReactiveJwtDecoder decoder() {
return mock(ReactiveJwtDecoder.class);
}
}
@Configuration(proxyBeanMethods = false)
static class OpaqueTokenIntrospectorConfig {
@Bean
ReactiveOpaqueTokenIntrospector decoder() {
return mock(ReactiveOpaqueTokenIntrospector.class);
}
}
@Configuration(proxyBeanMethods = false)
static class SecurityWebFilterChainConfig {
@Bean
SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange((exchanges) -> {
exchanges.pathMatchers("/message/**").hasRole("ADMIN");
exchanges.anyExchange().authenticated();
});
http.httpBasic(withDefaults());
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomJwtClaimValidatorConfig {
@Bean
JwtClaimValidator<String> customJwtClaimValidator() {
return new JwtClaimValidator<>("custom_claim", "custom_claim_value"::equals);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomJwtConverterConfig {
static String PRINCIPAL_CLAIM = "principal_from_user_configuration";
@Bean
ReactiveJwtAuthenticationConverter customReactiveJwtAuthenticationConverter() {
ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setPrincipalClaimName(PRINCIPAL_CLAIM);
return converter;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@WithResource(name = "public-key-location", content = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd
UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs
HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D
o2kQ+X5xK9cipRgEKwIDAQAB
-----END PUBLIC KEY-----
""")
@interface WithPublicKeyResource {
}
}

View File

@@ -0,0 +1,904 @@
/*
* Copyright 2012-2025 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.security.oauth2.server.resource.autoconfigure.servlet;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JWSAlgorithm;
import jakarta.servlet.Filter;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.mockito.InOrder;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.security.oauth2.server.resource.autoconfigure.JwtConverterCustomizationsArgumentsProvider;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
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;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.GrantedAuthority;
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.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.FilterChainProxy;
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;
/**
* Tests for {@link OAuth2ResourceServerAutoConfiguration}.
*
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Mushtaq Ahmed
* @author Roman Golovin
* @author Yan Kardziyaka
*/
class OAuth2ResourceServerAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OAuth2ResourceServerAutoConfiguration.class))
.withUserConfiguration(TestConfig.class);
private MockWebServer server;
private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\","
+ "\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGm"
+ "uLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtd"
+ "F4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAj"
+ "jDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
@AfterEach
void cleanup() throws Exception {
if (this.server != null) {
this.server.shutdown();
}
}
@Test
void autoConfigurationShouldConfigureResourceServer() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.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
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.run((context) -> {
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
assertThat(jwtDecoder).extracting("jwtProcessor.jwsKeySelector.jwsAlgs")
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
.containsExactlyInAnyOrder(JWSAlgorithm.RS256);
});
}
@Test
void autoConfigurationShouldConfigureResourceServerWithSingleJwsAlgorithm() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384")
.run((context) -> {
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
assertThat(jwtDecoder).extracting("jwtProcessor.jwsKeySelector.jwsAlgs")
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
.containsExactlyInAnyOrder(JWSAlgorithm.RS384);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
@Test
void autoConfigurationShouldConfigureResourceServerWithMultipleJwsAlgorithms() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS256, RS384, RS512")
.run((context) -> {
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
assertThat(jwtDecoder).extracting("jwtProcessor.jwsKeySelector.jwsAlgs")
.asInstanceOf(InstanceOfAssertFactories.collection(JWSAlgorithm.class))
.containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS384, JWSAlgorithm.RS512);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
@Test
@WithPublicKeyResource
void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingSingleJwsAlgorithm() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RS384")
.run((context) -> {
NimbusJwtDecoder nimbusJwtDecoder = context.getBean(NimbusJwtDecoder.class);
assertThat(nimbusJwtDecoder).extracting("jwtProcessor.jwsKeySelector.expectedJWSAlg")
.isEqualTo(JWSAlgorithm.RS384);
});
}
@Test
@WithPublicKeyResource
void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=RSA256,RS384")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).hasRootCauseMessage(
"Creating a JWT decoder using a public key requires exactly one JWS algorithm but 2 were "
+ "configured");
});
}
@SuppressWarnings("unchecked")
@Test
void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() 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.issuer-uri=http://"
+ this.server.getHostName() + ":" + this.server.getPort() + "/" + path)
.run((context) -> {
assertThat(context).hasSingleBean(SupplierJwtDecoder.class);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
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);
}
@SuppressWarnings("unchecked")
@Test
void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
String path = "test";
String issuer = this.server.url(path).toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponsesWithErrors(cleanIssuerPath, 1);
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://"
+ this.server.getHostName() + ":" + this.server.getPort() + "/" + path)
.run((context) -> {
assertThat(context).hasSingleBean(SupplierJwtDecoder.class);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
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);
}
@SuppressWarnings("unchecked")
@Test
void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
String path = "test";
String issuer = this.server.url(path).toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponsesWithErrors(cleanIssuerPath, 2);
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://"
+ this.server.getHostName() + ":" + this.server.getPort() + "/" + path)
.run((context) -> {
assertThat(context).hasSingleBean(SupplierJwtDecoder.class);
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
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);
}
@Test
@WithPublicKeyResource
void autoConfigurationShouldConfigureResourceServerUsingPublicKeyValue() throws Exception {
this.server = new MockWebServer();
this.server.start();
String issuer = this.server.url("").toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath);
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location")
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
@Test
void autoConfigurationShouldFailIfPublicKeyLocationDoesNotExist() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:does-not-exist")
.run((context) -> assertThat(context).hasFailed()
.getFailure()
.hasMessageContaining("class path resource [does-not-exist]")
.hasMessageContaining("Public key location does not exist"));
}
@Test
@WithPublicKeyResource
void autoConfigurationShouldFailIfAlgorithmIsInvalid() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.jws-algorithms=NOT_VALID")
.run((context) -> assertThat(context).hasFailed()
.getFailure()
.hasMessageContaining("signatureAlgorithm cannot be null"));
}
@Test
void autoConfigurationWhenSetUriKeyLocationAndIssuerUriPresentShouldUseSetUri() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=https://issuer-uri.com",
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
"spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
assertThat(context.containsBean("jwtDecoderByJwkKeySetUri")).isTrue();
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isFalse();
});
}
@Test
void autoConfigurationWhenKeyLocationAndIssuerUriPresentShouldUseIssuerUri() throws Exception {
this.server = new MockWebServer();
this.server.start();
String issuer = this.server.url("").toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath);
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":"
+ this.server.getPort(),
"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location")
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue();
});
}
@Test
void autoConfigurationWhenJwkSetUriNullShouldNotFail() {
this.contextRunner.run((context) -> assertThat(getBearerTokenFilter(context)).isNull());
}
@Test
void jwtDecoderByJwkSetUriIsConditionalOnMissingBean() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull());
}
@Test
void jwtDecoderByOidcIssuerUriIsConditionalOnMissingBean() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com")
.withUserConfiguration(JwtDecoderConfig.class)
.run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull());
}
@Test
void autoConfigurationShouldBeConditionalOnResourceServerClass() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class))
.run((context) -> {
assertThat(context).doesNotHaveBean(OAuth2ResourceServerAutoConfiguration.class);
assertThat(getBearerTokenFilter(context)).isNull();
});
}
@Test
void autoConfigurationForJwtShouldBeConditionalOnJwtDecoderClass() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.withClassLoader(new FilteredClassLoader(JwtDecoder.class))
.run((context) -> {
assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class);
assertThat(getBearerTokenFilter(context)).isNull();
});
}
@Test
void jwtSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class)
.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class))
.run((context) -> {
assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class);
assertThat(getBearerTokenFilter(context)).isNull();
});
}
@Test
void opaqueTokenSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class))
.run((context) -> {
assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class);
assertThat(getBearerTokenFilter(context)).isNull();
});
}
@Test
void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() {
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> {
assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class);
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).extracting("authenticationManagerResolver.arg$1.providers")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.hasAtLeastOneElementOfType(JwtAuthenticationProvider.class);
});
}
@Test
void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> {
assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
@Test
void opaqueTokenIntrospectorIsConditionalOnMissingBean() {
this.contextRunner
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com")
.withUserConfiguration(OpaqueTokenIntrospectorConfig.class)
.run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull());
}
@Test
void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass() {
this.contextRunner.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class))
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> assertThat(context).doesNotHaveBean(OpaqueTokenIntrospector.class));
}
@Test
void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() 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",
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":"
+ this.server.getPort() + "/" + path)
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
validate(jwt().claim("iss", issuer), jwtDecoder,
(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class));
});
}
@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(JwtDecoder.class);
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
validate(jwt(), jwtDecoder,
(validators) -> assertThat(validators).hasSize(3).noneSatisfy(audClaimValidator()));
});
}
@Test
void autoConfigurationShouldConfigureAudienceAndIssuerJwtValidatorIfPropertyProvided() throws Exception {
this.server = new MockWebServer();
this.server.start();
String path = "test";
String issuer = this.server.url(path).toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath);
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
this.contextRunner.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
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")
@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, "delegate");
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
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")
@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.issuer-uri=" + issuerUri)
.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
.run((context) -> {
SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
Supplier<JwtDecoder> jwtDecoderSupplier = (Supplier<JwtDecoder>) ReflectionTestUtils
.getField(supplierJwtDecoderBean, "delegate");
JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
assertThat(context).hasBean("customJwtClaimValidator");
OAuth2TokenValidator<Jwt> customValidator = (OAuth2TokenValidator<Jwt>) context
.getBean("customJwtClaimValidator");
validate(jwt().claim("iss", URI.create(issuerUri).toURL()).claim("custom_claim", "custom_claim_value"),
jwtDecoder, (validators) -> assertThat(validators).contains(customValidator)
.hasAtLeastOneElementOfType(JwtIssuerValidator.class));
});
}
@Test
@WithPublicKeyResource
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(jwt().claim("aud", List.of("https://test-audience.com")), jwtDecoder,
(validators) -> assertThat(validators).satisfiesOnlyOnce(audClaimValidator()));
});
}
@SuppressWarnings("unchecked")
@Test
void audienceValidatorWhenAudienceInvalid() throws Exception {
this.server = new MockWebServer();
this.server.start();
String path = "test";
String issuer = this.server.url(path).toString();
String cleanIssuerPath = cleanIssuerPath(issuer);
setupMockResponse(cleanIssuerPath);
String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
this.contextRunner.withPropertyValues(
"spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
.run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
DelegatingOAuth2TokenValidator<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();
});
}
@Test
void jwtSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() {
this.contextRunner.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class))
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
.withUserConfiguration(JwtDecoderConfig.class, TestSecurityFilterChainConfig.class)
.run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class));
}
@Test
void opaqueTokenSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() {
this.contextRunner.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class))
.withUserConfiguration(TestSecurityFilterChainConfig.class)
.withPropertyValues(
"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
"spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret")
.run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class));
}
@ParameterizedTest(name = "{0}")
@ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class)
void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt,
String expectedPrincipal, String[] expectedAuthorities) {
this.contextRunner.withPropertyValues(properties).run((context) -> {
JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class);
AbstractAuthenticationToken token = converter.convert(jwt);
assertThat(token).isNotNull().extracting(AbstractAuthenticationToken::getName).isEqualTo(expectedPrincipal);
assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority)
.containsExactlyInAnyOrder(expectedAuthorities);
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
@Test
void shouldNotConfigureJwtConverterIfNoPropertiesAreSet() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(JwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfPrincipalClaimNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.principal-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityPrefixIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authority-prefix=dummy")
.run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class));
}
@Test
void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() {
this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-name=dummy")
.run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class));
}
@Test
void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() {
String propertiesPrincipalClaim = "principal_from_properties";
String propertiesPrincipalValue = "from_props";
String userConfigPrincipalValue = "from_user_config";
this.contextRunner
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
"spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + propertiesPrincipalClaim)
.withUserConfiguration(CustomJwtConverterConfig.class)
.run((context) -> {
JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class);
Jwt jwt = jwt().claim(propertiesPrincipalClaim, propertiesPrincipalValue)
.claim(CustomJwtConverterConfig.PRINCIPAL_CLAIM, userConfigPrincipalValue)
.build();
AbstractAuthenticationToken token = converter.convert(jwt);
assertThat(token).isNotNull()
.extracting(AbstractAuthenticationToken::getName)
.isEqualTo(userConfigPrincipalValue)
.isNotEqualTo(propertiesPrincipalValue);
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(getBearerTokenFilter(context)).isNotNull();
});
}
private Filter getBearerTokenFilter(AssertableWebApplicationContext context) {
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
List<Filter> filters = filterChains.get(0).getFilters();
return filters.stream().filter((f) -> f instanceof BearerTokenAuthenticationFilter).findFirst().orElse(null);
}
private String cleanIssuerPath(String issuer) {
if (issuer.endsWith("/")) {
return issuer.substring(0, issuer.length() - 1);
}
return issuer;
}
private void setupMockResponse(String issuer) throws JsonProcessingException {
MockResponse mockResponse = new MockResponse().setResponseCode(HttpStatus.OK.value())
.setBody(new ObjectMapper().writeValueAsString(getResponse(issuer)))
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.server.enqueue(mockResponse);
this.server.enqueue(
new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json").setBody(JWK_SET));
}
private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException {
for (int i = 0; i < errorResponseCount; i++) {
MockResponse emptyResponse = new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value());
this.server.enqueue(emptyResponse);
}
setupMockResponse(issuer);
}
private Map<String, Object> getResponse(String issuer) {
Map<String, Object> response = new HashMap<>();
response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth");
response.put("claims_supported", Collections.emptyList());
response.put("code_challenge_methods_supported", Collections.emptyList());
response.put("id_token_signing_alg_values_supported", Collections.emptyList());
response.put("issuer", issuer);
response.put("jwks_uri", issuer + "/.well-known/jwks.json");
response.put("response_types_supported", Collections.emptyList());
response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke");
response.put("scopes_supported", Collections.singletonList("openid"));
response.put("subject_types_supported", Collections.singletonList("public"));
response.put("grant_types_supported", Collections.singletonList("authorization_code"));
response.put("token_endpoint", "https://example.com/oauth2/v4/token");
response.put("token_endpoint_auth_methods_supported", Collections.singletonList("client_secret_basic"));
response.put("userinfo_endpoint", "https://example.com/oauth2/v3/userinfo");
return response;
}
static Jwt.Builder jwt() {
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");
}
@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)
@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)
@EnableWebSecurity
static class JwtDecoderConfig {
@Bean
JwtDecoder decoder() {
return mock(JwtDecoder.class);
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class OpaqueTokenIntrospectorConfig {
@Bean
OpaqueTokenIntrospector decoder() {
return mock(OpaqueTokenIntrospector.class);
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class TestSecurityFilterChainConfig {
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher("/**");
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
return http.build();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomJwtClaimValidatorConfig {
@Bean
JwtClaimValidator<String> customJwtClaimValidator() {
return new JwtClaimValidator<>("custom_claim", "custom_claim_value"::equals);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomJwtConverterConfig {
static String PRINCIPAL_CLAIM = "principal_from_user_configuration";
@Bean
JwtAuthenticationConverter customJwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setPrincipalClaimName(PRINCIPAL_CLAIM);
return converter;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@WithResource(name = "public-key-location", content = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd
UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs
HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D
o2kQ+X5xK9cipRgEKwIDAQAB
-----END PUBLIC KEY-----
""")
@interface WithPublicKeyResource {
}
}