Skip to content

Commit 7952167

Browse files
authored
Merge pull request #3751 from jaimesf/support-client-registration-id-token-relay
Support token relay clientRegistrationId on properties
2 parents b6623a9 + 03b0f4e commit 7952167

19 files changed

+235
-23
lines changed

docs/modules/ROOT/pages/spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,27 @@ forwards the incoming token to outgoing resource requests. The
66
consumer can be a pure Client (like an SSO application) or a Resource
77
Server.
88

9-
////
10-
TODO: support TokenRelay clientRegistrationId
119
Spring Cloud Gateway Server MVC can forward OAuth2 access tokens downstream to the services
1210
it is proxying using the `TokenRelay` filter.
1311

1412
The `TokenRelay` filter takes one optional parameter, `clientRegistrationId`.
1513
The following example configures a `TokenRelay` filter:
1614

17-
.App.java
15+
.RouteConfiguration.java
1816
[source,java]
1917
----
2018
21-
@Bean
22-
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
23-
return builder.routes()
24-
.route("resource", r -> r.path("/resource")
25-
.filters(f -> f.tokenRelay("myregistrationid"))
26-
.uri("http://localhost:9000"))
19+
@Configuration
20+
class RouteConfiguration {
21+
22+
@Bean
23+
public RouterFunction<ServerResponse> gatewayRouterFunctionsTokenRelay() {
24+
return route("resource")
25+
.GET("/resource", http())
26+
.before(uri("https://localhost:9000"))
27+
.filter(tokenRelay("myregistrationid"))
2728
.build();
29+
}
2830
}
2931
----
3032

@@ -46,19 +48,13 @@ spring:
4648
----
4749

4850
The example above specifies a `clientRegistrationId`, which can be used to obtain and forward an OAuth2 access token for any available `ClientRegistration`.
49-
////
5051

5152
Spring Cloud Gateway Server MVC can forward the OAuth2 access token of the currently authenticated user `oauth2Login()` is used to authenticate the user.
52-
//To add this functionality to the gateway, you can omit the `clientRegistrationId` parameter like this:
53+
To add this functionality to the gateway, you can omit the `clientRegistrationId` parameter like this:
5354

5455
.RouteConfiguration.java
5556
[source,java]
5657
----
57-
import static org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions.uri;
58-
import static org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions.tokenRelay;
59-
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
60-
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
61-
6258
@Configuration
6359
class RouteConfiguration {
6460
@@ -100,9 +96,9 @@ To enable this for Spring Cloud Gateway Server MVC add the following dependencie
10096
- `org.springframework.boot:spring-boot-starter-oauth2-client`
10197

10298
How does it work?
103-
// The filter extracts an OAuth2 access token from the currently authenticated user for the provided `clientRegistrationId`.
104-
// If no `clientRegistrationId` is provided,
105-
The currently authenticated user's own access token (obtained during login) is used and the extracted access token is placed in a request header for the downstream requests.
99+
The filter extracts an OAuth2 access token from the currently authenticated user for the provided `clientRegistrationId`.
100+
If no `clientRegistrationId` is provided,
101+
the currently authenticated user's own access token (obtained during login) is used and the extracted access token is placed in a request header for the downstream requests.
106102

107103
//For a full working sample see https://github.com/spring-cloud-samples/sample-gateway-oauth2login[this project].
108104

spring-cloud-gateway-server-mvc/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@
104104
<artifactId>spring-boot-testcontainers</artifactId>
105105
<scope>test</scope>
106106
</dependency>
107+
<dependency>
108+
<groupId>org.springframework.boot</groupId>
109+
<artifactId>spring-boot-starter-security</artifactId>
110+
<scope>test</scope>
111+
</dependency>
112+
<dependency>
113+
<groupId>org.springframework.security</groupId>
114+
<artifactId>spring-security-test</artifactId>
115+
<scope>test</scope>
116+
</dependency>
107117
<dependency>
108118
<groupId>org.springframework.cloud</groupId>
109119
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/TokenRelayFilterFunctions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public static HandlerFilterFunction<ServerResponse, ServerResponse> tokenRelay()
3939
return tokenRelay(null);
4040
}
4141

