Add security for Cloud Foundry actuators

Add security to Cloud Foundry actuator endpoints. Security is enforced
by a `HanderInterceptor` on `CloudFoundryEndpointHandlerMapping`. Each
endpoint call expects an 'Authorization' header containing a bearer
token. The token signature is checked against the UAA public keys then
passed to the Cloud Controller to obtain an ultimate access level.

The client may either have 'RESTRICTED' or FULL' access, with the latter
only providing access to a limited set of endpoints.

See gh-7108
pull/7387/head
Madhura Bhave 8 years ago committed by Phillip Webb
parent f15e0482c5
commit 340f1d5574

@ -0,0 +1,61 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
* The specific access level granted to the cloud foundry user that's calling the
* endpoints.
*
* @author Madhura Bhave
*/
enum AccessLevel {
/**
* Restricted access to a limited set of endpoints.
*/
RESTRICTED("", "/health", "/info"),
/**
* Full access to all endpoints.
*/
FULL;
private final List<String> endpointPaths;
AccessLevel(String... endpointPaths) {
this.endpointPaths = Arrays.asList(endpointPaths);
}
/**
* Returns if the access level should allow access to the specified endpoint path.
* @param endpointPath the endpoitn path
* @return {@code true} if access is allowed
*/
public boolean isAccessAllowed(String endpointPath) {
return this.endpointPaths.isEmpty() || this.endpointPaths.contains(endpointPath);
}
public void put(HttpServletRequest request) {
request.setAttribute("cloudFoundryAccessLevel", this);
}
}

@ -28,9 +28,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
@ -48,15 +51,38 @@ public class CloudFoundryActuatorAutoConfiguration {
@Bean
public CloudFoundryEndpointHandlerMapping cloudFoundryEndpointHandlerMapping(
MvcEndpoints mvcEndpoints) {
MvcEndpoints mvcEndpoints, RestTemplateBuilder restTemplateBuilder,
Environment environment) {
Set<NamedMvcEndpoint> endpoints = new LinkedHashSet<NamedMvcEndpoint>(
mvcEndpoints.getEndpoints(NamedMvcEndpoint.class));
HandlerInterceptor securityInterceptor = getSecurityInterceptor(
restTemplateBuilder, environment);
CorsConfiguration corsConfiguration = getCorsConfiguration();
CloudFoundryEndpointHandlerMapping mapping = new CloudFoundryEndpointHandlerMapping(
endpoints, getCorsConfiguration());
endpoints, corsConfiguration, securityInterceptor);
mapping.setPrefix("/cloudfoundryapplication");
return mapping;
}
private HandlerInterceptor getSecurityInterceptor(
RestTemplateBuilder restTemplateBuilder, Environment environment) {
CloudFoundrySecurityService cloudfoundrySecurityService = getCloudFoundrySecurityService(
restTemplateBuilder, environment);
TokenValidator tokenValidator = new TokenValidator(cloudfoundrySecurityService);
HandlerInterceptor securityInterceptor = new CloudFoundrySecurityInterceptor(
tokenValidator, cloudfoundrySecurityService,
environment.getProperty("vcap.application.application_id"));
return securityInterceptor;
}
private CloudFoundrySecurityService getCloudFoundrySecurityService(
RestTemplateBuilder restTemplateBuilder, Environment environment) {
String cloudControllerUrl = environment.getProperty("vcap.application.cf_api");
return cloudControllerUrl == null ? null
: new CloudFoundrySecurityService(restTemplateBuilder,
cloudControllerUrl);
}
private CorsConfiguration getCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);

@ -0,0 +1,90 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import org.springframework.http.HttpStatus;
/**
* Authorization exceptions thrown to limit access to the endpoints.
*
* @author Madhura Bhave
*/
class CloudFoundryAuthorizationException extends RuntimeException {
private final Reason reason;
CloudFoundryAuthorizationException(Reason reason, String message) {
this(reason, message, null);
}
CloudFoundryAuthorizationException(Reason reason, String message, Throwable cause) {
super(message);
this.reason = reason;
}
/**
* Return the status code that should be returned to the client.
* @return the HTTP status code
*/
public HttpStatus getStatusCode() {
return getReason().getStatus();
}
/**
* Return the reason why the authorization exception was thrown.
* @return the reason
*/
public Reason getReason() {
return this.reason;
}
/**
* Reasons why the exception can be thrown.
*/
enum Reason {
ACCESS_DENIED(HttpStatus.FORBIDDEN),
INVALID_AUDIENCE(HttpStatus.UNAUTHORIZED),
INVALID_ISSUER(HttpStatus.UNAUTHORIZED),
INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED),
MISSING_AUTHORIZATION(HttpStatus.UNAUTHORIZED),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED),
UNSUPPORTED_TOKEN_SIGNING_ALGORITHM(HttpStatus.UNAUTHORIZED),
SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE);
private final HttpStatus status;
Reason(HttpStatus status) {
this.status = status;
}
public HttpStatus getStatus() {
return this.status;
}
}
}

@ -18,13 +18,11 @@ package org.springframework.boot.actuate.cloudfoundry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointHandlerMapping;
@ -35,7 +33,6 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* {@link HandlerMapping} to map {@link Endpoint}s to Cloud Foundry specific URLs.
@ -45,13 +42,12 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
class CloudFoundryEndpointHandlerMapping
extends AbstractEndpointHandlerMapping<NamedMvcEndpoint> {
CloudFoundryEndpointHandlerMapping(Collection<? extends NamedMvcEndpoint> endpoints) {
super(endpoints);
}
private final HandlerInterceptor securityInterceptor;
CloudFoundryEndpointHandlerMapping(Set<NamedMvcEndpoint> endpoints,
CorsConfiguration corsConfiguration) {
CloudFoundryEndpointHandlerMapping(Set<? extends NamedMvcEndpoint> endpoints,
CorsConfiguration corsConfiguration, HandlerInterceptor securityInterceptor) {
super(endpoints, corsConfiguration);
this.securityInterceptor = securityInterceptor;
}
@Override
@ -90,24 +86,11 @@ class CloudFoundryEndpointHandlerMapping
private HandlerInterceptor[] addSecurityInterceptor(HandlerInterceptor[] existing) {
List<HandlerInterceptor> interceptors = new ArrayList<HandlerInterceptor>();
interceptors.add(new SecurityInterceptor());
interceptors.add(this.securityInterceptor);
if (existing != null) {
interceptors.addAll(Arrays.asList(existing));
}
return interceptors.toArray(new HandlerInterceptor[interceptors.size()]);
}
/**
* Security interceptor to check cloud foundry token.
*/
static class SecurityInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// Currently open
return true;
}
}
}

