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);
 
   /**
    *