Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OAuth2 feature for Config Clients #2363

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions spring-cloud-config-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
]]>
</description>

<properties>
<jasypt-spring-boot.version>3.0.5</jasypt-spring-boot.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -80,6 +84,11 @@
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot</artifactId>
<version>${jasypt-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -95,6 +104,16 @@
<artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2013-2019 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.config.client;

import com.fasterxml.jackson.annotation.JsonProperty;

public class AccessTokenResponse {

@JsonProperty("access_token")
private String accessToken;

@JsonProperty("token_type")
private String tokenType;

public String getAccessToken() {
return accessToken;
}

public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}

public String getTokenType() {
return tokenType;
}

public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}

public String getBearerHeader() {
return getTokenType() + " " + getAccessToken();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,47 @@ public class ConfigClientProperties {
*/
private String password;

/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be cleaner to place these properties in a subclass called oauth2 so we would use spring.cloud.config.oauth2.*

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. See ConfigClientOauth2Properties

* The OAuth2 token URI of the IDP issuing JWT tokens. When present enables OAuth2
* client calls.
*/
private String tokenUri;

/**
* The OAuth2 grant type (client_credentials, password).
*/
private String grantType;

/**
* The OAuth2 client id should it be needed in JWT token request.
*/
private String clientId;

/**
* The OAuth2 client secret should it be needed in JWT token request.
*/
private String clientSecret;

/**
* The OAuth2 username to use when contacting the IDP.
*/
private String oauthUsername;

/**
* The OAuth2 user password to use when contacting the IDP.
*/
private String oauthPassword;

/**
* The Jasypt encryption algorithm.
*/
private String encryptorAlgorithm;

/**
* Encryption iterations.
*/
private Integer encryptorIterations = 1000;

/**
* The URI of the remote server (default http://localhost:8888).
*/
Expand Down Expand Up @@ -248,6 +289,70 @@ public void setPassword(String password) {
this.password = password;
}

public String getTokenUri() {
return tokenUri;
}

public void setTokenUri(String tokenUri) {
this.tokenUri = tokenUri;
}

public String getGrantType() {
return grantType;
}

public void setGrantType(String grantType) {
this.grantType = grantType;
}

public String getClientId() {
return clientId;
}

public void setClientId(String clientId) {
this.clientId = clientId;
}

public String getClientSecret() {
return clientSecret;
}

public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}

public String getOauthUsername() {
return oauthUsername;
}

public void setOauthUsername(String oauthUsername) {
this.oauthUsername = oauthUsername;
}

public String getOauthPassword() {
return oauthPassword;
}

public void setOauthPassword(String oauthPassword) {
this.oauthPassword = oauthPassword;
}

public String getEncryptorAlgorithm() {
return encryptorAlgorithm;
}

public void setEncryptorAlgorithm(String encryptorAlgorithm) {
this.encryptorAlgorithm = encryptorAlgorithm;
}

public Integer getEncryptorIterations() {
return encryptorIterations;
}

public void setEncryptorIterations(Integer encryptorIterations) {
this.encryptorIterations = encryptorIterations;
}

public Credentials getCredentials(int index) {
return extractCredentials(index);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ulisesbocchio.jasyptspringboot.encryptor.SimplePBEByteEncryptor;
import com.ulisesbocchio.jasyptspringboot.encryptor.SimplePBEStringEncryptor;
import org.apache.commons.logging.Log;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
Expand All @@ -33,30 +40,38 @@
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
import org.jasypt.salt.RandomSaltGenerator;

import org.springframework.cloud.configuration.SSLContextFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION;

public class ConfigClientRequestTemplateFactory {

private final SimplePBEStringEncryptor encryptor;

private final Log log;

private final ConfigClientProperties properties;

public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties) {
this.log = log;
this.properties = properties;
this.encryptor = new SimplePBEStringEncryptor(buildEncryptor());
}

public Log getLog() {
Expand All @@ -67,6 +82,17 @@ public ConfigClientProperties getProperties() {
return this.properties;
}

public SimplePBEByteEncryptor buildEncryptor() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be some kind of bean that the user can override

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I embedded it into the ConfigClientProperties. It got real messy trying to pass it into the pass additional configurations into the ConfigClientRequestTemplateFactory constructor.

SimplePBEByteEncryptor byteEncryptor = new SimplePBEByteEncryptor();
byteEncryptor.setPassword(System.getProperty("encryptor-password"));
byteEncryptor.setSaltGenerator(new RandomSaltGenerator());
byteEncryptor.setIterations(properties.getEncryptorIterations());
if (StringUtils.hasText(properties.getEncryptorAlgorithm())) {
byteEncryptor.setAlgorithm(properties.getEncryptorAlgorithm());
}
return byteEncryptor;
}

public RestTemplate create() {
if (properties.getRequestReadTimeout() < 0) {
throw new IllegalStateException("Invalid Value for Read Timeout set.");
Expand All @@ -79,13 +105,61 @@ public RestTemplate create() {
RestTemplate template = new RestTemplate(requestFactory);
Map<String, String> headers = new HashMap<>(properties.getHeaders());
headers.remove(AUTHORIZATION); // To avoid redundant addition of header
if (!headers.isEmpty()) {
template.setInterceptors(Arrays.asList(new GenericRequestHeaderInterceptor(headers)));
if (StringUtils.hasText(properties.getTokenUri())) {
Optional<AccessTokenResponse> responseOpt = getOAuthToken(template, properties.getTokenUri());
if (responseOpt.isPresent()) {
AccessTokenResponse accessTokenResponse = responseOpt.get();
headers.put(AUTHORIZATION, accessTokenResponse.getBearerHeader());
properties.setHeaders(headers);
}
}

return template;
}

private Optional<AccessTokenResponse> getOAuthToken(RestTemplate template, String tokenUri) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
if (StringUtils.hasText(properties.getGrantType())) {
map.put("grant_type", List.of(properties.getGrantType()));
}
else {
throw new IllegalStateException("Grant type is required for OAuth2 requests.");
}
if (StringUtils.hasText(properties.getClientId())) {
map.put("client_id", List.of(properties.getClientId()));
map.put("client_secret", List.of(decryptProperty(properties.getClientSecret())));
}
if (StringUtils.hasText(properties.getOauthUsername())) {
map.put("username", List.of(properties.getOauthUsername()));
map.put("password", List.of(decryptProperty(properties.getOauthPassword())));
}
HttpEntity<MultiValueMap<String, String>> requestBodyFormUrlEncoded = new HttpEntity<>(map, httpHeaders);

String tokenJson = template.postForObject(tokenUri, requestBodyFormUrlEncoded, String.class);
return parseTokenResponse(tokenJson);
}

private String decryptProperty(String prop) {
if (prop.startsWith("ENC(")) {
prop = prop.substring(4, prop.lastIndexOf(")"));
return encryptor.decrypt(prop);
}
return prop;
}

private Optional<AccessTokenResponse> parseTokenResponse(String tokenJson) {
try {
ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
return Optional.of(objectMapper.readValue(tokenJson, AccessTokenResponse.class));
}
catch (JsonProcessingException e) {
return Optional.empty();
}
}

protected ClientHttpRequestFactory createHttpRequestFactory(ConfigClientProperties client) {
if (client.getTls().isEnabled()) {
try {
Expand Down Expand Up @@ -137,7 +211,7 @@ public void addAuthorizationToken(HttpHeaders httpHeaders, String username, Stri
}

if (password != null) {
byte[] token = Base64Utils.encode((username + ":" + password).getBytes());
byte[] token = Base64.getEncoder().encode((username + ":" + password).getBytes());
httpHeaders.add("Authorization", "Basic " + new String(token));
}
else if (authorization != null) {
Expand Down
Loading
Loading