@ -0,0 +1,103 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* {@link HandlerInterceptor} to check the cloud foundry token.
*
* @author Madhura Bhave
*/
class CloudFoundrySecurityInterceptor extends HandlerInterceptorAdapter {
protected final Log logger = LogFactory.getLog(getClass());
private final TokenValidator tokenValidator;
private final CloudFoundrySecurityService cloudFoundrySecurityService;
private final String applicationId;
CloudFoundrySecurityInterceptor(TokenValidator tokenValidator,
CloudFoundrySecurityService cloudFoundrySecurityService,
String applicationId) {
this.tokenValidator = tokenValidator;
this.cloudFoundrySecurityService = cloudFoundrySecurityService;
this.applicationId = applicationId;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object o) throws Exception {
try {
if (!StringUtils.hasText(this.applicationId)) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Application id is not available");
}
if (this.cloudFoundrySecurityService == null) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Cloud controller URL is not available");
}
HandlerMethod handlerMethod = (HandlerMethod) o;
MvcEndpoint mvcEndpoint = (MvcEndpoint) handlerMethod.getBean();
check(request, mvcEndpoint);
}
catch (CloudFoundryAuthorizationException ex) {
this.logger.error(ex);
response.setStatus(ex.getStatusCode().value());
return false;
}
return true;
}
private void check(HttpServletRequest request, MvcEndpoint mvcEndpoint)
throws Exception {
Token token = getToken(request);
this.tokenValidator.validate(token);
AccessLevel accessLevel = this.cloudFoundrySecurityService
.getAccessLevel(token.toString(), this.applicationId);
if (!accessLevel.isAccessAllowed(mvcEndpoint.getPath())) {
throw new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
"Access denied");
}
accessLevel.put(request);
}
private Token getToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
String bearerPrefix = "bearer ";
if (authorization == null
|| !authorization.toLowerCase().startsWith(bearerPrefix)) {
throw new CloudFoundryAuthorizationException(Reason.MISSING_AUTHORIZATION,
"Authorization header is missing or invalid");
}
return new Token(authorization.substring(bearerPrefix.length()));
}
}

@ -0,0 +1,142 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
/**
* Cloud Foundry security service to handle REST calls to the cloud controller and UAA.
*
* @author Madhura Bhave
*/
class CloudFoundrySecurityService {
private final RestTemplate restTemplate;
private final String cloudControllerUrl;
private String uaaUrl;
CloudFoundrySecurityService(RestTemplateBuilder restTemplateBuilder,
String cloudControllerUrl) {
Assert.notNull(restTemplateBuilder, "RestTemplateBuilder must not be null");
Assert.notNull(cloudControllerUrl, "CloudControllerUrl must not be null");
this.restTemplate = restTemplateBuilder.build();
this.cloudControllerUrl = cloudControllerUrl;
}
/**
* Return the access level that should be granted to the given token.
* @param token the token
* @param applicationId the cloud foundry application ID
* @return the access level that should be granted
* @throws CloudFoundryAuthorizationException if the token is not authorized
*/
public AccessLevel getAccessLevel(String token, String applicationId)
throws CloudFoundryAuthorizationException {
try {
URI uri = getPermissionsUri(applicationId);
RequestEntity<?> request = RequestEntity.get(uri)
.header("Authorization", "bearer " + token).build();
Map<?, ?> body = this.restTemplate.exchange(request, Map.class).getBody();
if (Boolean.TRUE.equals(body.get("read_sensitive_data"))) {
return AccessLevel.FULL;
}
return AccessLevel.RESTRICTED;
}
catch (HttpClientErrorException ex) {
if (ex.getStatusCode().equals(HttpStatus.FORBIDDEN)) {
throw new CloudFoundryAuthorizationException(Reason.ACCESS_DENIED,
"Access denied");
}
throw new CloudFoundryAuthorizationException(Reason.INVALID_TOKEN,
"Invalid token", ex);
}
catch (HttpServerErrorException ex) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Cloud controller not reachable");
}
}
private URI getPermissionsUri(String applicationId) {
try {
return new URI(this.cloudControllerUrl + "/v2/apps/" + applicationId
+ "/permissions");
}
catch (URISyntaxException ex) {
throw new IllegalStateException(ex);
}
}
/**
* Return all token keys known by the UAA.
* @return a list of token keys
*/
public List<String> fetchTokenKeys() {
try {
return extractTokenKeys(this.restTemplate
.getForObject(getUaaUrl() + "/token_keys", Map.class));
}
catch (HttpStatusCodeException e) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"UAA not reachable");
}
}
private List<String> extractTokenKeys(Map<?, ?> response) {
List<String> tokenKeys = new ArrayList<String>();
List<?> keys = (List<?>) response.get("keys");
for (Object key : keys) {
tokenKeys.add((String) ((Map<?, ?>) key).get("value"));
}
return tokenKeys;
}
/**
* Return the URL of the UAA.
* @return the UAA url
*/
public String getUaaUrl() {
if (this.uaaUrl == null) {
try {
Map<?, ?> response = this.restTemplate
.getForObject(this.cloudControllerUrl + "/info", Map.class);
this.uaaUrl = (String) response.get("token_endpoint");
}
catch (HttpStatusCodeException ex) {
throw new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE,
"Unable to fetch token keys from UAA");
}
}
return this.uaaUrl;
}
}

