/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
* <p>
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
* <p>
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.util;
import org.cloudfoundry.identity.uaa.oauth.token.RevocableToken;
import org.cloudfoundry.identity.uaa.oauth.token.RevocableTokenProvisioning;
import org.cloudfoundry.identity.uaa.user.InMemoryUaaUserDatabase;
import org.cloudfoundry.identity.uaa.user.MockUaaUserDatabase;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserDatabase;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Collections.EMPTY_LIST;
import static org.cloudfoundry.identity.uaa.oauth.client.ClientConstants.REQUIRED_USER_GROUPS;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EMAIL;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.USER_NAME;
import static org.cloudfoundry.identity.uaa.util.TokenValidation.validate;
import static org.cloudfoundry.identity.uaa.util.UaaMapUtils.entry;
import static org.cloudfoundry.identity.uaa.util.UaaMapUtils.map;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
public class TokenValidationTest {
public static final String CLIENT_ID = "app";
public static final String USER_ID = "a7f07bf6-e720-4652-8999-e980189cef54";
private final SignatureVerifier verifier = new MacSigner("secret");
private final Instant oneSecondAfterTheTokenExpires = Instant.ofEpochSecond(1458997132 + 1);
private final Instant oneSecondBeforeTheTokenExpires = Instant.ofEpochSecond(1458997132 - 1);
private Map<String, Object> header;
private Map<String, Object> content;
private Signer signer;
private RevocableTokenProvisioning revocableTokenProvisioning;
private InMemoryClientDetailsService clientDetailsService;
private UaaUserDatabase userDb;
private UaaUser uaaUser;
private BaseClientDetails uaaClient;
private Collection<String> uaaUserGroups;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setup() {
header = map(
entry("alg", "HS256")
);
content = map(
entry("jti", "8b14f193-8212-4af2-9927-e3ae903f94a6"),
entry("nonce", "04e2e934200b4b9fbe5d4e70ae18ba8e"),
entry("sub", "a7f07bf6-e720-4652-8999-e980189cef54"),
entry("scope", Arrays.asList("acme.dev")),
entry("client_id", "app"),
entry("cid", "app"),
entry("azp", "app"),
entry("grant_type", "authorization_code"),
entry("user_id", "a7f07bf6-e720-4652-8999-e980189cef54"),
entry("origin", "uaa"),
entry("user_name", "marissa"),
entry("email", "marissa@test.org"),
entry("auth_time", 1458953554),
entry("rev_sig", "fa1c787d"),
entry("iat", 1458953932),
entry("exp", 1458997132),
entry("iss", "http://localhost:8080/uaa/oauth/token"),
entry("zid", "uaa"),
entry("aud", Arrays.asList("app", "acme")),
entry("revocable", true)
);
signer = new MacSigner("secret");
clientDetailsService = new InMemoryClientDetailsService();
uaaClient = new BaseClientDetails("app", "acme", "acme.dev", "authorization_code", "");
uaaClient.addAdditionalInformation(REQUIRED_USER_GROUPS, Arrays.asList());
clientDetailsService.setClientDetailsStore(Collections.singletonMap(CLIENT_ID, uaaClient));
revocableTokenProvisioning = mock(RevocableTokenProvisioning.class);
when(revocableTokenProvisioning.retrieve("8b14f193-8212-4af2-9927-e3ae903f94a6"))
.thenReturn(new RevocableToken().setValue(UaaTokenUtils.constructToken(header, content, signer)));
userDb = new MockUaaUserDatabase(u -> u
.withUsername("marissa")
.withId(USER_ID)
.withEmail("marissa@test.org")
.withAuthorities(Collections.singletonList(new SimpleGrantedAuthority("acme.dev"))));
uaaUser = userDb.retrieveUserById(USER_ID);
uaaUserGroups = uaaUser.getAuthorities().stream().map(a -> a.getAuthority()).collect(Collectors.toList());
}
private String getToken() {
return getToken(EMPTY_LIST);
}
private String getToken(Collection<String> excludedClaims) {
Map<String, Object> content = this.content != null ? new HashMap(this.content) : null;
for (String key : excludedClaims) {
content.remove(key);
}
return UaaTokenUtils.constructToken(header, content, signer);
}
@Test
public void validate_required_groups_is_invoked() throws Exception {
TokenValidation validation = spy(validate(getToken()));
validation.checkClientAndUser(uaaClient, uaaUser);
verify(validation, times(1))
.checkRequiredUserGroups((Collection<String>) argThat(containsInAnyOrder(new String[0])),
(Collection<String>) argThat(containsInAnyOrder(uaaUserGroups.toArray(new String[0])))
);
Mockito.reset(validation);
uaaClient.addAdditionalInformation(REQUIRED_USER_GROUPS, null);
validation.checkClientAndUser(uaaClient, uaaUser);
verify(validation, times(1))
.checkRequiredUserGroups((Collection<String>) argThat(containsInAnyOrder(new String[0])),
(Collection<String>) argThat(containsInAnyOrder(uaaUserGroups.toArray(new String[0])))
);
uaaClient.addAdditionalInformation(REQUIRED_USER_GROUPS, Arrays.asList("group1", "group2"));
validation.checkClientAndUser(uaaClient, uaaUser);
verify(validation, times(1))
.checkRequiredUserGroups((Collection<String>) argThat(containsInAnyOrder(new String[] {"group1", "group2"})),
(Collection<String>) argThat(containsInAnyOrder(uaaUserGroups.toArray(new String[0])))
);
}
@Test
public void required_groups_are_present() throws Exception {
TokenValidation validation = validate(getToken());
uaaClient.addAdditionalInformation(REQUIRED_USER_GROUPS, uaaUserGroups);
assertTrue(validation.checkClientAndUser(uaaClient, uaaUser).throwIfInvalid().isValid());
}
@Test
public void required_groups_are_missing() throws Exception {
TokenValidation validation = validate(getToken());
uaaUserGroups.add("group-missing-from-user");
uaaClient.addAdditionalInformation(REQUIRED_USER_GROUPS, uaaUserGroups);
assertFalse(validation.checkClientAndUser(uaaClient, uaaUser).isValid());
expectedException.expect(InvalidTokenException.class);
expectedException.expectMessage("User does not meet the client's required group criteria.");
validation.throwIfInvalid();
}
@Test
public void validateToken() throws Exception {
TokenValidation validation = validate(getToken())
.checkSignature(verifier)
.checkIssuer("http://localhost:8080/uaa/oauth/token")
.checkClient((clientId) -> clientDetailsService.loadClientByClientId(clientId))
.checkExpiry(oneSecondBeforeTheTokenExpires)
.checkUser((uid) -> userDb.retrieveUserById(uid))
.checkScopesInclude("acme.dev")
.checkScopesWithin("acme.dev", "another.scope")
.checkRevocationSignature(Collections.singletonList("fa1c787d"))
.checkAudience("acme", "app")
.checkRevocableTokenStore(revocableTokenProvisioning)
;
assertThat(validation.getValidationErrors(), empty());
assertTrue(validation.isValid());
}
@Test
public void validateToken_Without_Email_And_Username() throws Exception {
TokenValidation validation = validate(getToken(Arrays.asList(EMAIL, USER_NAME)))
.checkSignature(verifier)
.checkIssuer("http://localhost:8080/uaa/oauth/token")
.checkClient((clientId) -> clientDetailsService.loadClientByClientId(clientId))
.checkExpiry(oneSecondBeforeTheTokenExpires)
.checkUser((uid) -> userDb.retrieveUserById(uid))
.checkScopesInclude("acme.dev")
.checkScopesWithin("acme.dev", "another.scope")
.checkRevocationSignature(Collections.singletonList("fa1c787d"))
.checkAudience("acme", "app")
.checkRevocableTokenStore(revocableTokenProvisioning)
;
assertThat(validation.getValidationErrors(), empty());
assertTrue(validation.isValid());
}
@Test
public void tokenSignedWithDifferentKey() throws Exception {
signer = new MacSigner("some_other_key");
TokenValidation validation = validate(getToken())
.checkSignature(verifier);
// opaque tokens should remain valid even through a signing key being removed
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void invalidJwt() throws Exception {
TokenValidation validation = validate("invalid.jwt.token");
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void tokenWithInvalidIssuer() throws Exception {
TokenValidation validation = validate(getToken())
.checkIssuer("http://wrong.issuer/");
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void emptyBodyJwt() throws Exception {
content = null;
TokenValidation validation = validate(getToken());
assertThat(validation.getValidationErrors(), empty());
assertTrue("Token with no claims is valid after decoding.", validation.isValid());
assertFalse("Token with no claims fails issuer check.", validation.clone().checkIssuer("http://localhost:8080/uaa/oauth/token").isValid());
assertFalse("Token with no claims fails expiry check.", validation.clone().checkExpiry(oneSecondBeforeTheTokenExpires).isValid());
}
@Test
public void expiredToken() {
TokenValidation validation = validate(getToken())
.checkExpiry(oneSecondAfterTheTokenExpires);
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void nonExistentUser() {
UaaUserDatabase userDb = new InMemoryUaaUserDatabase(Collections.emptySet());
TokenValidation validation = validate(getToken())
.checkUser((uid) -> userDb.retrieveUserById(uid));
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void userHadScopeRevoked() {
UaaUserDatabase userDb = new MockUaaUserDatabase(u -> u
.withUsername("marissa")
.withId("a7f07bf6-e720-4652-8999-e980189cef54")
.withEmail("marissa@test.org")
.withAuthorities(Collections.singletonList(new SimpleGrantedAuthority("a.different.scope"))));
TokenValidation validation = validate(getToken())
.checkUser((uid) -> userDb.retrieveUserById(uid));
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void tokenHasInsufficientScope() {
TokenValidation validation = validate(getToken())
.checkScopesInclude("a.different.scope");
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InsufficientScopeException.class)));
}
@Test
public void tokenContainsRevokedScope() {
TokenValidation validation = validate(getToken())
.checkScopesWithin("a.different.scope");
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void nonExistentClient() {
InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
clientDetailsService.setClientDetailsStore(Collections.emptyMap());
TokenValidation validation = validate(getToken())
.checkClient((clientId) -> clientDetailsService.loadClientByClientId(clientId));
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void clientHasScopeRevoked() {
InMemoryClientDetailsService clientDetailsService = new InMemoryClientDetailsService();
clientDetailsService.setClientDetailsStore(Collections.singletonMap("app", new BaseClientDetails("app", "acme", "a.different.scope", "authorization_code", "")));
TokenValidation validation = validate(getToken())
.checkClient((clientId) -> clientDetailsService.loadClientByClientId(clientId));
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void clientRevocationHashChanged() {
TokenValidation validation = validate(getToken()).checkRevocationSignature(Collections.singletonList("New-Hash"));
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void clientRevocationHashChanged_and_Should_Pass() {
TokenValidation validation = validate(getToken()).checkRevocationSignature(Arrays.asList("fa1c787d", "New-Hash"));
assertTrue(validation.isValid());
validation = validate(getToken()).checkRevocationSignature(Arrays.asList("New-Hash", "fa1c787d"));
assertTrue(validation.isValid());
}
@Test
public void incorrectAudience() {
TokenValidation validation = validate(getToken())
.checkAudience("app", "somethingelse");
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void tokenIsRevoked() {
RevocableTokenProvisioning revocableTokenProvisioning = mock(RevocableTokenProvisioning.class);
when(revocableTokenProvisioning.retrieve("8b14f193-8212-4af2-9927-e3ae903f94a6"))
.thenThrow(new EmptyResultDataAccessException(1));
TokenValidation validation = validate(getToken())
.checkRevocableTokenStore(revocableTokenProvisioning);
assertFalse(validation.isValid());
assertThat(validation.getValidationErrors(), hasItem(instanceOf(InvalidTokenException.class)));
}
@Test
public void nonRevocableToken() {
revocableTokenProvisioning = mock(RevocableTokenProvisioning.class);
when(revocableTokenProvisioning.retrieve("8b14f193-8212-4af2-9927-e3ae903f94a6"))
.thenThrow(new EmptyResultDataAccessException(1)); // should not occur
content.remove("revocable");
TokenValidation validation = validate(getToken())
.checkRevocableTokenStore(revocableTokenProvisioning);
verifyZeroInteractions(revocableTokenProvisioning);
assertThat(validation.getValidationErrors(), empty());
assertTrue(validation.isValid());
}
}