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:
1
pom.xml
1
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
81
spring-cloud-gateway-server-security/pom.xml
Normal file
81
spring-cloud-gateway-server-security/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Auto Configure
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.gateway.security.TokenRelayAutoConfiguration
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user