Skip to content

Commit 2a3f03a

Browse files
committed
Add Request AuthenticationManagerResolvers
Fixes gh-6762
1 parent 26a6524 commit 2a3f03a

File tree

6 files changed

+349
-15
lines changed

6 files changed

+349
-15
lines changed

samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,12 @@ public void tenantTwoPerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedM
122122
containsString("Bearer error=\"insufficient_scope\"")));
123123
}
124124

125-
@Test(expected = IllegalArgumentException.class)
125+
@Test
126126
public void invalidTenantPerformWhenValidBearerTokenThenThrowsException()
127127
throws Exception {
128128

129-
this.mvc.perform(get("/tenantThree").with(bearerToken(this.tenantOneNoScopesToken)));
129+
this.mvc.perform(get("/tenantThree").with(bearerToken(this.tenantOneNoScopesToken)))
130+
.andExpect(status().isUnauthorized());
130131
}
131132

132133
private static class BearerTokenRequestPostProcessor implements RequestPostProcessor {

samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
*/
1616
package sample;
1717

18-
import java.util.HashMap;
19-
import java.util.Map;
20-
import java.util.Optional;
18+
import java.util.LinkedHashMap;
2119
import javax.servlet.http.HttpServletRequest;
2220

2321
import org.springframework.beans.factory.annotation.Value;
@@ -34,6 +32,9 @@
3432
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
3533
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
3634
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
35+
import org.springframework.security.web.authentication.RequestMatchingAuthenticationManagerResolver;
36+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
37+
import org.springframework.security.web.util.matcher.RequestMatcher;
3738

3839
/**
3940
* @author Josh Cummings
@@ -71,16 +72,10 @@ protected void configure(HttpSecurity http) throws Exception {
7172

7273
@Bean
7374
AuthenticationManagerResolver<HttpServletRequest> multitenantAuthenticationManager() {
74-
Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
75-
authenticationManagers.put("tenantOne", jwt());
76-
authenticationManagers.put("tenantTwo", opaque());
77-
return request -> {
78-
String[] pathParts = request.getRequestURI().split("/");
79-
String tenantId = pathParts.length > 0 ? pathParts[1] : null;
80-
return Optional.ofNullable(tenantId)
81-
.map(authenticationManagers::get)
82-
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
83-
};
75+
LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers = new LinkedHashMap<>();
76+
authenticationManagers.put(new AntPathRequestMatcher("/tenantOne/**"), jwt());
77+
authenticationManagers.put(new AntPathRequestMatcher("/tenantTwo/**"), opaque());
78+
return new RequestMatchingAuthenticationManagerResolver(authenticationManagers);
8479
}
8580

8681
AuthenticationManager jwt() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.web.authentication;
18+
19+
import java.util.LinkedHashMap;
20+
import java.util.Map;
21+
import javax.servlet.http.HttpServletRequest;
22+
23+
import org.springframework.security.authentication.AuthenticationManager;
24+
import org.springframework.security.authentication.AuthenticationManagerResolver;
25+
import org.springframework.security.authentication.AuthenticationServiceException;
26+
import org.springframework.security.web.util.matcher.RequestMatcher;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager}
31+
* instances based upon the type of {@link HttpServletRequest} passed into
32+
* {@link #resolve(HttpServletRequest)}.
33+
*
34+
* @author Josh Cummings
35+
* @since 5.2
36+
*/
37+
public class RequestMatchingAuthenticationManagerResolver
38+
implements AuthenticationManagerResolver<HttpServletRequest> {
39+
40+
private final LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers;
41+
private AuthenticationManager defaultAuthenticationManager = authentication -> {
42+
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
43+
};
44+
45+
/**
46+
* Construct an {@link RequestMatchingAuthenticationManagerResolver}
47+
* based on the provided parameters
48+
*
49+
* @param authenticationManagers a {@link Map} of {@link RequestMatcher}/{@link AuthenticationManager} pairs
50+
*/
51+
public RequestMatchingAuthenticationManagerResolver
52+
(LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers) {
53+
Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
54+
this.authenticationManagers = authenticationManagers;
55+
}
56+
57+
/**
58+
* {@inheritDoc}
59+
*/
60+
@Override
61+
public AuthenticationManager resolve(HttpServletRequest context) {
62+
for (Map.Entry<RequestMatcher, AuthenticationManager> entry : this.authenticationManagers.entrySet()) {
63+
if (entry.getKey().matches(context)) {
64+
return entry.getValue();
65+
}
66+
}
67+
68+
return this.defaultAuthenticationManager;
69+
}
70+
71+
/**
72+
* Set the default {@link AuthenticationManager} to use when a request does not match
73+
*
74+
* @param defaultAuthenticationManager the default {@link AuthenticationManager} to use
75+
*/
76+
public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) {
77+
Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
78+
this.defaultAuthenticationManager = defaultAuthenticationManager;
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.web.server.authentication;
18+
19+
import java.util.List;
20+
21+
import reactor.core.publisher.Flux;
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.security.authentication.AuthenticationServiceException;
25+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
26+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
27+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
28+
import org.springframework.util.Assert;
29+
import org.springframework.web.server.ServerWebExchange;
30+
31+
/**
32+
* A {@link ReactiveAuthenticationManagerResolver} that returns a {@link ReactiveAuthenticationManager}
33+
* instances based upon the type of {@link ServerWebExchange} passed into
34+
* {@link #resolve(ServerWebExchange)}.
35+
*
36+
* @author Josh Cummings
37+
* @since 5.2
38+
*
39+
*/
40+
public class ServerWebExchangeMatchingReactiveAuthenticationManagerResolver
41+
implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
42+
43+
private final List<MatcherEntry> authenticationManagers;
44+
private ReactiveAuthenticationManager defaultAuthenticationManager = authentication ->
45+
Mono.error(new AuthenticationServiceException("Cannot authenticate " + authentication));
46+
47+
/**
48+
* Construct an {@link ServerWebExchangeMatchingReactiveAuthenticationManagerResolver}
49+
* based on the provided parameters
50+
*
51+
* @param authenticationManagers a {@link List} of
52+
* {@link ServerWebExchangeMatcher}/{@link ReactiveAuthenticationManager} pairs
53+
*/
54+
public ServerWebExchangeMatchingReactiveAuthenticationManagerResolver
55+
(List<MatcherEntry> authenticationManagers) {
56+
57+
Assert.notNull(authenticationManagers, "authenticationManagers cannot be null");
58+
this.authenticationManagers = authenticationManagers;
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
@Override
65+
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
66+
return Flux.fromIterable(this.authenticationManagers)
67+
.filterWhen(entry -> isMatch(exchange, entry))
68+
.next()
69+
.map(MatcherEntry::getAuthenticationManager)
70+
.defaultIfEmpty(this.defaultAuthenticationManager);
71+
}
72+
73+
/**
74+
* Set the default {@link ReactiveAuthenticationManager} to use when a request does not match
75+
*
76+
* @param defaultAuthenticationManager the default {@link ReactiveAuthenticationManager} to use
77+
*/
78+
public void setDefaultAuthenticationManager(ReactiveAuthenticationManager defaultAuthenticationManager) {
79+
Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
80+
this.defaultAuthenticationManager = defaultAuthenticationManager;
81+
}
82+
83+
public static class MatcherEntry {
84+
private final ServerWebExchangeMatcher matcher;
85+
private final ReactiveAuthenticationManager authenticationManager;
86+
87+
public MatcherEntry(ServerWebExchangeMatcher matcher,
88+
ReactiveAuthenticationManager authenticationManager) {
89+
this.matcher = matcher;
90+
this.authenticationManager = authenticationManager;
91+
}
92+
93+
public ServerWebExchangeMatcher getMatcher() {
94+
return this.matcher;
95+
}
96+
97+
public ReactiveAuthenticationManager getAuthenticationManager() {
98+
return this.authenticationManager;
99+
}
100+
}
101+
102+
private Mono<Boolean> isMatch(ServerWebExchange exchange, MatcherEntry entry) {
103+
ServerWebExchangeMatcher matcher = entry.getMatcher();
104+
return matcher.matches(exchange)
105+
.map(ServerWebExchangeMatcher.MatchResult::isMatch);
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.web.authentication;
18+
19+
import java.util.LinkedHashMap;
20+
21+
import org.junit.Test;
22+
23+
import org.springframework.mock.web.MockHttpServletRequest;
24+
import org.springframework.security.authentication.AuthenticationManager;
25+
import org.springframework.security.authentication.AuthenticationServiceException;
26+
import org.springframework.security.authentication.TestingAuthenticationToken;
27+
import org.springframework.security.core.Authentication;
28+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
29+
import org.springframework.security.web.util.matcher.RequestMatcher;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatCode;
33+
import static org.mockito.Mockito.mock;
34+
35+
/**
36+
* Tests for {@link RequestMatchingAuthenticationManagerResolverTests}
37+
*
38+
* @author Josh Cummings
39+
*/
40+
public class RequestMatchingAuthenticationManagerResolverTests {
41+
private AuthenticationManager one = mock(AuthenticationManager.class);
42+
private AuthenticationManager two = mock(AuthenticationManager.class);
43+
44+
@Test
45+
public void resolveWhenMatchesThenReturnsAuthenticationManager() {
46+
LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers = new LinkedHashMap<>();
47+
authenticationManagers.put(new AntPathRequestMatcher("/one/**"), this.one);
48+
authenticationManagers.put(new AntPathRequestMatcher("/two/**"), this.two);
49+
RequestMatchingAuthenticationManagerResolver resolver =
50+
new RequestMatchingAuthenticationManagerResolver(authenticationManagers);
51+
52+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/one/location");
53+
request.setServletPath("/one/location");
54+
assertThat(resolver.resolve(request)).isEqualTo(this.one);
55+
}
56+
57+
@Test
58+
public void resolveWhenDoesNotMatchThenReturnsDefaultAuthenticationManager() {
59+
LinkedHashMap<RequestMatcher, AuthenticationManager> authenticationManagers = new LinkedHashMap<>();
60+
authenticationManagers.put(new AntPathRequestMatcher("/one/**"), this.one);
61+
authenticationManagers.put(new AntPathRequestMatcher("/two/**"), this.two);
62+
RequestMatchingAuthenticationManagerResolver resolver =
63+
new RequestMatchingAuthenticationManagerResolver(authenticationManagers);
64+
65+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/wrong/location");
66+
AuthenticationManager authenticationManager = resolver.resolve(request);
67+
68+
Authentication authentication = new TestingAuthenticationToken("principal", "creds");
69+
assertThatCode(() -> authenticationManager.authenticate(authentication))
70+
.isInstanceOf(AuthenticationServiceException.class);
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.web.server.authentication;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
22+
import org.junit.Test;
23+
24+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
25+
import org.springframework.security.authentication.AuthenticationServiceException;
26+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
27+
import org.springframework.security.authentication.TestingAuthenticationToken;
28+
import org.springframework.security.core.Authentication;
29+
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
30+
31+
import static org.assertj.core.api.Assertions.assertThat;
32+
import static org.assertj.core.api.Assertions.assertThatCode;
33+
import static org.mockito.Mockito.mock;
34+
import static org.springframework.mock.web.server.MockServerWebExchange.from;
35+
36+
/**
37+
* Tests for {@link ServerWebExchangeMatchingReactiveAuthenticationManagerResolver}
38+
*
39+
* @author Josh Cummings
40+
*/
41+
public class ServerWebExchangeMatchingReactiveAuthenticationManagerResolverTests {
42+
private ReactiveAuthenticationManager one = mock(ReactiveAuthenticationManager.class);
43+
private ReactiveAuthenticationManager two = mock(ReactiveAuthenticationManager.class);
44+
45+
@Test
46+
public void resolveWhenMatchesThenReturnsReactiveAuthenticationManager() {
47+
List<ServerWebExchangeMatchingReactiveAuthenticationManagerResolver.MatcherEntry> authenticationManagers =
48+
Arrays.asList(
49+
new ServerWebExchangeMatchingReactiveAuthenticationManagerResolver.MatcherEntry
50+
(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one),
51+
new ServerWebExchangeMatchingReactiveAuthenticationManagerResolver.MatcherEntry
52+
(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two));
53+
ServerWebExchangeMatchingReactiveAuthenticationManagerResolver resolver =
54+
new ServerWebExchangeMatchingReactiveAuthenticationManagerResolver(authenticationManagers);
55+
56+
MockServerHttpRequest request = MockServerHttpRequest.get("/one/location").build();
57+
assertThat(resolver.resolve(from(request)).block()).isEqualTo(this.one);
58+
}
59+
60+
@Test
61+
public void resolveWhenDoesNotMatchThenReturnsDefaultReactiveAuthenticationManager() {
62+
List<ServerWebExchangeMatchingReactiveAuthenticationManagerResolver.MatcherEntry> authenticationManagers =
63+
Arrays.asList(
64+
new ServerWebExchangeMatchingReactiveAuthenticationManagerResolver.MatcherEntry
65+
(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one),
66+
new ServerWebExchangeMatchingReactiveAuthenticationManagerResolver.MatcherEntry
67+
(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two));
68+
ServerWebExchangeMatchingReactiveAuthenticationManagerResolver resolver =
69+
new ServerWebExchangeMatchingReactiveAuthenticationManagerResolver(authenticationManagers);
70+
71+
MockServerHttpRequest request = MockServerHttpRequest.get("/wrong/location").build();
72+
ReactiveAuthenticationManager authenticationManager =
73+
resolver.resolve(from(request)).block();
74+
75+
Authentication authentication = new TestingAuthenticationToken("principal", "creds");
76+
assertThatCode(() -> authenticationManager.authenticate(authentication).block())
77+
.isInstanceOf(AuthenticationServiceException.class);
78+
}
79+
}

0 commit comments

Comments
 (0)