Skip to content

Commit 9baf113

Browse files
committed
Add Request-based AuthenticationManagerResolvers
Closes gh-6762
1 parent 620081e commit 9baf113

File tree

4 files changed

+434
-0
lines changed

4 files changed

+434
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2002-2022 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.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import javax.servlet.http.HttpServletRequest;
25+
26+
import org.springframework.security.authentication.AuthenticationManager;
27+
import org.springframework.security.authentication.AuthenticationManagerResolver;
28+
import org.springframework.security.authentication.AuthenticationServiceException;
29+
import org.springframework.security.authorization.AuthorizationManager;
30+
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
31+
import org.springframework.security.web.util.matcher.RequestMatcher;
32+
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
33+
import org.springframework.util.Assert;
34+
35+
/**
36+
* An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager}
37+
* instances based upon the type of {@link HttpServletRequest} passed into
38+
* {@link #resolve(HttpServletRequest)}.
39+
*
40+
* @author Josh Cummings
41+
* @since 5.7
42+
*/
43+
public final class RequestMatcherDelegatingAuthenticationManagerResolver
44+
implements AuthenticationManagerResolver<HttpServletRequest> {
45+
46+
private final List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers;
47+
48+
private AuthenticationManager defaultAuthenticationManager = (authentication) -> {
49+
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
50+
};
51+
52+
/**
53+
* Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on
54+
* the provided parameters
55+
* @param authenticationManagers a {@link Map} of
56+
* {@link RequestMatcher}/{@link AuthenticationManager} pairs
57+
*/
58+
RequestMatcherDelegatingAuthenticationManagerResolver(
59+
RequestMatcherEntry<AuthenticationManager>... authenticationManagers) {
60+
Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
61+
this.authenticationManagers = Arrays.asList(authenticationManagers);
62+
}
63+
64+
/**
65+
* Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on
66+
* the provided parameters
67+
* @param authenticationManagers a {@link Map} of
68+
* {@link RequestMatcher}/{@link AuthenticationManager} pairs
69+
*/
70+
RequestMatcherDelegatingAuthenticationManagerResolver(
71+
List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers) {
72+
Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
73+
this.authenticationManagers = authenticationManagers;
74+
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*/
79+
@Override
80+
public AuthenticationManager resolve(HttpServletRequest context) {
81+
for (RequestMatcherEntry<AuthenticationManager> entry : this.authenticationManagers) {
82+
if (entry.getRequestMatcher().matches(context)) {
83+
return entry.getEntry();
84+
}
85+
}
86+
87+
return this.defaultAuthenticationManager;
88+
}
89+
90+
/**
91+
* Set the default {@link AuthenticationManager} to use when a request does not match
92+
* @param defaultAuthenticationManager the default {@link AuthenticationManager} to
93+
* use
94+
*/
95+
public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) {
96+
Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
97+
this.defaultAuthenticationManager = defaultAuthenticationManager;
98+
}
99+
100+
/**
101+
* Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
102+
* @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder}
103+
* instance
104+
*/
105+
public static Builder builder() {
106+
return new Builder();
107+
}
108+
109+
/**
110+
* A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}.
111+
*/
112+
public static final class Builder {
113+
114+
private final List<RequestMatcherEntry<AuthenticationManager>> entries = new ArrayList<>();
115+
116+
private Builder() {
117+
118+
}
119+
120+
/**
121+
* Maps a {@link RequestMatcher} to an {@link AuthorizationManager}.
122+
* @param matcher the {@link RequestMatcher} to use
123+
* @param manager the {@link AuthenticationManager} to use
124+
* @return the {@link Builder} for further
125+
* customizationServerWebExchangeDelegatingReactiveAuthenticationManagerResolvers
126+
*/
127+
public Builder add(RequestMatcher matcher, AuthenticationManager manager) {
128+
Assert.notNull(matcher, "matcher cannot be null");
129+
Assert.notNull(manager, "manager cannot be null");
130+
this.entries.add(new RequestMatcherEntry<>(matcher, manager));
131+
return this;
132+
}
133+
134+
/**
135+
* Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver}
136+
* instance.
137+
* @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver}
138+
* instance
139+
*/
140+
public RequestMatcherDelegatingAuthenticationManagerResolver build() {
141+
return new RequestMatcherDelegatingAuthenticationManagerResolver(this.entries);
142+
}
143+
144+
}
145+
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2002-2022 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.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import reactor.core.publisher.Flux;
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.security.authentication.AuthenticationServiceException;
27+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
28+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
29+
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
30+
import org.springframework.security.web.authentication.RequestMatcherDelegatingAuthenticationManagerResolver;
31+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
32+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
33+
import org.springframework.util.Assert;
34+
import org.springframework.web.server.ServerWebExchange;
35+
36+
/**
37+
* A {@link ReactiveAuthenticationManagerResolver} that returns a
38+
* {@link ReactiveAuthenticationManager} instances based upon the type of
39+
* {@link ServerWebExchange} passed into {@link #resolve(ServerWebExchange)}.
40+
*
41+
* @author Josh Cummings
42+
* @since 5.7
43+
*
44+
*/
45+
public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver
46+
implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
47+
48+
private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> authenticationManagers;
49+
50+
private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono
51+
.error(new AuthenticationServiceException("Cannot authenticate " + authentication));
52+
53+
/**
54+
* Construct an
55+
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on
56+
* the provided parameters
57+
* @param managers a set of {@link ServerWebExchangeMatcherEntry}s
58+
*/
59+
ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(
60+
ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>... managers) {
61+
this(Arrays.asList(managers));
62+
}
63+
64+
/**
65+
* Construct an
66+
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on
67+
* the provided parameters
68+
* @param managers a {@link List} of {@link ServerWebExchangeMatcherEntry}s
69+
*/
70+
ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(
71+
List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> managers) {
72+
Assert.notNull(managers, "entries cannot be null");
73+
this.authenticationManagers = managers;
74+
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*/
79+
@Override
80+
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
81+
return Flux.fromIterable(this.authenticationManagers).filterWhen((entry) -> isMatch(exchange, entry)).next()
82+
.map(ServerWebExchangeMatcherEntry::getEntry).defaultIfEmpty(this.defaultAuthenticationManager);
83+
}
84+
85+
/**
86+
* Set the default {@link ReactiveAuthenticationManager} to use when a request does
87+
* not match
88+
* @param defaultAuthenticationManager the default
89+
* {@link ReactiveAuthenticationManager} to use
90+
*/
91+
public void setDefaultAuthenticationManager(ReactiveAuthenticationManager defaultAuthenticationManager) {
92+
Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
93+
this.defaultAuthenticationManager = defaultAuthenticationManager;
94+
}
95+
96+
/**
97+
* Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
98+
* @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder}
99+
* instance
100+
*/
101+
public static Builder builder() {
102+
return new Builder();
103+
}
104+
105+
private Mono<Boolean> isMatch(ServerWebExchange exchange,
106+
ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager> entry) {
107+
ServerWebExchangeMatcher matcher = entry.getMatcher();
108+
return matcher.matches(exchange).map(ServerWebExchangeMatcher.MatchResult::isMatch);
109+
}
110+
111+
/**
112+
* A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}.
113+
*/
114+
public static final class Builder {
115+
116+
private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> entries = new ArrayList<>();
117+
118+
private Builder() {
119+
120+
}
121+
122+
/**
123+
* Maps a {@link ServerWebExchangeMatcher} to an
124+
* {@link ReactiveAuthenticationManager}.
125+
* @param matcher the {@link ServerWebExchangeMatcher} to use
126+
* @param manager the {@link ReactiveAuthenticationManager} to use
127+
* @return the
128+
* {@link RequestMatcherDelegatingAuthenticationManagerResolver.Builder} for
129+
* further customizations
130+
*/
131+
public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder add(
132+
ServerWebExchangeMatcher matcher, ReactiveAuthenticationManager manager) {
133+
Assert.notNull(matcher, "matcher cannot be null");
134+
Assert.notNull(manager, "manager cannot be null");
135+
this.entries.add(new ServerWebExchangeMatcherEntry<>(matcher, manager));
136+
return this;
137+
}
138+
139+
/**
140+
* Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver}
141+
* instance.
142+
* @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver}
143+
* instance
144+
*/
145+
public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver build() {
146+
return new ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(this.entries);
147+
}
148+
149+
}
150+
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2002-2022 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 org.junit.jupiter.api.Test;
20+
21+
import org.springframework.mock.web.MockHttpServletRequest;
22+
import org.springframework.security.authentication.AuthenticationManager;
23+
import org.springframework.security.authentication.AuthenticationServiceException;
24+
import org.springframework.security.authentication.TestingAuthenticationToken;
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
30+
import static org.mockito.Mockito.mock;
31+
32+
/**
33+
* Tests for {@link RequestMatcherDelegatingAuthenticationManagerResolverTests}
34+
*
35+
* @author Josh Cummings
36+
*/
37+
public class RequestMatcherDelegatingAuthenticationManagerResolverTests {
38+
39+
private AuthenticationManager one = mock(AuthenticationManager.class);
40+
41+
private AuthenticationManager two = mock(AuthenticationManager.class);
42+
43+
@Test
44+
public void resolveWhenMatchesThenReturnsAuthenticationManager() {
45+
RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver
46+
.builder().add(new AntPathRequestMatcher("/one/**"), this.one)
47+
.add(new AntPathRequestMatcher("/two/**"), this.two).build();
48+
49+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/one/location");
50+
request.setServletPath("/one/location");
51+
assertThat(resolver.resolve(request)).isEqualTo(this.one);
52+
}
53+
54+
@Test
55+
public void resolveWhenDoesNotMatchThenReturnsDefaultAuthenticationManager() {
56+
RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver
57+
.builder().add(new AntPathRequestMatcher("/one/**"), this.one)
58+
.add(new AntPathRequestMatcher("/two/**"), this.two).build();
59+
60+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/wrong/location");
61+
AuthenticationManager authenticationManager = resolver.resolve(request);
62+
63+
Authentication authentication = new TestingAuthenticationToken("principal", "creds");
64+
assertThatExceptionOfType(AuthenticationServiceException.class)
65+
.isThrownBy(() -> authenticationManager.authenticate(authentication));
66+
}
67+
68+
}

0 commit comments

Comments
 (0)