Commit 2560b54f authored by Madhura Bhave's avatar Madhura Bhave

Add configuration support for Opaque Token authentication

Closes gh-15872
parent 8d44e318
...@@ -19,6 +19,8 @@ import java.io.IOException; ...@@ -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 { ...@@ -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 { ...@@ -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; ...@@ -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 ...@@ -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; ...@@ -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 ...@@ -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; ...@@ -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 {
ReactiveOAuth2ResourceServerJwkConfiguration(OAuth2ResourceServerProperties properties) { private final OAuth2ResourceServerProperties.Jwt properties;
this.properties = properties.getJwt();
}
@Bean JwtConfiguration(OAuth2ResourceServerProperties properties) {
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") this.properties = properties.getJwt();
@ConditionalOnMissingBean }
public ReactiveJwtDecoder jwtDecoder() {
return new NimbusReactiveJwtDecoder(this.properties.getJwkSetUri());
}
@Bean @Bean
@Conditional(KeyValueCondition.class) @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
@ConditionalOnMissingBean public ReactiveJwtDecoder jwtDecoder() {
public NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { return new NimbusReactiveJwtDecoder(this.properties.getJwkSetUri());
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") }
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey())));
return NimbusReactiveJwtDecoder.withPublicKey(publicKey).build(); @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());
}
private byte[] getKeySpec(String keyValue) {
keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
return Base64.getMimeDecoder().decode(keyValue);
} }
@Bean @Configuration(proxyBeanMethods = false)
@Conditional(IssuerUriCondition.class) @ConditionalOnMissingBean(SecurityWebFilterChain.class)
@ConditionalOnMissingBean static class WebSecurityConfiguration {
public ReactiveJwtDecoder jwtDecoderByIssuerUri() {
return ReactiveJwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); @Bean
@ConditionalOnBean(ReactiveJwtDecoder.class)
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveJwtDecoder jwtDecoder) {
http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt()
.jwtDecoder(jwtDecoder);
return http.build();
}
} }
} }
...@@ -13,32 +13,55 @@ ...@@ -13,32 +13,55 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 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; import org.springframework.security.web.server.SecurityWebFilterChain;
/** /**
* Configures a {@link SecurityWebFilterChain} for Reactive OAuth2 resource server support * Configures a {@link ReactiveOAuth2TokenIntrospectionClient} when a token introspection
* if a {@link ReactiveJwtDecoder} bean is present. * endpoint is available. Also configures a {@link SecurityWebFilterChain} if a
* {@link ReactiveOAuth2TokenIntrospectionClient} bean is found.
* *
* @author Madhura Bhave * @author Madhura Bhave
*/ */
@Configuration(proxyBeanMethods = false) class ReactiveOAuth2ResourceServerOpaqueTokenConfiguration {
@ConditionalOnBean(ReactiveJwtDecoder.class)
class ReactiveOAuth2ResourceServerWebSecurityConfiguration { @Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(ReactiveOAuth2TokenIntrospectionClient.class)
@Bean static class OpaqueTokenIntrospectionClientConfiguration {
@ConditionalOnMissingBean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) { @Bean
http.authorizeExchange().anyExchange().authenticated().and().oauth2ResourceServer().jwt() @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaque-token.introspection-uri")
.jwtDecoder(jwtDecoder); public NimbusReactiveOAuth2TokenIntrospectionClient oAuth2TokenIntrospectionClient(
return http.build(); 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();
}
} }
} }
...@@ -26,9 +26,11 @@ import org.springframework.context.annotation.Configuration; ...@@ -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 ...@@ -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; ...@@ -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 ...@@ -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; ...@@ -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; ...@@ -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 {
OAuth2ResourceServerJwtConfiguration(OAuth2ResourceServerProperties properties) { private final OAuth2ResourceServerProperties.Jwt properties;
this.properties = properties.getJwt();
}
@Bean JwtDecoderConfiguration(OAuth2ResourceServerProperties properties) {
@ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") this.properties = properties.getJwt();
@ConditionalOnMissingBean }
public JwtDecoder jwtDecoderByJwkKeySetUri() {
return NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
}
@Bean @Bean
@Conditional(KeyValueCondition.class) @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri")
@ConditionalOnMissingBean public JwtDecoder jwtDecoderByJwkKeySetUri() {
public JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { return NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
.generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); }
return NimbusJwtDecoder.withPublicKey(publicKey).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());
}
private byte[] getKeySpec(String keyValue) {
keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
return Base64.getMimeDecoder().decode(keyValue);
} }
@Bean @Configuration(proxyBeanMethods = false)
@Conditional(IssuerUriCondition.class) @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean static class OAuth2WebSecurityConfigurerAdapter {
public JwtDecoder jwtDecoderByIssuerUri() {
return JwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); @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();
}
};
}
} }
} }
...@@ -17,27 +17,53 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; ...@@ -17,27 +17,53 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.context.annotation.Bean;
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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2TokenIntrospectionClient;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2TokenIntrospectionClient;
/** /**
* {@link WebSecurityConfigurerAdapter} for OAuth2 resource server support. * 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 * @author Madhura Bhave
*/ */
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) class OAuth2ResourceServerOpaqueTokenConfiguration {
class OAuth2ResourceServerWebSecurityConfiguration {
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnBean(JwtDecoder.class) @ConditionalOnMissingBean(OAuth2TokenIntrospectionClient.class)
static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { static class OpaqueTokenIntrospectionClientConfiguration {
@Override @Bean
protected void configure(HttpSecurity http) throws Exception { @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaque-token.introspection-uri")
http.authorizeRequests().anyRequest().authenticated().and().oauth2ResourceServer().jwt(); 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();
}
};
} }
} }
......
...@@ -46,6 +46,10 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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. ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment