Add configuration support for Opaque Token authentication

Closes gh-15872
This commit is contained in:
Madhura Bhave 2019-06-10 12:27:07 -07:00
parent 8d44e31898
commit 2560b54f7c
12 changed files with 543 additions and 156 deletions

View File

@ -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;
}
}
}

View File

@ -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 {
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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 {
}
}

View File

@ -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());
}
}

View File

@ -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();
}
};
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<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) {
@ -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();
}

View File

@ -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<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);
}
}
}

View File

@ -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