/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* 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.
*
* 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.oauth.token;
import org.cloudfoundry.identity.uaa.audit.event.AbstractUaaEvent;
import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.test.JdbcTestBase;
import org.cloudfoundry.identity.uaa.user.UaaUser;
import org.cloudfoundry.identity.uaa.user.UaaUserPrototype;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.cloudfoundry.identity.uaa.oauth.token.RevocableToken.TokenType.ACCESS_TOKEN;
import static org.cloudfoundry.identity.uaa.oauth.token.RevocableToken.TokenType.REFRESH_TOKEN;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class JdbcRevocableTokenProvisioningTest extends JdbcTestBase {
JdbcRevocableTokenProvisioning dao;
private RevocableToken expected;
private String tokenId;
private long issuedAt;
private String clientId;
private char[] value;
private String scope;
private String userId;
private String format;
private Random random = new Random(System.currentTimeMillis());
private RandomValueStringGenerator generator = new RandomValueStringGenerator();
@Rule
public ExpectedException error = ExpectedException.none();
@Before
public void createData() {
JdbcTemplate template = spy(jdbcTemplate);
dao = spy(new JdbcRevocableTokenProvisioning(template));
createData("test-token-id", "test-user-id", "test-client-id");
}
public void createData(String tokenId, String userId, String clientId) {
value = new char[100*1024];
Arrays.fill(value, 'X');
this.tokenId = tokenId;
this.clientId = clientId;
this.userId = userId;
issuedAt = System.currentTimeMillis();
scope = "test1,test2";
format = "format";
expected = new RevocableToken()
.setTokenId(tokenId)
.setClientId(clientId)
.setResponseType(ACCESS_TOKEN)
.setIssuedAt(issuedAt)
.setExpiresAt(issuedAt+10000)
.setValue(new String(value))
.setScope(scope)
.setFormat(format)
.setUserId(userId)
.setZoneId(IdentityZoneHolder.get().getId());
}
@After
public void clear() {
IdentityZoneHolder.clear();
jdbcTemplate.update("DELETE FROM revocable_tokens");
}
@Test
public void on_application_event_calls_internal_delete_method() throws Exception {
BaseClientDetails clientDetails = new BaseClientDetails("id","","","","","");
IdentityZone otherZone = MultitenancyFixture.identityZone("other","other");
for (IdentityZone zone : Arrays.asList(IdentityZone.getUaa(), otherZone)) {
IdentityZoneHolder.set(zone);
reset(dao);
try {
dao.onApplicationEvent(new EntityDeletedEvent<>(clientDetails, mock(UaaAuthentication.class)));
} catch (Exception e) {
}
try {
dao.onApplicationEvent((AbstractUaaEvent) new EntityDeletedEvent<>(clientDetails, mock(UaaAuthentication.class)));
} catch (Exception e) {
}
verify(dao, times(2)).deleteByClient(eq("id"), eq(zone.getId()));
}
}
@Test
public void revocable_tokens_deleted_when_client_is() throws Exception {
BaseClientDetails clientDetails = new BaseClientDetails(clientId,"","","","","");
IdentityZone otherZone = MultitenancyFixture.identityZone("other","other");
for (IdentityZone zone : Arrays.asList(IdentityZone.getUaa(), otherZone)) {
IdentityZoneHolder.set(zone);
insertToken();
countTokens(1);
assertEquals(zone.getId(), dao.retrieve(tokenId).getZoneId());
dao.onApplicationEvent((AbstractUaaEvent) new EntityDeletedEvent<>(clientDetails, mock(UaaAuthentication.class)));
countTokens(0);
}
}
@Test
public void revocable_tokens_deleted_when_user_is() throws Exception {
IdentityZone otherZone = MultitenancyFixture.identityZone("other","other");
for (IdentityZone zone : Arrays.asList(IdentityZone.getUaa(), otherZone)) {
IdentityZoneHolder.set(zone);
UaaUser user = new UaaUser(
new UaaUserPrototype()
.withId(userId)
.withUsername("username")
.withEmail("test@test.com")
.withZoneId(zone.getId())
);
insertToken();
countTokens(1);
assertEquals(zone.getId(), dao.retrieve(tokenId).getZoneId());
dao.onApplicationEvent((AbstractUaaEvent) new EntityDeletedEvent<>(user, mock(UaaAuthentication.class)));
countTokens(0);
}
}
@Test
public void retrieve_all_returns_nothing() {
assertNull(dao.retrieveAll());
}
@Test(expected = EmptyResultDataAccessException.class)
public void testNotFound() {
dao.retrieve(tokenId);
}
@Test()
public void testGetFound() throws Exception {
insertToken();
assertNotNull(dao.retrieve(tokenId));
}
@Test
public void testAdd_Duplicate_Fails() throws Exception {
insertToken();
error.expect(DuplicateKeyException.class);
insertToken();
}
@Test()
public void testGetFound_In_Zone() throws Exception {
IdentityZoneHolder.set(MultitenancyFixture.identityZone("new-zone", "new-zone"));
insertToken();
assertNotNull(dao.retrieve(tokenId));
IdentityZoneHolder.clear();
error.expect(EmptyResultDataAccessException.class);
dao.retrieve(tokenId);
}
@Test
public void insertToken() throws Exception {
RevocableToken actual = dao.create(expected);
evaluateToken(expected, actual);
}
protected void evaluateToken(RevocableToken expected, RevocableToken actual) {
assertNotNull(actual);
assertNotNull(actual.getTokenId());
assertEquals(expected.getTokenId(), actual.getTokenId());
assertEquals(expected.getClientId(), actual.getClientId());
assertEquals(expected.getExpiresAt(), actual.getExpiresAt());
assertEquals(expected.getIssuedAt(), actual.getIssuedAt());
assertEquals(expected.getFormat(), actual.getFormat());
assertEquals(expected.getScope(), actual.getScope());
assertEquals(expected.getValue(), actual.getValue());
assertEquals(expected.getTokenId(), actual.getTokenId());
assertEquals(expected.getResponseType(), actual.getResponseType());
assertEquals(IdentityZoneHolder.get().getId(), actual.getZoneId());
}
@Test
public void listUserTokens() throws Exception {
listTokens(false);
}
@Test(expected = NullPointerException.class)
public void listUserTokens_Null_ClientId() {
dao.getUserTokens("userid", null);
}
@Test(expected = NullPointerException.class)
public void listUserTokens_Empty_ClientId() {
dao.getUserTokens("userid", "");
}
@Test
public void listUserTokenForClient() throws Exception {
String clientId = "test-client-id";
String userId = "test-user-id";
List<RevocableToken> expectedTokens = new ArrayList<>();
int count = 37;
RandomValueStringGenerator generator = new RandomValueStringGenerator(36);
for (int i=0; i<count; i++) {
createData(generator.generate(), userId, clientId);
insertToken();
expectedTokens.add(this.expected);
}
for (int i=0; i<count; i++) {
//create a random record that should not show up
createData(generator.generate(), generator.generate(), generator.generate());
insertToken();
}
List<RevocableToken> actualTokens = dao.getUserTokens(userId, clientId);
assertThat(actualTokens, containsInAnyOrder(expectedTokens.toArray()));
}
@Test
public void listClientTokens() throws Exception {
listTokens(true);
}
public void listTokens(boolean client) throws Exception {
String clientId = "test-client-id";
String userId = "test-user-id";
List<RevocableToken> expectedTokens = new ArrayList<>();
int count = 37;
RandomValueStringGenerator generator = new RandomValueStringGenerator(36);
for (int i=0; i<count; i++) {
if (client) {
userId = generator.generate();
} else {
clientId = generator.generate();
}
createData(generator.generate(), userId, clientId);
insertToken();
expectedTokens.add(this.expected);
}
//create a random record that should not show up
createData(generator.generate(), generator.generate(), generator.generate());
insertToken();
List<RevocableToken> actualTokens = client ? dao.getClientTokens(clientId) : dao.getUserTokens(userId);
assertThat(actualTokens, containsInAnyOrder(expectedTokens.toArray()));
}
@Test
public void testUpdate() throws Exception {
char[] data = new char[200*1024];
Arrays.fill(data, 'Y');
insertToken();
RevocableToken toUpdate = dao.retrieve(tokenId);
long expiresAt = System.currentTimeMillis()+1000;
String scope = "scope1,scope2,scope3";
toUpdate.setFormat("format")
.setExpiresAt(expiresAt)
.setIssuedAt(expiresAt)
.setClientId("new-client-id")
.setScope(scope)
.setValue(new String(data))
.setUserId("new-user-id")
.setZoneId("arbitrary-zone-id")
.setResponseType(REFRESH_TOKEN);
RevocableToken revocableToken = dao.update(tokenId, toUpdate);
evaluateToken(toUpdate, revocableToken);
}
@Test
public void testDelete() throws Exception {
insertToken();
dao.retrieve(tokenId);
dao.delete(tokenId, 8);
error.expect(EmptyResultDataAccessException.class);
dao.retrieve(tokenId);
}
@Test
public void testDeleteRefreshTokenForClientIdUserId() throws Exception {
expected.setResponseType(REFRESH_TOKEN);
insertToken();
createData(new RandomValueStringGenerator().generate(), userId, clientId);
expected.setResponseType(REFRESH_TOKEN);
insertToken();
assertEquals(2, dao.deleteRefreshTokensForClientAndUserId(clientId,userId));
assertEquals(0, dao.deleteRefreshTokensForClientAndUserId(clientId,userId));
List<RevocableToken> userTokens = dao.getUserTokens(userId, clientId);
assertEquals(0, userTokens.stream().filter(t -> t.getResponseType().equals(REFRESH_TOKEN)).count());
}
@Test
public void ensure_expired_token_is_deleted() throws Exception {
insertToken();
jdbcTemplate.update("UPDATE revocable_tokens SET expires_at=? WHERE token_id=?", System.currentTimeMillis() - 10000, tokenId);
try {
dao.retrieve(tokenId);
fail("Token should have been deleted prior to retrieval");
} catch (EmptyResultDataAccessException x) {}
countTokens(0);
}
public void countTokens(int expected) {
assertEquals(expected, (int) jdbcTemplate.queryForObject("select count(1) from revocable_tokens", Integer.class));
}
public void countTokens(int expected, String tokenId) {
assertEquals(expected, (int) jdbcTemplate.queryForObject("select count(1) from revocable_tokens where token_id=?", Integer.class, tokenId));
}
@Test
public void ensure_expired_token_is_deleted_on_create() throws Exception {
jdbcTemplate.update("DELETE FROM revocable_tokens");
insertToken();
jdbcTemplate.update("UPDATE revocable_tokens SET expires_at=? WHERE token_id=?", System.currentTimeMillis() - 10000, tokenId);
expected.setTokenId(generator.generate());
dao.lastExpiredCheck.set(0); //simulate time has passed
dao.create(expected);
countTokens(1);
countTokens(1, expected.getTokenId());
countTokens(0, tokenId);
}
@Test
public void test_periodic_deletion_of_expired_tokens() throws Exception {
insertToken();
expected.setTokenId(new RandomValueStringGenerator().generate());
insertToken();
countTokens(2);
jdbcTemplate.update("UPDATE revocable_tokens SET expires_at=?", System.currentTimeMillis() - 10000);
try {
dao.lastExpiredCheck.set(0);
dao.retrieve(tokenId);
fail("Token should have been deleted prior to retrieval");
} catch (EmptyResultDataAccessException x) {}
countTokens(0);
}
@Test
public void testDeleteByIdentityZone() throws Exception {
IdentityZone zone = MultitenancyFixture.identityZone("test-zone","test-zone");
IdentityZoneHolder.set(zone);
insertToken();
dao.retrieve(tokenId);
EntityDeletedEvent<IdentityZone> zoneDeleted = new EntityDeletedEvent<>(zone, null);
dao.onApplicationEvent(zoneDeleted);
error.expect(EmptyResultDataAccessException.class);
dao.retrieve(tokenId);
}
@Test
public void testDeleteByOrigin() throws Exception {
//no op - doesn't affect tokens
}
}