blob: ab8d9de5bd91b86d8ef0cd36343a5f2a694d145b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.accumulo.server.security.delegation;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.crypto.KeyGenerator;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.admin.DelegationTokenConfig;
import org.apache.accumulo.core.clientImpl.AuthenticationTokenIdentifier;
import org.apache.hadoop.security.token.SecretManager.InvalidToken;
import org.apache.hadoop.security.token.Token;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Iterables;
public class AuthenticationTokenSecretManagerTest {
private static final Logger log =
LoggerFactory.getLogger(AuthenticationTokenSecretManagerTest.class);
// From org.apache.hadoop.security.token.SecretManager
private static final String DEFAULT_HMAC_ALGORITHM = "HmacSHA1";
private static final int KEY_LENGTH = 64;
private static KeyGenerator keyGen;
@BeforeClass
public static void setupKeyGenerator() throws Exception {
// From org.apache.hadoop.security.token.SecretManager
keyGen = KeyGenerator.getInstance(DEFAULT_HMAC_ALGORITHM);
keyGen.init(KEY_LENGTH);
}
private String instanceId;
private DelegationTokenConfig cfg;
@Before
public void setup() {
instanceId = UUID.randomUUID().toString();
cfg = new DelegationTokenConfig();
}
@Test
public void testAddKey() {
// 1 minute
long tokenLifetime = 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a single key
AuthenticationKey authKey = new AuthenticationKey(1, 0, tokenLifetime, keyGen.generateKey());
secretManager.addKey(authKey);
// Ensure it's in the cache
Map<Integer,AuthenticationKey> keys = secretManager.getKeys();
assertNotNull(keys);
assertEquals(1, keys.size());
assertEquals(authKey, Iterables.getOnlyElement(keys.values()));
// Add the same key
secretManager.addKey(authKey);
// Ensure we still have only one key
keys = secretManager.getKeys();
assertNotNull(keys);
assertEquals(1, keys.size());
assertEquals(authKey, Iterables.getOnlyElement(keys.values()));
}
@Test
public void testRemoveKey() {
// 1 minute
long tokenLifetime = 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a single key
AuthenticationKey authKey = new AuthenticationKey(1, 0, tokenLifetime, keyGen.generateKey());
secretManager.addKey(authKey);
// Ensure it's in the cache
Map<Integer,AuthenticationKey> keys = secretManager.getKeys();
assertNotNull(keys);
assertEquals(1, keys.size());
assertEquals(authKey, Iterables.getOnlyElement(keys.values()));
assertTrue(secretManager.removeKey(authKey.getKeyId()));
assertEquals(0, secretManager.getKeys().size());
}
@Test
public void testGenerateToken() throws Exception {
// start of the test
long then = System.currentTimeMillis();
// 1 minute
long tokenLifetime = 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
secretManager
.addKey(new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()));
String principal = "user@EXAMPLE.COM";
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair =
secretManager.generateToken(principal, cfg);
assertNotNull(pair);
Token<AuthenticationTokenIdentifier> token = pair.getKey();
assertNotNull(token);
assertEquals(AuthenticationTokenIdentifier.TOKEN_KIND, token.getKind());
// Reconstitute the token identifier (will happen when clients are involved)
AuthenticationTokenIdentifier id = new AuthenticationTokenIdentifier();
id.readFields(new DataInputStream(new ByteArrayInputStream(token.getIdentifier())));
long now = System.currentTimeMillis();
// Issue date should be after the test started, but before we deserialized the token
assertTrue("Issue date did not fall within the expected upper bound. Expected less than " + now
+ ", but was " + id.getIssueDate(), id.getIssueDate() <= now);
assertTrue("Issue date did not fall within the expected lower bound. Expected greater than "
+ then + ", but was " + id.getIssueDate(), id.getIssueDate() >= then);
// Expiration is the token lifetime plus the issue date
assertEquals(id.getIssueDate() + tokenLifetime, id.getExpirationDate());
// Verify instance ID
assertEquals(instanceId, id.getInstanceId());
// The returned id should be the same as the reconstructed id
assertEquals(pair.getValue(), id);
}
@Test
public void testVerifyPassword() throws Exception {
// start of the test
long then = System.currentTimeMillis();
// 1 minute
long tokenLifetime = 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
secretManager
.addKey(new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()));
String principal = "user@EXAMPLE.COM";
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair =
secretManager.generateToken(principal, cfg);
Token<AuthenticationTokenIdentifier> token = pair.getKey();
AuthenticationTokenIdentifier id = new AuthenticationTokenIdentifier();
id.readFields(new DataInputStream(new ByteArrayInputStream(token.getIdentifier())));
byte[] password = secretManager.retrievePassword(id);
// The passwords line up against multiple calls with the same ID
assertArrayEquals(password, secretManager.retrievePassword(id));
// Make a second token for the same user
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair2 =
secretManager.generateToken(principal, cfg);
Token<AuthenticationTokenIdentifier> token2 = pair2.getKey();
// Reconstitute the token identifier (will happen when clients are involved)
AuthenticationTokenIdentifier id2 = new AuthenticationTokenIdentifier();
id2.readFields(new DataInputStream(new ByteArrayInputStream(token2.getIdentifier())));
// Get the password
byte[] password2 = secretManager.retrievePassword(id2);
// It should be different than the password for the first user.
assertFalse("Different tokens for the same user shouldn't have the same password",
Arrays.equals(password, password2));
}
@Test(expected = InvalidToken.class)
public void testExpiredPasswordsThrowError() throws Exception {
// start of the test
long then = System.currentTimeMillis();
// 500ms lifetime
long tokenLifetime = 500;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
secretManager
.addKey(new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()));
String principal = "user@EXAMPLE.COM";
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair =
secretManager.generateToken(principal, cfg);
Token<AuthenticationTokenIdentifier> token = pair.getKey();
// Add a small buffer to make sure we move past the expiration of 0 for the token.
Thread.sleep(1000);
// Reconstitute the token identifier (will happen when clients are involved)
AuthenticationTokenIdentifier id = new AuthenticationTokenIdentifier();
id.readFields(new DataInputStream(new ByteArrayInputStream(token.getIdentifier())));
secretManager.retrievePassword(id);
}
@Test(expected = InvalidToken.class)
public void testTokenIssuedInFuture() throws Exception {
// start of the test
long then = System.currentTimeMillis();
long tokenLifetime = 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
secretManager
.addKey(new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()));
String principal = "user@EXAMPLE.COM";
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair =
secretManager.generateToken(principal, cfg);
Token<AuthenticationTokenIdentifier> token = pair.getKey();
// Reconstitute the token identifier (will happen when clients are involved)
AuthenticationTokenIdentifier id = new AuthenticationTokenIdentifier();
id.readFields(new DataInputStream(new ByteArrayInputStream(token.getIdentifier())));
// Increase the value of issueDate
id.setIssueDate(Long.MAX_VALUE);
secretManager.retrievePassword(id);
}
@Test(expected = InvalidToken.class)
public void testRolledManagerKey() throws Exception {
// start of the test
long then = System.currentTimeMillis();
long tokenLifetime = 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
AuthenticationKey authKey1 =
new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey());
secretManager.addKey(authKey1);
String principal = "user@EXAMPLE.COM";
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair =
secretManager.generateToken(principal, cfg);
Token<AuthenticationTokenIdentifier> token = pair.getKey();
AuthenticationTokenIdentifier id = new AuthenticationTokenIdentifier();
id.readFields(new DataInputStream(new ByteArrayInputStream(token.getIdentifier())));
long now = System.currentTimeMillis();
secretManager.addKey(new AuthenticationKey(2, now, now + tokenLifetime, keyGen.generateKey()));
// Should succeed -- the SecretManager still has authKey1
secretManager.retrievePassword(id);
// Remove authKey1
secretManager.removeKey(authKey1.getKeyId());
// Should fail -- authKey1 (presumably) expired, cannot authenticate
secretManager.retrievePassword(id);
}
@Test(timeout = 20 * 1000)
public void testManagerKeyExpiration() throws Exception {
ZooAuthenticationKeyDistributor keyDistributor =
createMock(ZooAuthenticationKeyDistributor.class);
// start of the test
long then = System.currentTimeMillis();
// 10s lifetime
long tokenLifetime = 10 * 1000L;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Make 2 keys, and add only one. The second has double the expiration of the first
AuthenticationKey authKey1 =
new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()),
authKey2 = new AuthenticationKey(2, then + tokenLifetime, then + tokenLifetime * 2,
keyGen.generateKey());
secretManager.addKey(authKey1);
keyDistributor.remove(authKey1);
expectLastCall().once();
replay(keyDistributor);
// Make sure expiration doesn't trigger anything yet
assertEquals(0, secretManager.removeExpiredKeys(keyDistributor));
assertEquals(1, secretManager.getKeys().size());
// Add the second key, still no expiration
secretManager.addKey(authKey2);
assertEquals(0, secretManager.removeExpiredKeys(keyDistributor));
assertEquals(2, secretManager.getKeys().size());
assertEquals(authKey2, secretManager.getCurrentKey());
// Wait for the expiration
long now = System.currentTimeMillis();
while (now - (then + tokenLifetime) < 0) {
Thread.sleep(500);
now = System.currentTimeMillis();
}
// Expire the first
assertEquals(1, secretManager.removeExpiredKeys(keyDistributor));
// Ensure the second still exists
assertEquals(1, secretManager.getKeys().size());
assertEquals(authKey2, Iterables.getOnlyElement(secretManager.getKeys().values()));
assertEquals(authKey2, secretManager.getCurrentKey());
verify(keyDistributor);
}
@Test
public void testRestrictExpirationDate() throws Exception {
// start of the test
long then = System.currentTimeMillis();
// 1 hr
long tokenLifetime = 60 * 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
secretManager
.addKey(new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()));
// 1 minute
cfg.setTokenLifetime(1, TimeUnit.MINUTES);
String principal = "user@EXAMPLE.COM";
Entry<Token<AuthenticationTokenIdentifier>,AuthenticationTokenIdentifier> pair =
secretManager.generateToken(principal, cfg);
assertNotNull(pair);
long now = System.currentTimeMillis();
long actualExpiration = pair.getValue().getExpirationDate();
long approximateLifetime = actualExpiration - now;
log.info("actualExpiration={}, approximateLifetime={}", actualExpiration, approximateLifetime);
// We don't know the exact lifetime, but we know that it can be no more than what was requested
assertTrue(
"Expected lifetime to be on thet order of the token lifetime, but was "
+ approximateLifetime,
approximateLifetime <= cfg.getTokenLifetime(TimeUnit.MILLISECONDS));
}
@Test(expected = AccumuloException.class)
public void testInvalidRequestedExpirationDate() throws Exception {
// start of the test
long then = System.currentTimeMillis();
// 1 hr
long tokenLifetime = 60 * 60 * 1000;
AuthenticationTokenSecretManager secretManager =
new AuthenticationTokenSecretManager(instanceId, tokenLifetime);
// Add a current key
secretManager
.addKey(new AuthenticationKey(1, then, then + tokenLifetime, keyGen.generateKey()));
// A longer timeout than the secret key has
cfg.setTokenLifetime(tokenLifetime + 1, TimeUnit.MILLISECONDS);
// Should throw an exception
secretManager.generateToken("user@EXAMPLE.COM", cfg);
}
}