@ -0,0 +1,121 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import org.springframework.boot.json.JsonParserFactory;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
/**
* The JSON web token provided with each request that originates from Cloud Foundry.
*
* @author Madhura Bhave
*/
class Token {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final String encoded;
private final String signature;
private final Map<String, Object> header;
private final Map<String, Object> claims;
Token(String encoded) {
this.encoded = encoded;
int firstPeriod = encoded.indexOf('.');
int lastPeriod = encoded.lastIndexOf('.');
if (firstPeriod <= 0 || lastPeriod <= firstPeriod) {
throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN,
"JWT must have header, body and signature");
}
this.header = parseJson(encoded.substring(0, firstPeriod));
this.claims = parseJson(encoded.substring(firstPeriod + 1, lastPeriod));
this.signature = encoded.substring(lastPeriod + 1);
if (!StringUtils.hasLength(this.signature)) {
throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN,
"Token must have non-empty crypto segment");
}
}
private Map<String, Object> parseJson(String base64) {
try {
byte[] bytes = Base64Utils.decodeFromUrlSafeString(base64);
return JsonParserFactory.getJsonParser().parseMap(new String(bytes, UTF_8));
}
catch (RuntimeException ex) {
throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN,
"Token could not be parsed", ex);
}
}
public byte[] getContent() {
return this.encoded.substring(0, this.encoded.lastIndexOf(".")).getBytes();
}
public byte[] getSignature() {
return Base64Utils.decodeFromUrlSafeString(this.signature);
}
public String getSignatureAlgorithm() {
return getRequired(this.header, "alg", String.class);
}
public String getIssuer() {
return getRequired(this.claims, "iss", String.class);
}
public long getExpiry() {
return getRequired(this.claims, "exp", Integer.class).longValue();
}
@SuppressWarnings("unchecked")
public List<String> getScope() {
return getRequired(this.claims, "scope", List.class);
}
@SuppressWarnings("unchecked")
private <T> T getRequired(Map<String, Object> map, String key, Class<T> type) {
Object value = map.get(key);
if (value == null) {
throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN,
"Unable to get value from key " + key);
}
if (!type.isInstance(value)) {
throw new CloudFoundryAuthorizationException(
CloudFoundryAuthorizationException.Reason.INVALID_TOKEN,
"Unexpected value type from key " + key + " value " + value);
}
return (T) value;
}
@Override
public String toString() {
return this.encoded;
};
}

@ -0,0 +1,135 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.util.Base64Utils;
/**
* Validator used to ensure that a signed {@link Token} has not been tampered with.
*
* @author Madhura Bhave
*/
class TokenValidator {
private final CloudFoundrySecurityService securityService;
private List<String> tokenKeys;
TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) {
this.securityService = cloudFoundrySecurityService;
}
public void validate(Token token) {
validateAlgorithm(token);
validateSignature(token);
validateExpiry(token);
validateIssuer(token);
validateAudience(token);
}
private void validateAlgorithm(Token token) {
String algorithm = token.getSignatureAlgorithm();
if (algorithm == null) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE,
"Signing algorithm cannot be null");
}
if (!algorithm.equals("RS256")) {
throw new CloudFoundryAuthorizationException(
Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM,
"Signing algorithm " + algorithm + " not supported");
}
}
private void validateSignature(Token token) {
if (this.tokenKeys == null || !hasValidSignature(token)) {
this.tokenKeys = this.securityService.fetchTokenKeys();
if (!hasValidSignature(token)) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE,
"RSA Signature did not match content");
}
}
}
private boolean hasValidSignature(Token token) {
for (String key : this.tokenKeys) {
if (hasValidSignature(token, key)) {
return true;
}
}
return false;
}
private boolean hasValidSignature(Token token, String key) {
try {
PublicKey publicKey = getPublicKey(key);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(token.getContent());
return signature.verify(token.getSignature());
}
catch (GeneralSecurityException ex) {
return false;
}
}
private PublicKey getPublicKey(String key)
throws NoSuchAlgorithmException, InvalidKeySpecException {
key = key.replace("-----BEGIN PUBLIC KEY-----\n", "");
key = key.replace("-----END PUBLIC KEY-----", "");
key = key.trim().replace("\n", "");
byte[] bytes = Base64Utils.decodeFromString(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
private void validateExpiry(Token token) {
long currentTime = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
if (currentTime > token.getExpiry()) {
throw new CloudFoundryAuthorizationException(Reason.TOKEN_EXPIRED,
"Token expired");
}
}
private void validateIssuer(Token token) {
String uaaUrl = this.securityService.getUaaUrl();
String issuerUri = String.format("%s/oauth/token", uaaUrl);
if (!issuerUri.equals(token.getIssuer())) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_ISSUER,
"Token issuer does not match " + uaaUrl + "/oauth/token");
}
}
private void validateAudience(Token token) {
if (!token.getScope().contains("actuator.read")) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_AUDIENCE,
"Token does not have audience actuator");
}
}
}

@ -0,0 +1,54 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AccessLevel}.
*
* @author Madhura Bhave
*/
public class AccessLevelTests {
@Test
public void accessToHealthEndpointShouldNotBeRestricted() throws Exception {
assertThat(AccessLevel.RESTRICTED.isAccessAllowed("/health")).isTrue();
assertThat(AccessLevel.FULL.isAccessAllowed("/health")).isTrue();
}
@Test
public void accessToInfoEndpointShouldNotBeRestricted() throws Exception {
assertThat(AccessLevel.RESTRICTED.isAccessAllowed("/info")).isTrue();
assertThat(AccessLevel.FULL.isAccessAllowed("/info")).isTrue();
}
@Test
public void accessToDiscoveryEndpointShouldNotBeRestricted() throws Exception {
assertThat(AccessLevel.RESTRICTED.isAccessAllowed("")).isTrue();
assertThat(AccessLevel.FULL.isAccessAllowed("")).isTrue();
}
@Test
public void accessToAnyOtherEndpointShouldBeRestricted() throws Exception {
assertThat(AccessLevel.RESTRICTED.isAccessAllowed("env")).isFalse();
assertThat(AccessLevel.FULL.isAccessAllowed("")).isTrue();
}
}

@ -0,0 +1,48 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import org.hamcrest.CustomMatcher;
import org.hamcrest.Matcher;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
/**
* Hamcrest matcher to check the {@link AuthorizationExceptionMatcher} {@link Reason}.
*
* @author Madhura Bhave
*/
final class AuthorizationExceptionMatcher {
private AuthorizationExceptionMatcher() {
}
static Matcher<?> withReason(final Reason reason) {
return new CustomMatcher<Object>(
"CloudFoundryAuthorizationException with " + reason + " reason") {
@Override
public boolean matches(Object object) {
return ((object instanceof CloudFoundryAuthorizationException)
&& ((CloudFoundryAuthorizationException) object)
.getReason() == reason);
}
};
}
}

@ -29,9 +29,11 @@ import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfigurati
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@ -46,7 +48,7 @@ public class CloudFoundryActuatorAutoConfigurationTests {
private AnnotationConfigWebApplicationContext context;
@Before
public void setUp() {
public void setup() {
this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityAutoConfiguration.class,
@ -57,6 +59,7 @@ public class CloudFoundryActuatorAutoConfigurationTests {
EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class,
WebClientAutoConfiguration.class,
EndpointWebMvcManagementContextConfiguration.class,
CloudFoundryActuatorAutoConfiguration.class);
}
@ -70,13 +73,58 @@ public class CloudFoundryActuatorAutoConfigurationTests {
@Test
public void cloudFoundryPlatformActive() throws Exception {
CloudFoundryEndpointHandlerMapping handlerMapping = x();
assertThat(handlerMapping.getPrefix()).isEqualTo("/cloudfoundryapplication");
}
@Test
public void cloudFoundryPlatformActiveSetsApplicationId() throws Exception {
CloudFoundryEndpointHandlerMapping handlerMapping = x();
Object interceptor = ReflectionTestUtils.getField(handlerMapping,
"securityInterceptor");
String applicationId = (String) ReflectionTestUtils.getField(interceptor,
"applicationId");
assertThat(applicationId).isEqualTo("my-app-id");
}
@Test
public void cloudFoundryPlatformActiveSetsCloudControllerUrl() throws Exception {
CloudFoundryEndpointHandlerMapping handlerMapping = x();
Object interceptor = ReflectionTestUtils.getField(handlerMapping,
"securityInterceptor");
Object interceptorSecurityService = ReflectionTestUtils.getField(interceptor,
"cloudFoundrySecurityService");
String cloudControllerUrl = (String) ReflectionTestUtils
.getField(interceptorSecurityService, "cloudControllerUrl");
assertThat(cloudControllerUrl).isEqualTo("http://my-cloud-controller.com");
}
@Test
public void cloudFoundryPlatformActiveAndCloudControllerUrlNotPresent()
throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---",
"management.cloudfoundry.enabled:true");
"management.cloudfoundry.enabled:true",
"vcap.application.application_id:my-app-id");
this.context.refresh();
CloudFoundryEndpointHandlerMapping handlerMapping = this.context.getBean(
CloudFoundryEndpointHandlerMapping handlerMapping1 = this.context.getBean(
"cloudFoundryEndpointHandlerMapping",
CloudFoundryEndpointHandlerMapping.class);
assertThat(handlerMapping.getPrefix()).isEqualTo("/cloudfoundryapplication");
CloudFoundryEndpointHandlerMapping handlerMapping = handlerMapping1;
Object securityInterceptor = ReflectionTestUtils.getField(handlerMapping,
"securityInterceptor");
Object interceptorSecurityService = ReflectionTestUtils
.getField(securityInterceptor, "cloudFoundrySecurityService");
assertThat(interceptorSecurityService).isNull();
}
private CloudFoundryEndpointHandlerMapping x() {
EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---",
"management.cloudfoundry.enabled:true",
"vcap.application.application_id:my-app-id",
"vcap.application.cf_api:http://my-cloud-controller.com");
this.context.refresh();
return this.context.getBean("cloudFoundryEndpointHandlerMapping",
CloudFoundryEndpointHandlerMapping.class);
}
@Test

@ -0,0 +1,92 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import org.junit.Test;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.http.HttpStatus;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CloudFoundryAuthorizationException}.
*
* @author Madhura Bhave
*/
public class CloudFoundryAuthorizationExceptionTests {
@Test
public void statusCodeForInvalidTokenReasonShouldBe401() throws Exception {
assertThat(createException(Reason.INVALID_TOKEN).getStatusCode())
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForInvalidIssuerReasonShouldBe401() throws Exception {
assertThat(createException(Reason.INVALID_ISSUER).getStatusCode())
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForInvalidAudienceReasonShouldBe401() throws Exception {
assertThat(createException(Reason.INVALID_AUDIENCE).getStatusCode())
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForInvalidSignatureReasonShouldBe401() throws Exception {
assertThat(createException(Reason.INVALID_SIGNATURE).getStatusCode())
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForMissingAuthorizationReasonShouldBe401() throws Exception {
assertThat(createException(Reason.MISSING_AUTHORIZATION).getStatusCode())
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForUnsupportedSignatureAlgorithmReasonShouldBe401()
throws Exception {
assertThat(createException(Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM)
.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForTokenExpiredReasonShouldBe401() throws Exception {
assertThat(createException(Reason.TOKEN_EXPIRED).getStatusCode())
.isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void statusCodeForAccessDeniedReasonShouldBe403() throws Exception {
assertThat(createException(Reason.ACCESS_DENIED).getStatusCode())
.isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
public void statusCodeForServiceUnavailableReasonShouldBe503() throws Exception {
assertThat(createException(Reason.SERVICE_UNAVAILABLE).getStatusCode())
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
}
private CloudFoundryAuthorizationException createException(Reason reason) {
return new CloudFoundryAuthorizationException(reason, "message");
}
}

@ -16,10 +16,10 @@
package org.springframework.boot.actuate.cloudfoundry;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointHandlerMappingTests;
@ -40,18 +40,20 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Madhura Bhave
*/
public class CloudFoundryEndpointHandlerMappingTests extends AbstractEndpointHandlerMappingTests {
public class CloudFoundryEndpointHandlerMappingTests
extends AbstractEndpointHandlerMappingTests {
@Test
public void getHandlerExecutionChainShouldHaveSecurityInterceptor() throws Exception {
CloudFoundrySecurityInterceptor securityInterceptor = Mockito
.mock(CloudFoundrySecurityInterceptor.class);
TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a"));
CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping(
Arrays.asList(endpoint));
Collections.singleton(endpoint), null, securityInterceptor);
HandlerExecutionChain handlerExecutionChain = handlerMapping
.getHandlerExecutionChain(endpoint, new MockHttpServletRequest());
HandlerInterceptor[] interceptors = handlerExecutionChain.getInterceptors();
assertThat(interceptors).hasAtLeastOneElementOfType(
CloudFoundryEndpointHandlerMapping.SecurityInterceptor.class);
assertThat(interceptors).contains(securityInterceptor);
}
@Test
@ -60,14 +62,14 @@ public class CloudFoundryEndpointHandlerMappingTests extends AbstractEndpointHan
TestMvcEndpoint testMvcEndpoint = new TestMvcEndpoint(new TestEndpoint("a"));
testMvcEndpoint.setPath("something-else");
CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping(
Arrays.asList(testMvcEndpoint));
Collections.singleton(testMvcEndpoint), null, null);
assertThat(handlerMapping.getPath(testMvcEndpoint)).isEqualTo("/a");
}
@Test
public void doesNotRegisterHalJsonMvcEndpoint() throws Exception {
CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping(
Collections.<NamedMvcEndpoint>singleton(new TestHalJsonMvcEndpoint()));
Collections.singleton(new TestHalJsonMvcEndpoint()), null, null);
assertThat(handlerMapping.getEndpoints()).hasSize(0);
}
@ -75,7 +77,7 @@ public class CloudFoundryEndpointHandlerMappingTests extends AbstractEndpointHan
public void registersCloudFoundryDiscoveryEndpoint() throws Exception {
StaticApplicationContext context = new StaticApplicationContext();
CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping(
Collections.<NamedMvcEndpoint>emptyList());
Collections.<NamedMvcEndpoint>emptySet(), null, null);
handlerMapping.setPrefix("/test");
handlerMapping.setApplicationContext(context);
handlerMapping.afterPropertiesSet();
@ -119,6 +121,7 @@ public class CloudFoundryEndpointHandlerMappingTests extends AbstractEndpointHan
});
}
}
}

@ -0,0 +1,196 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.BDDMockito;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.Base64Utils;
import org.springframework.web.method.HandlerMethod;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link CloudFoundrySecurityInterceptor}.
*
* @author Madhura Bhave
*/
public class CloudFoundrySecurityInterceptorTests {
@Mock
private TokenValidator tokenValidator;
@Mock
private CloudFoundrySecurityService securityService;
private CloudFoundrySecurityInterceptor interceptor;
private TestMvcEndpoint endpoint;
private HandlerMethod handlerMethod;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator,
this.securityService, "my-app-id");
this.endpoint = new TestMvcEndpoint(new TestEndpoint("a"));
this.handlerMethod = new HandlerMethod(this.endpoint, "invoke");
this.request = new MockHttpServletRequest();
this.response = new MockHttpServletResponse();
}
@Test
public void preHandleWhenTokenIsMissingShouldReturnFalse() throws Exception {
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
assertThat(preHandle).isFalse();
assertThat(this.response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus().value());
}
@Test
public void preHandleWhenTokenIsNotBearerShouldReturnFalse() throws Exception {
this.request.addHeader("Authorization", mockAccessToken());
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
assertThat(preHandle).isFalse();
assertThat(this.response.getStatus())
.isEqualTo(Reason.MISSING_AUTHORIZATION.getStatus().value());
}
@Test
public void preHandleWhenApplicationIdIsNullShouldReturnFalse() throws Exception {
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator,
this.securityService, null);
this.request.addHeader("Authorization", "bearer " + mockAccessToken());
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
assertThat(preHandle).isFalse();
assertThat(this.response.getStatus())
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus().value());
}
@Test
public void preHandleWhenCloudFoundrySecurityServiceIsNullShouldReturnFalse()
throws Exception {
this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, null,
"my-app-id");
this.request.addHeader("Authorization", "bearer " + mockAccessToken());
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
assertThat(preHandle).isFalse();
assertThat(this.response.getStatus())
.isEqualTo(Reason.SERVICE_UNAVAILABLE.getStatus().value());
}
@Test
public void preHandleWhenAccessIsNotAllowedShouldReturnFalse() throws Exception {
this.endpoint = new TestMvcEndpoint(new TestEndpoint("env"));
this.handlerMethod = new HandlerMethod(this.endpoint, "invoke");
String accessToken = mockAccessToken();
this.request.addHeader("Authorization", "bearer " + accessToken);
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.RESTRICTED);
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
assertThat(preHandle).isFalse();
assertThat(this.response.getStatus())
.isEqualTo(Reason.ACCESS_DENIED.getStatus().value());
}
@Test
public void preHandleSuccessfulWithFullAccess() throws Exception {
String accessToken = mockAccessToken();
this.request.addHeader("Authorization", "Bearer " + accessToken);
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.FULL);
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
Token token = tokenArgumentCaptor.getValue();
assertThat(token.toString()).isEqualTo(accessToken);
assertThat(preHandle).isTrue();
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(this.request.getAttribute("cloudFoundryAccessLevel"))
.isEqualTo(AccessLevel.FULL);
}
@Test
public void preHandleSuccessfulWithRestrictedAccess() throws Exception {
this.endpoint = new TestMvcEndpoint(new TestEndpoint("info"));
this.handlerMethod = new HandlerMethod(this.endpoint, "invoke");
String accessToken = mockAccessToken();
this.request.addHeader("Authorization", "Bearer " + accessToken);
BDDMockito.given(this.securityService.getAccessLevel(accessToken, "my-app-id"))
.willReturn(AccessLevel.RESTRICTED);
boolean preHandle = this.interceptor.preHandle(this.request, this.response,
this.handlerMethod);
ArgumentCaptor<Token> tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class);
verify(this.tokenValidator).validate(tokenArgumentCaptor.capture());
Token token = tokenArgumentCaptor.getValue();
assertThat(token.toString()).isEqualTo(accessToken);
assertThat(preHandle).isTrue();
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(this.request.getAttribute("cloudFoundryAccessLevel"))
.isEqualTo(AccessLevel.RESTRICTED);
}
private String mockAccessToken() {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwu"
+ "Y29tIiwiZXhwIjoxNDI2NDIwODAwLCJhd2Vzb21lIjp0cnVlfQ."
+ Base64Utils.encodeToString("signature".getBytes());
}
private static class TestEndpoint extends AbstractEndpoint<Object> {
TestEndpoint(String id) {
super(id);
}
@Override
public Object invoke() {
return null;
}
}
private static class TestMvcEndpoint extends EndpointMvcAdapter {
TestMvcEndpoint(TestEndpoint delegate) {
super(delegate);
}
}
}

