瀏覽代碼

Allow configuration of oauth2 resource server through nested builder

Issue: gh-5557
Eleftheria Stein 6 年之前
父節點
當前提交
4b2539df10

+ 49 - 0
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.
+	 *
+	 * <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.

+ 40 - 5
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
  * <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);

+ 183 - 0
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