KNOX-2071 - Configurable maximum token lifetime for TokenStateService (#178)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
index 4da9ad6..30258c4 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
@@ -39,7 +39,7 @@
}
@Override
- public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+ public void init(final GatewayConfig config, final Map<String, String> options) throws ServiceLifecycleException {
super.init(config, options);
if (aliasService == null) {
throw new ServiceLifecycleException("The required AliasService reference has not been set.");
@@ -47,30 +47,33 @@
}
@Override
- public void addToken(final String token, final long issueTime, final long expiration) {
+ public void addToken(final String token,
+ long issueTime,
+ long expiration,
+ long maxLifetimeDuration) {
isValidIdentifier(token);
try {
aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, token, String.valueOf(expiration));
- setMaxLifetime(token, issueTime);
+ setMaxLifetime(token, issueTime, maxLifetimeDuration);
} catch (AliasServiceException e) {
LOG.failedToSaveTokenState(e);
}
}
@Override
- protected void setMaxLifetime(final String token, final long issueTime) {
+ protected void setMaxLifetime(final String token, long issueTime, long maxLifetimeDuration) {
try {
aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME,
token + "--max",
- String.valueOf(issueTime + getMaxLifetimeInterval()));
+ String.valueOf(issueTime + maxLifetimeDuration));
} catch (AliasServiceException e) {
LOG.failedToSaveTokenState(e);
}
}
@Override
- protected long getMaxLifetime(String token) {
+ protected long getMaxLifetime(final String token) {
long result = 0;
try {
char[] maxLifetimeStr =
@@ -85,7 +88,7 @@
}
@Override
- public long getTokenExpiration(String token) {
+ public long getTokenExpiration(final String token) {
long expiration = 0;
validateToken(token);
@@ -103,18 +106,18 @@
}
@Override
- public void revokeToken(String token) {
+ public void revokeToken(final String token) {
// Record the revocation by setting the expiration to -1
- updateExpiration(token, -1);
+ updateExpiration(token, -1L);
}
@Override
- protected boolean isRevoked(String token) {
+ protected boolean isRevoked(final String token) {
return (getTokenExpiration(token) < 0);
}
@Override
- protected boolean isUnknown(String token) {
+ protected boolean isUnknown(final String token) {
boolean isUnknown = false;
try {
isUnknown = (aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, token) == null);
@@ -125,7 +128,7 @@
}
@Override
- protected void updateExpiration(String token, long expiration) {
+ protected void updateExpiration(final String token, long expiration) {
if (isUnknown(token)) {
throw new IllegalArgumentException("Unknown token.");
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
index 3d351e4..b77e678 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
@@ -43,12 +43,9 @@
private final Map<String, Long> maxTokenLifetimes = new HashMap<>();
- private long maxLifetimeInterval = DEFAULT_MAX_LIFETIME;
-
@Override
- public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
-// maxLifetimeInterval = ??; // TODO: PJZ: Honor gateway configuration for this value, if specified ?
+ public void init(final GatewayConfig config, final Map<String, String> options) throws ServiceLifecycleException {
}
@Override
@@ -60,7 +57,17 @@
}
@Override
- public void addToken(final JWTToken token, final long issueTime) {
+ public long getDefaultRenewInterval() {
+ return DEFAULT_RENEWAL_INTERVAL;
+ }
+
+ @Override
+ public long getDefaultMaxLifetimeDuration() {
+ return DEFAULT_MAX_LIFETIME;
+ }
+
+ @Override
+ public void addToken(final JWTToken token, long issueTime) {
if (token == null) {
throw new IllegalArgumentException("Token data cannot be null.");
}
@@ -68,18 +75,26 @@
}
@Override
- public void addToken(final String token, final long issueTime, final long expiration) {
+ public void addToken(final String token, long issueTime, long expiration) {
+ addToken(token, issueTime, expiration, getDefaultMaxLifetimeDuration());
+ }
+
+ @Override
+ public void addToken(final String token,
+ long issueTime,
+ long expiration,
+ long maxLifetimeDuration) {
if (!isValidIdentifier(token)) {
throw new IllegalArgumentException("Token data cannot be null.");
}
synchronized (tokenExpirations) {
tokenExpirations.put(token, expiration);
}
- setMaxLifetime(token, issueTime);
+ setMaxLifetime(token, issueTime, maxLifetimeDuration);
}
@Override
- public long getTokenExpiration(String token) {
+ public long getTokenExpiration(final String token) {
long expiration;
validateToken(token);
@@ -97,7 +112,7 @@
}
@Override
- public long renewToken(final JWTToken token, final Long renewInterval) {
+ public long renewToken(final JWTToken token, long renewInterval) {
if (token == null) {
throw new IllegalArgumentException("Token data cannot be null.");
}
@@ -110,14 +125,14 @@
}
@Override
- public long renewToken(final String token, final Long renewInterval) { // Should return new expiration?
+ public long renewToken(final String token, long renewInterval) {
long expiration;
validateToken(token, true);
// Make sure the maximum lifetime has not been (and will not be) exceeded
- if (hasRemainingRenewals(token, (renewInterval != null ? renewInterval : DEFAULT_RENEWAL_INTERVAL))) {
- expiration = System.currentTimeMillis() + (renewInterval != null ? renewInterval : DEFAULT_RENEWAL_INTERVAL);
+ if (hasRemainingRenewals(token, renewInterval)) {
+ expiration = System.currentTimeMillis() + renewInterval;
updateExpiration(token, expiration);
} else {
throw new IllegalArgumentException("The renewal limit for the token has been exceeded");
@@ -159,9 +174,9 @@
return isExpired;
}
- protected void setMaxLifetime(final String token, final long issueTime) {
+ protected void setMaxLifetime(final String token, long issueTime, long maxLifetimeDuration) {
synchronized (maxTokenLifetimes) {
- maxTokenLifetimes.put(token, issueTime + maxLifetimeInterval);
+ maxTokenLifetimes.put(token, issueTime + maxLifetimeDuration);
}
}
@@ -185,7 +200,7 @@
}
}
- protected boolean hasRemainingRenewals(final String token, final Long renewInterval) {
+ protected boolean hasRemainingRenewals(final String token, long renewInterval) {
// Is the current time + 30-second buffer + the renewal interval is less than the max lifetime for the token?
return ((System.currentTimeMillis() + 30000 + renewInterval) < getMaxLifetime(token));
}
@@ -202,10 +217,6 @@
return revokedTokens.contains(token);
}
- protected long getMaxLifetimeInterval() {
- return maxLifetimeInterval;
- }
-
protected boolean isValidIdentifier(final String token) {
return token != null && !token.isEmpty();
}
@@ -229,7 +240,7 @@
*
* @throws IllegalArgumentException if the specified token in invalid.
*/
- protected void validateToken(final String token, final boolean includeRevocation) throws IllegalArgumentException {
+ protected void validateToken(final String token, boolean includeRevocation) throws IllegalArgumentException {
if (!isValidIdentifier(token)) {
throw new IllegalArgumentException("Token data cannot be null.");
}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
index cb909d8..0440dce 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
@@ -123,6 +123,27 @@
}
+ @Test
+ public void testRenewalBeyondMaxLifetime() {
+ long issueTime = System.currentTimeMillis();
+ long expiration = issueTime + 5000;
+ final JWTToken token = createMockToken(expiration);
+ final TokenStateService tss = createTokenStateService();
+
+ // Add the token with a short maximum lifetime
+ tss.addToken(token.getPayload(), issueTime, expiration, 5000L);
+
+ 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());
+ }
+ }
+
+
protected static JWTToken createMockToken(final long expiration) {
return createMockToken("ABCD1234", expiration);
}
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index fd01428..06ac747 100644
--- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -26,6 +26,7 @@
import java.util.Map;
import java.util.HashMap;
import java.util.List;
+import java.util.Optional;
import javax.annotation.PostConstruct;
import javax.inject.Singleton;
@@ -71,6 +72,7 @@
private static final String TOKEN_ALLOWED_PRINCIPALS = "knox.token.allowed.principals";
private static final String TOKEN_SIG_ALG = "knox.token.sigalg";
private static final String TOKEN_EXP_RENEWAL_INTERVAL = "knox.token.exp.renew-interval";
+ private static final String TOKEN_EXP_RENEWAL_MAX_LIFETIME = "knox.token.exp.max-lifetime";
private static final String TOKEN_RENEWER_WHITELIST = "knox.token.renewer.whitelist";
private static final long TOKEN_TTL_DEFAULT = 30000L;
static final String RESOURCE_PATH = "knoxtoken/api/v1/token";
@@ -90,7 +92,9 @@
// Optional token store service
private TokenStateService tokenStateService;
- private Long renewInterval;
+ private Optional<Long> renewInterval = Optional.empty();
+
+ private Optional<Long> maxTokenLifetime = Optional.empty();
private List<String> allowedRenewers;
@@ -162,12 +166,21 @@
String renewIntervalValue = context.getInitParameter(TOKEN_EXP_RENEWAL_INTERVAL);
if (renewIntervalValue != null && !renewIntervalValue.isEmpty()) {
try {
- renewInterval = Long.parseLong(renewIntervalValue);
+ renewInterval = Optional.of(Long.parseLong(renewIntervalValue));
} catch (NumberFormatException e) {
log.invalidConfigValue(TOKEN_EXP_RENEWAL_INTERVAL, renewIntervalValue, e);
}
}
+ String maxLifetimeValue = context.getInitParameter(TOKEN_EXP_RENEWAL_MAX_LIFETIME);
+ if (maxLifetimeValue != null && !maxLifetimeValue.isEmpty()) {
+ try {
+ maxTokenLifetime = Optional.of(Long.parseLong(maxLifetimeValue));
+ } catch (NumberFormatException e) {
+ log.invalidConfigValue(TOKEN_EXP_RENEWAL_MAX_LIFETIME, maxLifetimeValue, e);
+ }
+ }
+
allowedRenewers = new ArrayList<>();
String renewerList = context.getInitParameter(TOKEN_RENEWER_WHITELIST);
if (renewerList != null && !renewerList.isEmpty()) {
@@ -206,7 +219,8 @@
if (allowedRenewers.contains(renewer)) {
try {
// If renewal fails, it should be an exception
- expiration = tokenStateService.renewToken(token, renewInterval);
+ expiration = tokenStateService.renewToken(token,
+ renewInterval.orElse(tokenStateService.getDefaultRenewInterval()));
} catch (Exception e) {
error = e.getMessage();
}
@@ -334,7 +348,10 @@
// Optional token store service persistence
if (tokenStateService != null) {
- tokenStateService.addToken(accessToken, System.currentTimeMillis(), expires);
+ tokenStateService.addToken(accessToken,
+ System.currentTimeMillis(),
+ expires,
+ maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
}
return Response.ok().entity(jsonResponse).build();
diff --git a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index e78ae1b..e2eed03 100644
--- a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -49,6 +49,7 @@
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
+import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -680,6 +681,40 @@
}
@Test
+ public void testTokenRenewal_Enabled_WithDefaultMaxTokenLifetime() throws Exception {
+ final String caller = "yarn";
+
+ // Max lifetime duration is 10ms
+ Map.Entry<TestTokenStateService, Response> testResult =
+ doTestTokenRenewal(true, caller, null, createTestSubject(caller));
+
+ TestTokenStateService tss = testResult.getKey();
+ assertEquals(1, tss.issueTimes.size());
+ String token = tss.issueTimes.keySet().iterator().next();
+
+ // Verify that the configured max lifetime was honored
+ assertEquals(tss.getDefaultMaxLifetimeDuration(), tss.getMaxLifetime(token) - tss.getIssueTime(token));
+ }
+
+
+ @Test
+ public void testTokenRenewal_Enabled_WithConfigurableMaxTokenLifetime() throws Exception {
+ final String caller = "yarn";
+
+ // Max lifetime duration is 10ms
+ Map.Entry<TestTokenStateService, Response> testResult =
+ doTestTokenRenewal(true, caller, 10L, createTestSubject(caller));
+
+ TestTokenStateService tss = testResult.getKey();
+ assertEquals(1, tss.issueTimes.size());
+ String token = tss.issueTimes.keySet().iterator().next();
+
+ // Verify that the configured max lifetime was honored
+ assertEquals(10L, tss.getMaxLifetime(token) - tss.getIssueTime(token));
+ }
+
+
+ @Test
public void testTokenRevocation_ServerManagedStateNotConfigured() throws Exception {
Response renewalResponse = doTestTokenRevocation(null, null, null);
validateRevocationResponse(renewalResponse,
@@ -743,6 +778,7 @@
validateSuccessfulRevocationResponse(renewalResponse);
}
+
/**
*
* @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
@@ -756,7 +792,29 @@
private Response doTestTokenRenewal(final Boolean isTokenStateServerManaged,
final String renewers,
final Subject caller) throws Exception {
- return doTestTokenLifecyle(TokenLifecycleOperation.Renew, isTokenStateServerManaged, renewers, caller);
+ return doTestTokenRenewal(isTokenStateServerManaged, renewers, null, caller).getValue();
+ }
+
+ /**
+ *
+ * @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
+ * @param renewers A comma-delimited list of permitted renewer user names
+ * @param maxTokenLifetime The maximum duration (milliseconds) for a token's lifetime
+ * @param caller The user name making the request
+ *
+ * @return The Response from the token renewal request
+ *
+ * @throws Exception
+ */
+ private Map.Entry<TestTokenStateService, Response> doTestTokenRenewal(final Boolean isTokenStateServerManaged,
+ final String renewers,
+ final Long maxTokenLifetime,
+ final Subject caller) throws Exception {
+ return doTestTokenLifecyle(TokenLifecycleOperation.Renew,
+ isTokenStateServerManaged,
+ renewers,
+ maxTokenLifetime,
+ caller);
}
/**
@@ -776,19 +834,38 @@
}
/**
- * @param operation A TokenLifecycleOperation
- * @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
- * @param renewers A comma-delimited list of permitted renewer user names
- * @param caller The user name making the request
+ * @param operation A TokenLifecycleOperation
+ * @param serverManaged true, if server-side token state management should be enabled; Otherwise, false or null.
+ * @param renewers A comma-delimited list of permitted renewer user names
+ * @param caller The user name making the request
*
* @return The Response from the token revocation request
*
* @throws Exception
*/
private Response doTestTokenLifecyle(final TokenLifecycleOperation operation,
- final Boolean isTokenStateServerManaged,
- final String renewers,
- final Subject caller) throws Exception {
+ final Boolean serverManaged,
+ final String renewers,
+ final Subject caller) throws Exception {
+ return doTestTokenLifecyle(operation, serverManaged, renewers, null, caller).getValue();
+ }
+
+ /**
+ * @param operation A TokenLifecycleOperation
+ * @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
+ * @param renewers A comma-delimited list of permitted renewer user names
+ * @param maxTokenLifetime The maximum lifetime duration for a token.
+ * @param caller The user name making the request
+ *
+ * @return The Response from the token revocation request
+ *
+ * @throws Exception
+ */
+ private Map.Entry<TestTokenStateService, Response> doTestTokenLifecyle(final TokenLifecycleOperation operation,
+ final Boolean isTokenStateServerManaged,
+ final String renewers,
+ final Long maxTokenLifetime,
+ final Subject caller) throws Exception {
ServletContext context = EasyMock.createNiceMock(ServletContext.class);
EasyMock.expect(context.getInitParameter("knox.token.audiences")).andReturn("recipient1,recipient2");
EasyMock.expect(context.getInitParameter("knox.token.ttl")).andReturn(String.valueOf(Long.MAX_VALUE));
@@ -796,7 +873,11 @@
EasyMock.expect(context.getInitParameter("knox.token.client.data")).andReturn(null);
if (isTokenStateServerManaged != null) {
EasyMock.expect(context.getInitParameter("knox.token.exp.server-managed"))
- .andReturn(String.valueOf(isTokenStateServerManaged));
+ .andReturn(String.valueOf(isTokenStateServerManaged));
+ if (maxTokenLifetime != null) {
+ EasyMock.expect(context.getInitParameter("knox.token.exp.renew-interval")).andReturn(String.valueOf(maxTokenLifetime / 2));
+ EasyMock.expect(context.getInitParameter("knox.token.exp.max-lifetime")).andReturn(maxTokenLifetime.toString());
+ }
}
EasyMock.expect(context.getInitParameter("knox.token.renewer.whitelist")).andReturn(renewers);
@@ -812,7 +893,7 @@
JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey);
EasyMock.expect(services.getService(ServiceType.TOKEN_SERVICE)).andReturn(authority).anyTimes();
- TokenStateService tss = new TestTokenStateService();
+ TestTokenStateService tss = new TestTokenStateService();
EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
EasyMock.replay(principal, services, context, request);
@@ -842,7 +923,8 @@
default:
throw new Exception("Invalid operation: " + operation);
}
- return response;
+
+ return new AbstractMap.SimpleEntry<>(tss, response);
}
private static Response requestTokenRenewal(final TokenResource tr, final String tokenData, final Subject caller) {
@@ -941,13 +1023,48 @@
private static class TestTokenStateService implements TokenStateService {
+
+ private Map<String, Long> expirationData = new HashMap<>();
+ private Map<String, Long> issueTimes = new HashMap<>();
+ private Map<String, Long> maxLifetimes = new HashMap<>();
+
+ long getIssueTime(final String token) {
+ return issueTimes.get(token);
+ }
+
+ long getMaxLifetime(final String token) {
+ return maxLifetimes.get(token);
+ }
+
+ long getExpiration(final String token) {
+ return expirationData.get(token);
+ }
+
@Override
public void addToken(JWTToken token, long issueTime) {
addToken(token.getPayload(), issueTime, token.getExpiresDate().getTime());
}
@Override
+ public long getDefaultRenewInterval() {
+ return 250;
+ }
+
+ @Override
+ public long getDefaultMaxLifetimeDuration() {
+ return 500;
+ }
+
+ @Override
public void addToken(String token, long issueTime, long expiration) {
+ addToken(token, issueTime, expiration, getDefaultMaxLifetimeDuration());
+ }
+
+ @Override
+ public void addToken(String token, long issueTime, long expiration, long maxLifetimeDuration) {
+ issueTimes.put(token, issueTime);
+ expirationData.put(token, expiration);
+ maxLifetimes.put(token, issueTime + maxLifetimeDuration);
}
@Override
@@ -980,12 +1097,12 @@
}
@Override
- public long renewToken(JWTToken token, Long renewInterval) {
+ public long renewToken(JWTToken token, long renewInterval) {
return renewToken(token.getPayload());
}
@Override
- public long renewToken(String token, Long renewInterval) {
+ public long renewToken(String token, long renewInterval) {
return 0;
}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
index 2ab5721..dc0b736 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
@@ -28,13 +28,22 @@
String CONFIG_SERVER_MANAGED = "knox.token.exp.server-managed";
/**
+ * @return The default duration (in milliseconds) for which a token's life will be extended when it is renewed.
+ */
+ long getDefaultRenewInterval();
+
+ /**
+ * @return The default maximum lifetime duration (in milliseconds) of a token.
+ */
+ long getDefaultMaxLifetimeDuration();
+
+ /**
* Add state for the specified token.
*
* @param token The token.
* @param issueTime The time the token was issued.
*/
void addToken(JWTToken token, long issueTime);
-
/**
* Add state for the specified token.
*
@@ -45,6 +54,16 @@
void addToken(String token, long issueTime, long expiration);
/**
+ * Add state for the specified token.
+ *
+ * @param token The token.
+ * @param issueTime The time the token was issued.
+ * @param expiration The token expiration time.
+ * @param maxLifetimeDuration The maximum allowed lifetime for the token.
+ */
+ void addToken(String token, long issueTime, long expiration, long maxLifetimeDuration);
+
+ /**
*
* @param token The token.
*
@@ -91,7 +110,7 @@
*
* @return The token's updated expiration time in milliseconds.
*/
- long renewToken(JWTToken token, Long renewInterval);
+ long renewToken(JWTToken token, long renewInterval);
/**
* Extend the lifetime of the specified token by the default amount of time.
@@ -110,7 +129,7 @@
*
* @return The token's updated expiration time in milliseconds.
*/
- long renewToken(String token, Long renewInterval);
+ long renewToken(String token, long renewInterval);
/**
*