Specify clientRegistrationId in TokenRelay filter (#2922)

This commit is contained in:
Steve Riesenberg
2023-09-06 16:19:20 -05:00
committed by GitHub
parent 6f95267317
commit 984882f75a
4 changed files with 187 additions and 24 deletions

View File

@@ -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].

View File

@@ -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(

View File

@@ -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

View File

@@ -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());
}
}