Skip to content

Commit e978820

Browse files
committed
Bearer WebClient Filter Authentication Propagation
Fixes: gh-7418
1 parent 96d44cd commit e978820

File tree

7 files changed

+319
-30
lines changed

7 files changed

+319
-30
lines changed

config/spring-security-config.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies {
3535
testCompile project(':spring-security-test')
3636
testCompile project(path : ':spring-security-core', configuration : 'tests')
3737
testCompile project(path : ':spring-security-oauth2-client', configuration : 'tests')
38+
testCompile project(path : ':spring-security-oauth2-resource-server', configuration : 'tests')
3839
testCompile project(path : ':spring-security-web', configuration : 'tests')
3940
testCompile apachedsDependencies
4041
testCompile powerMock2Dependencies

config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ImportSelector.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,40 @@
1515
*/
1616
package org.springframework.security.config.annotation.web.configuration;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
20+
1821
import org.springframework.context.annotation.ImportSelector;
1922
import org.springframework.core.type.AnnotationMetadata;
2023
import org.springframework.util.ClassUtils;
2124

2225
/**
2326
* Used by {@link EnableWebSecurity} to conditionally import {@link OAuth2ClientConfiguration}
24-
* when the {@code spring-security-oauth2-client} module is present on the classpath.
25-
27+
* when the {@code spring-security-oauth2-client} module is present on the classpath and
28+
* {@link OAuth2ResourceServerConfiguration} when the {@code spring-security-oauth2-resource-server}
29+
* module is on the classpath.
30+
*
2631
* @author Joe Grandja
32+
* @author Josh Cummings
2733
* @since 5.1
2834
* @see OAuth2ClientConfiguration
2935
*/
3036
final class OAuth2ImportSelector implements ImportSelector {
3137

3238
@Override
3339
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
34-
boolean oauth2ClientPresent = ClassUtils.isPresent(
35-
"org.springframework.security.oauth2.client.registration.ClientRegistration", getClass().getClassLoader());
40+
List<String> imports = new ArrayList<>();
41+
42+
if (ClassUtils.isPresent(
43+
"org.springframework.security.oauth2.client.registration.ClientRegistration", getClass().getClassLoader())) {
44+
imports.add("org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration");
45+
}
46+
47+
if (ClassUtils.isPresent(
48+
"org.springframework.security.oauth2.server.resource.BearerTokenError", getClass().getClassLoader())) {
49+
imports.add("org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration");
50+
}
3651

37-
return oauth2ClientPresent ?
38-
new String[] { "org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration" } :
39-
new String[] {};
52+
return imports.toArray(new String[0]);
4053
}
4154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.config.annotation.web.configuration;
18+
19+
import org.reactivestreams.Subscription;
20+
import reactor.core.CoreSubscriber;
21+
import reactor.core.publisher.Hooks;
22+
import reactor.core.publisher.Operators;
23+
import reactor.util.context.Context;
24+
25+
import org.springframework.beans.factory.DisposableBean;
26+
import org.springframework.beans.factory.InitializingBean;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.context.annotation.Import;
30+
import org.springframework.context.annotation.ImportSelector;
31+
import org.springframework.core.type.AnnotationMetadata;
32+
import org.springframework.security.core.Authentication;
33+
import org.springframework.security.core.context.SecurityContextHolder;
34+
import org.springframework.util.ClassUtils;
35+
36+
/**
37+
* {@link Configuration} for OAuth 2.0 Resource Server support.
38+
*
39+
* <p>
40+
* This {@code Configuration} is conditionally imported by {@link OAuth2ImportSelector}
41+
* when the {@code spring-security-oauth2-resource-server} module is present on the classpath.
42+
*
43+
* @author Josh Cummings
44+
* @since 5.2
45+
* @see OAuth2ImportSelector
46+
*/
47+
@Import(OAuth2ResourceServerConfiguration.OAuth2ClientWebFluxImportSelector.class)
48+
final class OAuth2ResourceServerConfiguration {
49+
50+
static class OAuth2ClientWebFluxImportSelector implements ImportSelector {
51+
52+
@Override
53+
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
54+
boolean webfluxPresent = ClassUtils.isPresent(
55+
"org.springframework.web.reactive.function.client.WebClient", getClass().getClassLoader());
56+
57+
return webfluxPresent ?
58+
new String[] { "org.springframework.security.config.annotation.web.configuration.OAuth2ResourceServerConfiguration.OAuth2ResourceServerWebFluxSecurityConfiguration" } :
59+
new String[] {};
60+
}
61+
}
62+
63+
@Configuration(proxyBeanMethods = false)
64+
static class OAuth2ResourceServerWebFluxSecurityConfiguration {
65+
@Bean
66+
BearerRequestContextSubscriberRegistrar bearerRequestContextSubscriberRegistrar() {
67+
return new BearerRequestContextSubscriberRegistrar();
68+
}
69+
70+
static class BearerRequestContextSubscriberRegistrar
71+
implements InitializingBean, DisposableBean {
72+
73+
private static final String REQUEST_CONTEXT_OPERATOR_KEY = BearerRequestContextSubscriber.class.getName();
74+
75+
@Override
76+
public void afterPropertiesSet() throws Exception {
77+
Hooks.onLastOperator(REQUEST_CONTEXT_OPERATOR_KEY,
78+
Operators.liftPublisher((s, sub) -> createRequestContextSubscriber(sub)));
79+
}
80+
81+
@Override
82+
public void destroy() throws Exception {
83+
Hooks.resetOnLastOperator(REQUEST_CONTEXT_OPERATOR_KEY);
84+
}
85+
86+
private <T> CoreSubscriber<T> createRequestContextSubscriber(CoreSubscriber<T> delegate) {
87+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
88+
return new BearerRequestContextSubscriber<>(delegate, authentication);
89+
}
90+
91+
static class BearerRequestContextSubscriber<T> implements CoreSubscriber<T> {
92+
private CoreSubscriber<T> delegate;
93+
private final Context context;
94+
95+
private BearerRequestContextSubscriber(CoreSubscriber<T> delegate,
96+
Authentication authentication) {
97+
98+
this.delegate = delegate;
99+
Context parentContext = this.delegate.currentContext();
100+
Context context;
101+
if (authentication == null || parentContext.hasKey(Authentication.class)) {
102+
context = parentContext;
103+
} else {
104+
context = parentContext.put(Authentication.class, authentication);
105+
}
106+
107+
this.context = context;
108+
}
109+
110+
@Override
111+
public Context currentContext() {
112+
return this.context;
113+
}
114+
115+
@Override
116+
public void onSubscribe(Subscription s) {
117+
this.delegate.onSubscribe(s);
118+
}
119+
120+
@Override
121+
public void onNext(T t) {
122+
this.delegate.onNext(t);
123+
}
124+
125+
@Override
126+
public void onError(Throwable t) {
127+
this.delegate.onError(t);
128+
}
129+
130+
@Override
131+
public void onComplete() {
132+
this.delegate.onComplete();
133+
}
134+
}
135+
}
136+
}
137+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
package org.springframework.security.config.annotation.web.configuration;
17+
18+
import java.net.URI;
19+
20+
import org.junit.Rule;
21+
import org.junit.Test;
22+
import reactor.core.publisher.Flux;
23+
import reactor.core.scheduler.Schedulers;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
27+
import org.springframework.security.config.test.SpringTestRule;
28+
import org.springframework.security.oauth2.client.web.reactive.function.client.MockExchangeFunction;
29+
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
30+
import org.springframework.security.oauth2.server.resource.web.reactive.function.client.ServletBearerExchangeFilterFunction;
31+
import org.springframework.test.web.servlet.MockMvc;
32+
import org.springframework.web.bind.annotation.GetMapping;
33+
import org.springframework.web.bind.annotation.RestController;
34+
import org.springframework.web.reactive.function.client.ClientRequest;
35+
36+
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
37+
import static org.springframework.http.HttpMethod.GET;
38+
import static org.springframework.security.oauth2.server.resource.authentication.TestBearerTokenAuthentications.bearer;
39+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
40+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
41+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
42+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
43+
44+
/**
45+
* Tests for {@link OAuth2ResourceServerConfiguration}.
46+
*
47+
* @author Josh Cummings
48+
*/
49+
public class OAuth2ResourceServerConfigurationTests {
50+
@Rule
51+
public final SpringTestRule spring = new SpringTestRule();
52+
53+
@Autowired
54+
private MockMvc mockMvc;
55+
56+
// gh-7418
57+
@Test
58+
public void requestWhenAuthenticatedThenBearerTokenPropagated() throws Exception {
59+
BearerTokenAuthentication authentication = bearer();
60+
this.spring.register(BearerWebClientConfig.class).autowire();
61+
62+
this.mockMvc.perform(get("/token")
63+
.with(authentication(authentication)))
64+
.andExpect(status().isOk())
65+
.andExpect(content().string("Bearer token"));
66+
}
67+
68+
69+
@EnableWebSecurity
70+
static class BearerWebClientConfig extends WebSecurityConfigurerAdapter {
71+
@Override
72+
protected void configure(HttpSecurity http) throws Exception {
73+
}
74+
75+
@RestController
76+
public class Controller {
77+
78+
@GetMapping("/token")
79+
public String message() {
80+
ServletBearerExchangeFilterFunction bearer = new ServletBearerExchangeFilterFunction();
81+
ClientRequest request =
82+
ClientRequest.create(GET, URI.create("https://example.org")).build();
83+
MockExchangeFunction exchange = new MockExchangeFunction();
84+
Flux.concat(bearer.filter(request, exchange))
85+
.subscribeOn(Schedulers.elastic())
86+
.collectList().block();
87+
return exchange.getRequest().headers().getFirst(AUTHORIZATION);
88+
}
89+
}
90+
}
91+
}

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package org.springframework.security.oauth2.server.resource.web.reactive.function.client;
1818

1919
import reactor.core.publisher.Mono;
20+
import reactor.util.context.Context;
2021

2122
import org.springframework.security.core.Authentication;
22-
import org.springframework.security.core.context.SecurityContextHolder;
2323
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
2424
import org.springframework.web.reactive.function.client.ClientRequest;
2525
import org.springframework.web.reactive.function.client.ClientResponse;
@@ -61,14 +61,16 @@ public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
6161
}
6262

