-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Changes from 4 commits
4064b79
840d70b
d7a0f24
24e1e27
eb89313
7b498b2
2bbf465
a9f768e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -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() { | ||
|
@@ -67,6 +82,17 @@ public ConfigClientProperties getProperties() { | |
return this.properties; | ||
} | ||
|
||
public SimplePBEByteEncryptor buildEncryptor() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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."); | ||
|
@@ -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 { | ||
|
@@ -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) { | ||
|
There was a problem hiding this comment.
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.*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. See ConfigClientOauth2Properties