Add configuration support for Opaque Token authentication
Closes gh-15872
This commit is contained in:
parent
8d44e31898
commit
2560b54f7c
|
|
@ -19,6 +19,8 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
@ -41,6 +43,32 @@ public class OAuth2ResourceServerProperties {
|
||||||
return this.jwt;
|
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 {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
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
|
* {@link EnableAutoConfiguration Auto-configuration} for Reactive OAuth2 resource server
|
||||||
|
|
@ -38,10 +40,24 @@ import org.springframework.security.oauth2.server.resource.BearerTokenAuthentica
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@AutoConfigureBefore(ReactiveSecurityAutoConfiguration.class)
|
@AutoConfigureBefore(ReactiveSecurityAutoConfiguration.class)
|
||||||
@EnableConfigurationProperties(OAuth2ResourceServerProperties.class)
|
@EnableConfigurationProperties(OAuth2ResourceServerProperties.class)
|
||||||
@ConditionalOnClass({ EnableWebFluxSecurity.class, BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
|
@ConditionalOnClass({ EnableWebFluxSecurity.class })
|
||||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
|
||||||
@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.class,
|
|
||||||
ReactiveOAuth2ResourceServerWebSecurityConfiguration.class })
|
|
||||||
public class ReactiveOAuth2ResourceServerAutoConfiguration {
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import java.security.interfaces.RSAPublicKey;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.NimbusReactiveJwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
|
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
|
* 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 Madhura Bhave
|
||||||
* @author Artsiom Yudovin
|
* @author Artsiom Yudovin
|
||||||
|
|
@ -42,38 +46,56 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
class ReactiveOAuth2ResourceServerJwkConfiguration {
|
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
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
|
@ConditionalOnMissingBean(SecurityWebFilterChain.class)
|
||||||
@ConditionalOnMissingBean
|
static class WebSecurityConfiguration {
|
||||||
public ReactiveJwtDecoder jwtDecoder() {
|
|
||||||
return new NimbusReactiveJwtDecoder(this.properties.getJwkSetUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Conditional(KeyValueCondition.class)
|
@ConditionalOnBean(ReactiveJwtDecoder.class)
|
||||||
@ConditionalOnMissingBean
|
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
|
||||||
public NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
|
ReactiveJwtDecoder jwtDecoder) {
|
||||||
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
|
http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt()
|
||||||
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
|
.jwtDecoder(jwtDecoder);
|
||||||
return NimbusReactiveJwtDecoder.withPublicKey(publicKey).build();
|
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -26,9 +26,11 @@ import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
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
|
* @author Madhura Bhave
|
||||||
* @since 2.1.0
|
* @since 2.1.0
|
||||||
|
|
@ -36,9 +38,24 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
||||||
@EnableConfigurationProperties(OAuth2ResourceServerProperties.class)
|
@EnableConfigurationProperties(OAuth2ResourceServerProperties.class)
|
||||||
@ConditionalOnClass({ JwtAuthenticationToken.class, JwtDecoder.class })
|
|
||||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||||
@Import({ OAuth2ResourceServerJwtConfiguration.class, OAuth2ResourceServerWebSecurityConfiguration.class })
|
|
||||||
public class OAuth2ResourceServerAutoConfiguration {
|
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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import java.security.interfaces.RSAPublicKey;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.jose.jws.SignatureAlgorithm;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoders;
|
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
|
* 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 Madhura Bhave
|
||||||
* @author Artsiom Yudovin
|
* @author Artsiom Yudovin
|
||||||
|
|
@ -43,39 +47,59 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
class OAuth2ResourceServerJwtConfiguration {
|
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
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
|
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
|
||||||
@ConditionalOnMissingBean
|
static class OAuth2WebSecurityConfigurerAdapter {
|
||||||
public JwtDecoder jwtDecoderByJwkKeySetUri() {
|
|
||||||
return NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
|
|
||||||
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Conditional(KeyValueCondition.class)
|
@ConditionalOnBean(JwtDecoder.class)
|
||||||
@ConditionalOnMissingBean
|
public WebSecurityConfigurerAdapter jwtDecoderWebSecurityConfigurerAdapter() {
|
||||||
public JwtDecoder jwtDecoderByPublicKeyValue() throws Exception {
|
return new WebSecurityConfigurerAdapter() {
|
||||||
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
|
@Override
|
||||||
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
return NimbusJwtDecoder.withPublicKey(publicKey).build();
|
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -46,6 +46,10 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
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.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.MatcherSecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
|
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) {
|
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
|
||||||
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
|
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
|
||||||
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||||
|
|
@ -213,7 +292,18 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||||
ReactiveAuthenticationManager authenticationManager = (ReactiveAuthenticationManager) ReflectionTestUtils
|
ReactiveAuthenticationManager authenticationManager = (ReactiveAuthenticationManager) ReflectionTestUtils
|
||||||
.getField(webFilter, "authenticationManager");
|
.getField(webFilter, "authenticationManager");
|
||||||
assertThat(authenticationManager).isInstanceOf(JwtReactiveAuthenticationManager.class);
|
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);
|
||||||
|
ReactiveAuthenticationManager authenticationManager = (ReactiveAuthenticationManager) ReflectionTestUtils
|
||||||
|
.getField(webFilter, "authenticationManager");
|
||||||
|
assertThat(authenticationManager).isInstanceOf(OAuth2IntrospectionReactiveAuthenticationManager.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String cleanIssuerPath(String issuer) {
|
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)
|
@Configuration(proxyBeanMethods = false)
|
||||||
static class SecurityWebFilterChainConfig {
|
static class SecurityWebFilterChainConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder decoder) {
|
SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) {
|
||||||
http.authorizeExchange().pathMatchers("/message/**").hasRole("ADMIN").anyExchange().authenticated().and()
|
http.authorizeExchange().pathMatchers("/message/**").hasRole("ADMIN").anyExchange().authenticated().and()
|
||||||
.oauth2ResourceServer().jwt().jwtDecoder(decoder);
|
.httpBasic();
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ import org.springframework.security.config.BeanIds;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
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.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||||
import org.springframework.security.web.FilterChainProxy;
|
import org.springframework.security.web.FilterChainProxy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
@ -221,6 +223,68 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
.run((context) -> assertThat(getBearerTokenFilter(context)).isNull());
|
.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) {
|
private Filter getBearerTokenFilter(AssertableWebApplicationContext context) {
|
||||||
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||||
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
|
List<SecurityFilterChain> filterChains = filterChain.getFilterChains();
|
||||||
|
|
@ -278,4 +342,15 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableWebSecurity
|
||||||
|
static class OAuth2TokenIntrospectionClientConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OAuth2TokenIntrospectionClient decoder() {
|
||||||
|
return mock(OAuth2TokenIntrospectionClient.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3801,8 +3801,8 @@ In other words, the two configurations in the following example use the Google p
|
||||||
[[boot-features-security-oauth2-server]]
|
[[boot-features-security-oauth2-server]]
|
||||||
==== Resource Server
|
==== Resource Server
|
||||||
If you have `spring-security-oauth2-resource-server` on your classpath, Spring Boot can
|
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,
|
set up an OAuth2 Resource Server. For JWT configuration, a JWK Set URI or OIDC Issuer URI
|
||||||
as shown in the following examples:
|
needs to be specified, as shown in the following examples:
|
||||||
|
|
||||||
[source,properties,indent=0]
|
[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
|
Alternatively, you can define your own `JwtDecoder` bean for servlet applications
|
||||||
or a `ReactiveJwtDecoder` for reactive 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
|
==== Authorization Server
|
||||||
Currently, Spring Security does not provide support for implementing an OAuth 2.0
|
Currently, Spring Security does not provide support for implementing an OAuth 2.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue