Allow configuration of oauth2 resource server through nested builder

Issue: gh-5557
This commit is contained in:
Eleftheria Stein 2019-07-09 14:22:16 -04:00
parent 415760838f
commit 4b2539df10
3 changed files with 272 additions and 5 deletions

View File

@ -2108,6 +2108,55 @@ public final class HttpSecurity extends
return configurer;
}
/**
* Configures OAuth 2.0 Resource Server support.
*
* <h2>Example Configuration</h2>
*
* The following example demonstrates how to configure a custom JWT authentication converter.
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSecurity
* public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
* &#064;Override
* protected void configure(HttpSecurity http) throws Exception {
* http
* .authorizeRequests(authorizeRequests ->
* authorizeRequests
* .anyRequest().authenticated()
* )
* .oauth2ResourceServer(oauth2ResourceServer ->
* oauth2ResourceServer
* .jwt(jwt ->
* jwt
* .jwtAuthenticationConverter(jwtDecoder())
* )
* );
* }
*
* &#064;Bean
* public JwtDecoder jwtDecoder() {
* return JwtDecoders.fromOidcIssuerLocation(issuerUri);
* }
* }
* </pre>
*
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.1">OAuth 2.0 Authorization Framework</a>
*
* @param oauth2ResourceServerCustomizer the {@link Customizer} to provide more options for
* the {@link OAuth2ResourceServerConfigurer}
* @return the {@link HttpSecurity} for further customizations
* @throws Exception
*/
public HttpSecurity oauth2ResourceServer(Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2ResourceServerCustomizer)
throws Exception {
OAuth2ResourceServerConfigurer<HttpSecurity> configurer = getOrApply(new OAuth2ResourceServerConfigurer<>(getContext()));
this.postProcess(configurer);
oauth2ResourceServerCustomizer.customize(configurer);
return HttpSecurity.this;
}
/**
* Configures channel security. In order for this configuration to be useful at least
* one mapping to a required channel must be provided.

View File

@ -25,6 +25,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
@ -65,11 +66,12 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe
* <li>{@link #accessDeniedHandler(AccessDeniedHandler)}</li> - customizes how access denied errors are handled
* <li>{@link #authenticationEntryPoint(AuthenticationEntryPoint)}</li> - customizes how authentication failures are handled
* <li>{@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request</li>
* <li>{@link #jwt()} - enables Jwt-encoded bearer token support</li>
* <li>{@link #jwt(Customizer)} - enables Jwt-encoded bearer token support</li>
* <li>{@link #opaqueToken(Customizer)} - enables opaque bearer token support</li>
* </ul>
*
* <p>
* When using {@link #jwt()}, either
* When using {@link #jwt(Customizer)}, either
*
* <ul>
* <li>
@ -83,7 +85,7 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe
* </li>
* </ul>
*
* Also with {@link #jwt()} consider
* Also with {@link #jwt(Customizer)} consider
*
* <ul>
* <li>
@ -93,12 +95,12 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe
* </ul>
*
* <p>
* When using {@link #opaque()}, supply an introspection endpoint and its authentication configuration
* When using {@link #opaqueToken(Customizer)}, supply an introspection endpoint and its authentication configuration
* </p>
*
* <h2>Security Filters</h2>
*
* The following {@code Filter}s are populated when {@link #jwt()} is configured:
* The following {@code Filter}s are populated when {@link #jwt(Customizer)} is configured:
*
* <ul>
* <li>{@link BearerTokenAuthenticationFilter}</li>
@ -180,6 +182,22 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.jwtConfigurer;
}
/**
* Enables Jwt-encoded bearer token support.
*
* @param jwtCustomizer the {@link Customizer} to provide more options for
* the {@link JwtConfigurer}
* @return the {@link OAuth2ResourceServerConfigurer} for further customizations
* @throws Exception
*/
public OAuth2ResourceServerConfigurer<H> jwt(Customizer<JwtConfigurer> jwtCustomizer) throws Exception {
if ( this.jwtConfigurer == null ) {
this.jwtConfigurer = new JwtConfigurer(this.context);
}
jwtCustomizer.customize(this.jwtConfigurer);
return this;
}
public OpaqueTokenConfigurer opaqueToken() {
if (this.opaqueTokenConfigurer == null) {
this.opaqueTokenConfigurer = new OpaqueTokenConfigurer(this.context);
@ -188,6 +206,23 @@ public final class OAuth2ResourceServerConfigurer<H extends HttpSecurityBuilder<
return this.opaqueTokenConfigurer;
}
/**
* Enables opaque bearer token support.
*
* @param opaqueTokenCustomizer the {@link Customizer} to provide more options for
* the {@link OpaqueTokenConfigurer}
* @return the {@link OAuth2ResourceServerConfigurer} for further customizations
* @throws Exception
*/
public OAuth2ResourceServerConfigurer<H> opaqueToken(Customizer<OpaqueTokenConfigurer> opaqueTokenCustomizer)
throws Exception {
if (this.opaqueTokenConfigurer == null) {
this.opaqueTokenConfigurer = new OpaqueTokenConfigurer(this.context);
}
opaqueTokenCustomizer.customize(this.opaqueTokenConfigurer);
return this;
}
@Override
public void init(H http) throws Exception {
registerDefaultAccessDeniedHandler(http);

View File

@ -127,6 +127,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
@ -184,6 +185,19 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(content().string("ok"));
}
@Test
public void getWhenUsingDefaultsInLambdaWithValidBearerTokenThenAcceptsRequest()
throws Exception {
this.spring.register(RestOperationsConfig.class, DefaultInLambdaConfig.class, BasicController.class).autowire();
mockRestOperations(jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("ok"));
}
@Test
public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception {
this.spring.register(WebServerConfig.class, JwkSetUriConfig.class, BasicController.class).autowire();
@ -195,6 +209,16 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(content().string("ok"));
}
@Test
public void getWhenUsingJwkSetUriInLambdaThenAcceptsRequest() throws Exception {
this.spring.register(WebServerConfig.class, JwkSetUriInLambdaConfig.class, BasicController.class).autowire();
mockWebServer(jwks("Default"));
String token = this.token("ValidNoScopes");
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isOk())
.andExpect(content().string("ok"));
}
@Test
public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken()
@ -756,6 +780,23 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(content().string(JWT_SUBJECT));
}
@Test
public void requestWhenCustomJwtDecoderInLambdaOnDslThenUsed()
throws Exception {
this.spring.register(CustomJwtDecoderInLambdaOnDsl.class, BasicController.class).autowire();
CustomJwtDecoderInLambdaOnDsl config = this.spring.getContext().getBean(CustomJwtDecoderInLambdaOnDsl.class);
JwtDecoder decoder = config.decoder();
when(decoder.decode(anyString())).thenReturn(JWT);
this.mvc.perform(get("/authenticated")
.with(bearerToken(JWT_TOKEN)))
.andExpect(status().isOk())
.andExpect(content().string(JWT_SUBJECT));
}
@Test
public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed()
throws Exception {
@ -1067,6 +1108,17 @@ public class OAuth2ResourceServerConfigurerTests {
.andExpect(content().string("test-subject"));
}
@Test
public void getWhenOpaqueTokenInLambdaAndIntrospectingThenOk() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenInLambdaConfig.class, BasicController.class).autowire();
mockRestOperations(json("Active"));
this.mvc.perform(get("/authenticated")
.with(bearerToken("token")))
.andExpect(status().isOk())
.andExpect(content().string("test-subject"));
}
@Test
public void getWhenIntrospectionFailsThenUnauthorized() throws Exception {
this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class).autowire();
@ -1104,6 +1156,20 @@ public class OAuth2ResourceServerConfigurerTests {
verifyBean(AuthenticationProvider.class).authenticate(any(Authentication.class));
}
@Test
public void getWhenCustomIntrospectionAuthenticationManagerInLambdaThenUsed() throws Exception {
this.spring.register(OpaqueTokenAuthenticationManagerInLambdaConfig.class, BasicController.class).autowire();
when(bean(AuthenticationProvider.class).authenticate(any(Authentication.class)))
.thenReturn(INTROSPECTION_AUTHENTICATION_TOKEN);
this.mvc.perform(get("/authenticated")
.with(bearerToken("token")))
.andExpect(status().isOk())
.andExpect(content().string("mock-test-subject"));
verifyBean(AuthenticationProvider.class).authenticate(any(Authentication.class));
}
@Test
public void configureWhenOnlyIntrospectionUrlThenException() throws Exception {
assertThatCode(() -> this.spring.register(OpaqueTokenHalfConfiguredConfig.class).autowire())
@ -1311,6 +1377,26 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class DefaultInLambdaConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(withDefaults())
);
// @formatter:on
}
}
@EnableWebSecurity
static class JwkSetUriConfig extends WebSecurityConfigurerAdapter {
@Value("${mockwebserver.url:https://example.org}")
@ -1331,6 +1417,31 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class JwkSetUriInLambdaConfig extends WebSecurityConfigurerAdapter {
@Value("${mockwebserver.url:https://example.org}")
String jwkSetUri;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(jwt ->
jwt
.jwkSetUri(this.jwkSetUri)
)
);
// @formatter:on
}
}
@EnableWebSecurity
static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter {
@Value("${mockwebserver.url:https://example.org}")
@ -1677,6 +1788,33 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class CustomJwtDecoderInLambdaOnDsl extends WebSecurityConfigurerAdapter {
JwtDecoder decoder = mock(JwtDecoder.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(jwt ->
jwt
.decoder(decoder())
)
);
// @formatter:on
}
JwtDecoder decoder() {
return this.decoder;
}
}
@EnableWebSecurity
static class CustomJwtDecoderAsBean extends WebSecurityConfigurerAdapter {
@Override
@ -1831,6 +1969,25 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class OpaqueTokenInLambdaConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/requires-read-scope").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.opaqueToken(withDefaults())
);
// @formatter:on
}
}
@EnableWebSecurity
static class OpaqueTokenAuthenticationManagerConfig extends WebSecurityConfigurerAdapter {
@Override
@ -1852,6 +2009,32 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class OpaqueTokenAuthenticationManagerInLambdaConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.opaqueToken(opaqueToken ->
opaqueToken
.authenticationManager(authenticationProvider()::authenticate)
)
);
// @formatter:on
}
@Bean
public AuthenticationProvider authenticationProvider() {
return mock(AuthenticationProvider.class);
}
}
@EnableWebSecurity
static class OpaqueAndJwtConfig extends WebSecurityConfigurerAdapter {
@Override