Allow configuration of oauth2 resource server through nested builder
Issue: gh-5557
This commit is contained in:
parent
415760838f
commit
4b2539df10
|
@ -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>
|
||||
* @Configuration
|
||||
* @EnableWebSecurity
|
||||
* public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
* @Override
|
||||
* protected void configure(HttpSecurity http) throws Exception {
|
||||
* http
|
||||
* .authorizeRequests(authorizeRequests ->
|
||||
* authorizeRequests
|
||||
* .anyRequest().authenticated()
|
||||
* )
|
||||
* .oauth2ResourceServer(oauth2ResourceServer ->
|
||||
* oauth2ResourceServer
|
||||
* .jwt(jwt ->
|
||||
* jwt
|
||||
* .jwtAuthenticationConverter(jwtDecoder())
|
||||
* )
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* @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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue