Skip to content

Commit a26521c

Browse files
committed
Add Request-based AuthenticationManagerResolvers
Closes gh-6762
1 parent cbf0e1d commit a26521c

File tree

6 files changed

+477
-14
lines changed

6 files changed

+477
-14
lines changed

docs/modules/ROOT/pages/reactive/oauth2/resource-server/multitenancy.adoc

+71
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,76 @@
11
= OAuth 2.0 Resource Server Multitenancy
22

3+
[[webflux-oauth2reourceserver-opaqueandjwt]]
4+
== Supporting both JWT and Opaque Token
5+
6+
In some cases, you may have a need to access both kinds of tokens.
7+
For example, you may support more than one tenant where one tenant issues JWTs and the other issues opaque tokens.
8+
9+
If this decision must be made at request-time, then you can use an `AuthenticationManagerResolver` to achieve it, like so:
10+
11+
====
12+
.Java
13+
[source,java,role="primary"]
14+
----
15+
@Bean
16+
ReactiveAuthenticationManagerResolver<ServerWebExchange> tokenAuthenticationManagerResolver
17+
(ReactiveJwtDecoder jwtDecoder, ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector) {
18+
ReactiveAuthenticationManager jwt = new JwtReactiveAuthenticationManager(jwtDecoder);
19+
ReactiveAuthenticationManager opaque = new OpaqueTokenReactiveAuthenticationManager(opaqueTokenIntrospector);
20+
return ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder()
21+
.add(new ServerWebExchangeMatcherEntry<>(ServerWebExchangeMatchers.pathPatterns("/opaque/**"), opaque))
22+
.add(new ServerWebExchangeMatcherEntry<>(ServerWebExchangeMatchers.anyExchange(), jwt))
23+
.build();
24+
}
25+
----
26+
27+
.Kotlin
28+
[source,kotlin,role="secondary"]
29+
----
30+
@Bean
31+
fun tokenAuthenticationManagerResolver
32+
(jwtDecoder: JwtDecoder, opaqueTokenIntrospector: OpaqueTokenIntrospector):
33+
AuthenticationManagerResolver<HttpServletRequest> {
34+
val jwt = JwtReactiveAuthenticationManager(jwtDecoder)
35+
val opaque = OpaqueTokenReactiveAuthenticationManager(opaqueTokenIntrospector)
36+
return ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.builder()
37+
.add(ServerWebExchangeMatcherEntry(ServerWebExchangeMatchers.pathPatterns("/opaque/**"), opaque))
38+
.add(ServerWebExchangeMatcherEntry(ServerWebExchangeMatchers.anyExchange(), jwt))
39+
.build()
40+
}
41+
----
42+
====
43+
44+
And then specify this `ReactiveAuthenticationManagerResolver` in the DSL:
45+
46+
.Authentication Manager Resolver
47+
====
48+
.Java
49+
[source,java,role="primary"]
50+
----
51+
http
52+
.authorizeExchange((authorize) -> authorize
53+
.anyExchange().authenticated()
54+
)
55+
.oauth2ResourceServer((oauth2) -> oauth2
56+
.authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
57+
);
58+
----
59+
60+
.Kotlin
61+
[source,kotlin,role="secondary"]
62+
----
63+
http {
64+
authorizeExchange {
65+
authorize(anyExchange, authenticated)
66+
}
67+
oauth2ResourceServer {
68+
authenticationManagerResolver = tokenAuthenticationManagerResolver()
69+
}
70+
}
71+
----
72+
====
73+
374
[[webflux-oauth2resourceserver-multitenancy]]
475
== Multi-tenancy
576

docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc

+10-14
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ If this decision must be made at request-time, then you can use an `Authenticati
1515
@Bean
1616
AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver
1717
(JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) {
18-
AuthenticationManager jwt = new ProviderManager(new JwtAuthenticationProvider(jwtDecoder));
19-
AuthenticationManager opaqueToken = new ProviderManager(
20-
new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
21-
return (request) -> useJwt(request) ? jwt : opaqueToken;
18+
AuthenticationManager jwt = new JwtAuthenticationProvider(jwtDecoder)::authenticate;
19+
AuthenticationManager opaque = new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)::authenticate;
20+
return RequestMatcherDelegatingAuthenticationManagerResolver.builder()
21+
.add(new RequestMatcherEntry<>(new AntPathRequestMatcher("/opaque/**"), opaque))
22+
.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, jwt))
23+
.build();
2224
}
2325
----
2426
@@ -31,20 +33,14 @@ fun tokenAuthenticationManagerResolver
3133
AuthenticationManagerResolver<HttpServletRequest> {
3234
val jwt = ProviderManager(JwtAuthenticationProvider(jwtDecoder))
3335
val opaqueToken = ProviderManager(OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector));
34-
35-
return AuthenticationManagerResolver { request ->
36-
if (useJwt(request)) {
37-
jwt
38-
} else {
39-
opaqueToken
40-
}
41-
}
36+
return RequestMatcherDelegatingAuthenticationManagerResolver.builder()
37+
.add(RequestMatcherEntry(AntPathRequestMatcher("/opaque/**"), opaque))
38+
.add(RequestMatcherEntry(AnyRequestMatcher.INSTANCE, jwt))
39+
.build()
4240
}
4341
----
4442
====
4543

46-
NOTE: The implementation of `useJwt(HttpServletRequest)` will likely depend on custom request material like the path.
47-
4844
And then specify this `AuthenticationManagerResolver` in the DSL:
4945

5046
.Authentication Manager Resolver
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2002-2021 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.List;
21+
22+
import jakarta.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.security.authentication.AuthenticationManager;
25+
import org.springframework.security.authentication.AuthenticationManagerResolver;
26+
import org.springframework.security.authentication.AuthenticationServiceException;
27+
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
28+
import org.springframework.security.web.util.matcher.RequestMatcher;
29+
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager}
34+
* instances based upon the type of {@link HttpServletRequest} passed into
35+
* {@link #resolve(HttpServletRequest)}.
36+
*
37+
* @author Josh Cummings
38+
* @since 5.7
39+
*/
40+
public final class RequestMatcherDelegatingAuthenticationManagerResolver
41+
implements AuthenticationManagerResolver<HttpServletRequest> {
42+
43+
private final List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers;
44+
45+
private AuthenticationManager defaultAuthenticationManager = (authentication) -> {
46+
throw new AuthenticationServiceException("Cannot authenticate " + authentication);
47+
};
48+
49+
/**
50+
* Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on
51+
* the provided parameters
52+
* @param authenticationManagers a {@link List} of
53+
* {@link RequestMatcher}/{@link AuthenticationManager} pairs
54+
*/
55+
private RequestMatcherDelegatingAuthenticationManagerResolver(
56+
List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers) {
57+
Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
58+
this.authenticationManagers = authenticationManagers;
59+
}
60+
61+
/**
62+
* {@inheritDoc}
63+
*/
64+
@Override
65+
public AuthenticationManager resolve(HttpServletRequest context) {
66+
for (RequestMatcherEntry<AuthenticationManager> entry : this.authenticationManagers) {
67+
if (entry.getRequestMatcher().matches(context)) {
68+
return entry.getEntry();
69+
}
70+
}
71+
72+
return this.defaultAuthenticationManager;
73+
}
74+
75+
/**
76+
* Set the default {@link AuthenticationManager} to use when a request does not match
77+
* @param defaultAuthenticationManager the default {@link AuthenticationManager} to
78+
* use
79+
*/
80+
public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) {
81+
Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
82+
this.defaultAuthenticationManager = defaultAuthenticationManager;
83+
}
84+
85+
/**
86+
* Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
87+
* @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder}
88+
* instance
89+
*/
90+
public static Builder builder() {
91+
return new Builder();
92+
}
93+
94+
/**
95+
* A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}.
96+
*/
97+
public static final class Builder {
98+
99+
private final List<RequestMatcherEntry<AuthenticationManager>> mappings = new ArrayList<>();
100+
101+
/**
102+
* Maps a {@link RequestMatcher} to a {@link AuthenticationManager}.
103+
* @param mapping a {@link RequestMatcher} to {@link AuthenticationManager}
104+
* mapping
105+
* @return the {@link Builder} for further customizations
106+
*/
107+
public Builder add(RequestMatcherEntry<AuthenticationManager> mapping) {
108+
Assert.notNull(mapping, "mapping cannot be null");
109+
this.mappings.add(mapping);
110+
return this;
111+
}
112+
113+
/**
114+
* Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver}
115+
* instance.
116+
* @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver}
117+
* instance
118+
*/
119+
public RequestMatcherDelegatingAuthenticationManagerResolver build() {
120+
return new RequestMatcherDelegatingAuthenticationManagerResolver(this.mappings);
121+
}
122+
123+
}
124+
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2002-2021 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.List;
21+
22+
import reactor.core.publisher.Flux;
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.security.authentication.AuthenticationServiceException;
26+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
27+
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
28+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
29+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
30+
import org.springframework.util.Assert;
31+
import org.springframework.web.server.ServerWebExchange;
32+
33+
/**
34+
* A {@link ReactiveAuthenticationManagerResolver} that returns a
35+
* {@link ReactiveAuthenticationManager} instances based upon the type of
36+
* {@link ServerWebExchange} passed into {@link #resolve(ServerWebExchange)}.
37+
*
38+
* @author Josh Cummings
39+
* @since 5.7
40+
*
41+
*/
42+
public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver
43+
implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
44+
45+
private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> authenticationManagers;
46+
47+
private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono
48+
.error(new AuthenticationServiceException("Cannot authenticate " + authentication));
49+
50+
/**
51+
* Construct an
52+
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on
53+
* the provided parameters
54+
* @param managers a {@link List} of {@link ServerWebExchangeMatcherEntry}s
55+
*/
56+
private ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(
57+
List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> managers) {
58+
Assert.notNull(managers, "entries cannot be null");
59+
this.authenticationManagers = managers;
60+
}
61+
62+
/**
63+
* {@inheritDoc}
64+
*/
65+
@Override
66+
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
67+
return Flux.fromIterable(this.authenticationManagers).filterWhen((entry) -> isMatch(exchange, entry)).next()
68+
.map(ServerWebExchangeMatcherEntry::getEntry).defaultIfEmpty(this.defaultAuthenticationManager);
69+
}
70+
71+
/**
72+
* Set the default {@link ReactiveAuthenticationManager} to use when a request does
73+
* not match
74+
* @param defaultAuthenticationManager the default
75+
* {@link ReactiveAuthenticationManager} to use
76+
*/
77+
public void setDefaultAuthenticationManager(ReactiveAuthenticationManager defaultAuthenticationManager) {
78+
Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
79+
this.defaultAuthenticationManager = defaultAuthenticationManager;
80+
}
81+
82+
private Mono<Boolean> isMatch(ServerWebExchange exchange, ServerWebExchangeMatcherEntry entry) {
83+
ServerWebExchangeMatcher matcher = entry.getMatcher();
84+
return matcher.matches(exchange).map(ServerWebExchangeMatcher.MatchResult::isMatch);
85+
}
86+
87+
/**
88+
* Creates a builder for
89+
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver}.
90+
* @return the new
91+
* {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder}
92+
* instance
93+
*/
94+
public static Builder builder() {
95+
return new Builder();
96+
}
97+
98+
public static final class Builder {
99+
100+
private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> mappings = new ArrayList<>();
101+
102+
private Builder() {
103+
}
104+
105+
public Builder add(ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager> entry) {
106+
this.mappings.add(entry);
107+
return this;
108+
}
109+
110+
public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver build() {
111+
return new ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(this.mappings);
112+
}
113+
114+
}
115+
116+
}

0 commit comments

Comments
 (0)