Skip to content

Commit f350988

Browse files
committed
Add Servlet and ServerBearerExchangeFilterFunction
Fixes gh-5334 Fixes gh-7284
1 parent dbd1819 commit f350988

File tree

5 files changed

+646
-0
lines changed

5 files changed

+646
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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.oauth2.server.resource.web;
18+
19+
import java.util.Map;
20+
import java.util.function.Consumer;
21+
22+
import org.reactivestreams.Subscription;
23+
import reactor.core.CoreSubscriber;
24+
import reactor.core.publisher.Hooks;
25+
import reactor.core.publisher.Mono;
26+
import reactor.core.publisher.Operators;
27+
import reactor.util.context.Context;
28+
29+
import org.springframework.beans.factory.DisposableBean;
30+
import org.springframework.beans.factory.InitializingBean;
31+
import org.springframework.lang.Nullable;
32+
import org.springframework.security.core.Authentication;
33+
import org.springframework.security.core.context.SecurityContextHolder;
34+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
35+
import org.springframework.web.reactive.function.client.ClientRequest;
36+
import org.springframework.web.reactive.function.client.ClientResponse;
37+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
38+
import org.springframework.web.reactive.function.client.ExchangeFunction;
39+
import org.springframework.web.reactive.function.client.WebClient;
40+
41+
/**
42+
* An {@link ExchangeFilterFunction} that adds the
43+
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>
44+
* from an existing {@link AbstractOAuth2Token} tied to the current {@link Authentication}.
45+
*
46+
* Suitable for Servlet applications, applying it to a typical {@link org.springframework.web.reactive.function.client.WebClient}
47+
* configuration:
48+
*
49+
* <pre>
50+
* @Bean
51+
* WebClient webClient() {
52+
* ServletBearerExchangeFilterFunction bearer = new ServletBearerExchangeFilterFunction();
53+
* return WebClient.builder()
54+
* .apply(bearer.oauth2Configuration())
55+
* .build();
56+
* }
57+
* </pre>
58+
*
59+
* @author Josh Cummings
60+
* @since 5.2
61+
*/
62+
public class ServletBearerExchangeFilterFunction
63+
implements ExchangeFilterFunction, InitializingBean, DisposableBean {
64+
65+
private static final String AUTHENTICATION_ATTR_NAME = Authentication.class.getName();
66+
67+
private static final String REQUEST_CONTEXT_OPERATOR_KEY = RequestContextSubscriber.class.getName();
68+
69+
/**
70+
* {@inheritDoc}
71+
*/
72+
@Override
73+
public void afterPropertiesSet() throws Exception {
74+
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY,
75+
Operators.liftPublisher((s, sub) -> createRequestContextSubscriber(sub)));
76+
}
77+
78+
/**
79+
* {@inheritDoc}
80+
*/
81+
@Override
82+
public void destroy() throws Exception {
83+
Hooks.resetOnLastOperator(REQUEST_CONTEXT_OPERATOR_KEY);
84+
}
85+
86+
/**
87+
* Configures the builder with {@link #defaultRequest()} and adds this as a {@link ExchangeFilterFunction}
88+
* @return the {@link Consumer} to configure the builder
89+
*/
90+
public Consumer<WebClient.Builder> oauth2Configuration() {
91+
return builder -> builder.defaultRequest(defaultRequest()).filter(this);
92+
}
93+
94+
/**
95+
* Provides defaults for the {@link Authentication} using
96+
* {@link SecurityContextHolder}. It also can default the {@link AbstractOAuth2Token} using the
97+
* {@link #authentication(Authentication)}.
98+
* @return the {@link Consumer} to populate the attributes
99+
*/
100+
public Consumer<WebClient.RequestHeadersSpec<?>> defaultRequest() {
101+
return spec -> spec.attributes(attrs -> {
102+
populateDefaultAuthentication(attrs);
103+
});
104+
}
105+
106+
/**
107+
* Modifies the {@link ClientRequest#attributes()} to include the {@link Authentication} used to
108+
* look up and save the {@link AbstractOAuth2Token}. The value is defaulted in
109+
* {@link ServletBearerExchangeFilterFunction#defaultRequest()}
110+
*
111+
* @param authentication the {@link Authentication} to use.
112+
* @return the {@link Consumer} to populate the attributes
113+
*/
114+
public static Consumer<Map<String, Object>> authentication(Authentication authentication) {
115+
return attributes -> attributes.put(AUTHENTICATION_ATTR_NAME, authentication);
116+
}
117+
118+
/**
119+
* {@inheritDoc}
120+
*/
121+
@Override
122+
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
123+
return mergeRequestAttributesIfNecessary(request)
124+
.filter(req -> req.attribute(AUTHENTICATION_ATTR_NAME).isPresent())
125+
.map(req -> getOAuth2Token(req.attributes()))
126+
.map(token -> bearer(request, token))
127+
.flatMap(next::exchange)
128+
.switchIfEmpty(Mono.defer(() -> next.exchange(request)));
129+
}
130+
131+
private Mono<ClientRequest> mergeRequestAttributesIfNecessary(ClientRequest request) {
132+
if (request.attribute(AUTHENTICATION_ATTR_NAME).isPresent()) {
133+
return Mono.just(request);
134+
}
135+
136+
return mergeRequestAttributesFromContext(request);
137+
}
138+
139+
private Mono<ClientRequest> mergeRequestAttributesFromContext(ClientRequest request) {
140+
ClientRequest.Builder builder = ClientRequest.from(request);
141+
return Mono.subscriberContext()
142+
.map(ctx -> builder.attributes(attrs -> populateRequestAttributes(attrs, ctx)))
143+
.map(ClientRequest.Builder::build);
144+
}
145+
146+
private void populateRequestAttributes(Map<String, Object> attrs, Context ctx) {
147+
RequestContextDataHolder holder = RequestContextSubscriber.getRequestContext(ctx);
148+
if (holder == null) {
149+
return;
150+
}
151+
if (holder.getAuthentication() != null) {
152+
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, holder.getAuthentication());
153+
}
154+
}
155+
156+
private AbstractOAuth2Token getOAuth2Token(Map<String, Object> attrs) {
157+
Authentication authentication = (Authentication) attrs.get(AUTHENTICATION_ATTR_NAME);
158+
if (authentication.getCredentials() instanceof AbstractOAuth2Token) {
159+
return (AbstractOAuth2Token) authentication.getCredentials();
160+
}
161+
return null;
162+
}
163+
164+
private ClientRequest bearer(ClientRequest request, AbstractOAuth2Token token) {
165+
return ClientRequest.from(request)
166+
.headers(headers -> headers.setBearerAuth(token.getTokenValue()))
167+
.build();
168+
}
169+
170+
private <T> CoreSubscriber<T> createRequestContextSubscriber(CoreSubscriber<T> delegate) {
171+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
172+
return new RequestContextSubscriber<>(delegate, authentication);
173+
}
174+
175+
private void populateDefaultAuthentication(Map<String, Object> attrs) {
176+
if (attrs.containsKey(AUTHENTICATION_ATTR_NAME)) {
177+
return;
178+
}
179+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
180+
attrs.putIfAbsent(AUTHENTICATION_ATTR_NAME, authentication);
181+
}
182+
183+
private static class RequestContextDataHolder {
184+
private final Authentication authentication;
185+
186+
RequestContextDataHolder(Authentication authentication) {
187+
this.authentication = authentication;
188+
}
189+
190+
public Authentication getAuthentication() {
191+
return this.authentication;
192+
}
193+
}
194+
195+
private static class RequestContextSubscriber<T> implements CoreSubscriber<T> {
196+
private static final String REQUEST_CONTEXT_DATA_HOLDER_ATTR_NAME =
197+
RequestContextSubscriber.class.getName().concat(".REQUEST_CONTEXT_DATA_HOLDER");
198+
199+
private CoreSubscriber<T> delegate;
200+
private final Context context;
201+
202+
private RequestContextSubscriber(CoreSubscriber<T> delegate,
203+
Authentication authentication) {
204+
205+
this.delegate = delegate;
206+
Context parentContext = this.delegate.currentContext();
207+
Context context;
208+
if (authentication == null || parentContext.hasKey(REQUEST_CONTEXT_DATA_HOLDER_ATTR_NAME)) {
209+
context = parentContext;
210+
} else {
211+
context = parentContext.put(REQUEST_CONTEXT_DATA_HOLDER_ATTR_NAME,
212+
new RequestContextDataHolder(authentication));
213+
}
214+
215+
this.context = context;
216+
}
217+
218+
@Nullable
219+
static RequestContextDataHolder getRequestContext(Context ctx) {
220+
return ctx.getOrDefault(REQUEST_CONTEXT_DATA_HOLDER_ATTR_NAME, null);
221+
}
222+
223+
@Override
224+
public Context currentContext() {
225+
return this.context;
226+
}
227+
228+
@Override
229+
public void onSubscribe(Subscription s) {
230+
this.delegate.onSubscribe(s);
231+
}
232+
233+
@Override
234+
public void onNext(T t) {
235+
this.delegate.onNext(t);
236+
}
237+
238+
@Override
239+
public void onError(Throwable t) {
240+
this.delegate.onError(t);
241+
}
242+
243+
@Override
244+
public void onComplete() {
245+
this.delegate.onComplete();
246+
}
247+
}
248+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.oauth2.server.resource.web.server;
18+
19+
import java.util.Map;
20+
import java.util.function.Consumer;
21+
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
25+
import org.springframework.security.core.Authentication;
26+
import org.springframework.security.core.authority.AuthorityUtils;
27+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
28+
import org.springframework.security.core.context.SecurityContext;
29+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
30+
import org.springframework.web.reactive.function.client.ClientRequest;
31+
import org.springframework.web.reactive.function.client.ClientResponse;
32+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
33+
import org.springframework.web.reactive.function.client.ExchangeFunction;
34+
35+
/**
36+
* An {@link ExchangeFilterFunction} that adds the
37+
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>
38+
* from an existing {@link AbstractOAuth2Token} tied to the current {@link Authentication}.
39+
*
40+
* Suitable for Reactive applications, applying it to a typical {@link org.springframework.web.reactive.function.client.WebClient}
41+
* configuration:
42+
*
43+
* <pre>
44+
* @Bean
45+
* WebClient webClient() {
46+
* ServerBearerExchangeFilterFunction bearer = new ServerBearerExchangeFilterFunction();
47+
* return WebClient.builder()
48+
* .filter(bearer).build();
49+
* }
50+
* </pre>
51+
*
52+
* @author Josh Cummings
53+
* @since 5.2
54+
*/
55+
public class ServerBearerExchangeFilterFunction
56+
implements ExchangeFilterFunction {
57+
58+
private static final String AUTHENTICATION_ATTR_NAME = Authentication.class.getName();
59+
60+
private static final AnonymousAuthenticationToken ANONYMOUS_USER_TOKEN = new AnonymousAuthenticationToken("anonymous", "anonymousUser",
61+
AuthorityUtils.createAuthorityList("ROLE_USER"));
62+
63+
/**
64+
* Modifies the {@link ClientRequest#attributes()} to include the {@link Authentication} to be used for
65+
* providing the Bearer Token. Example usage:
66+
*
67+
* <pre>
68+
* WebClient webClient = WebClient.builder()
69+
* .filter(new ServerBearerExchangeFilterFunction())
70+
* .build();
71+
* Mono<String> response = webClient
72+
* .get()
73+
* .uri(uri)
74+
* .attributes(authentication(authentication))
75+
* // ...
76+
* .retrieve()
77+
* .bodyToMono(String.class);
78+
* </pre>
79+
* @param authentication the {@link Authentication} to use
80+
* @return the {@link Consumer} to populate the client request attributes
81+
*/
82+
public static Consumer<Map<String, Object>> authentication(Authentication authentication) {
83+
return attributes -> attributes.put(AUTHENTICATION_ATTR_NAME, authentication);
84+
}
85+
86+
/**
87+
* {@inheritDoc}
88+
*/
89+
@Override
90+
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
91+
return oauth2Token(request.attributes())
92+
.map(oauth2Token -> bearer(request, oauth2Token))
93+
.defaultIfEmpty(request)
94+
.flatMap(next::exchange);
95+
}
96+
97+
private Mono<AbstractOAuth2Token> oauth2Token(Map<String, Object> attrs) {
98+
return Mono.justOrEmpty(attrs.get(AUTHENTICATION_ATTR_NAME))
99+
.cast(Authentication.class)
100+
.switchIfEmpty(currentAuthentication())
101+
.filter(authentication -> authentication.getCredentials() instanceof AbstractOAuth2Token)
102+
.map(Authentication::getCredentials)
103+
.cast(AbstractOAuth2Token.class);
104+
}
105+
106+
private Mono<Authentication> currentAuthentication() {
107+
return ReactiveSecurityContextHolder.getContext()
108+
.map(SecurityContext::getAuthentication)
109+
.defaultIfEmpty(ANONYMOUS_USER_TOKEN);
110+
}
111+
112+
private ClientRequest bearer(ClientRequest request, AbstractOAuth2Token token) {
113+
return ClientRequest.from(request)
114+
.headers(headers -> headers.setBearerAuth(token.getTokenValue()))
115+
.build();
116+
}
117+
}

0 commit comments

Comments
 (0)