@ -0,0 +1,194 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withUnauthorizedRequest;
/**
* Tests for {@link CloudFoundrySecurityService}.
*
* @author Madhura Bhave
*/
public class CloudFoundrySecurityServiceTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private static final String CLOUD_CONTROLLER = "http://my-cloud-controller.com";
private static final String CLOUD_CONTROLLER_PERMISSIONS = CLOUD_CONTROLLER
+ "/v2/apps/my-app-id/permissions";
private static final String UAA_URL = "http://my-uaa.com";
private CloudFoundrySecurityService securityService;
private MockRestServiceServer server;
@Before
public void setup() throws Exception {
MockServerRestTemplateCustomizer mockServerCustomizer = new MockServerRestTemplateCustomizer();
RestTemplateBuilder builder = new RestTemplateBuilder(mockServerCustomizer);
this.securityService = new CloudFoundrySecurityService(builder, CLOUD_CONTROLLER);
this.server = mockServerCustomizer.getServer();
}
@Test
public void getAccessLevelWhenSpaceDeveloperShouldReturnFull() throws Exception {
String responseBody = "{\"read_sensitive_data\": true,\"read_basic_data\": true}";
this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
.andExpect(header("Authorization", "bearer my-access-token"))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
AccessLevel accessLevel = this.securityService.getAccessLevel("my-access-token",
"my-app-id");
this.server.verify();
assertThat(accessLevel).isEqualTo(AccessLevel.FULL);
}
@Test
public void getAccessLevelWhenNotSpaceDeveloperShouldReturnRestricted()
throws Exception {
String responseBody = "{\"read_sensitive_data\": false,\"read_basic_data\": true}";
this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
.andExpect(header("Authorization", "bearer my-access-token"))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
AccessLevel accessLevel = this.securityService.getAccessLevel("my-access-token",
"my-app-id");
this.server.verify();
assertThat(accessLevel).isEqualTo(AccessLevel.RESTRICTED);
}
@Test
public void getAccessLevelWhenTokenIsNotValidShouldThrowException() throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
.andExpect(header("Authorization", "bearer my-access-token"))
.andRespond(withUnauthorizedRequest());
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.INVALID_TOKEN));
this.securityService.getAccessLevel("my-access-token", "my-app-id");
}
@Test
public void getAccessLevelWhenForbiddenShouldThrowException() throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
.andExpect(header("Authorization", "bearer my-access-token"))
.andRespond(withStatus(HttpStatus.FORBIDDEN));
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.ACCESS_DENIED));
this.securityService.getAccessLevel("my-access-token", "my-app-id");
}
@Test
public void getAccessLevelWhenCloudControllerIsNotReachableThrowsException()
throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
.andExpect(header("Authorization", "bearer my-access-token"))
.andRespond(withServerError());
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.SERVICE_UNAVAILABLE));
this.securityService.getAccessLevel("my-access-token", "my-app-id");
}
@Test
public void fetchTokenKeysWhenSuccessfulShouldReturnListOfKeysFromUAA()
throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER + "/info"))
.andRespond(withSuccess("{\"token_endpoint\":\"http://my-uaa.com\"}",
MediaType.APPLICATION_JSON));
String tokenKeyValue = "-----BEGIN PUBLIC KEY-----\n"
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\n"
+ "rn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\n"
+ "fYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\n"
+ "LCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\n"
+ "kqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\n"
+ "jfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\n"
+ "JwIDAQAB\n-----END PUBLIC KEY-----";
String responseBody = "{\"keys\" : [ {\"value\" : \""
+ tokenKeyValue.replace("\n", "\\n") + "\"} ]}";
this.server.expect(requestTo(UAA_URL + "/token_keys"))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
List<String> tokenKeys = this.securityService.fetchTokenKeys();
this.server.verify();
assertThat(tokenKeys).containsExactly(tokenKeyValue);
}
@Test
public void fetchTokenKeysWhenNoKeysReturnedFromUAA() throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER + "/info")).andRespond(withSuccess(
"{\"token_endpoint\":\"" + UAA_URL + "\"}", MediaType.APPLICATION_JSON));
String responseBody = "{\"keys\": []}";
this.server.expect(requestTo(UAA_URL + "/token_keys"))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
List<String> tokenKeys = this.securityService.fetchTokenKeys();
this.server.verify();
assertThat(tokenKeys).hasSize(0);
}
@Test
public void fetchTokenKeysWhenUnsuccessfulShouldThrowException() throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER + "/info")).andRespond(withSuccess(
"{\"token_endpoint\":\"" + UAA_URL + "\"}", MediaType.APPLICATION_JSON));
this.server.expect(requestTo(UAA_URL + "/token_keys"))
.andRespond(withServerError());
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.SERVICE_UNAVAILABLE));
this.securityService.fetchTokenKeys();
}
@Test
public void getUaaUrlShouldCallCloudControllerInfoOnlyOnce() throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER + "/info")).andRespond(withSuccess(
"{\"token_endpoint\":\"" + UAA_URL + "\"}", MediaType.APPLICATION_JSON));
String uaaUrl = this.securityService.getUaaUrl();
this.server.verify();
assertThat(uaaUrl).isEqualTo(UAA_URL);
// Second call should not need to hit server
uaaUrl = this.securityService.getUaaUrl();
assertThat(uaaUrl).isEqualTo(UAA_URL);
}
@Test
public void getUaaUrlWhenCloudControllerUrlIsNotReachableShouldThrowException()
throws Exception {
this.server.expect(requestTo(CLOUD_CONTROLLER + "/info"))
.andRespond(withServerError());
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.SERVICE_UNAVAILABLE));
this.securityService.getUaaUrl();
}
}

