Introduced ReactiveAuthenticationManagerResolver
Suitable for multi-tenant reactive applications needing to branch authentication strategies based on request details.
This commit is contained in:
parent
e0e66c62fc
commit
f6ed1db702
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue