Merge branch '1.5.x'

This commit is contained in:
Madhura Bhave
2017-02-09 17:15:20 -08:00
7 changed files with 76 additions and 37 deletions

View File

@@ -63,6 +63,8 @@ class CloudFoundryAuthorizationException extends RuntimeException {
INVALID_ISSUER(HttpStatus.UNAUTHORIZED),
INVALID_KEY_ID(HttpStatus.UNAUTHORIZED),
INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED),

View File

@@ -18,7 +18,7 @@ package org.springframework.boot.actuate.cloudfoundry;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -104,7 +104,7 @@ class CloudFoundrySecurityService {
* Return all token keys known by the UAA.
* @return a list of token keys
*/
public List<String> fetchTokenKeys() {
public Map<String, String> fetchTokenKeys() {
try {
return extractTokenKeys(this.restTemplate
.getForObject(getUaaUrl() + "/token_keys", Map.class));
@@ -115,11 +115,12 @@ class CloudFoundrySecurityService {
}
}
private List<String> extractTokenKeys(Map<?, ?> response) {
List<String> tokenKeys = new ArrayList<String>();
private Map<String, String> extractTokenKeys(Map<?, ?> response) {
Map<String, String> tokenKeys = new HashMap<String, String>();
List<?> keys = (List<?>) response.get("keys");
for (Object key : keys) {
tokenKeys.add((String) ((Map<?, ?>) key).get("value"));
Map<?, ?> tokenKey = (Map<?, ?>) key;
tokenKeys.put((String) (tokenKey).get("kid"), (String) (tokenKey).get("value"));
}
return tokenKeys;
}

View File

@@ -97,6 +97,11 @@ class Token {
return getRequired(this.claims, "scope", List.class);
}
@SuppressWarnings("unchecked")
public String getKeyId() {
return getRequired(this.header, "kid", String.class);
}
@SuppressWarnings("unchecked")
private <T> T getRequired(Map<String, Object> map, String key, Class<T> type) {
Object value = map.get(key);

View File

@@ -23,7 +23,7 @@ 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.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
@@ -38,7 +38,7 @@ class TokenValidator {
private final CloudFoundrySecurityService securityService;
private List<String> tokenKeys;
private Map<String, String> tokenKeys;
TokenValidator(CloudFoundrySecurityService cloudFoundrySecurityService) {
this.securityService = cloudFoundrySecurityService;
@@ -46,12 +46,14 @@ class TokenValidator {
public void validate(Token token) {
validateAlgorithm(token);
validateSignature(token);
validateKeyIdAndSignature(token);
validateExpiry(token);
validateIssuer(token);
validateAudience(token);
}
private void validateAlgorithm(Token token) {
String algorithm = token.getSignatureAlgorithm();
if (algorithm == null) {
@@ -65,19 +67,25 @@ class TokenValidator {
}
}
private void validateSignature(Token token) {
if (this.tokenKeys == null || !hasValidSignature(token)) {
private void validateKeyIdAndSignature(Token token) {
String keyId = token.getKeyId();
if (this.tokenKeys == null || !hasValidKeyId(keyId)) {
this.tokenKeys = this.securityService.fetchTokenKeys();
if (!hasValidSignature(token)) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_SIGNATURE,
"RSA Signature did not match content");
if (!hasValidKeyId(keyId)) {
throw new CloudFoundryAuthorizationException(Reason.INVALID_KEY_ID,
"Key Id present in token header does not match");
}
}
if (!hasValidSignature(token, this.tokenKeys.get(keyId))) {
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)) {
private boolean hasValidKeyId(String tokenKeyId) {
for (String keyId: this.tokenKeys.keySet()) {
if (tokenKeyId.equals(keyId)) {
return true;
}
}

View File

@@ -16,7 +16,7 @@
package org.springframework.boot.actuate.cloudfoundry;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Rule;
@@ -162,13 +162,13 @@ public class CloudFoundrySecurityServiceTests {
+ "kqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\n"
+ "jfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\n"
+ "JwIDAQAB\n-----END PUBLIC KEY-----";
String responseBody = "{\"keys\" : [ {\"value\" : \""
String responseBody = "{\"keys\" : [ {\"kid\":\"test-key\",\"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();
Map<String, String> tokenKeys = this.securityService.fetchTokenKeys();
this.server.verify();
assertThat(tokenKeys).containsExactly(tokenKeyValue);
assertThat(tokenKeys.get("test-key")).isEqualTo(tokenKeyValue);
}
@Test
@@ -178,7 +178,7 @@ public class CloudFoundrySecurityServiceTests {
String responseBody = "{\"keys\": []}";
this.server.expect(requestTo(UAA_URL + "/token_keys"))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
List<String> tokenKeys = this.securityService.fetchTokenKeys();
Map<String, String> tokenKeys = this.securityService.fetchTokenKeys();
this.server.verify();
assertThat(tokenKeys).hasSize(0);
}

View File

@@ -82,6 +82,7 @@ public class TokenTests {
assertThat(token.getExpiry()).isEqualTo(2147483647);
assertThat(token.getIssuer()).isEqualTo("http://localhost:8080/uaa/oauth/token");
assertThat(token.getSignatureAlgorithm()).isEqualTo("RS256");
assertThat(token.getKeyId()).isEqualTo("key-id");
assertThat(token.getContent()).isEqualTo(content.getBytes());
assertThat(token.getSignature())
.isEqualTo(Base64Utils.decodeFromString(signature));
@@ -100,7 +101,7 @@ public class TokenTests {
@Test
public void getIssuerWhenIssIsNullShouldThrowException() throws Exception {
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647}";
Token token = createToken(header, claims);
this.thrown
@@ -108,6 +109,16 @@ public class TokenTests {
token.getIssuer();
}
@Test
public void getKidWhenKidIsNullShouldThrowException() throws Exception {
String header = "{\"alg\": \"RS256\", \"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647}";
Token token = createToken(header, claims);
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.INVALID_TOKEN));
token.getKeyId();
}
@Test
public void getExpiryWhenExpIsNullShouldThrowException() throws Exception {
String header = "{\"alg\": \"RS256\", \"kid\": \"key-id\", \"typ\": \"JWT\"}";

View File

@@ -26,7 +26,7 @@ import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
import org.junit.Before;
@@ -82,10 +82,10 @@ public class TokenValidatorTests {
+ "r3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY+XkiCcrkyS1cnghnllh+LCwQu1s\n"
+ "YwIDAQAB\n-----END PUBLIC KEY-----";
private static final List<String> INVALID_KEYS = Collections
.singletonList(INVALID_KEY);
private static final Map<String, String> INVALID_KEYS = Collections
.singletonMap("invalid-key", INVALID_KEY);
private static final List<String> VALID_KEYS = Collections.singletonList(VALID_KEY);
private static final Map<String, String> VALID_KEYS = Collections.singletonMap("valid-key", VALID_KEY);
@Before
public void setup() throws Exception {
@@ -94,25 +94,25 @@ public class TokenValidatorTests {
}
@Test
public void validateTokenWhenSignatureValidationFailsTwiceShouldThrowException()
public void validateTokenWhenKidValidationFailsTwiceShouldThrowException()
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 header = "{\"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}";
String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}";
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.INVALID_SIGNATURE));
AuthorizationExceptionMatcher.withReason(Reason.INVALID_KEY_ID));
this.tokenValidator.validate(
new Token(getSignedToken(header.getBytes(), claims.getBytes())));
}
@Test
public void validateTokenWhenSignatureValidationSucceedsInTheSecondAttempt()
public void validateTokenWhenKidValidationSucceedsInTheSecondAttempt()
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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"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())));
@@ -123,7 +123,7 @@ public class TokenValidatorTests {
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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"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())));
@@ -131,17 +131,29 @@ public class TokenValidatorTests {
}
@Test
public void validateTokenWhenSignatureValidShouldNotFetchTokenKeys()
public void validateTokenWhenValidShouldNotFetchTokenKeys()
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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"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 validateTokenWhenSignatureInvalidShouldThrowException() throws Exception {
ReflectionTestUtils.setField(this.tokenValidator, "tokenKeys", Collections.singletonMap("valid-key", INVALID_KEY));
given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa");
String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"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 validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException()
throws Exception {
@@ -158,7 +170,7 @@ public class TokenValidatorTests {
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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
String claims = "{ \"jti\": \"0236399c350c47f3ae77e67a75e75e7d\", \"exp\": 1477509977, \"scope\": [\"actuator.read\"]}";
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.TOKEN_EXPIRED));
@@ -170,7 +182,7 @@ public class TokenValidatorTests {
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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\", \"scope\": [\"actuator.read\"]}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\"}";
this.thrown
.expect(AuthorizationExceptionMatcher.withReason(Reason.INVALID_ISSUER));
@@ -183,7 +195,7 @@ public class TokenValidatorTests {
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 header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}";
String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"foo.bar\"]}";
this.thrown.expect(
AuthorizationExceptionMatcher.withReason(Reason.INVALID_AUDIENCE));