@ -0,0 +1,122 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.actuate.cloudfoundry.AuthorizationExceptionMatcher.withReason;
/**
* Tests for {@link Token}.
*
* @author Madhura Bhave
*/
public class TokenTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void invalidJwtShouldThrowException() throws Exception {
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
new Token("invalid-token");
}
@Test
public void invalidJwtClaimsShouldThrowException() throws Exception {
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "invalid-claims";
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
new Token(Base64Utils.encodeToString(header.getBytes()) + "."
+ Base64Utils.encodeToString(claims.getBytes()));
}
@Test
public void invalidJwtHeaderShouldThrowException() throws Exception {
String header = "invalid-header";
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\"}";
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
new Token(Base64Utils.encodeToString(header.getBytes()) + "."
+ Base64Utils.encodeToString(claims.getBytes()));
}
@Test
public void emptyJwtSignatureShouldThrowException() throws Exception {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b3B0YWwu"
+ "Y29tIiwiZXhwIjoxNDI2NDIwODAwLCJhd2Vzb21lIjp0cnVlfQ.";
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
new Token(token);
}
@Test
public void validJwt() throws Exception {
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\"}";
String content = Base64Utils.encodeToString(header.getBytes()) + "."
+ Base64Utils.encodeToString(claims.getBytes());
String signature = Base64Utils.encodeToString("signature".getBytes());
Token token = new Token(content + "." + signature);
assertThat(token.getExpiry()).isEqualTo(2147483647);
assertThat(token.getIssuer()).isEqualTo("http://localhost:8080/uaa/oauth/token");
assertThat(token.getSignatureAlgorithm()).isEqualTo("RS256");
assertThat(token.getContent()).isEqualTo(content.getBytes());
assertThat(token.getSignature())
.isEqualTo(Base64Utils.decodeFromString(signature));
}
@Test
public void getSignatureAlgorithmWhenAlgIsNullShouldThrowException()
throws Exception {
String header = "{\"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\"}";
Token token = createToken(header, claims);
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
token.getSignatureAlgorithm();
}
@Test
public void getIssuerWhenIssIsNullShouldThrowException() throws Exception {
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647}";
Token token = createToken(header, claims);
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
token.getIssuer();
}
@Test
public void getExpiryWhenExpIsNullShouldThrowException() throws Exception {
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{\"iss\": \"http://localhost:8080/uaa/oauth/token\"" + "}";
Token token = createToken(header, claims);
this.thrown.expect(withReason(Reason.INVALID_TOKEN));
token.getExpiry();
}
private Token createToken(String header, String claims) {
Token token = new Token(Base64Utils.encodeToString(header.getBytes()) + "."
+ Base64Utils.encodeToString(claims.getBytes()) + "."
+ Base64Utils.encodeToString("signature".getBytes()));
return token;
}
}

