Migrates token relay from spring-cloud-security. (#1976)

* Migrates token relay from spring-cloud-security.

Adds refresh token support.

Fixes https://github.com/spring-cloud/spring-cloud-security/issues/175

Fixes gh-1975

See https://github.com/spring-cloud/spring-cloud-security/issues/231
This commit is contained in:
Spencer Gibb
2020-10-08 16:17:25 -04:00
committed by GitHub
parent 8fce2008b5
commit 837db44dfa
8 changed files with 416 additions and 0 deletions

View File

@@ -123,6 +123,7 @@
<module>spring-cloud-gateway-mvc</module>
<module>spring-cloud-gateway-webflux</module>
<module>spring-cloud-gateway-server</module>
<module>spring-cloud-gateway-server-security</module>
<module>spring-cloud-starter-gateway</module>
<module>spring-cloud-gateway-sample</module>
<module>docs</module>

View File

@@ -37,6 +37,11 @@
<artifactId>spring-cloud-gateway-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server-security</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>3.0.0-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-server-security</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Gateway Server Security</name>
<description>Spring Cloud Gateway Server Security</description>
<properties>
<main.basedir>${basedir}/..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit-pioneer</groupId>
<artifactId>junit-pioneer</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2013-2014 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.security;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* @author Dave Syer
*
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
@ConditionalOnClass({ GatewayFilter.class, OAuth2AuthorizedClient.class, SecurityWebFilterChain.class,
SecurityProperties.class })
@ConditionalOnWebApplication(type = Type.REACTIVE)
public class TokenRelayAutoConfiguration {
@Bean
public TokenRelayGatewayFilterFactory tokenRelayGatewayFilterFactory(
ReactiveOAuth2AuthorizedClientManager clientManager) {
return new TokenRelayGatewayFilterFactory(clientManager);
}
@Bean
public ReactiveOAuth2AuthorizedClientManager gatewayReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().authorizationCode().refreshToken().build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2002-2018 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.security;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* @author Joe Grandja
*/
@Component
public class TokenRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
private final ReactiveOAuth2AuthorizedClientManager clientManager;
public TokenRelayGatewayFilterFactory(ReactiveOAuth2AuthorizedClientManager clientManager) {
super(Object.class);
this.clientManager = clientManager;
}
public GatewayFilter apply() {
return apply((Object) null);
}
@Override
public GatewayFilter apply(Object config) {
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))
// 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();
// TODO: use Mono.defer() for request above?
return clientManager.authorize(request);
}
private ServerWebExchange withBearerAuth(ServerWebExchange exchange, OAuth2AccessToken accessToken) {
return exchange.mutate().request(r -> r.headers(headers -> headers.setBearerAuth(accessToken.getTokenValue())))
.build();
}
}

View File

@@ -0,0 +1,3 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.gateway.security.TokenRelayAutoConfiguration

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2014-2018 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.security;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Spencer Gibb
*
*/
public class TokenRelayAutoConfigurationTests {
@Test
public void beansAreCreated() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class,
ReactiveOAuth2ClientAutoConfiguration.class, TokenRelayAutoConfiguration.class))
.withPropertyValues(
"spring.security.oauth2.client.provider[testprovider].authorization-uri=http://localhost",
"spring.security.oauth2.client.provider[testprovider].token-uri=http://localhost/token",
"spring.security.oauth2.client.registration[test].provider=testprovider",
"spring.security.oauth2.client.registration[test].authorization-grant-type=authorization_code",
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.withUserConfiguration(TestConfig.class).withPropertyValues("debug=true").run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasSingleBean(TokenRelayGatewayFilterFactory.class);
});
}
@Configuration
protected static class TestConfig {
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2014-2018 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.security;
import java.time.Duration;
import java.util.Collections;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
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.context.SecurityContextImpl;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.server.context.SecurityContextServerWebExchange;
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.when;
/**
* @author Spencer Gibb
*
*/
public class TokenRelayGatewayFilterFactoryTests {
private static final Duration TIMEOUT = Duration.ofSeconds(30);
private ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
private MockServerHttpRequest request;
private MockServerWebExchange mockExchange;
private GatewayFilterChain filterChain;
private GatewayFilter filter;
public TokenRelayGatewayFilterFactoryTests() {
}
@Before
public void init() {
request = MockServerHttpRequest.get("/hello").build();
mockExchange = MockServerWebExchange.from(request);
filterChain = mock(GatewayFilterChain.class);
when(filterChain.filter(any(ServerWebExchange.class))).thenReturn(Mono.empty());
authorizedClientManager = mock(ReactiveOAuth2AuthorizedClientManager.class);
filter = new TokenRelayGatewayFilterFactory(authorizedClientManager).apply();
}
@After
public void after() {
}
@Test
public void emptyPrincipal() {
filter.filter(mockExchange, filterChain).block(TIMEOUT);
assertThat(request.getHeaders()).doesNotContainKeys(HttpHeaders.AUTHORIZATION);
}
@Test
public void whenPrincipalExistsAuthorizationHeaderAdded() {
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, "joe", 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));
filter.filter(exchange, filterChain).block(TIMEOUT);
assertThat(request.getHeaders()).containsEntry(HttpHeaders.AUTHORIZATION,
Collections.singletonList("Bearer mytoken"));
}
@Test
public void principalIsNotOAuth2AuthenticationToken() {
SecurityContextImpl securityContext = new SecurityContextImpl(new TestingAuthenticationToken("my", null));
SecurityContextServerWebExchange exchange = new SecurityContextServerWebExchange(mockExchange,
Mono.just(securityContext));
filter.filter(exchange, filterChain).block(TIMEOUT);
assertThat(request.getHeaders()).doesNotContainKeys(HttpHeaders.AUTHORIZATION);
}
}