From 2560b54f7cf79a910f8defc9e74caf74a7d315d2 Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Mon, 10 Jun 2019 12:27:07 -0700 Subject: [PATCH] Add configuration support for Opaque Token authentication Closes gh-15872 --- .../OAuth2ResourceServerProperties.java | 71 ++++++++++++ ...OAuth2ResourceServerAutoConfiguration.java | 22 +++- ...eOAuth2ResourceServerJwkConfiguration.java | 78 ++++++++----- ...esourceServerOpaqueTokenConfiguration.java | 67 +++++++++++ ...esourceServerWebSecurityConfiguration.java | 44 -------- ...OAuth2ResourceServerAutoConfiguration.java | 23 +++- .../OAuth2ResourceServerJwtConfiguration.java | 82 +++++++++----- ...esourceServerOpaqueTokenConfiguration.java | 71 ++++++++++++ ...esourceServerWebSecurityConfiguration.java | 45 -------- ...2ResourceServerAutoConfigurationTests.java | 104 +++++++++++++++++- ...2ResourceServerAutoConfigurationTests.java | 75 +++++++++++++ .../main/asciidoc/spring-boot-features.adoc | 17 ++- 12 files changed, 543 insertions(+), 156 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerWebSecurityConfiguration.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java index 29e574f1c8..333358617e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import javax.annotation.PostConstruct; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.core.io.Resource; @@ -41,6 +43,32 @@ public class OAuth2ResourceServerProperties { return this.jwt; } + private final OpaqueToken opaqueToken = new OpaqueToken(); + + public OpaqueToken getOpaqueToken() { + return this.opaqueToken; + } + + @PostConstruct + public void validate() { + if (this.getOpaqueToken().getIntrospectionUri() != null) { + if (this.getJwt().getJwkSetUri() != null) { + handleError("jwt.jwk-set-uri"); + } + if (this.getJwt().getIssuerUri() != null) { + handleError("jwt.issuer-uri"); + } + if (this.getJwt().getPublicKeyLocation() != null) { + handleError("jwt.public-key-location"); + } + } + } + + private void handleError(String property) { + throw new IllegalStateException( + "Only one of " + property + " and opaque-token.introspection-uri should be configured."); + } + public static class Jwt { /** @@ -109,4 +137,47 @@ public class OAuth2ResourceServerProperties { } + 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; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java index 9d492cd0b5..bce0c99213 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java @@ -27,6 +27,8 @@ import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOAuth2TokenIntrospectionClient; /** * {@link EnableAutoConfiguration Auto-configuration} for Reactive OAuth2 resource server @@ -38,10 +40,24 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica @Configuration(proxyBeanMethods = false) @AutoConfigureBefore(ReactiveSecurityAutoConfiguration.class) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) -@ConditionalOnClass({ EnableWebFluxSecurity.class, BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) +@ConditionalOnClass({ EnableWebFluxSecurity.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) -@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.class, - ReactiveOAuth2ResourceServerWebSecurityConfiguration.class }) public class ReactiveOAuth2ResourceServerAutoConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) + @Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class, + ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class }) + static class JwtConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ OAuth2IntrospectionAuthenticationToken.class, ReactiveOAuth2TokenIntrospectionClient.class }) + @Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class }) + static class OpaqueTokenConfiguration { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 424db3fa88..9ccb1050de 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -20,6 +20,7 @@ import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +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.autoconfigure.security.oauth2.resource.IssuerUriCondition; @@ -28,13 +29,16 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2Res 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.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; +import org.springframework.security.web.server.SecurityWebFilterChain; /** * Configures a {@link ReactiveJwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI - * or Public Key configuration is available. + * or Public Key configuration is available. Also configures a + * {@link SecurityWebFilterChain} if a {@link ReactiveJwtDecoder} bean is found. * * @author Madhura Bhave * @author Artsiom Yudovin @@ -42,38 +46,56 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; @Configuration(proxyBeanMethods = false) class ReactiveOAuth2ResourceServerJwkConfiguration { - private final OAuth2ResourceServerProperties.Jwt properties; + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(ReactiveJwtDecoder.class) + static class JwtConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + JwtConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") + public ReactiveJwtDecoder jwtDecoder() { + return new NimbusReactiveJwtDecoder(this.properties.getJwkSetUri()); + } + + @Bean + @Conditional(KeyValueCondition.class) + public NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); + return NimbusReactiveJwtDecoder.withPublicKey(publicKey).build(); + } + + private byte[] getKeySpec(String keyValue) { + keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); + return Base64.getMimeDecoder().decode(keyValue); + } + + @Bean + @Conditional(IssuerUriCondition.class) + public ReactiveJwtDecoder jwtDecoderByIssuerUri() { + return ReactiveJwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); + } - ReactiveOAuth2ResourceServerJwkConfiguration(OAuth2ResourceServerProperties properties) { - this.properties = properties.getJwt(); } - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - @ConditionalOnMissingBean - public ReactiveJwtDecoder jwtDecoder() { - return new NimbusReactiveJwtDecoder(this.properties.getJwkSetUri()); - } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(SecurityWebFilterChain.class) + static class WebSecurityConfiguration { - @Bean - @Conditional(KeyValueCondition.class) - @ConditionalOnMissingBean - public NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { - RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - return NimbusReactiveJwtDecoder.withPublicKey(publicKey).build(); - } + @Bean + @ConditionalOnBean(ReactiveJwtDecoder.class) + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, + ReactiveJwtDecoder jwtDecoder) { + http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt() + .jwtDecoder(jwtDecoder); + return http.build(); + } - private byte[] getKeySpec(String keyValue) { - keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); - return Base64.getMimeDecoder().decode(keyValue); - } - - @Bean - @Conditional(IssuerUriCondition.class) - @ConditionalOnMissingBean - public ReactiveJwtDecoder jwtDecoderByIssuerUri() { - return ReactiveJwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java new file mode 100644 index 0000000000..05ee0d5c3d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.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.NimbusReactiveOAuth2TokenIntrospectionClient; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOAuth2TokenIntrospectionClient; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * Configures a {@link ReactiveOAuth2TokenIntrospectionClient} when a token introspection + * endpoint is available. Also configures a {@link SecurityWebFilterChain} if a + * {@link ReactiveOAuth2TokenIntrospectionClient} bean is found. + * + * @author Madhura Bhave + */ +class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(ReactiveOAuth2TokenIntrospectionClient.class) + static class OpaqueTokenIntrospectionClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaque-token.introspection-uri") + public NimbusReactiveOAuth2TokenIntrospectionClient oAuth2TokenIntrospectionClient( + OAuth2ResourceServerProperties properties) { + OAuth2ResourceServerProperties.OpaqueToken opaqueToken = properties.getOpaqueToken(); + return new NimbusReactiveOAuth2TokenIntrospectionClient(opaqueToken.getIntrospectionUri(), + opaqueToken.getClientId(), opaqueToken.getClientSecret()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(SecurityWebFilterChain.class) + static class WebSecurityConfiguration { + + @Bean + @ConditionalOnBean(ReactiveOAuth2TokenIntrospectionClient.class) + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().opaqueToken(); + return http.build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java deleted file mode 100644 index 05c2c76944..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -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.jwt.ReactiveJwtDecoder; -import org.springframework.security.web.server.SecurityWebFilterChain; - -/** - * Configures a {@link SecurityWebFilterChain} for Reactive OAuth2 resource server support - * if a {@link ReactiveJwtDecoder} bean is present. - * - * @author Madhura Bhave - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnBean(ReactiveJwtDecoder.class) -class ReactiveOAuth2ResourceServerWebSecurityConfiguration { - - @Bean - @ConditionalOnMissingBean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) { - http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt() - .jwtDecoder(jwtDecoder); - return http.build(); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java index be42566962..d5e05b4890 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java @@ -26,9 +26,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient; /** - * {@link EnableAutoConfiguration Auto-configuration} for OAuth resource server support. + * {@link EnableAutoConfiguration Auto-configuration} for OAuth2 resource server support. * * @author Madhura Bhave * @since 2.1.0 @@ -36,9 +38,24 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut @Configuration(proxyBeanMethods = false) @AutoConfigureBefore(SecurityAutoConfiguration.class) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) -@ConditionalOnClass({ JwtAuthenticationToken.class, JwtDecoder.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@Import({ OAuth2ResourceServerJwtConfiguration.class, OAuth2ResourceServerWebSecurityConfiguration.class }) + public class OAuth2ResourceServerAutoConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ JwtAuthenticationToken.class, JwtDecoder.class }) + @Import({ OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, + OAuth2ResourceServerJwtConfiguration.OAuth2WebSecurityConfigurerAdapter.class }) + static class JwtConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ OAuth2IntrospectionAuthenticationToken.class, OAuth2TokenIntrospectionClient.class }) + @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2WebSecurityConfigurerAdapter.class }) + static class OpaqueTokenConfiguration { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index ece7b6ca13..8320bd221e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -20,6 +20,7 @@ import java.security.interfaces.RSAPublicKey; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; +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.autoconfigure.security.oauth2.resource.IssuerUriCondition; @@ -28,6 +29,8 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2Res 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.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoders; @@ -35,7 +38,8 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; /** * Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public - * Key configuration is available. + * Key configuration is available. Also configures a {@link WebSecurityConfigurerAdapter} + * if a {@link JwtDecoder} bean is found. * * @author Madhura Bhave * @author Artsiom Yudovin @@ -43,39 +47,59 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; @Configuration(proxyBeanMethods = false) class OAuth2ResourceServerJwtConfiguration { - private final OAuth2ResourceServerProperties.Jwt properties; + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(JwtDecoder.class) + static class JwtDecoderConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + JwtDecoderConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") + public JwtDecoder jwtDecoderByJwkKeySetUri() { + return NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) + .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); + } + + @Bean + @Conditional(KeyValueCondition.class) + public JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); + return NimbusJwtDecoder.withPublicKey(publicKey).build(); + } + + private byte[] getKeySpec(String keyValue) { + keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); + return Base64.getMimeDecoder().decode(keyValue); + } + + @Bean + @Conditional(IssuerUriCondition.class) + public JwtDecoder jwtDecoderByIssuerUri() { + return JwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); + } - OAuth2ResourceServerJwtConfiguration(OAuth2ResourceServerProperties properties) { - this.properties = properties.getJwt(); } - @Bean - @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - @ConditionalOnMissingBean - public JwtDecoder jwtDecoderByJwkKeySetUri() { - return NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); - } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) + static class OAuth2WebSecurityConfigurerAdapter { - @Bean - @Conditional(KeyValueCondition.class) - @ConditionalOnMissingBean - public JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { - RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") - .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - return NimbusJwtDecoder.withPublicKey(publicKey).build(); - } + @Bean + @ConditionalOnBean(JwtDecoder.class) + public WebSecurityConfigurerAdapter jwtDecoderWebSecurityConfigurerAdapter() { + return new WebSecurityConfigurerAdapter() { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt(); + } + }; + } - private byte[] getKeySpec(String keyValue) { - keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); - return Base64.getMimeDecoder().decode(keyValue); - } - - @Bean - @Conditional(IssuerUriCondition.class) - @ConditionalOnMissingBean - public JwtDecoder jwtDecoderByIssuerUri() { - return JwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java new file mode 100644 index 0000000000..b4695c233b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.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.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2TokenIntrospectionClient; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient; + +/** + * Configures a {@link OAuth2TokenIntrospectionClient} when a token introspection endpoint + * is available. Also configures a {@link WebSecurityConfigurerAdapter} if a + * {@link OAuth2TokenIntrospectionClient} bean is found. + * + * @author Madhura Bhave + */ +@Configuration(proxyBeanMethods = false) +class OAuth2ResourceServerOpaqueTokenConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(OAuth2TokenIntrospectionClient.class) + static class OpaqueTokenIntrospectionClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaque-token.introspection-uri") + public NimbusOAuth2TokenIntrospectionClient oAuth2TokenIntrospectionClient( + OAuth2ResourceServerProperties properties) { + OAuth2ResourceServerProperties.OpaqueToken opaqueToken = properties.getOpaqueToken(); + return new NimbusOAuth2TokenIntrospectionClient(opaqueToken.getIntrospectionUri(), + opaqueToken.getClientId(), opaqueToken.getClientSecret()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) + static class OAuth2WebSecurityConfigurerAdapter { + + @Bean + @ConditionalOnBean(OAuth2TokenIntrospectionClient.class) + public WebSecurityConfigurerAdapter opaqueTokenWebSecurityConfigurerAdapter() { + return new WebSecurityConfigurerAdapter() { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().opaqueToken(); + } + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerWebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerWebSecurityConfiguration.java deleted file mode 100644 index 68cc9eab2c..0000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerWebSecurityConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.oauth2.jwt.JwtDecoder; - -/** - * {@link WebSecurityConfigurerAdapter} for OAuth2 resource server support. - * - * @author Madhura Bhave - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) -class OAuth2ResourceServerWebSecurityConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(JwtDecoder.class) - static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 1edeea138f..4bfa04c7c9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -46,6 +46,10 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOAuth2TokenIntrospectionClient; import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.AuthenticationWebFilter; @@ -204,6 +208,81 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { }); } + @Test + void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaque-token.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaque-token.client-secret=my-client-secret") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveOAuth2TokenIntrospectionClient.class); + assertFilterConfiguredWithOpaqueTokenAuthenticationManager(context); + }); + } + + @Test + void oAuth2TokenIntrospectionClientIsConditionalOnMissingBean() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com") + .withUserConfiguration(OAuth2TokenIntrospectionClientConfig.class) + .run((this::assertFilterConfiguredWithOpaqueTokenAuthenticationManager)); + } + + @Test + void autoConfigurationForOpaqueTokenWhenSecurityWebFilterChainConfigPresentShouldNotAddOne() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaque-token.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaque-token.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(OAuth2IntrospectionAuthenticationToken.class)) + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaque-token.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaque-token.client-secret=my-client-secret") + .run((context) -> assertThat(context).doesNotHaveBean(OAuth2TokenIntrospectionClient.class)); + } + + @Test + void autoConfigurationWhenBothJwkSetUriAndTokenIntrospectionUriSetShouldFail() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") + .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( + "Only one of jwt.jwk-set-uri and opaque-token.introspection-uri should be configured.")); + } + + @Test + void autoConfigurationWhenBothJwtIssuerUriAndTokenIntrospectionUriSetShouldFail() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") + .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( + "Only one of jwt.issuer-uri and opaque-token.introspection-uri should be configured.")); + } + + @Test + void autoConfigurationWhenBothJwtKeyLocationAndTokenIntrospectionUriSetShouldFail() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") + .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( + "Only one of jwt.public-key-location and opaque-token.introspection-uri should be configured.")); + } + private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) { MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); @@ -213,7 +292,18 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { ReactiveAuthenticationManager authenticationManager = (ReactiveAuthenticationManager) ReflectionTestUtils .getField(webFilter, "authenticationManager"); assertThat(authenticationManager).isInstanceOf(JwtReactiveAuthenticationManager.class); + } + private void assertFilterConfiguredWithOpaqueTokenAuthenticationManager( + AssertableReactiveWebApplicationContext context) { + MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context + .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); + Stream filters = filterChain.getWebFilters().toStream(); + AuthenticationWebFilter webFilter = (AuthenticationWebFilter) filters + .filter((f) -> f instanceof AuthenticationWebFilter).findFirst().orElse(null); + ReactiveAuthenticationManager authenticationManager = (ReactiveAuthenticationManager) ReflectionTestUtils + .getField(webFilter, "authenticationManager"); + assertThat(authenticationManager).isInstanceOf(OAuth2IntrospectionReactiveAuthenticationManager.class); } private String cleanIssuerPath(String issuer) { @@ -269,13 +359,23 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class OAuth2TokenIntrospectionClientConfig { + + @Bean + public ReactiveOAuth2TokenIntrospectionClient decoder() { + return mock(ReactiveOAuth2TokenIntrospectionClient.class); + } + + } + @Configuration(proxyBeanMethods = false) static class SecurityWebFilterChainConfig { @Bean - SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder decoder) { + SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange().pathMatchers("/message/**").hasRole("ADMIN").anyExchange().authenticated().and() - .oauth2ResourceServer().jwt().jwtDecoder(decoder); + .httpBasic(); return http.build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index 1c1840b66f..51bbf84158 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -43,6 +43,8 @@ import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; @@ -221,6 +223,68 @@ class OAuth2ResourceServerAutoConfigurationTests { .run((context) -> assertThat(getBearerTokenFilter(context)).isNull()); } + @Test + void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaque-token.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaque-token.client-secret=my-client-secret") + .run((context) -> { + assertThat(context).hasSingleBean(OAuth2TokenIntrospectionClient.class); + assertThat(getBearerTokenFilter(context)).isNotNull(); + }); + } + + @Test + void oAuth2TokenIntrospectionClientIsConditionalOnMissingBean() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com") + .withUserConfiguration(OAuth2TokenIntrospectionClientConfig.class) + .run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull()); + } + + @Test + void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader(OAuth2IntrospectionAuthenticationToken.class)) + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaque-token.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaque-token.client-secret=my-client-secret") + .run((context) -> assertThat(context).doesNotHaveBean(OAuth2TokenIntrospectionClient.class)); + } + + @Test + void autoConfigurationWhenBothJwkSetUriAndTokenIntrospectionUriSetShouldFail() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") + .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( + "Only one of jwt.jwk-set-uri and opaque-token.introspection-uri should be configured.")); + } + + @Test + void autoConfigurationWhenBothJwtIssuerUriAndTokenIntrospectionUriSetShouldFail() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") + .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( + "Only one of jwt.issuer-uri and opaque-token.introspection-uri should be configured.")); + } + + @Test + void autoConfigurationWhenBothJwtKeyLocationAndTokenIntrospectionUriSetShouldFail() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") + .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( + "Only one of jwt.public-key-location and opaque-token.introspection-uri should be configured.")); + } + private Filter getBearerTokenFilter(AssertableWebApplicationContext context) { FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); List filterChains = filterChain.getFilterChains(); @@ -278,4 +342,15 @@ class OAuth2ResourceServerAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + static class OAuth2TokenIntrospectionClientConfig { + + @Bean + public OAuth2TokenIntrospectionClient decoder() { + return mock(OAuth2TokenIntrospectionClient.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 47e890ab26..459376c8a5 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -3801,8 +3801,8 @@ In other words, the two configurations in the following example use the Google p [[boot-features-security-oauth2-server]] ==== Resource Server If you have `spring-security-oauth2-resource-server` on your classpath, Spring Boot can -set up an OAuth2 Resource Server as long as a JWK Set URI or OIDC Issuer URI is specified, -as shown in the following examples: +set up an OAuth2 Resource Server. For JWT configuration, a JWK Set URI or OIDC Issuer URI +needs to be specified, as shown in the following examples: [source,properties,indent=0] ---- @@ -3825,7 +3825,20 @@ The same properties are applicable for both servlet and reactive applications. Alternatively, you can define your own `JwtDecoder` bean for servlet applications or a `ReactiveJwtDecoder` for reactive applications. +In cases where opaque tokens are used instead of JWTs, you can configure the following properties +to validate tokens via introspection: +[source,properties,indent=0] +---- + spring.security.oauth2.resourceserver.opaque-token.introspection-uri=https://example.com/check-token + spring.security.oauth2.resourceserver.opaque-token.client-id=my-client-id + spring.security.oauth2.resourceserver.opaque-token.client-secret-my-client-secret +---- + +Again, the same properties are applicable for both servlet and reactive applications. + +Alternatively, you can define your own `OAuth2TokenIntrospectionClient` bean for servlet applications +or a `ReactiveOAuth2TokenIntrospectionClient` for reactive applications. ==== Authorization Server Currently, Spring Security does not provide support for implementing an OAuth 2.0