Skip to content

Commit

Permalink
Introduced ReactiveAuthenticationManagerResolver
Browse files Browse the repository at this point in the history
Suitable for multi-tenant reactive applications needing to branch
authentication strategies based on request details.
  • Loading branch information
rhamedy authored and jzheaux committed Jun 13, 2019
1 parent e0e66c6 commit f6ed1db
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@
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;
Expand Down Expand Up @@ -230,6 +232,7 @@
*
* @author Rob Winch
* @author Vedran Pavic
* @author Rafiullah Hamedy
* @since 5.0
*/
public class ServerHttpSecurity {
Expand Down Expand Up @@ -1124,6 +1127,7 @@ public class OAuth2ResourceServerSpec {

private JwtSpec jwt;
private OpaqueTokenSpec opaqueToken;
private ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;

/**
* Configures the {@link ServerAccessDeniedHandler} to use for requests authenticating with
Expand Down Expand Up @@ -1168,6 +1172,20 @@ public OAuth2ResourceServerSpec bearerTokenConverter(ServerAuthenticationConvert
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();
Expand Down Expand Up @@ -1195,18 +1213,21 @@ protected void configure(ServerHttpSecurity http) {
"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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@
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;
Expand Down Expand Up @@ -228,6 +230,28 @@ public void getWhenUsingCustomAuthenticationManagerThenUsesItAccordingly() {
.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();
Expand Down Expand Up @@ -507,6 +531,34 @@ ReactiveAuthenticationManager authenticationManager() {
}
}

@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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -17,7 +17,9 @@

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;
Expand Down Expand Up @@ -51,18 +53,23 @@
* 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
* </li>
* </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();

Expand All @@ -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
Expand All @@ -95,7 +112,9 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,8 +23,10 @@
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;
Expand All @@ -40,6 +42,7 @@

/**
* @author Rob Winch
* @author Rafiullah Hamedy
* @since 5.0
*/
@RunWith(MockitoJUnitRunner.class)
Expand All @@ -54,6 +57,8 @@ public class AuthenticationWebFilterTests {
private ServerAuthenticationFailureHandler failureHandler;
@Mock
private ServerSecurityContextRepository securityContextRepository;
@Mock
private ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;

private AuthenticationWebFilter filter;

Expand Down Expand Up @@ -85,6 +90,25 @@ public void filterWhenDefaultsAndNoAuthenticationThenContinues() {
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")));
Expand All @@ -106,6 +130,29 @@ public void filterWhenDefaultsAndAuthenticationSuccessThenContinues() {
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")));
Expand All @@ -127,6 +174,29 @@ public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() {
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());
Expand Down

0 comments on commit f6ed1db

Please sign in to comment.