Create spring-boot-security-oauth2-resource-server module
This commit is contained in:
committed by
Phillip Webb
parent
d5e84c627d
commit
0c0af48fea
@@ -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")
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.security.oauth2.server.resource.autoconfigure.reactive.ReactiveOAuth2ResourceServerAutoConfiguration
|
||||
org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user