Specify clientRegistrationId in TokenRelay filter (#2922)
This commit is contained in:
@@ -2032,7 +2032,46 @@ consumer can be a pure Client (like an SSO application) or a Resource
|
||||
Server.
|
||||
|
||||
Spring Cloud Gateway can forward OAuth2 access tokens downstream to the services
|
||||
it is proxying. To add this functionality to the gateway, you need to add the `TokenRelayGatewayFilterFactory` like this:
|
||||
it is proxying using the `TokenRelay` `GatewayFilter`.
|
||||
|
||||
The `TokenRelay` `GatewayFilter` takes one optional parameter, `clientRegistrationId`.
|
||||
The following example configures a `TokenRelay` `GatewayFilter`:
|
||||
|
||||
.App.java
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@Bean
|
||||
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("resource", r -> r.path("/resource")
|
||||
.filters(f -> f.tokenRelay("myregistrationid"))
|
||||
.uri("http://localhost:9000"))
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
or this
|
||||
|
||||
.application.yaml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: resource
|
||||
uri: http://localhost:9000
|
||||
predicates:
|
||||
- Path=/resource
|
||||
filters:
|
||||
- TokenRelay=myregistrationid
|
||||
----
|
||||
|
||||
The example above specifies a `clientRegistrationId`, which can be used to obtain and forward an OAuth2 access token for any available `ClientRegistration`.
|
||||
|
||||
Spring Cloud Gateway can also forward the OAuth2 access token of the currently authenticated user `oauth2Login()` is used to authenticate the user.
|
||||
To add this functionality to the gateway, you can omit the `clientRegistrationId` parameter like this:
|
||||
|
||||
.App.java
|
||||
[source,java]
|
||||
@@ -2073,10 +2112,10 @@ To enable this for Spring Cloud Gateway add the following dependencies
|
||||
|
||||
- `org.springframework.boot:spring-boot-starter-oauth2-client`
|
||||
|
||||
How does it work? The
|
||||
{githubmaster}/src/main/java/org/springframework/cloud/gateway/security/TokenRelayGatewayFilterFactory.java[filter]
|
||||
extracts an access token from the currently authenticated user,
|
||||
and puts it in a request header for the downstream requests.
|
||||
How does it work? The {github-code}/src/main/java/org/springframework/cloud/gateway/security/TokenRelayGatewayFilterFactory.java[filter]
|
||||
extracts an OAuth2 access token from the currently authenticated user for the provided `clientRegistrationId`.
|
||||
If no `clientRegistrationId` is provided, the currently authenticated user's own access token (obtained during login) is used.
|
||||
In either case, the extracted access token is placed in a request header for the downstream requests.
|
||||
|
||||
For a full working sample see https://github.com/spring-cloud-samples/sample-gateway-oauth2login[this project].
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,10 +16,14 @@
|
||||
|
||||
package org.springframework.cloud.gateway.filter.factory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
@@ -29,37 +33,51 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class TokenRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
|
||||
public class TokenRelayGatewayFilterFactory
|
||||
extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
|
||||
|
||||
private final ObjectProvider<ReactiveOAuth2AuthorizedClientManager> clientManagerProvider;
|
||||
|
||||
public TokenRelayGatewayFilterFactory(ObjectProvider<ReactiveOAuth2AuthorizedClientManager> clientManagerProvider) {
|
||||
super(Object.class);
|
||||
super(NameConfig.class);
|
||||
this.clientManagerProvider = clientManagerProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Collections.singletonList(NAME_KEY);
|
||||
}
|
||||
|
||||
public GatewayFilter apply() {
|
||||
return apply((Object) null);
|
||||
return apply((NameConfig) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Object config) {
|
||||
public GatewayFilter apply(NameConfig config) {
|
||||
String defaultClientRegistrationId = (config == null) ? null : config.getName();
|
||||
return (exchange, chain) -> exchange.getPrincipal()
|
||||
// .log("token-relay-filter")
|
||||
.filter(principal -> principal instanceof OAuth2AuthenticationToken)
|
||||
.cast(OAuth2AuthenticationToken.class)
|
||||
.flatMap(authentication -> authorizedClient(exchange, authentication))
|
||||
.map(OAuth2AuthorizedClient::getAccessToken).map(token -> withBearerAuth(exchange, token))
|
||||
.filter(principal -> principal instanceof Authentication).cast(Authentication.class)
|
||||
.flatMap(principal -> authorizationRequest(defaultClientRegistrationId, principal))
|
||||
.flatMap(this::authorizedClient).map(OAuth2AuthorizedClient::getAccessToken)
|
||||
.map(token -> withBearerAuth(exchange, token))
|
||||
// TODO: adjustable behavior if empty
|
||||
.defaultIfEmpty(exchange).flatMap(chain::filter);
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizedClient> authorizedClient(ServerWebExchange exchange,
|
||||
OAuth2AuthenticationToken oauth2Authentication) {
|
||||
String clientRegistrationId = oauth2Authentication.getAuthorizedClientRegistrationId();
|
||||
OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId)
|
||||
.principal(oauth2Authentication).build();
|
||||
private Mono<OAuth2AuthorizeRequest> authorizationRequest(String defaultClientRegistrationId,
|
||||
Authentication principal) {
|
||||
String clientRegistrationId = defaultClientRegistrationId;
|
||||
if (clientRegistrationId == null && principal instanceof OAuth2AuthenticationToken) {
|
||||
clientRegistrationId = ((OAuth2AuthenticationToken) principal).getAuthorizedClientRegistrationId();
|
||||
}
|
||||
return Mono.justOrEmpty(clientRegistrationId).map(OAuth2AuthorizeRequest::withClientRegistrationId)
|
||||
.map(builder -> builder.principal(principal).build());
|
||||
}
|
||||
|
||||
private Mono<OAuth2AuthorizedClient> authorizedClient(OAuth2AuthorizeRequest request) {
|
||||
ReactiveOAuth2AuthorizedClientManager clientManager = clientManagerProvider.getIfAvailable();
|
||||
if (clientManager == null) {
|
||||
return Mono.error(new IllegalStateException(
|
||||
|
||||
@@ -796,12 +796,13 @@ public class GatewayFilterSpec extends UriSpec {
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter that enables token relay.
|
||||
* A filter that enables token relay by extracting the access token from the currently
|
||||
* authenticated user and puts it in a request header for downstream requests.
|
||||
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
|
||||
*/
|
||||
public GatewayFilterSpec tokenRelay() {
|
||||
try {
|
||||
return filter(getBean(TokenRelayGatewayFilterFactory.class).apply(o -> {
|
||||
return filter(getBean(TokenRelayGatewayFilterFactory.class).apply(c -> {
|
||||
}));
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException e) {
|
||||
@@ -810,6 +811,23 @@ public class GatewayFilterSpec extends UriSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A filter that enables token relay by extracting the access token of a specified
|
||||
* {@code ClientRegistration} and puts it in a request header for downstream requests.
|
||||
* @param clientRegistrationId the client registration id to use for building the
|
||||
* authorization request
|
||||
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
|
||||
*/
|
||||
public GatewayFilterSpec tokenRelay(String clientRegistrationId) {
|
||||
try {
|
||||
return filter(getBean(TokenRelayGatewayFilterFactory.class).apply(c -> c.setName(clientRegistrationId)));
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException e) {
|
||||
throw new IllegalStateException("No TokenRelayGatewayFilterFactory bean was found. Did you include the "
|
||||
+ "org.springframework.boot:spring-boot-starter-oauth2-client dependency?");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hystrix execution exception headers to fallback request. Depends on @{code
|
||||
* org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the
|
||||
|
||||
@@ -22,15 +22,18 @@ import java.util.Collections;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory.NameConfig;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextImpl;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
@@ -46,6 +49,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -64,7 +68,7 @@ public class TokenRelayGatewayFilterFactoryTests {
|
||||
|
||||
private GatewayFilterChain filterChain;
|
||||
|
||||
private GatewayFilter filter;
|
||||
private ObjectProvider<ReactiveOAuth2AuthorizedClientManager> objectProvider;
|
||||
|
||||
public TokenRelayGatewayFilterFactoryTests() {
|
||||
}
|
||||
@@ -78,9 +82,8 @@ public class TokenRelayGatewayFilterFactoryTests {
|
||||
when(filterChain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
|
||||
|
||||
authorizedClientManager = mock(ReactiveOAuth2AuthorizedClientManager.class);
|
||||
ObjectProvider<ReactiveOAuth2AuthorizedClientManager> objectProvider = mock(ObjectProvider.class);
|
||||
objectProvider = mock(ObjectProvider.class);
|
||||
when(objectProvider.getIfAvailable()).thenReturn(authorizedClientManager);
|
||||
filter = new TokenRelayGatewayFilterFactory(objectProvider).apply();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -89,6 +92,7 @@ public class TokenRelayGatewayFilterFactoryTests {
|
||||
|
||||
@Test
|
||||
public void emptyPrincipal() {
|
||||
GatewayFilter filter = new TokenRelayGatewayFilterFactory(objectProvider).apply();
|
||||
filter.filter(mockExchange, filterChain).block(TIMEOUT);
|
||||
assertThat(request.getHeaders()).doesNotContainKeys(HttpHeaders.AUTHORIZATION);
|
||||
}
|
||||
@@ -112,10 +116,58 @@ public class TokenRelayGatewayFilterFactoryTests {
|
||||
SecurityContextServerWebExchange exchange = new SecurityContextServerWebExchange(mockExchange,
|
||||
Mono.just(securityContext));
|
||||
|
||||
GatewayFilter filter = new TokenRelayGatewayFilterFactory(objectProvider).apply();
|
||||
filter.filter(exchange, filterChain).block(TIMEOUT);
|
||||
|
||||
assertThat(request.getHeaders()).containsEntry(HttpHeaders.AUTHORIZATION,
|
||||
Collections.singletonList("Bearer mytoken"));
|
||||
|
||||
ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2AuthorizeRequest.class);
|
||||
verify(authorizedClientManager).authorize(authorizeRequestCaptor.capture());
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = authorizeRequestCaptor.getValue();
|
||||
assertThat(authorizeRequest.getClientRegistrationId())
|
||||
.isEqualTo(authenticationToken.getAuthorizedClientRegistrationId());
|
||||
assertThat(authorizeRequest.getClientRegistrationId()).isNotEqualTo(clientRegistration.getRegistrationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenClientRegistrationIdConfiguredAuthorizationHeaderAdded() {
|
||||
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
|
||||
when(accessToken.getTokenValue()).thenReturn("mytoken");
|
||||
|
||||
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("myregistrationid")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).clientId("myclientid")
|
||||
.tokenUri("mytokenuri").build();
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, "steve", accessToken);
|
||||
|
||||
when(authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
|
||||
.thenReturn(Mono.just(authorizedClient));
|
||||
|
||||
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(mock(OAuth2User.class),
|
||||
Collections.emptyList(), "myId");
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(authenticationToken);
|
||||
SecurityContextServerWebExchange exchange = new SecurityContextServerWebExchange(mockExchange,
|
||||
Mono.just(securityContext));
|
||||
|
||||
NameConfig config = new NameConfig();
|
||||
config.setName(clientRegistration.getRegistrationId());
|
||||
|
||||
GatewayFilter filter = new TokenRelayGatewayFilterFactory(objectProvider).apply(config);
|
||||
filter.filter(exchange, filterChain).block(TIMEOUT);
|
||||
|
||||
assertThat(request.getHeaders()).containsEntry(HttpHeaders.AUTHORIZATION,
|
||||
Collections.singletonList("Bearer mytoken"));
|
||||
|
||||
ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2AuthorizeRequest.class);
|
||||
verify(authorizedClientManager).authorize(authorizeRequestCaptor.capture());
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = authorizeRequestCaptor.getValue();
|
||||
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(clientRegistration.getRegistrationId());
|
||||
assertThat(authorizeRequest.getClientRegistrationId())
|
||||
.isNotEqualTo(authenticationToken.getAuthorizedClientRegistrationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -124,9 +176,45 @@ public class TokenRelayGatewayFilterFactoryTests {
|
||||
SecurityContextServerWebExchange exchange = new SecurityContextServerWebExchange(mockExchange,
|
||||
Mono.just(securityContext));
|
||||
|
||||
GatewayFilter filter = new TokenRelayGatewayFilterFactory(objectProvider).apply();
|
||||
filter.filter(exchange, filterChain).block(TIMEOUT);
|
||||
|
||||
assertThat(request.getHeaders()).doesNotContainKeys(HttpHeaders.AUTHORIZATION);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenPrincipalIsNotOAuth2AuthenticationTokenAndClientRegistrationIdConfiguredAuthorizationHeaderAdded() {
|
||||
OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
|
||||
when(accessToken.getTokenValue()).thenReturn("mytoken");
|
||||
|
||||
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId("myregistrationid")
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).clientId("myclientid")
|
||||
.tokenUri("mytokenuri").build();
|
||||
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, "steve", accessToken);
|
||||
|
||||
when(authorizedClientManager.authorize(any(OAuth2AuthorizeRequest.class)))
|
||||
.thenReturn(Mono.just(authorizedClient));
|
||||
|
||||
Authentication authenticationToken = new TestingAuthenticationToken("my", null);
|
||||
SecurityContextImpl securityContext = new SecurityContextImpl(authenticationToken);
|
||||
SecurityContextServerWebExchange exchange = new SecurityContextServerWebExchange(mockExchange,
|
||||
Mono.just(securityContext));
|
||||
|
||||
NameConfig config = new NameConfig();
|
||||
config.setName(clientRegistration.getRegistrationId());
|
||||
|
||||
GatewayFilter filter = new TokenRelayGatewayFilterFactory(objectProvider).apply(config);
|
||||
filter.filter(exchange, filterChain).block(TIMEOUT);
|
||||
|
||||
assertThat(request.getHeaders()).containsEntry(HttpHeaders.AUTHORIZATION,
|
||||
Collections.singletonList("Bearer mytoken"));
|
||||
|
||||
ArgumentCaptor<OAuth2AuthorizeRequest> authorizeRequestCaptor = ArgumentCaptor
|
||||
.forClass(OAuth2AuthorizeRequest.class);
|
||||
verify(authorizedClientManager).authorize(authorizeRequestCaptor.capture());
|
||||
|
||||
OAuth2AuthorizeRequest authorizeRequest = authorizeRequestCaptor.getValue();
|
||||
assertThat(authorizeRequest.getClientRegistrationId()).isEqualTo(clientRegistration.getRegistrationId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user