ソースを参照

Introduced ReactiveAuthenticationManagerResolver

Suitable for multi-tenant reactive applications needing to branch
authentication strategies based on request details.
Rafiullah Hamedy 6 年 前
コミット
f6ed1db702

+ 26 - 5
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -43,9 +43,11 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
 import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager;
 import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
 import org.springframework.security.authorization.AuthorizationDecision;
@@ -230,6 +232,7 @@ import static org.springframework.security.web.server.util.matcher.ServerWebExch
  *
  * @author Rob Winch
  * @author Vedran Pavic
+ * @author Rafiullah Hamedy
  * @since 5.0
  */
 public class ServerHttpSecurity {
@@ -1124,6 +1127,7 @@ public class ServerHttpSecurity {
 
 		private JwtSpec jwt;
 		private OpaqueTokenSpec opaqueToken;
+		private ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
 
 		/**
 		 * Configures the {@link ServerAccessDeniedHandler} to use for requests authenticating with
@@ -1168,6 +1172,20 @@ public class ServerHttpSecurity {
 			return this;
 		}
 
+		/**
+		 * Configures the {@link ReactiveAuthenticationManagerResolver}
+		 *
+		 * @param authenticationManagerResolver the {@link ReactiveAuthenticationManagerResolver}
+		 * @return the {@link OAuth2ResourceServerSpec} for additional configuration
+		 * @since 5.2
+		 */
+		public OAuth2ResourceServerSpec authenticationManagerResolver(
+				ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver) {
+			Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
+			this.authenticationManagerResolver = authenticationManagerResolver;
+			return this;
+		}
+
 		public JwtSpec jwt() {
 			if (this.jwt == null) {
 				this.jwt = new JwtSpec();
@@ -1195,18 +1213,21 @@ public class ServerHttpSecurity {
 						"same time");
 			}
 
-			if (this.jwt == null && this.opaqueToken == null) {
+			if (this.jwt == null && this.opaqueToken == null && this.authenticationManagerResolver == null) {
 				throw new IllegalStateException("Jwt and Opaque Token are the only supported formats for bearer tokens " +
 						"in Spring Security and neither was found. Make sure to configure JWT " +
 						"via http.oauth2ResourceServer().jwt() or Opaque Tokens via " +
 						"http.oauth2ResourceServer().opaqueToken().");
 			}
 
-			if (this.jwt != null) {
+			if (this.authenticationManagerResolver != null) {
+				AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver);
+				oauth2.setServerAuthenticationConverter(bearerTokenConverter);
+				oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
+				http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
+			} else if (this.jwt != null) {
 				this.jwt.configure(http);
-			}
-
-			if (this.opaqueToken != null) {
+			} else if (this.opaqueToken != null) {
 				this.opaqueToken.configure(http);
 			}
 		}

+ 52 - 0
config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

@@ -50,8 +50,10 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
+import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.test.SpringTestRule;
 import org.springframework.security.core.Authentication;
@@ -228,6 +230,28 @@ public class OAuth2ResourceServerSpecTests {
 				.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"mock-failure\""));
 	}
 
+	@Test
+	public void getWhenUsingCustomAuthenticationManagerResolverThenUsesItAccordingly() {
+		this.spring.register(CustomAuthenticationManagerResolverConfig.class).autowire();
+
+		ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver =
+				this.spring.getContext().getBean(ReactiveAuthenticationManagerResolver.class);
+
+		ReactiveAuthenticationManager authenticationManager =
+				this.spring.getContext().getBean(ReactiveAuthenticationManager.class);
+
+		when(authenticationManagerResolver.resolve(any(ServerHttpRequest.class)))
+			.thenReturn(Mono.just(authenticationManager));
+		when(authenticationManager.authenticate(any(Authentication.class)))
+			.thenReturn(Mono.error(new OAuth2AuthenticationException(new OAuth2Error("mock-failure"))));
+
+		this.client.get()
+				.headers(headers -> headers.setBearerAuth(this.messageReadToken))
+				.exchange()
+				.expectStatus().isUnauthorized()
+				.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"mock-failure\""));
+	}
+
 	@Test
 	public void postWhenSignedThenReturnsOk() {
 		this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
@@ -507,6 +531,34 @@ public class OAuth2ResourceServerSpecTests {
 		}
 	}
 