@ -0,0 +1,255 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.actuate.cloudfoundry;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collections;
import java.util.List;
import org.apache.commons.codec.binary.Base64;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link TokenValidator}.
*
* @author Madhura Bhave
*/
public class TokenValidatorTests {
private static final byte[] DOT = ".".getBytes();
private static final Charset UTF_8 = Charset.forName("UTF-8");
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private CloudFoundrySecurityService securityService;
private TokenValidator tokenValidator;
private static final String VALID_KEY = "-----BEGIN PUBLIC KEY-----\n"
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\n"
+ "rn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\n"
+ "fYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\n"
+ "LCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\n"
+ "kqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\n"
+ "jfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\n"
+ "JwIDAQAB\n-----END PUBLIC KEY-----";
private static final String INVALID_KEY = "-----BEGIN PUBLIC KEY-----\n"
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzYuc22QSst/dS7geYYK\n"
+ "5l5kLxU0tayNdixkEQ17ix+CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOa\n"
+ "vkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi/0rpoUwPMEh8+Bw7CgYR0\n"
+ "FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd/fAC\n"
+ "VTr4+KEY+IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz/M\n"
+ "r3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s\n"
+ "YwIDAQAB\n-----END PUBLIC KEY-----";
private static final List<String> INVALID_KEYS = Collections
.singletonList(INVALID_KEY);
private static final List<String> VALID_KEYS = Collections.singletonList(VALID_KEY);
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
this.tokenValidator = new TokenValidator(this.securityService);
}
@Test
public void validateTokenWhenSignatureValidationFailsTwiceShouldThrowException()
throws Exception {
ReflectionTestUtils.setField(this.tokenValidator, "tokenKeys", INVALID_KEYS);
given(this.securityService.fetchTokenKeys()).willReturn(INVALID_KEYS);
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\",\"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.INVALID_SIGNATURE));
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
}
@Test
public void validateTokenWhenSignatureValidationSucceedsInTheSecondAttempt()
throws Exception {
ReflectionTestUtils.setField(this.tokenValidator, "tokenKeys", INVALID_KEYS);
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa");
String header = "{ \"alg\": \"RS256\", \"kid\": \"key-id\",\"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
verify(this.securityService).fetchTokenKeys();
}
@Test
public void validateTokenShouldFetchTokenKeysIfNull() throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa");
String header = "{ \"alg\": \"RS256\", \"kid\": \"key-id\",\"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
verify(this.securityService).fetchTokenKeys();
}
@Test
public void validateTokenWhenSignatureValidShouldNotFetchTokenKeys()
throws Exception {
ReflectionTestUtils.setField(this.tokenValidator, "tokenKeys", VALID_KEYS);
given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa");
String header = "{ \"alg\": \"RS256\", \"kid\": \"key-id\",\"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
verify(this.securityService, Mockito.never()).fetchTokenKeys();
}
@Test
public void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException()
throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
String header = "{ \"alg\": \"HS256\", \"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
this.thrown.expect(AuthorizationExceptionMatcher
.withReason(Reason.UNSUPPORTED_TOKEN_SIGNING_ALGORITHM));
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
}
@Test
public void validateTokenWhenExpiredShouldThrowException() throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
String header = "{ \"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{ \"jti\": \"0236399c350c47f3ae77e67a75e75e7d\", \"exp\": 1477509977, \"scope\": [\"actuator.read\"]}";
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.TOKEN_EXPIRED));
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
}
@Test
public void validateTokenWhenIssuerIsNotValidShouldThrowException() throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
given(this.securityService.getUaaUrl()).willReturn("http://other-uaa.com");
String header = "{ \"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\", \"scope\": [\"actuator.read\"]}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\"}";
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.INVALID_ISSUER));
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
}
@Test
public void validateTokenWhenAudienceIsNotValidShouldThrowException()
throws Exception {
given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS);
given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa");
String header = "{ \"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}";
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.INVALID_AUDIENCE));
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
}
private String getSignedToken(byte[] header, byte[] claims) throws Exception {
PrivateKey privateKey = getPrivateKey();
Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initSign(privateKey);
byte[] content = dotConcat(Base64Utils.encodeUrlSafe(header),
Base64Utils.encode(claims));
signature.update(content);
byte[] crypto = signature.sign();
byte[] token = dotConcat(Base64Utils.encodeUrlSafe(header),
Base64Utils.encodeUrlSafe(claims), Base64Utils.encodeUrlSafe(crypto));
return new String(token, UTF_8);
}
private PrivateKey getPrivateKey()
throws InvalidKeySpecException, NoSuchAlgorithmException {
String signingKey = "-----BEGIN PRIVATE KEY-----\n"
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDSbn2Xa72IOcxu\n"
+ "tcd+qQ6ufZ1VDe98EmpwO4VQrTd37U9kZtWU0KqeSkgnyzIWmlbyWOdbB4/v4uJa\n"
+ "lGjPQjt9hvd3xOOFXzpj33sWXgMGvGAzopMk64T+7GegOFlDXguA5TZyReM7M51O\n"
+ "ycYwpAEsKXS+lxcG0UsxpJum/WjOLyHsMnJVnoScVBlRYZ2BMyEOuap69/H3lT/X\n"
+ "pzlYEM6SrAifsaWvL2f1K7HKBt/yDkDOlZy6xmAMsghnslNSV0FvypTZrQOXia8t\n"
+ "k6fjA+iN+P0LDZAgKxzn4/B/bV8/6HN/7VZJEdudi/y5qdE7SBnx6QZqCEz/YfqC\n"
+ "olujacgnAgMBAAECggEAc9X2tJ/OWWrXqinOg160gkELloJxTi8lAFsDbAGuAwpT\n"
+ "JcWl1KF5CmGBjsY/8ElNi2J9GJL1HOwcBhikCVNARD1DhF6RkB13mvquWwWtTMvt\n"
+ "eP8JWM19DIc+E+hw2rCuTGngqs7l4vTqpzBTNPtS2eiIJ1IsjsgvSEiAlk/wnW48\n"
+ "11cf6SQMQcT3HNTWrS+yLycEuWKb6Khh8RpD9D+i8w2+IspWz5lTP7BrKCUNsLOx\n"
+ "6+5T52HcaZ9z3wMnDqfqIKWl3h8M+q+HFQ4EN5BPWYV4fF7EOx7+Qf2fKDFPoTjC\n"
+ "VTWzDRNAA1xPqwdF7IdPVOXCdaUJDOhHeXZGaTNSwQKBgQDxb9UiR/Jh1R3muL7I\n"
+ "neIt1gXa0O+SK7NWYl4DkArYo7V81ztxI8r+xKEeu5zRZZkpaJHxOnd3VfADascw\n"
+ "UfALvxGxN2z42lE6zdhrmxZ3ma+akQFsv7NyXcBT00sdW+xmOiCaAj0cgxNOXiV3\n"
+ "sYOwUy3SqUIPO2obpb+KC5ALHwKBgQDfH+NSQ/jn89oVZ3lzUORa+Z+aL1TGsgzs\n"
+ "p7IG0MTEYiR9/AExYUwJab0M4PDXhumeoACMfkCFALNVhpch2nXZv7X5445yRgfD\n"
+ "ONY4WknecuA0rfCLTruNWnQ3RR+BXmd9jD/5igd9hEIawz3V+jCHvAtzI8/CZIBt\n"
+ "AArBs5kp+QKBgQCdxwN1n6baIDemK10iJWtFoPO6h4fH8h8EeMwPb/ZmlLVpnA4Q\n"
+ "Zd+mlkDkoJ5eiRKKaPfWuOqRZeuvj/wTq7g/NOIO+bWQ+rrSvuqLh5IrHpgPXmub\n"
+ "8bsHJhUlspMH4KagN6ROgOAG3fGj6Qp7KdpxRCpR3KJ66czxvGNrhxre6QKBgB+s\n"
+ "MCGiYnfSprd5G8VhyziazKwfYeJerfT+DQhopDXYVKPJnQW8cQW5C8wDNkzx6sHI\n"
+ "pqtK1K/MnKhcVaHJmAcT7qoNQlA4Xqu4qrgPIQNBvU/dDRNJVthG6c5aspEzrG8m\n"
+ "9IHgtRV9K8EOy/1O6YqrB9kNUVWf3JccdWpvqyNJAoGAORzJiQCOk4egbdcozDTo\n"
+ "4Tg4qk/03qpTy5k64DxkX1nJHu8V/hsKwq9Af7Fj/iHy2Av54BLPlBaGPwMi2bzB\n"
+ "gYjmUomvx/fqOTQks9Rc4PIMB43p6Rdj0sh+52SKPDR2eHbwsmpuQUXnAs20BPPI\n"
+ "J/OOn5zOs8yf26os0q3+JUM=\n-----END PRIVATE KEY-----";
String privateKey = signingKey.replace("-----BEGIN PRIVATE KEY-----\n", "");
privateKey = privateKey.replace("-----END PRIVATE KEY-----", "");
byte[] pkcs8EncodedBytes = Base64.decodeBase64(privateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8EncodedBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
private byte[] dotConcat(byte[]... bytes) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
for (int i = 0; i < bytes.length; i++) {
if (i > 0) {
StreamUtils.copy(DOT, result);
}
StreamUtils.copy(bytes[i], result);
}
return result.toByteArray();
}
}
Loading…
Cancel
Save