blob: 190dd70568a6c96ed9e10d0ded95e3c307a7f87f [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.knox.gateway.services.token.impl;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import org.easymock.EasyMock;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class DefaultTokenStateServiceTest {
private static RSAPrivateKey privateKey;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.genKeyPair();
privateKey = (RSAPrivateKey) kp.getPrivate();
}
@Test
public void testGetExpiration() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
addToken(tss, token, System.currentTimeMillis());
long expiration = tss.getTokenExpiration(TokenUtils.getTokenId(token));
assertEquals(token.getExpiresDate().getTime(), expiration);
}
@Test(expected = IllegalArgumentException.class)
public void testGetExpiration_NullToken() throws Exception {
// Expecting an IllegalArgumentException because the token is null
createTokenStateService().getTokenExpiration((String) null);
}
@Test(expected = IllegalArgumentException.class)
public void testGetExpiration_EmptyToken() throws Exception {
// Expecting an IllegalArgumentException because the token is empty
createTokenStateService().getTokenExpiration("");
}
@Test(expected = UnknownTokenException.class)
public void testGetExpiration_InvalidToken() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
// Expecting an UnknownTokenException because the token is not known to the TokenStateService
createTokenStateService().getTokenExpiration(TokenUtils.getTokenId(token));
}
@Test
public void testGetExpiration_AfterRenewal() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
addToken(tss, token, System.currentTimeMillis());
long expiration = tss.getTokenExpiration(TokenUtils.getTokenId(token));
assertEquals(token.getExpiresDate().getTime(), expiration);
long newExpiration = tss.renewToken(token);
assertTrue(newExpiration > token.getExpiresDate().getTime());
assertTrue(tss.getTokenExpiration(TokenUtils.getTokenId(token)) > token.getExpiresDate().getTime());
}
@Test
public void testIsExpired_Negative() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
addToken(tss, token, System.currentTimeMillis());
assertFalse(tss.isExpired(token));
}
@Test
public void testIsExpired_Positive() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
addToken(tss, token, System.currentTimeMillis());
assertTrue(tss.isExpired(token));
}
@Test(expected = UnknownTokenException.class)
public void testIsExpired_Revoked() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
addToken(tss, token, System.currentTimeMillis());
assertFalse("Expected the token to be valid.", tss.isExpired(token));
tss.revokeToken(token);
tss.isExpired(token);
}
@Test
public void testRenewal() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
// Add the expired token
addToken(tss, token, System.currentTimeMillis());
assertTrue("Expected the token to have expired.", tss.isExpired(token));
tss.renewToken(token);
assertFalse("Expected the token to have been renewed.", tss.isExpired(token));
}
@Test
public void testRenewalBeyondMaxLifetime() throws Exception {
long maxLifetimeDuration = TimeUnit.SECONDS.toMillis(5);
long issueTime = System.currentTimeMillis();
long expiration = issueTime + maxLifetimeDuration;
final JWTToken token = createMockToken(expiration);
final TokenStateService tss = createTokenStateService();
// Add the token with a short maximum lifetime
tss.addToken(TokenUtils.getTokenId(token), issueTime, expiration, maxLifetimeDuration);
try {
// Attempt to renew the token for the default interval, which should exceed the specified short maximum lifetime
// for this token.
tss.renewToken(token);
fail("Token renewal should have been disallowed because the maximum lifetime will have been exceeded.");
} catch (IllegalArgumentException e) {
assertEquals("The renewal limit for the token has been exceeded", e.getMessage());
}
}
@Test
public void testNegativeTokenEviction() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = TimeUnit.MINUTES.toMillis(2);
// Add the expired token
addToken(tss,
token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
assertTrue("Expected the token to have expired.", tss.isExpired(token));
// Sleep to allow the eviction evaluation to be performed prior to the maximum token lifetime
Thread.sleep(evictionInterval + (evictionInterval / 2));
// Renewal should succeed because there is sufficient time until expiration + grace period is exceeded
tss.renewToken(token, TimeUnit.SECONDS.toMillis(10));
assertFalse("Expected the token to have been renewed.", tss.isExpired(token));
}
@Test
public void testTokenEviction() throws Exception {
final JWTToken token = createMockToken(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(60));
final TokenStateService tss = createTokenStateService();
final long evictionInterval = TimeUnit.SECONDS.toMillis(3);
final long maxTokenLifetime = evictionInterval * 3;
try {
tss.start();
// Add the expired token
addToken(tss,
token.getClaim(JWTToken.KNOX_ID_CLAIM),
System.currentTimeMillis(),
token.getExpiresDate().getTime(),
maxTokenLifetime);
assertTrue("Expected the token to have expired.", tss.isExpired(token));
// Sleep to allow the eviction evaluation to be performed
Thread.sleep(evictionInterval + (evictionInterval / 2));
// Expect the renew call to fail since the token should have been evicted
final UnknownTokenException e = assertThrows(UnknownTokenException.class, () -> tss.renewToken(token));
assertEquals("Unknown token: " + TokenUtils.getTokenId(token), e.getMessage());
} finally {
tss.stop();
}
}
@Test
public void testTokenPermissiveness() throws Exception {
final long expiry = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(300);
final JWT token = getJWTToken(expiry);
TokenStateService tss = new DefaultTokenStateService();
try {
tss.init(createMockGatewayConfig(true), Collections.emptyMap());
} catch (ServiceLifecycleException e) {
fail("Error creating TokenStateService: " + e.getMessage());
}
assertEquals(TimeUnit.MILLISECONDS.toSeconds(expiry),
TimeUnit.MILLISECONDS.toSeconds(tss.getTokenExpiration(token)));
}
@Test(expected = UnknownTokenException.class)
public void testTokenPermissivenessNoExpiry() throws Exception {
final JWT token = getJWTToken(-1L);
TokenStateService tss = new DefaultTokenStateService();
try {
tss.init(createMockGatewayConfig(true), Collections.emptyMap());
} catch (ServiceLifecycleException e) {
fail("Error creating TokenStateService: " + e.getMessage());
}
tss.getTokenExpiration(token);
}
protected static JWTToken createMockToken(final long expiration) {
return createMockToken("abcD1234eFGHIJKLmnoPQRSTUVwXYz", expiration);
}
protected static JWTToken createMockToken(final String payload, final long expiration) {
UUID tokenUID = UUID.randomUUID();
JWTToken token = EasyMock.createNiceMock(JWTToken.class);
EasyMock.expect(token.getPayload()).andReturn(payload).anyTimes();
EasyMock.expect(token.getClaim(JWTToken.KNOX_ID_CLAIM)).andReturn(String.valueOf(tokenUID)).anyTimes();
EasyMock.expect(token.getExpiresDate()).andReturn(new Date(expiration)).anyTimes();
EasyMock.replay(token);
return token;
}
protected GatewayConfig createMockGatewayConfig(boolean tokenPermissiveness) throws Exception {
return createMockGatewayConfig(tokenPermissiveness, getGatewaySecurityDir(), getTokenStatePersistenceInterval());
}
protected GatewayConfig createMockGatewayConfig(boolean tokenPermissiveness,
final String securityDir,
long statePersistenceInterval) {
GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
/* configure token eviction time to be 2 secs for test */
EasyMock.expect(config.getKnoxTokenEvictionInterval()).andReturn(2L).anyTimes();
EasyMock.expect(config.getKnoxTokenEvictionGracePeriod()).andReturn(0L).anyTimes();
EasyMock.expect(config.isKnoxTokenPermissiveValidationEnabled()).andReturn(tokenPermissiveness).anyTimes();
EasyMock.expect(config.getKnoxTokenStateAliasPersistenceInterval()).andReturn(statePersistenceInterval).anyTimes();
EasyMock.expect(config.getGatewaySecurityDir()).andReturn(securityDir).anyTimes();
EasyMock.replay(config);
return config;
}
protected void initTokenStateService(TokenStateService tss) throws Exception {
try {
tss.init(createMockGatewayConfig(false), Collections.emptyMap());
} catch (ServiceLifecycleException e) {
fail("Error creating TokenStateService: " + e.getMessage());
}
}
protected long getTokenStatePersistenceInterval() {
return TimeUnit.SECONDS.toMillis(15);
}
protected String getGatewaySecurityDir() throws IOException {
return null;
}
protected TokenStateService createTokenStateService() throws Exception {
TokenStateService tss = new DefaultTokenStateService();
initTokenStateService(tss);
return tss;
}
/* create a test JWT token */
protected JWT getJWTToken(final long expiry) {
String[] claims = new String[4];
claims[0] = "KNOXSSO";
claims[1] = "john.doe@example.com";
claims[2] = "https://login.example.com";
if(expiry > 0) {
claims[3] = Long.toString(expiry);
}
JWT token = new JWTToken("RS256", claims);
// Sign the token
JWSSigner signer = new RSASSASigner(privateKey);
token.sign(signer);
return token;
}
protected void addToken(TokenStateService tss, JWTToken token, long issueTime) {
tss.addToken(token, issueTime);
}
protected void addToken(TokenStateService tss, String tokenId, long issueTime, long expiration, long maxLifetime) {
tss.addToken(tokenId, issueTime, expiration, maxLifetime);
}
}