42+
@Shortcut
4243
public static HandlerFilterFunction<ServerResponse, ServerResponse> tokenRelay(String defaultClientRegistrationId) {
4344
return (request, next) -> {
4445
Authentication principal = (Authentication) request.servletRequest().getUserPrincipal();

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@
6161
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
6262
import org.springframework.cloud.gateway.server.mvc.test.HttpbinUriResolver;
6363
import org.springframework.cloud.gateway.server.mvc.test.LocalServerPortUriResolver;
64+
import org.springframework.cloud.gateway.server.mvc.test.PermitAllSecurityConfiguration;
6465
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
6566
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
6667
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
6768
import org.springframework.context.annotation.Bean;
69+
import org.springframework.context.annotation.Import;
70+
import org.springframework.context.annotation.Lazy;
6871
import org.springframework.core.Ordered;
6972
import org.springframework.core.io.ClassPathResource;
7073
import org.springframework.http.HttpEntity;
@@ -83,10 +86,12 @@
8386
import org.springframework.web.bind.annotation.PostMapping;
8487
import org.springframework.web.bind.annotation.RequestBody;
8588
import org.springframework.web.bind.annotation.RestController;
89+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
8690
import org.springframework.web.servlet.function.HandlerFunction;
8791
import org.springframework.web.servlet.function.RouterFunction;
8892
import org.springframework.web.servlet.function.ServerRequest;
8993
import org.springframework.web.servlet.function.ServerResponse;
94+
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
9095

9196
import static org.assertj.core.api.Assertions.assertThat;
9297
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.DedupeStrategy.RETAIN_FIRST;
@@ -142,8 +147,8 @@
142147
import static org.springframework.web.servlet.function.RequestPredicates.path;
143148

144149
@SuppressWarnings("unchecked")
145-
@SpringBootTest(properties = { "spring.http.client.factory=jdk", "spring.cloud.gateway.function.enabled=false" },
146-
webEnvironment = WebEnvironment.RANDOM_PORT)
150+
@SpringBootTest(properties = { "spring.http.client.factory=jdk", "spring.cloud.gateway.function.enabled=false",
151+
"logging.level.org.springframework.security=TRACE" }, webEnvironment = WebEnvironment.RANDOM_PORT)
147152
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
148153
@ExtendWith(OutputCaptureExtension.class)
149154
public class ServerMvcIntegrationTests {
@@ -317,7 +322,7 @@ public void setStatusGatewayRouterFunctionWorks() {
317322
.isEqualTo(HttpStatus.TOO_MANY_REQUESTS)
318323
.expectHeader()
319324
.valueEquals("x-status", "201"); // .expectBody(String.class).isEqualTo("Failed
320-
// with 201");
325+
// with 201");
321326
}
322327

323328
@Test
@@ -1026,7 +1031,8 @@ void logsArtifactDeprecatedWarning(CapturedOutput output) {
10261031
@SpringBootConfiguration
10271032
@EnableAutoConfiguration
10281033
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
1029-
protected static class TestConfiguration {
1034+
@Import(PermitAllSecurityConfiguration.class)
1035+
protected static class TestConfiguration extends WebMvcConfigurationSupport {
10301036

10311037
@Bean
10321038
StaticPortController staticPortController() {
@@ -1043,6 +1049,23 @@ EventController eventController() {
10431049
return new EventController();
10441050
}
10451051

1052+
// TODO This is needed to work around https://github.com/spring-cloud/spring-cloud-gateway/issues/3816
1053+
// which results from Spring Security being on the classpath. Once we can address this issue we should
1054+
// remove this bean and no longer extend WebMvcConfigurationSupport in this configuration class
1055+
@Bean
1056+
@Lazy
1057+
@Override
1058+
public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
1059+
return new HandlerMappingIntrospector() {
1060+
@Override
1061+
public Filter createCacheFilter() {
1062+
return (request, response, chain) -> {
1063+
chain.doFilter(request, response);
1064+
};
1065+
}
1066+
};
1067+
}
1068+
10461069
@Bean
10471070
public AsyncProxyManager<String> caffeineProxyManager() {
10481071
Caffeine<String, RemoteBucketState> builder = (Caffeine) Caffeine.newBuilder().maximumSize(100);

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/VanillaRouterFunctionTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@
2929
import org.springframework.boot.test.web.server.LocalServerPort;
3030
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
3131
import org.springframework.cloud.gateway.server.mvc.test.HttpbinUriResolver;
32+
import org.springframework.cloud.gateway.server.mvc.test.PermitAllSecurityConfiguration;
3233
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
3334
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
3435
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
3536
import org.springframework.context.annotation.Bean;
37+
import org.springframework.context.annotation.Import;
3638
import org.springframework.test.context.ContextConfiguration;
3739
import org.springframework.web.servlet.function.RouterFunction;
3840
import org.springframework.web.servlet.function.RouterFunctions;
@@ -72,6 +74,7 @@ public void routerFunctionsRouteWorks() {
7274
@SpringBootConfiguration
7375
@EnableAutoConfiguration
7476
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
77+
@Import(PermitAllSecurityConfiguration.class)
7578
protected static class TestConfiguration {
7679

7780
@Bean

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/FunctionHandlerConfigTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import org.springframework.boot.SpringBootConfiguration;
2727
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2828
import org.springframework.boot.test.context.SpringBootTest;
29+
import org.springframework.cloud.gateway.server.mvc.test.PermitAllSecurityConfiguration;
2930
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
3031
import org.springframework.context.annotation.Bean;
32+
import org.springframework.context.annotation.Import;
3133
import org.springframework.http.MediaType;
3234
import org.springframework.test.context.ActiveProfiles;
3335

@@ -78,6 +80,7 @@ public void testSupplierFunctionWorks() {
7880

7981
@SpringBootConfiguration
8082
@EnableAutoConfiguration
83+
@Import(PermitAllSecurityConfiguration.class)
8184
protected static class TestConfiguration {
8285

8386
@Bean

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcPropertiesBeanDefinitionRegistrarTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
import org.springframework.cloud.context.refresh.ContextRefresher;
3535
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
3636
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
37+
import org.springframework.cloud.gateway.server.mvc.test.PermitAllSecurityConfiguration;
3738
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
3839
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
3940
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
4041
import org.springframework.context.ApplicationContext;
4142
import org.springframework.context.ConfigurableApplicationContext;
43+
import org.springframework.context.annotation.Import;
4244
import org.springframework.core.io.Resource;
4345
import org.springframework.http.HttpMethod;
4446
import org.springframework.test.context.ActiveProfiles;
@@ -230,6 +232,7 @@ void refreshWorks(ConfigurableApplicationContext context) {
230232
@SpringBootConfiguration
231233
@EnableAutoConfiguration
232234
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
235+
@Import(PermitAllSecurityConfiguration.class)
233236
static class Config {
234237

235238
}

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/config/StreamHandlerConfigTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3232
import org.springframework.boot.test.context.SpringBootTest;
3333
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
34+
import org.springframework.cloud.gateway.server.mvc.test.PermitAllSecurityConfiguration;
3435
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
3536
import org.springframework.context.annotation.Bean;
37+
import org.springframework.context.annotation.Import;
3638
import org.springframework.http.MediaType;
3739
import org.springframework.test.context.ActiveProfiles;
3840

@@ -88,6 +90,7 @@ public void testTemplatedStreamWorks() {
8890

8991
@SpringBootConfiguration
9092
@EnableAutoConfiguration
93+
@Import(PermitAllSecurityConfiguration.class)
9194
protected static class TestConfiguration {
9295

9396
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2013-2025 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.cloud.gateway.server.mvc.config;
18+
19+
import org.junit.jupiter.api.BeforeEach;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.beans.factory.annotation.Autowired;
23+
import org.springframework.boot.SpringBootConfiguration;
24+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
25+
import org.springframework.boot.test.context.SpringBootTest;
26+
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
27+
import org.springframework.cloud.gateway.server.mvc.test.TestAutoConfiguration;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Import;
30+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
31+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
32+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
33+
import org.springframework.security.test.context.support.WithMockUser;
34+
import org.springframework.test.context.ActiveProfiles;
35+
import org.springframework.test.context.ContextConfiguration;
36+
import org.springframework.test.web.servlet.MockMvc;
37+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
38+
import org.springframework.web.context.WebApplicationContext;
39+
40+
import static org.mockito.ArgumentMatchers.argThat;
41+
import static org.mockito.Mockito.mock;
42+
import static org.mockito.Mockito.when;
43+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
44+
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
45+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
46+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
47+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
48+
49+
/**
50+
* @author Ryan Baxter
51+
*/
52+
@SpringBootTest(webEnvironment = RANDOM_PORT)
53+
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
54+
@ActiveProfiles("tokenrelay")
55+
public class TokenRelayConfigTests {
56+
57+
@Autowired
58+
private WebApplicationContext context;
59+
60+
private MockMvc mvc;
61+
62+
@BeforeEach
63+
public void setup() {
64+
mvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
65+
}
66+
67+
@Test
68+
@WithMockUser
69+
public void testTokenRelay() throws Exception {
70+
mvc.perform(get("/bearer"))
71+
.andExpect(status().isOk())
72+
.andExpect(content().json("{\"authenticated\": true, \"token\": \"test\"}"));
73+
}
74+
75+
@EnableAutoConfiguration
76+
@SpringBootConfiguration
77+
@Import(TestAutoConfiguration.class)
78+
public static class TestConfig {
79+
80+
@Bean
81+
public OAuth2AuthorizedClientManager authorizedClientManager() {
82+
OAuth2AuthorizedClientManager manager = mock(OAuth2AuthorizedClientManager.class);
83+
OAuth2AuthorizedClient client = mock(OAuth2AuthorizedClient.class);
84+
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
85+
when(accessToken.getTokenValue()).thenReturn("test");
86+
when(client.getAccessToken()).thenReturn(accessToken);
87+
// The client registration id is set in the token relay filter and must match
88+
when(manager.authorize(argThat(
89+
oAuth2AuthorizeRequest -> "token".equals(oAuth2AuthorizeRequest.getClientRegistrationId()))))
90+
.thenReturn(client);
91+
return manager;
92+
}
93+
94+
}
95+
96+
}

spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/AfterFilterFunctionsTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
import org.springframework.boot.test.context.SpringBootTest;
2828
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
2929
import org.springframework.cloud.gateway.server.mvc.test.HttpbinUriResolver;
30+
import org.springframework.cloud.gateway.server.mvc.test.PermitAllSecurityConfiguration;
3031
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
3132
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
3233
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
3334
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Import;
3436
import org.springframework.http.HttpStatus;
3537
import org.springframework.http.MediaType;
3638
import org.springframework.http.ResponseEntity;
@@ -117,6 +119,7 @@ void raisedErrorWhenRemoveJsonAttributes() {
117119
@SpringBootConfiguration
118120
@EnableAutoConfiguration
119121
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
122+
@Import(PermitAllSecurityConfiguration.class)
120123
protected static class TestConfiguration {
121124

122125
@Bean

0 commit comments

Comments
 (0)