Skip to content

Commit f6ed1db

Browse files
rhamedyjzheaux
authored andcommitted
Introduced ReactiveAuthenticationManagerResolver
Suitable for multi-tenant reactive applications needing to branch authentication strategies based on request details.
1 parent e0e66c6 commit f6ed1db

File tree

5 files changed

+205
-11
lines changed

5 files changed

+205
-11
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@
4343
import org.springframework.core.convert.converter.Converter;
4444
import org.springframework.http.HttpMethod;
4545
import org.springframework.http.MediaType;
46+
import org.springframework.http.server.reactive.ServerHttpRequest;
4647
import org.springframework.security.authentication.AbstractAuthenticationToken;
4748
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
4849
import org.springframework.security.authentication.ReactiveAuthenticationManager;
50+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
4951
import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager;
5052
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
5153
import org.springframework.security.authorization.AuthorizationDecision;
@@ -230,6 +232,7 @@
230232
*
231233
* @author Rob Winch
232234
* @author Vedran Pavic
235+
* @author Rafiullah Hamedy
233236
* @since 5.0
234237
*/
235238
public class ServerHttpSecurity {
@@ -1124,6 +1127,7 @@ public class OAuth2ResourceServerSpec {
11241127

11251128
private JwtSpec jwt;
11261129
private OpaqueTokenSpec opaqueToken;
1130+
private ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
11271131

11281132
/**
11291133
* Configures the {@link ServerAccessDeniedHandler} to use for requests authenticating with
@@ -1168,6 +1172,20 @@ public OAuth2ResourceServerSpec bearerTokenConverter(ServerAuthenticationConvert
11681172
return this;
11691173
}
11701174

1175+
/**
1176+
* Configures the {@link ReactiveAuthenticationManagerResolver}
1177+
*
1178+
* @param authenticationManagerResolver the {@link ReactiveAuthenticationManagerResolver}
1179+
* @return the {@link OAuth2ResourceServerSpec} for additional configuration
1180+
* @since 5.2
1181+
*/
1182+
public OAuth2ResourceServerSpec authenticationManagerResolver(
1183+
ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver) {
1184+
Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
1185+
this.authenticationManagerResolver = authenticationManagerResolver;
1186+
return this;
1187+
}
1188+
11711189
public JwtSpec jwt() {
11721190
if (this.jwt == null) {
11731191
this.jwt = new JwtSpec();
@@ -1195,18 +1213,21 @@ protected void configure(ServerHttpSecurity http) {
11951213
"same time");
11961214
}
11971215

1198-
if (this.jwt == null && this.opaqueToken == null) {
1216+
if (this.jwt == null && this.opaqueToken == null && this.authenticationManagerResolver == null) {
11991217
throw new IllegalStateException("Jwt and Opaque Token are the only supported formats for bearer tokens " +
12001218
"in Spring Security and neither was found. Make sure to configure JWT " +
12011219
"via http.oauth2ResourceServer().jwt() or Opaque Tokens via " +
12021220
"http.oauth2ResourceServer().opaqueToken().");
12031221
}
12041222

1205-
if (this.jwt != null) {
1223+
if (this.authenticationManagerResolver != null) {
1224+
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(this.authenticationManagerResolver);
1225+
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
1226+
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
1227+
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
1228+
} else if (this.jwt != null) {
12061229
this.jwt.configure(http);
1207-
}
1208-
1209-
if (this.opaqueToken != null) {
1230+
} else if (this.opaqueToken != null) {
12101231
this.opaqueToken.configure(http);
12111232
}
12121233
}

config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@
5050
import org.springframework.core.convert.converter.Converter;
5151
import org.springframework.http.HttpStatus;
5252
import org.springframework.http.MediaType;
53+
import org.springframework.http.server.reactive.ServerHttpRequest;
5354
import org.springframework.security.authentication.AbstractAuthenticationToken;
5455
import org.springframework.security.authentication.ReactiveAuthenticationManager;
56+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
5557
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
5658
import org.springframework.security.config.test.SpringTestRule;
5759
import org.springframework.security.core.Authentication;
@@ -228,6 +230,28 @@ public void getWhenUsingCustomAuthenticationManagerThenUsesItAccordingly() {
228230
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"mock-failure\""));
229231
}
230232

233+
@Test
234+
public void getWhenUsingCustomAuthenticationManagerResolverThenUsesItAccordingly() {
235+
this.spring.register(CustomAuthenticationManagerResolverConfig.class).autowire();
236+
237+
ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver =
238+
this.spring.getContext().getBean(ReactiveAuthenticationManagerResolver.class);
239+
240+
ReactiveAuthenticationManager authenticationManager =
241+
this.spring.getContext().getBean(ReactiveAuthenticationManager.class);
242+
243+
when(authenticationManagerResolver.resolve(any(ServerHttpRequest.class)))
244+
.thenReturn(Mono.just(authenticationManager));
245+
when(authenticationManager.authenticate(any(Authentication.class)))
246+
.thenReturn(Mono.error(new OAuth2AuthenticationException(new OAuth2Error("mock-failure"))));
247+
248+
this.client.get()
249+
.headers(headers -> headers.setBearerAuth(this.messageReadToken))
250+
.exchange()
251+
.expectStatus().isUnauthorized()
252+
.expectHeader().value(HttpHeaders.WWW_AUTHENTICATE, startsWith("Bearer error=\"mock-failure\""));
253+
}
254+
231255
@Test
232256
public void postWhenSignedThenReturnsOk() {
233257
this.spring.register(PublicKeyConfig.class, RootController.class).autowire();
@@ -507,6 +531,34 @@ ReactiveAuthenticationManager authenticationManager() {
507531
}
508532
}
509533

534+
@EnableWebFlux
535+
@EnableWebFluxSecurity
536+
static class CustomAuthenticationManagerResolverConfig {
537+
@Bean
538+
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) throws Exception {
539+
// @formatter:off
540+
http
541+
.authorizeExchange()
542+
.pathMatchers("/**/message/**").hasAnyAuthority("SCOPE_message:read")
543+
.and()
544+
.oauth2ResourceServer()
545+
.authenticationManagerResolver(authenticationManagerResolver());
546+
// @formatter:on
547+
548+
return http.build();
549+
}
550+
551+
@Bean
552+
ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver() {
553+
return mock(ReactiveAuthenticationManagerResolver.class);
554+
}
555+
556+
@Bean
557+
ReactiveAuthenticationManager authenticationManager() {
558+
return mock(ReactiveAuthenticationManager.class);
559+
}
560+
}
561+
510562
@EnableWebFlux
511563
@EnableWebFluxSecurity
512564
static class CustomBearerTokenServerAuthenticationConverter {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authentication;
18+
19+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
20+
21+
import reactor.core.publisher.Mono;
22+
23+
/**
24+
* An interface for resolving a {@link ReactiveAuthenticationManager} based on the provided context
25+
*
26+
* @author Rafiullah Hamedy
27+
* @since 5.2
28+
*/
29+
@FunctionalInterface
30+
public interface ReactiveAuthenticationManagerResolver<C> {
31+
Mono<ReactiveAuthenticationManager> resolve(C context);
32+
}

web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,7 +17,9 @@
1717

1818
import java.util.function.Function;
1919

20+
import org.springframework.http.server.reactive.ServerHttpRequest;
2021
import org.springframework.security.authentication.ReactiveAuthenticationManager;
22+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
2123
import org.springframework.security.core.Authentication;
2224
import org.springframework.security.core.AuthenticationException;
2325
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
@@ -51,18 +53,23 @@
5153
* The {@link ReactiveAuthenticationManager} specified in
5254
* {@link #AuthenticationWebFilter(ReactiveAuthenticationManager)} is used to perform authentication.
5355
* </li>
56+
*<li>
57+
* The {@link ReactiveAuthenticationManagerResolver} specified in
58+
* {@link #AuthenticationWebFilter(ReactiveAuthenticationManagerResolver)} is used to resolve the appropriate
59+
* authentication manager from context to perform authentication.
60+
* </li>
5461
* <li>
5562
* If authentication is successful, {@link ServerAuthenticationSuccessHandler} is invoked and the authentication
5663
* is set on {@link ReactiveSecurityContextHolder}, else {@link ServerAuthenticationFailureHandler} is invoked
5764
* </li>
5865
* </ul>
5966
*
6067
* @author Rob Winch
68+
* @author Rafiullah Hamedy
6169
* @since 5.0
6270
*/
6371
public class AuthenticationWebFilter implements WebFilter {
64-
65-
private final ReactiveAuthenticationManager authenticationManager;
72+
private final ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
6673

6774
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = new WebFilterChainServerAuthenticationSuccessHandler();
6875

@@ -80,7 +87,17 @@ public class AuthenticationWebFilter implements WebFilter {
8087
*/
8188
public AuthenticationWebFilter(ReactiveAuthenticationManager authenticationManager) {
8289
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
83-
this.authenticationManager = authenticationManager;
90+
this.authenticationManagerResolver = request -> Mono.just(authenticationManager);
91+
}
92+
93+
/**
94+
* Creates an instance
95+
* @param authenticationManagerResolver the authentication manager resolver to use
96+
* @since 5.2
97+
*/
98+
public AuthenticationWebFilter(ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver) {
99+
Assert.notNull(authenticationManagerResolver, "authenticationResolverManager cannot be null");
100+
this.authenticationManagerResolver = authenticationManagerResolver;
84101
}
85102

86103
@Override
@@ -95,7 +112,9 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
95112
private Mono<Void> authenticate(ServerWebExchange exchange,
96113
WebFilterChain chain, Authentication token) {
97114
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
98-
return this.authenticationManager.authenticate(token)
115+
116+
return this.authenticationManagerResolver.resolve(exchange.getRequest())
117+
.flatMap(authenticationManager -> authenticationManager.authenticate(token))
99118
.switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
100119
.flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange))
101120
.onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler

web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationWebFilterTests.java

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,8 +23,10 @@
2323
import org.mockito.junit.MockitoJUnitRunner;
2424
import reactor.core.publisher.Mono;
2525

26+
import org.springframework.http.server.reactive.ServerHttpRequest;
2627
import org.springframework.security.authentication.BadCredentialsException;
2728
import org.springframework.security.authentication.ReactiveAuthenticationManager;
29+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
2830
import org.springframework.security.authentication.TestingAuthenticationToken;
2931
import org.springframework.security.core.Authentication;
3032
import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
@@ -40,6 +42,7 @@
4042

4143
/**
4244
* @author Rob Winch
45+
* @author Rafiullah Hamedy
4346
* @since 5.0
4447
*/
4548
@RunWith(MockitoJUnitRunner.class)
@@ -54,6 +57,8 @@ public class AuthenticationWebFilterTests {
5457
private ServerAuthenticationFailureHandler failureHandler;
5558
@Mock
5659
private ServerSecurityContextRepository securityContextRepository;
60+
@Mock
61+
private ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
5762

5863
private AuthenticationWebFilter filter;
5964

@@ -85,6 +90,25 @@ public void filterWhenDefaultsAndNoAuthenticationThenContinues() {
8590
assertThat(result.getResponseCookies()).isEmpty();
8691
}
8792

93+
@Test
94+
public void filterWhenAuthenticationManagerResolverDefaultsAndNoAuthenticationThenContinues() {
95+
this.filter = new AuthenticationWebFilter(this.authenticationManagerResolver);
96+
97+
WebTestClient client = WebTestClientBuilder
98+
.bindToWebFilters(this.filter)
99+
.build();
100+
101+
EntityExchangeResult<String> result = client.get()
102+
.uri("/")
103+
.exchange()
104+
.expectStatus().isOk()
105+
.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
106+
.returnResult();
107+
108+
verifyZeroInteractions(this.authenticationManagerResolver);
109+
assertThat(result.getResponseCookies()).isEmpty();
110+
}
111+
88112
@Test
89113
public void filterWhenDefaultsAndAuthenticationSuccessThenContinues() {
90114
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(new TestingAuthenticationToken("test", "this", "ROLE")));
@@ -106,6 +130,29 @@ public void filterWhenDefaultsAndAuthenticationSuccessThenContinues() {
106130
assertThat(result.getResponseCookies()).isEmpty();
107131
}
108132

133+
@Test
134+
public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationSuccessThenContinues() {
135+
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.just(new TestingAuthenticationToken("test", "this", "ROLE")));
136+
when(this.authenticationManagerResolver.resolve(any())).thenReturn(Mono.just(this.authenticationManager));
137+
138+
this.filter = new AuthenticationWebFilter(this.authenticationManagerResolver);
139+
140+
WebTestClient client = WebTestClientBuilder
141+
.bindToWebFilters(this.filter)
142+
.build();
143+
144+
EntityExchangeResult<String> result = client
145+
.get()
146+
.uri("/")
147+
.headers(headers -> headers.setBasicAuth("test", "this"))
148+
.exchange()
149+
.expectStatus().isOk()
150+
.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
151+
.returnResult();
152+
153+
assertThat(result.getResponseCookies()).isEmpty();
154+
}
155+
109156
@Test
110157
public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() {
111158
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("failed")));
@@ -127,6 +174,29 @@ public void filterWhenDefaultsAndAuthenticationFailThenUnauthorized() {
127174
assertThat(result.getResponseCookies()).isEmpty();
128175
}
129176

177+
@Test
178+
public void filterWhenAuthenticationManagerResolverDefaultsAndAuthenticationFailThenUnauthorized() {
179+
when(this.authenticationManager.authenticate(any())).thenReturn(Mono.error(new BadCredentialsException("failed")));
180+
when(this.authenticationManagerResolver.resolve(any())).thenReturn(Mono.just(this.authenticationManager));
181+
182+
this.filter = new AuthenticationWebFilter(this.authenticationManagerResolver);
183+
184+
WebTestClient client = WebTestClientBuilder
185+
.bindToWebFilters(this.filter)
186+
.build();
187+
188+
EntityExchangeResult<Void> result = client
189+
.get()
190+
.uri("/")
191+
.headers(headers -> headers.setBasicAuth("test", "this"))
192+
.exchange()
193+
.expectStatus().isUnauthorized()
194+
.expectHeader().valueMatches("WWW-Authenticate", "Basic realm=\"Realm\"")
195+
.expectBody().isEmpty();
196+
197+
assertThat(result.getResponseCookies()).isEmpty();
198+
}
199+
130200
@Test
131201
public void filterWhenConvertEmptyThenOk() {
132202
when(this.authenticationConverter.convert(any())).thenReturn(Mono.empty());

0 commit comments

Comments
 (0)