Skip to content

Add Multi-tenancy support for Reactive Resource Server #6861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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