6363
private Mono<AbstractOAuth2Token> oauth2Token() {
64-
return currentAuthentication()
64+
return Mono.subscriberContext()
65+
.flatMap(this::currentAuthentication)
6566
.filter(authentication -> authentication.getCredentials() instanceof AbstractOAuth2Token)
6667
.map(Authentication::getCredentials)
6768
.cast(AbstractOAuth2Token.class);
6869
}
6970

70-
private Mono<Authentication> currentAuthentication() {
71-
return Mono.justOrEmpty(SecurityContextHolder.getContext().getAuthentication());
71+
private Mono<Authentication> currentAuthentication(Context ctx) {
72+
Authentication authentication = ctx.getOrDefault(Authentication.class, null);
73+
return Mono.justOrEmpty(authentication);
7274
}
7375

7476
private ClientRequest bearer(ClientRequest request, AbstractOAuth2Token token) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.authentication;
18+
19+
import java.time.Instant;
20+
import java.util.Arrays;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
25+
import org.springframework.security.core.GrantedAuthority;
26+
import org.springframework.security.core.authority.AuthorityUtils;
27+
import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal;
28+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
29+
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
30+
31+
/**
32+
* Test instances of {@link BearerTokenAuthentication}
33+
*
34+
* @author Josh Cummings
35+
*/
36+
public class TestBearerTokenAuthentications {
37+
public static BearerTokenAuthentication bearer() {
38+
Collection<GrantedAuthority> authorities =
39+
AuthorityUtils.createAuthorityList("SCOPE_USER");
40+
OAuth2AuthenticatedPrincipal principal =
41+
new DefaultOAuth2AuthenticatedPrincipal(
42+
Collections.singletonMap("sub", "user"),
43+
authorities);
44+
OAuth2AccessToken token =
45+
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
46+
"token", Instant.now(), Instant.now().plusSeconds(86400),
47+
new HashSet<>(Arrays.asList("USER")));
48+
49+
return new BearerTokenAuthentication(principal, token, authorities);
50+
}
51+
}

0 commit comments

Comments
 (0)