+	@EnableWebFlux
+	@EnableWebFluxSecurity
+	static class CustomAuthenticationManagerResolverConfig {
+		@Bean
+		SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeExchange()
+					.pathMatchers("/**/message/**").hasAnyAuthority("SCOPE_message:read")
+					.and()
+				.oauth2ResourceServer()
+					.authenticationManagerResolver(authenticationManagerResolver());
+			// @formatter:on
+
+			return http.build();
+		}
+
+		@Bean
+		ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver() {
+			return mock(ReactiveAuthenticationManagerResolver.class);
+		}
+
+		@Bean
+		ReactiveAuthenticationManager authenticationManager() {
+			return mock(ReactiveAuthenticationManager.class);
+		}
+	}
+
 	@EnableWebFlux
 	@EnableWebFluxSecurity
 	static class CustomBearerTokenServerAuthenticationConverter {

+ 32 - 0
core/src/main/java/org/springframework/security/authentication/ReactiveAuthenticationManagerResolver.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2002-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.authentication;
+
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * An interface for resolving a {@link ReactiveAuthenticationManager} based on the provided context
+ *
+ * @author Rafiullah Hamedy
+ * @since 5.2
+ */
+@FunctionalInterface
+public interface ReactiveAuthenticationManagerResolver<C> {
+	Mono<ReactiveAuthenticationManager> resolve(C context);
+}

+ 24 - 5
web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,7 +17,9 @@ package org.springframework.security.web.server.authentication;
 
 import java.util.function.Function;
 
+import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@@ -51,6 +53,11 @@ import reactor.core.publisher.Mono;
  *     The {@link ReactiveAuthenticationManager} specified in
  *     {@link #AuthenticationWebFilter(ReactiveAuthenticationManager)} is used to perform authentication.
  * </li>
+ *<li>
+ *     The {@link ReactiveAuthenticationManagerResolver} specified in
+ *     {@link #AuthenticationWebFilter(ReactiveAuthenticationManagerResolver)} is used to resolve the appropriate
+ *     authentication manager from context to perform authentication.
+ * </li>
  * <li>
  *     If authentication is successful, {@link ServerAuthenticationSuccessHandler} is invoked and the authentication
  *     is set on {@link ReactiveSecurityContextHolder}, else {@link ServerAuthenticationFailureHandler} is invoked
@@ -58,11 +65,11 @@ import reactor.core.publisher.Mono;
  * </ul>
  *
  * @author Rob Winch
+ * @author Rafiullah Hamedy
  * @since 5.0
  */
 public class AuthenticationWebFilter implements WebFilter {
-
-	private final ReactiveAuthenticationManager authenticationManager;
+	private final ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
 
 	private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler();
 
@@ -80,7 +87,17 @@ public class AuthenticationWebFilter implements WebFilter {
 	 */
 	public AuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
 		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
-		this.authenticationManager = authenticationManager;
+		this.authenticationManagerResolver = request -> Mono.just(authenticationManager);
+	}
+
+	/**
+	 * Creates an instance
+	 * @param authenticationManagerResolver the authentication manager resolver to use
+	 * @since 5.2
+	 */
+	public AuthenticationWebFilter(ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver) {
+		Assert.notNull(authenticationManagerResolver, "authenticationResolverManager cannot be null");
+		this.authenticationManagerResolver = authenticationManagerResolver;
 	}
 
 	@Override
@@ -95,7 +112,9 @@ public class AuthenticationWebFilter implements WebFilter {
 	private Mono<Void> authenticate(ServerWebExchange exchange,
 		WebFilterChain chain, Authentication token) {
 		WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
-		return this.authenticationManager.authenticate(token)
+
+		return this.authenticationManagerResolver.resolve(exchange.getRequest())
+			.flatMap(authenticationManager -> authenticationManager.authenticate(token))
 			.switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
 			.flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange))
 			.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler

+ 71 - 1
web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2019 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,8 +23,10 @@ import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 import reactor.core.publisher.Mono;
 
+import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -40,6 +42,7 @@ import static org.mockito.Mockito.*;
 
 /**
  * @author Rob Winch
+ * @author Rafiullah Hamedy
  * @since 5.0
  */
 @RunWith(MockitoJUnitRunner.class)
@@ -54,6 +57,8 @@ public class AuthenticationWebFilterTests {
 	private ServerAuthenticationFailureHandler failureHandler;
 	@Mock
 	private ServerSecurityContextRepository securityContextRepository;
+	@Mock
+	private ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
 
 	private AuthenticationWebFilter filter;
 
@@ -85,6 +90,25 @@ public class AuthenticationWebFilterTests {
 		assertThat(result.getResponseCookies()).isEmpty();
 	}
 
+	@Test
+	public void filterWhenAuthenticationManagerResolverDefaultsAndNoAuthenticationThenContinues() {
+		this.filter = new AuthenticationWebFilter(this.authenticationManagerResolver);
+
+		WebTestClient client = WebTestClientBuilder
+			.bindToWebFilters(this.filter)
+			.build();
+
+		EntityExchangeResult<String> result = client.get()
+			.uri("/")
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
+			.returnResult();
+
+		verifyZeroInteractions(this.authenticationManagerResolver);
+		assertThat(result.getResponseCookies()).isEmpty();
+	}
+
 	@Test
 	public void filterWhenDefaultsAndAuthenticationSuccessThenContinues() {
 		when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(new TestingAuthenticationToken("test", "this", "ROLE")));
@@ -106,6 +130,29 @@ public class AuthenticationWebFilterTests {
 		assertThat(result.getResponseCookies()).isEmpty();
 	}
 
+	@Test
+	public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationSuccessThenContinues() {
+		when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(new TestingAuthenticationToken("test", "this", "ROLE")));
+		when(this.authenticationManagerResolver.resolve(any())).thenReturn(Mono.just(this.authenticationManager));
+
+		this.filter = new AuthenticationWebFilter(this.authenticationManagerResolver);
+
+		WebTestClient client = WebTestClientBuilder
+			.bindToWebFilters(this.filter)
+			.build();
+
+		EntityExchangeResult<String> result = client
+			.get()
+			.uri("/")
+			.headers(headers -> headers.setBasicAuth("test", "this"))
+			.exchange()
+			.expectStatus().isOk()
+			.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
+			.returnResult();
+
+		assertThat(result.getResponseCookies()).isEmpty();
+	}
+
 	@Test
 	public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() {
 		when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("failed")));
@@ -127,6 +174,29 @@ public class AuthenticationWebFilterTests {
 		assertThat(result.getResponseCookies()).isEmpty();
 	}
 
+	@Test
+	public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationFailThenUnauthorized() {
+		when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("failed")));
+		when(this.authenticationManagerResolver.resolve(any())).thenReturn(Mono.just(this.authenticationManager));
+
+		this.filter = new AuthenticationWebFilter(this.authenticationManagerResolver);
+
+		WebTestClient client = WebTestClientBuilder
+			.bindToWebFilters(this.filter)
+			.build();
+
+		EntityExchangeResult<Void> result = client
+			.get()
+			.uri("/")
+			.headers(headers -> headers.setBasicAuth("test", "this"))
+			.exchange()
+			.expectStatus().isUnauthorized()
+			.expectHeader().valueMatches("WWW-Authenticate", "Basic realm=\"Realm\"")
+			.expectBody().isEmpty();
+
+		assertThat(result.getResponseCookies()).isEmpty();
+	}
+
 	@Test
 	public void filterWhenConvertEmptyThenOk() {
 		when(this.authenticationConverter.convert(any())).thenReturn(Mono.empty());