From 4b2539df10dd346d3f4e77d8c6703cb40066ed09 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 14:22:16 -0400 Subject: [PATCH] Allow configuration of oauth2 resource server through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 49 +++++ .../OAuth2ResourceServerConfigurer.java | 45 ++++- .../OAuth2ResourceServerConfigurerTests.java | 183 ++++++++++++++++++ 3 files changed, 272 insertions(+), 5 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index cffdad7824..fac8ba9b6e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -2108,6 +2108,55 @@ public final class HttpSecurity extends return configurer; } + /** + * Configures OAuth 2.0 Resource Server support. + * + *

Example Configuration

+ * + * The following example demonstrates how to configure a custom JWT authentication converter. + * + *
+	 * @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);
+	 * 	}
+	 * }
+	 * 
+ * + * @see OAuth 2.0 Authorization Framework + * + * @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> oauth2ResourceServerCustomizer) + throws Exception { + OAuth2ResourceServerConfigurer 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. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 2d4ffb0cbd..589dd391e5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -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 *
  • {@link #accessDeniedHandler(AccessDeniedHandler)}
  • - customizes how access denied errors are handled *
  • {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
  • - customizes how authentication failures are handled *
  • {@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request
  • - *
  • {@link #jwt()} - enables Jwt-encoded bearer token support
  • + *
  • {@link #jwt(Customizer)} - enables Jwt-encoded bearer token support
  • + *
  • {@link #opaqueToken(Customizer)} - enables opaque bearer token support
  • * * *

    - * When using {@link #jwt()}, either + * When using {@link #jwt(Customizer)}, either * *

      *
    • @@ -83,7 +85,7 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe *
    • *
    * - * Also with {@link #jwt()} consider + * Also with {@link #jwt(Customizer)} consider * *
      *
    • @@ -93,12 +95,12 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe *
    * *

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

    * *

    Security Filters

    * - * 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: * *
      *
    • {@link BearerTokenAuthenticationFilter}
    • @@ -180,6 +182,22 @@ public final class OAuth2ResourceServerConfigurer jwt(Customizer 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 opaqueToken(Customizer 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); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 995e7d98a8..494ff7d74b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -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