knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pzamp...@apache.org
Subject [knox] branch master updated: KNOX-2067 - KnoxToken service support for renewal and revocation
Date Thu, 24 Oct 2019 18:09:02 GMT
This is an automated email from the ASF dual-hosted git repository.

pzampino pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new db8cc23  KNOX-2067 - KnoxToken service support for renewal and revocation
db8cc23 is described below

commit db8cc2347070ce5956ad6e881224c285f825acb6
Author: pzampino <pzampino@cloudera.com>
AuthorDate: Thu Oct 24 11:52:06 2019 -0400

    KNOX-2067 - KnoxToken service support for renewal and revocation
---
 build.xml                                          |   4 +-
 .../federation/jwt/filter/AbstractJWTFilter.java   |  19 +-
 .../jwt/filter/AccessTokenFederationFilter.java    |  26 +-
 .../federation/jwt/filter/JWTFederationFilter.java |   2 +-
 .../org/apache/knox/gateway/GatewayMessages.java   |   9 +
 .../gateway/services/DefaultGatewayServices.java   |   7 +-
 .../token/impl/AliasBasedTokenStateService.java    | 140 ++++++++
 .../token/impl/DefaultTokenStateService.java       | 248 +++++++++++++
 .../services/AbstractGatewayServicesTest.java      |   1 +
 .../impl/AliasBasedTokenStateServiceTest.java      | 146 ++++++++
 .../token/impl/DefaultTokenStateServiceTest.java   | 158 +++++++++
 gateway-service-knoxtoken/pom.xml                  |  14 +
 .../gateway/service/knoxtoken/TokenResource.java   | 118 ++++++-
 .../service/knoxtoken/TokenServiceMessages.java    |   4 +
 .../knoxtoken/TokenServiceResourceTest.java        | 382 +++++++++++++++++++++
 .../apache/knox/gateway/services/ServiceType.java  |   1 +
 .../services/security/token/TokenStateService.java | 123 +++++++
 pom.xml                                            |   6 +
 18 files changed, 1389 insertions(+), 19 deletions(-)

diff --git a/build.xml b/build.xml
index e69c8ef..9db6a17 100644
--- a/build.xml
+++ b/build.xml
@@ -24,8 +24,8 @@ Release build file for the Apache Knox Gateway
 <property name="gateway-name" value="Apache Knox"/>
 <property name="gateway-project" value="knox"/>
 <property name="gateway-artifact" value="knox"/>
-    <property name="knoxshell-artifact" value="knoxshell"/>
-    <property name="gateway-version" value="1.4.0-SNAPSHOT"/>
+<property name="knoxshell-artifact" value="knoxshell"/>
+<property name="gateway-version" value="1.4.0-SNAPSHOT"/>
 <property name="release-manager" value="kminder"/>
 
 <property name="gateway-home" value="${gateway-artifact}-${gateway-version}"/>
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
index 58a34ed..6e92241 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
@@ -56,6 +56,7 @@ import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
 import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 
 import com.nimbusds.jose.JWSHeader;
@@ -87,6 +88,8 @@ public abstract class AbstractJWTFilter implements Filter {
   private String expectedIssuer;
   private String expectedSigAlg;
 
+  private TokenStateService tokenStateService;
+
   @Override
   public abstract void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
       throws IOException, ServletException;
@@ -105,6 +108,9 @@ public abstract class AbstractJWTFilter implements Filter {
       GatewayServices services = (GatewayServices) context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
       if (services != null) {
         authority = services.getService(ServiceType.TOKEN_SERVICE);
+        if (Boolean.valueOf(filterConfig.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED))) {
+          tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+        }
       }
     }
   }
@@ -136,10 +142,15 @@ public abstract class AbstractJWTFilter implements Filter {
   }
 
   protected boolean tokenIsStillValid(JWT jwtToken) {
-    // if there is no expiration date then the lifecycle is tied entirely to
-    // the cookie validity - otherwise ensure that the current time is before
-    // the designated expiration time
-    Date expires = jwtToken.getExpiresDate();
+    Date expires;
+    if (tokenStateService != null) {
+      expires = new Date(tokenStateService.getTokenExpiration(jwtToken.toString()));
+    } else {
+      // if there is no expiration date then the lifecycle is tied entirely to
+      // the cookie validity - otherwise ensure that the current time is before
+      // the designated expiration time
+      expires = jwtToken.getExpiresDate();
+    }
     return expires == null || new Date().before(expires);
   }
 
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
index cf8d530..1b82fa0 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
@@ -24,6 +24,7 @@ import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
 import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
 
 import javax.security.auth.Subject;
@@ -50,10 +51,16 @@ public class AccessTokenFederationFilter implements Filter {
 
   private JWTokenAuthority authority;
 
+  private TokenStateService tokenStateService;
+
   @Override
   public void init( FilterConfig filterConfig ) throws ServletException {
     GatewayServices services = (GatewayServices) filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
     authority = services.getService(ServiceType.TOKEN_SERVICE);
+
+    if (Boolean.valueOf(filterConfig.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED))) {
+      tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+    }
   }
 
   @Override
@@ -81,33 +88,32 @@ public class AccessTokenFederationFilter implements Filter {
         log.unableToVerifyToken(e);
       }
       if (verified) {
-        long expires = Long.parseLong(token.getExpires());
-        if (expires > System.currentTimeMillis()) {
+        if (!isExpired(token)) {
           if (((HttpServletRequest) request).getRequestURL().indexOf(token.getAudience().toLowerCase(Locale.ROOT)) != -1) {
             Subject subject = createSubjectFromToken(token);
             continueWithEstablishedSecurityContext(subject, (HttpServletRequest)request, (HttpServletResponse)response, chain);
-          }
-          else {
+          } else {
             log.failedToValidateAudience();
             sendUnauthorized(response);
           }
-        }
-        else {
+        } else {
           log.tokenHasExpired();
           sendUnauthorized(response);
         }
-      }
-      else {
+      } else {
         log.failedToVerifyTokenSignature();
         sendUnauthorized(response);
       }
-    }
-    else {
+    } else {
       log.missingBearerToken();
       sendUnauthorized(response);
     }
   }
 
+  private boolean isExpired(JWTToken token) {
+    return (tokenStateService != null) ? tokenStateService.isExpired(token.toString()) : (Long.parseLong(token.getExpires()) <= System.currentTimeMillis());
+  }
+
   private void sendUnauthorized(ServletResponse response) throws IOException {
     ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
   }
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
index 1f40d57..8d49f7f 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
@@ -43,7 +43,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
 
   @Override
   public void init( FilterConfig filterConfig ) throws ServletException {
-      super.init(filterConfig);
+    super.init(filterConfig);
 
     // expected audiences or null
     String expectedAudiences = filterConfig.getInitParameter(KNOX_TOKEN_AUDIENCES);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index aa019da..0656842 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -653,6 +653,15 @@ public interface GatewayMessages {
   @Message(level = MessageLevel.ERROR, text = "Failed to remove credential: {1}")
   void failedToRemoveCredential(@StackTrace(level = MessageLevel.DEBUG) Exception e);
 
+  @Message(level = MessageLevel.ERROR, text = "Failed to save token state: {0}")
+  void failedToSaveTokenState(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR, text = "Error accessing token state: {0}")
+  void errorAccessingTokenState(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.ERROR, text = "Failed to update token expiration: {0}")
+  void failedToUpdateTokenExpiration(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
   @Message(level = MessageLevel.INFO, text = "Starting service: {0}")
   void startingService(String serviceTypeName);
 
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 7ebc80e..1a638b5 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -29,6 +29,7 @@ import org.apache.knox.gateway.services.registry.impl.DefaultServiceDefinitionRe
 import org.apache.knox.gateway.services.metrics.impl.DefaultMetricsService;
 import org.apache.knox.gateway.services.security.KeystoreService;
 import org.apache.knox.gateway.services.security.impl.RemoteAliasService;
+import org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService;
 import org.apache.knox.gateway.services.topology.impl.DefaultClusterConfigurationMonitorService;
 import org.apache.knox.gateway.services.topology.impl.DefaultTopologyService;
 import org.apache.knox.gateway.services.hostmap.impl.DefaultHostMapperService;
@@ -112,6 +113,11 @@ public class DefaultGatewayServices extends AbstractGatewayServices {
     // prolly should not allow the token service to be looked up?
     addService(ServiceType.TOKEN_SERVICE, ts);
 
+    AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+    tss.setAliasService(alias);
+    tss.init(config, options);
+    addService(ServiceType.TOKEN_STATE_SERVICE, tss);
+
     DefaultServiceRegistryService sr = new DefaultServiceRegistryService();
     sr.setCryptoService( crypto );
     sr.init( config, options );
@@ -125,7 +131,6 @@ public class DefaultGatewayServices extends AbstractGatewayServices {
     sis.init( config, options );
     addService(ServiceType.SERVER_INFO_SERVICE, sis );
 
-
     DefaultClusterConfigurationMonitorService ccs = new DefaultClusterConfigurationMonitorService();
     ccs.setAliasService(alias);
     ccs.init(config, options);
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
new file mode 100644
index 0000000..4da9ad6
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
@@ -0,0 +1,140 @@
+/*
+ * 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 org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+
+import java.util.Map;
+
+/**
+ * A TokenStateService implementation based on the AliasService.
+ */
+public class AliasBasedTokenStateService extends DefaultTokenStateService {
+
+  private static final GatewayMessages LOG = MessagesFactory.get( GatewayMessages.class);
+
+  private AliasService aliasService;
+
+  public void setAliasService(AliasService aliasService) {
+    this.aliasService = aliasService;
+  }
+
+  @Override
+  public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    super.init(config, options);
+    if (aliasService == null) {
+      throw new ServiceLifecycleException("The required AliasService reference has not been set.");
+    }
+  }
+
+  @Override
+  public void addToken(final String token, final long issueTime, final long expiration) {
+    isValidIdentifier(token);
+
+    try {
+      aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, token, String.valueOf(expiration));
+      setMaxLifetime(token, issueTime);
+    } catch (AliasServiceException e) {
+      LOG.failedToSaveTokenState(e);
+    }
+  }
+
+  @Override
+  protected void setMaxLifetime(final String token, final long issueTime) {
+    try {
+      aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME,
+                                      token + "--max",
+                                      String.valueOf(issueTime + getMaxLifetimeInterval()));
+    } catch (AliasServiceException e) {
+      LOG.failedToSaveTokenState(e);
+    }
+  }
+
+  @Override
+  protected long getMaxLifetime(String token) {
+    long result = 0;
+    try {
+      char[] maxLifetimeStr =
+                      aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, token + "--max");
+      if (maxLifetimeStr != null) {
+        result = Long.parseLong(new String(maxLifetimeStr));
+      }
+    } catch (AliasServiceException e) {
+      LOG.errorAccessingTokenState(e);
+    }
+    return result;
+  }
+
+  @Override
+  public long getTokenExpiration(String token) {
+    long expiration = 0;
+
+    validateToken(token);
+
+    try {
+      char[] expStr = aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, token);
+      if (expStr != null) {
+        expiration = Long.parseLong(new String(expStr));
+      }
+    } catch (Exception e) {
+      LOG.errorAccessingTokenState(e);
+    }
+
+    return expiration;
+  }
+
+  @Override
+  public void revokeToken(String token) {
+    // Record the revocation by setting the expiration to -1
+    updateExpiration(token, -1);
+  }
+
+  @Override
+  protected boolean isRevoked(String token) {
+    return (getTokenExpiration(token) < 0);
+  }
+
+  @Override
+  protected boolean isUnknown(String token) {
+    boolean isUnknown = false;
+    try {
+      isUnknown = (aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, token) == null);
+    } catch (AliasServiceException e) {
+      LOG.errorAccessingTokenState(e);
+    }
+    return isUnknown;
+  }
+
+  @Override
+  protected void updateExpiration(String token, long expiration) {
+    if (isUnknown(token)) {
+      throw new IllegalArgumentException("Unknown token.");
+    }
+
+    try {
+      aliasService.removeAliasForCluster(AliasService.NO_CLUSTER_NAME, token);
+      aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, token, String.valueOf(expiration));
+    } catch (AliasServiceException e) {
+      LOG.failedToUpdateTokenExpiration(e);
+    }
+  }
+}
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
new file mode 100644
index 0000000..6493450
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
@@ -0,0 +1,248 @@
+/*
+ * 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 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.impl.JWTToken;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * In-Memory authentication token state management implementation.
+ */
+public class DefaultTokenStateService implements TokenStateService {
+
+  protected static final long DEFAULT_RENEWAL_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
+
+  protected static final int MAX_RENEWALS = 7;
+
+  protected static final long DEFAULT_MAX_LIFETIME = MAX_RENEWALS * DEFAULT_RENEWAL_INTERVAL; // 7 days
+
+  private final Map<String, Long> tokenExpirations = new HashMap<>();
+
+  private final Set<String> revokedTokens = new HashSet<>();
+
+  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 ?
+  }
+
+  @Override
+  public void start() throws ServiceLifecycleException {
+  }
+
+  @Override
+  public void stop() throws ServiceLifecycleException {
+  }
+
+  @Override
+  public void addToken(final JWTToken token, final long issueTime) {
+    if (token == null) {
+      throw new IllegalArgumentException("Token data cannot be null.");
+    }
+    addToken(token.getPayload(), issueTime, token.getExpiresDate().getTime());
+  }
+
+  @Override
+  public void addToken(final String token, final long issueTime, final long expiration) {
+    if (!isValidIdentifier(token)) {
+      throw new IllegalArgumentException("Token data cannot be null.");
+    }
+    synchronized (tokenExpirations) {
+      tokenExpirations.put(token, expiration);
+    }
+    setMaxLifetime(token, issueTime);
+  }
+
+  @Override
+  public long getTokenExpiration(String token) {
+    long expiration;
+
+    validateToken(token);
+
+    synchronized (tokenExpirations) {
+      expiration = tokenExpirations.get(token);
+    }
+
+    return expiration;
+  }
+
+  @Override
+  public long renewToken(final JWTToken token) {
+    return renewToken(token, DEFAULT_RENEWAL_INTERVAL);
+  }
+
+  @Override
+  public long renewToken(final JWTToken token, final Long renewInterval) {
+    if (token == null) {
+      throw new IllegalArgumentException("Token data cannot be null.");
+    }
+    return renewToken(token.getPayload(), renewInterval);
+  }
+
+  @Override
+  public long renewToken(final String token) { // Should return new expiration?
+    return renewToken(token, DEFAULT_RENEWAL_INTERVAL);
+  }
+
+  @Override
+  public long renewToken(final String token, final Long renewInterval) { // Should return new expiration?
+    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);
+      updateExpiration(token, expiration);
+    } else {
+      throw new IllegalArgumentException("The renewal limit for the token has been exceeded");
+    }
+
+    return expiration;
+  }
+
+  @Override
+  public void revokeToken(final JWTToken token) {
+    if (token == null) {
+      throw new IllegalArgumentException("Token data cannot be null.");
+    }
+
+    revokeToken(token.getPayload());
+  }
+
+  @Override
+  public void revokeToken(final String token) {
+    validateToken(token);
+    revokedTokens.add(token);
+  }
+
+  @Override
+  public boolean isExpired(final JWTToken token) {
+    return isExpired(token.getPayload());
+  }
+
+  @Override
+  public boolean isExpired(final String token) {
+    boolean isExpired;
+
+    isExpired = isRevoked(token); // Check if it has been revoked first
+    if (!isExpired) {
+      // If it has not been revoked, check its expiration
+      isExpired = (getTokenExpiration(token) <= System.currentTimeMillis());
+    }
+
+    return isExpired;
+  }
+
+  protected void setMaxLifetime(final String token, final long issueTime) {
+    synchronized (maxTokenLifetimes) {
+      maxTokenLifetimes.put(token, issueTime + maxLifetimeInterval);
+    }
+  }
+
+  /**
+   * @param token
+   * @return false, if the service has previously stored the specified token; Otherwise, true.
+   */
+  protected boolean isUnknown(final String token) {
+    boolean isUnknown;
+
+    synchronized (tokenExpirations) {
+      isUnknown = !(tokenExpirations.containsKey(token));
+    }
+
+    return isUnknown;
+  }
+
+  protected void updateExpiration(final String token, long expiration) {
+    synchronized (tokenExpirations) {
+      tokenExpirations.replace(token, expiration);
+    }
+  }
+
+  protected boolean hasRemainingRenewals(final String token, final 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));
+  }
+
+  protected long getMaxLifetime(final String token) {
+    long result;
+    synchronized (maxTokenLifetimes) {
+      result = maxTokenLifetimes.getOrDefault(token, 0L);
+    }
+    return result;
+  }
+
+  protected boolean isRevoked(final String token) {
+    return revokedTokens.contains(token);
+  }
+
+  protected long getMaxLifetimeInterval() {
+    return maxLifetimeInterval;
+  }
+
+  protected boolean isValidIdentifier(final String token) {
+    return token != null && !token.isEmpty();
+  }
+
+  /**
+   * Validate the specified token identifier.
+   *
+   * @param token The token identifier to validate.
+   *
+   * @throws IllegalArgumentException if the specified token in invalid.
+   */
+  protected void validateToken(final String token) throws IllegalArgumentException {
+    validateToken(token, false);
+  }
+
+  /**
+   * Validate the specified token identifier.
+   *
+   * @param token              The token identifier to validate.
+   * @param includeRevocation  true, if the revocation status of the specified token should be considered in the validation.
+   *
+   * @throws IllegalArgumentException if the specified token in invalid.
+   */
+  protected void validateToken(final String token, final boolean includeRevocation) throws IllegalArgumentException {
+    if (!isValidIdentifier(token)) {
+      throw new IllegalArgumentException("Token data cannot be null.");
+    }
+
+    // First, make sure the token is one we know about
+    if (isUnknown(token)) {
+      throw new IllegalArgumentException("Unknown token");
+    }
+
+    // Then, make sure it has not been revoked
+    if (includeRevocation && isRevoked(token)) {
+      throw new IllegalArgumentException("The specified token has been revoked");
+    }
+  }
+
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
index e6c2ef1..1f0a352 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
@@ -52,6 +52,7 @@ public class AbstractGatewayServicesTest {
         ServiceType.KEYSTORE_SERVICE,
         ServiceType.ALIAS_SERVICE,
         ServiceType.SSL_SERVICE,
+        ServiceType.TOKEN_STATE_SERVICE,
         ServiceType.TOKEN_SERVICE,
         ServiceType.SERVER_INFO_SERVICE,
         ServiceType.REMOTE_REGISTRY_CLIENT_SERVICE,
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
new file mode 100644
index 0000000..d982186
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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 org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+
+import java.security.cert.Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTest {
+
+  @Override
+  protected TokenStateService createTokenStateService() {
+    AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+    tss.setAliasService(new TestAliasService());
+    initTokenStateService(tss);
+    return tss;
+  }
+
+  /**
+   * A dumbed-down AliasService implementation for testing purposes only.
+   */
+  private static final class TestAliasService implements AliasService {
+
+    private final Map<String, Map<String, String>> clusterAliases= new HashMap<>();
+
+
+    @Override
+    public List<String> getAliasesForCluster(String clusterName) throws AliasServiceException {
+      List<String> aliases = new ArrayList<>();
+
+      if (clusterAliases.containsKey(clusterName)) {
+          aliases.addAll(clusterAliases.get(clusterName).keySet());
+      }
+      return aliases;
+    }
+
+    @Override
+    public void addAliasForCluster(String clusterName, String alias, String value) throws AliasServiceException {
+      Map<String, String> aliases = null;
+      if (clusterAliases.containsKey(clusterName)) {
+        aliases = clusterAliases.get(clusterName);
+      } else {
+        aliases = new HashMap<>();
+        clusterAliases.put(clusterName, aliases);
+      }
+      aliases.put(alias, value);
+    }
+
+    @Override
+    public void removeAliasForCluster(String clusterName, String alias) throws AliasServiceException {
+      if (clusterAliases.containsKey(clusterName)) {
+        clusterAliases.get(clusterName).remove(alias);
+      }
+    }
+
+    @Override
+    public char[] getPasswordFromAliasForCluster(String clusterName, String alias) throws AliasServiceException {
+      char[] value = null;
+      if (clusterAliases.containsKey(clusterName)) {
+        String valString = clusterAliases.get(clusterName).get(alias);
+        if (valString != null) {
+          value = valString.toCharArray();
+        }
+      }
+      return value;
+    }
+
+    @Override
+    public char[] getPasswordFromAliasForCluster(String clusterName, String alias, boolean generate) throws AliasServiceException {
+      return new char[0];
+    }
+
+    @Override
+    public void generateAliasForCluster(String clusterName, String alias) throws AliasServiceException {
+    }
+
+    @Override
+    public char[] getPasswordFromAliasForGateway(String alias) throws AliasServiceException {
+      return getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, alias);
+    }
+
+    @Override
+    public char[] getGatewayIdentityPassphrase() throws AliasServiceException {
+      return new char[0];
+    }
+
+    @Override
+    public char[] getGatewayIdentityKeystorePassword() throws AliasServiceException {
+      return new char[0];
+    }
+
+    @Override
+    public char[] getSigningKeyPassphrase() throws AliasServiceException {
+      return new char[0];
+    }
+
+    @Override
+    public char[] getSigningKeystorePassword() throws AliasServiceException {
+      return new char[0];
+    }
+
+    @Override
+    public void generateAliasForGateway(String alias) throws AliasServiceException {
+    }
+
+    @Override
+    public Certificate getCertificateForGateway(String alias) throws AliasServiceException {
+      return null;
+    }
+
+    @Override
+    public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void start() throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void stop() throws ServiceLifecycleException {
+    }
+  }
+
+}
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
new file mode 100644
index 0000000..cb909d8
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 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.impl.JWTToken;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class DefaultTokenStateServiceTest {
+
+  @Test
+  public void testGetExpiration() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+    final TokenStateService tss = createTokenStateService();
+
+    tss.addToken(token, System.currentTimeMillis());
+    long expiration = tss.getTokenExpiration(token.getPayload());
+    assertEquals(token.getExpiresDate().getTime(), expiration);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testGetExpiration_NullToken() {
+    // Expecting an IllegalArgumentException because the token is null
+    createTokenStateService().getTokenExpiration(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testGetExpiration_EmptyToken() {
+    // Expecting an IllegalArgumentException because the token is empty
+    createTokenStateService().getTokenExpiration("");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testGetExpiration_InvalidToken() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+
+    // Expecting an IllegalArgumentException because the token is not known to the TokenStateService
+    createTokenStateService().getTokenExpiration(token.getPayload());
+  }
+
+  @Test
+  public void testGetExpiration_AfterRenewal() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+    final TokenStateService tss = createTokenStateService();
+
+    tss.addToken(token, System.currentTimeMillis());
+    long expiration = tss.getTokenExpiration(token.getPayload());
+    assertEquals(token.getExpiresDate().getTime(), expiration);
+
+    long newExpiration = tss.renewToken(token);
+    assertTrue(newExpiration > token.getExpiresDate().getTime());
+    assertTrue(tss.getTokenExpiration(token.getPayload()) > token.getExpiresDate().getTime());
+  }
+
+  @Test
+  public void testIsExpired_Negative() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+    final TokenStateService tss = createTokenStateService();
+
+    tss.addToken(token, System.currentTimeMillis());
+    assertFalse(tss.isExpired(token));
+  }
+
+  @Test
+  public void testIsExpired_Positive() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() - 60000);
+    final TokenStateService tss = createTokenStateService();
+
+    tss.addToken(token, System.currentTimeMillis());
+    assertTrue(tss.isExpired(token));
+  }
+
+
+  @Test
+  public void testIsExpired_Revoked() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+    final TokenStateService tss = createTokenStateService();
+
+    tss.addToken(token, System.currentTimeMillis());
+    assertFalse("Expected the token to be valid.", tss.isExpired(token));
+
+    tss.revokeToken(token);
+    assertTrue("Expected the token to have been marked as revoked.", tss.isExpired(token));
+  }
+
+
+  @Test
+  public void testRenewal() {
+    final JWTToken token = createMockToken(System.currentTimeMillis() - 60000);
+    final TokenStateService tss = createTokenStateService();
+
+    // Add the expired token
+    tss.addToken(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));
+  }
+
+
+  protected static JWTToken createMockToken(final long expiration) {
+    return createMockToken("ABCD1234", expiration);
+  }
+
+  protected static JWTToken createMockToken(final String payload, final long expiration) {
+    JWTToken token = EasyMock.createNiceMock(JWTToken.class);
+    EasyMock.expect(token.getPayload()).andReturn(payload).anyTimes();
+    EasyMock.expect(token.getExpiresDate()).andReturn(new Date(expiration)).anyTimes();
+    EasyMock.replay(token);
+    return token;
+  }
+
+  protected static GatewayConfig createMockGatewayConfig() {
+    GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.replay(config);
+    return config;
+  }
+
+  protected void initTokenStateService(TokenStateService tss) {
+    try {
+      tss.init(createMockGatewayConfig(), Collections.emptyMap());
+    } catch (ServiceLifecycleException e) {
+      fail("Error creating TokenStateService: " + e.getMessage());
+    }
+  }
+
+  protected TokenStateService createTokenStateService() {
+    TokenStateService tss = new DefaultTokenStateService();
+    initTokenStateService(tss);
+    return tss;
+  }
+
+}
diff --git a/gateway-service-knoxtoken/pom.xml b/gateway-service-knoxtoken/pom.xml
index 61c758d..5a4d2f1 100644
--- a/gateway-service-knoxtoken/pom.xml
+++ b/gateway-service-knoxtoken/pom.xml
@@ -60,6 +60,10 @@
             <artifactId>javax.annotation-api</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.glassfish.hk2.external</groupId>
+            <artifactId>javax.inject</artifactId>
+        </dependency>
+        <dependency>
             <groupId>javax.ws.rs</groupId>
             <artifactId>javax.ws.rs-api</artifactId>
         </dependency>
@@ -78,5 +82,15 @@
             <artifactId>gateway-test-utils</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
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 0d2c0e9..fd01428 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
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.List;
 
 import javax.annotation.PostConstruct;
+import javax.inject.Singleton;
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.GET;
@@ -39,18 +40,21 @@ import javax.ws.rs.core.Response;
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.security.SubjectUtils;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.security.KeystoreService;
 import org.apache.knox.gateway.services.security.KeystoreServiceException;
 import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
 import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.util.JsonUtils;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static javax.ws.rs.core.MediaType.APPLICATION_XML;
 
+@Singleton
 @Path(TokenResource.RESOURCE_PATH)
 public class TokenResource {
   private static final String EXPIRES_IN = "expires_in";
@@ -66,8 +70,12 @@ public class TokenResource {
   private static final String TOKEN_CLIENT_CERT_REQUIRED = "knox.token.client.cert.required";
   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_RENEWER_WHITELIST = "knox.token.renewer.whitelist";
   private static final long TOKEN_TTL_DEFAULT = 30000L;
   static final String RESOURCE_PATH = "knoxtoken/api/v1/token";
+  static final String RENEW_PATH = "/renew";
+  static final String REVOKE_PATH = "/revoke";
   private static final String TARGET_ENDPOINT_PULIC_CERT_PEM = "knox.token.target.endpoint.cert.pem";
   private static TokenServiceMessages log = MessagesFactory.get(TokenServiceMessages.class);
   private long tokenTTL = TOKEN_TTL_DEFAULT;
@@ -79,6 +87,13 @@ public class TokenResource {
   private String signatureAlgorithm = "RS256";
   private String endpointPublicCert;
 
+  // Optional token store service
+  private TokenStateService tokenStateService;
+
+  private Long renewInterval;
+
+  private List<String> allowedRenewers;
+
   @Context
   HttpServletRequest request;
 
@@ -138,6 +153,29 @@ public class TokenResource {
     if (targetEndpointPublicCert != null) {
       endpointPublicCert = targetEndpointPublicCert;
     }
+
+    // If server-managed token expiration is configured, set the token store service
+    if (Boolean.valueOf(context.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED))) {
+      GatewayServices services = (GatewayServices) context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+      tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+
+      String renewIntervalValue = context.getInitParameter(TOKEN_EXP_RENEWAL_INTERVAL);
+      if (renewIntervalValue != null && !renewIntervalValue.isEmpty()) {
+        try {
+          renewInterval = Long.parseLong(renewIntervalValue);
+        } catch (NumberFormatException e) {
+          log.invalidConfigValue(TOKEN_EXP_RENEWAL_INTERVAL, renewIntervalValue, e);
+        }
+      }
+
+      allowedRenewers = new ArrayList<>();
+      String renewerList = context.getInitParameter(TOKEN_RENEWER_WHITELIST);
+      if (renewerList != null && !renewerList.isEmpty()) {
+        for (String renewer : renewerList.split(",")) {
+          allowedRenewers.add(renewer.trim());
+        }
+      }
+    }
   }
 
   @GET
@@ -152,6 +190,80 @@ public class TokenResource {
     return getAuthenticationToken();
   }
 
+  @POST
+  @Path(RENEW_PATH)
+  @Produces({APPLICATION_JSON})
+  public Response renew(String token) {
+    Response resp;
+
+    long expiration = 0;
+    String  error   = "";
+
+    if (tokenStateService == null) {
+      error = "Token renewal support is not configured";
+    } else {
+      String renewer = SubjectUtils.getCurrentEffectivePrincipalName();
+      if (allowedRenewers.contains(renewer)) {
+        try {
+          // If renewal fails, it should be an exception
+          expiration = tokenStateService.renewToken(token, renewInterval);
+        } catch (Exception e) {
+          error = e.getMessage();
+        }
+      } else {
+        error = "Caller (" + renewer + ") not authorized to renew tokens.";
+      }
+    }
+
+    if(error.isEmpty()) {
+      resp =  Response.status(Response.Status.OK)
+                      .entity("{\n  \"renewed\": \"true\",\n  \"expires\": \"" + expiration + "\"\n}\n")
+                      .build();
+    } else {
+      resp = Response.status(Response.Status.BAD_REQUEST)
+                     .entity("{\n  \"renewed\": \"false\",\n  \"error\": \"" + error + "\"\n}\n")
+                     .build();
+    }
+
+    return resp;
+  }
+
+  @POST
+  @Path(REVOKE_PATH)
+  @Produces({APPLICATION_JSON})
+  public Response revoke(String token) {
+    Response resp;
+
+    String error = "";
+
+    if (tokenStateService == null) {
+      error = "Token revocation support is not configured";
+    } else {
+      String renewer = SubjectUtils.getCurrentEffectivePrincipalName();
+      if (allowedRenewers.contains(renewer)) {
+        try {
+          tokenStateService.revokeToken(token);
+        } catch (IllegalArgumentException e) {
+          error = e.getMessage();
+        }
+      } else {
+        error = "Caller (" + renewer + ") not authorized to revoke tokens.";
+      }
+    }
+
+    if(error.isEmpty()) {
+      resp =  Response.status(Response.Status.OK)
+                      .entity("{\n  \"revoked\": \"true\"\n}\n")
+                      .build();
+    } else {
+      resp = Response.status(Response.Status.BAD_REQUEST)
+                     .entity("{\n  \"revoked\": \"false\",\n  \"error\": \"" + error + "\"\n}\n")
+                     .build();
+    }
+
+    return resp;
+  }
+
   private X509Certificate extractCertificate(HttpServletRequest req) {
     X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
     if (null != certs && certs.length > 0) {
@@ -204,7 +316,6 @@ public class TokenResource {
 
       if (token != null) {
         String accessToken = token.toString();
-
         HashMap<String, Object> map = new HashMap<>();
         map.put(ACCESS_TOKEN, accessToken);
         map.put(TOKEN_TYPE, BEARER);
@@ -221,6 +332,11 @@ public class TokenResource {
 
         String jsonResponse = JsonUtils.renderAsJsonString(map);
 
+        // Optional token store service persistence
+        if (tokenStateService != null) {
+          tokenStateService.addToken(accessToken, System.currentTimeMillis(), expires);
+        }
+
         return Response.ok().entity(jsonResponse).build();
       } else {
         return Response.serverError().build();
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
index f9d97ad..c0c2e52 100644
--- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
@@ -66,4 +66,8 @@ public interface TokenServiceMessages {
 
   @Message( level = MessageLevel.WARN, text = "Unable to acquire cert for endpoint clients - assume trust will be provisioned separately: {0}.")
   void unableToAcquireCertForEndpointClients(@StackTrace( level = MessageLevel.DEBUG ) Exception e);
+
+  @Message( level = MessageLevel.ERROR, text = "The specified value for the {0} configuration property is not valid: {1}")
+  void invalidConfigValue(String name, String value, @StackTrace( level = MessageLevel.DEBUG ) Exception e);
+
 }
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 e6c106e..e78ae1b 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
@@ -17,14 +17,19 @@
  */
 package org.apache.knox.gateway.service.knoxtoken;
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JWSSigner;
 import com.nimbusds.jose.JWSVerifier;
 import com.nimbusds.jose.crypto.RSASSASigner;
 import com.nimbusds.jose.crypto.RSASSAVerifier;
+import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
 import org.easymock.EasyMock;
@@ -36,9 +41,11 @@ import javax.security.auth.Subject;
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.Response;
+import java.io.IOException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.Principal;
+import java.security.PrivilegedAction;
 import java.security.cert.X509Certificate;
 import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
@@ -49,8 +56,10 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -62,6 +71,11 @@ public class TokenServiceResourceTest {
   private static RSAPublicKey publicKey;
   private static RSAPrivateKey privateKey;
 
+  private enum TokenLifecycleOperation {
+    Renew,
+    Revoke
+  }
+
   @BeforeClass
   public static void setUpBeforeClass() throws Exception {
     KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
@@ -610,6 +624,286 @@ public class TokenServiceResourceTest {
     assertTrue((expiresDate.getTime() - now.getTime()) < 30000L);
   }
 
+  @Test
+  public void testTokenRenewal_ServerManagedStateNotConfigured() throws Exception {
+    Response renewalResponse = doTestTokenRenewal(null, null, null);
+    validateRenewalResponse(renewalResponse, 400, false, "Token renewal support is not configured");
+  }
+
+  @Test
+  public void testTokenRenewal_Disabled() throws Exception {
+    Response renewalResponse = doTestTokenRenewal(false, null, null);
+    validateRenewalResponse(renewalResponse, 400, false, "Token renewal support is not configured");
+  }
+
+  @Test
+  public void testTokenRenewal_Enabled_NoRenewersNoSubject() throws Exception {
+    Response renewalResponse = doTestTokenRenewal(true, null, null);
+    validateRenewalResponse(renewalResponse, 400, false, "Caller (null) not authorized to renew tokens.");
+  }
+
+  @Test
+  public void testTokenRenewal_Enabled_NoRenewersWithSubject() throws Exception {
+    final String caller = "yarn";
+    Response renewalResponse = doTestTokenRenewal(true, null, createTestSubject(caller));
+    validateRenewalResponse(renewalResponse,
+                            400,
+                            false,
+                            "Caller (" + caller + ") not authorized to renew tokens.");
+  }
+
+  @Test
+  public void testTokenRenewal_Enabled_WithRenewersNoSubject() throws Exception {
+    Response renewalResponse = doTestTokenRenewal(true, "larry, moe,  curly ", null);
+    validateRenewalResponse(renewalResponse,
+                            400,
+                            false,
+                            "Caller (null) not authorized to renew tokens.");
+  }
+
+  @Test
+  public void testTokenRenewal_Enabled_WithRenewersWithInvalidSubject() throws Exception {
+    final String caller = "shemp";
+    Response renewalResponse = doTestTokenRenewal(true, "larry, moe,  curly ", createTestSubject(caller));
+    validateRenewalResponse(renewalResponse,
+                            400,
+                            false,
+                            "Caller (" + caller + ") not authorized to renew tokens.");
+  }
+
+  @Test
+  public void testTokenRenewal_Enabled_WithRenewersWithValidSubject() throws Exception {
+    final String caller = "shemp";
+    Response renewalResponse =
+                      doTestTokenRenewal(true, ("larry, moe,  curly ," + caller), createTestSubject(caller));
+    validateSuccessfulRenewalResponse(renewalResponse);
+  }
+
+  @Test
+  public void testTokenRevocation_ServerManagedStateNotConfigured() throws Exception {
+    Response renewalResponse = doTestTokenRevocation(null, null, null);
+    validateRevocationResponse(renewalResponse,
+                               400,
+                               false,
+                               "Token revocation support is not configured");
+  }
+
+  @Test
+  public void testTokenRevocation_Disabled() throws Exception {
+    Response renewalResponse = doTestTokenRevocation(false, null, null);
+    validateRevocationResponse(renewalResponse,
+                               400,
+                               false,
+                               "Token revocation support is not configured");
+  }
+
+  @Test
+  public void testTokenRevocation_Enabled_NoRenewersNoSubject() throws Exception {
+    Response renewalResponse = doTestTokenRevocation(true, null, null);
+    validateRevocationResponse(renewalResponse,
+                               400,
+                               false,
+                               "Caller (null) not authorized to revoke tokens.");
+  }
+
+  @Test
+  public void testTokenRevocation_Enabled_NoRenewersWithSubject() throws Exception {
+    final String caller = "yarn";
+    Response renewalResponse = doTestTokenRevocation(true, null, createTestSubject(caller));
+    validateRevocationResponse(renewalResponse,
+                               400,
+                               false,
+                               "Caller (" + caller + ") not authorized to revoke tokens.");
+  }
+
+  @Test
+  public void testTokenRevocation_Enabled_WithRenewersNoSubject() throws Exception {
+    Response renewalResponse = doTestTokenRevocation(true, "larry, moe,  curly ", null);
+    validateRevocationResponse(renewalResponse,
+                               400,
+                               false,
+                               "Caller (null) not authorized to revoke tokens.");
+  }
+
+  @Test
+  public void testTokenRevocation_Enabled_WithRenewersWithInvalidSubject() throws Exception {
+    final String caller = "shemp";
+    Response renewalResponse = doTestTokenRevocation(true, "larry, moe,  curly ", createTestSubject(caller));
+    validateRevocationResponse(renewalResponse,
+                               400,
+                               false,
+                               "Caller (" + caller + ") not authorized to revoke tokens.");
+  }
+
+  @Test
+  public void testTokenRevocation_Enabled_WithRenewersWithValidSubject() throws Exception {
+    final String caller = "shemp";
+    Response renewalResponse =
+        doTestTokenRevocation(true, ("larry, moe,  curly ," + caller), createTestSubject(caller));
+    validateSuccessfulRevocationResponse(renewalResponse);
+  }
+
+  /**
+   *
+   * @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
+   *
+   * @return The Response from the token renewal request
+   *
+   * @throws Exception
+   */
+  private Response doTestTokenRenewal(final Boolean isTokenStateServerManaged,
+                                      final String  renewers,
+                                      final Subject caller) throws Exception {
+    return doTestTokenLifecyle(TokenLifecycleOperation.Renew, isTokenStateServerManaged, renewers, caller);
+  }
+
+  /**
+   *
+   * @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
+   *
+   * @return The Response from the token revocation request
+   *
+   * @throws Exception
+   */
+  private Response doTestTokenRevocation(final Boolean isTokenStateServerManaged,
+                                         final String  renewers,
+                                         final Subject caller) throws Exception {
+    return doTestTokenLifecyle(TokenLifecycleOperation.Revoke, isTokenStateServerManaged, renewers, caller);
+  }
+
+  /**
+   * @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
+   *
+   * @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 {
+    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));
+    EasyMock.expect(context.getInitParameter("knox.token.target.url")).andReturn(null);
+    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));
+    }
+    EasyMock.expect(context.getInitParameter("knox.token.renewer.whitelist")).andReturn(renewers);
+
+    HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes();
+    Principal principal = EasyMock.createNiceMock(Principal.class);
+    EasyMock.expect(principal.getName()).andReturn("alice").anyTimes();
+    EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes();
+
+    GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
+    EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes();
+
+    JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey);
+    EasyMock.expect(services.getService(ServiceType.TOKEN_SERVICE)).andReturn(authority).anyTimes();
+
+    TokenStateService tss = new TestTokenStateService();
+    EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
+
+    EasyMock.replay(principal, services, context, request);
+
+    TokenResource tr = new TokenResource();
+    tr.request = request;
+    tr.context = context;
+    tr.init();
+
+    // Request a token
+    Response retResponse = tr.doGet();
+    assertEquals(200, retResponse.getStatus());
+
+    // Parse the response
+    String retString = retResponse.getEntity().toString();
+    String accessToken = getTagValue(retString, "access_token");
+    assertNotNull(accessToken);
+
+    Response response;
+    switch (operation) {
+      case Renew:
+        response = requestTokenRenewal(tr, accessToken, caller);
+        break;
+      case Revoke:
+        response = requestTokenRevocation(tr, accessToken, caller);
+        break;
+      default:
+        throw new Exception("Invalid operation: " + operation);
+    }
+    return response;
+  }
+
+  private static Response requestTokenRenewal(final TokenResource tr, final String tokenData, final Subject caller) {
+    Response response;
+    if (caller != null) {
+      response = Subject.doAs(caller, (PrivilegedAction<Response>) () -> tr.renew(tokenData));
+    } else {
+      response = tr.renew(tokenData);
+    }
+    return response;
+  }
+
+  private static Response requestTokenRevocation(final TokenResource tr, final String tokenData, final Subject caller) {
+    Response response;
+    if (caller != null) {
+      response = Subject.doAs(caller, (PrivilegedAction<Response>) () -> tr.revoke(tokenData));
+    } else {
+      response = tr.revoke(tokenData);
+    }
+    return response;
+  }
+
+  private static void validateSuccessfulRenewalResponse(final Response response) throws IOException {
+    validateRenewalResponse(response, 200, true, null);
+  }
+
+  private static void validateRenewalResponse(final Response response,
+                                              final int      expectedStatusCode,
+                                              final boolean  expectedResult,
+                                              final String   expectedMessage) throws IOException {
+    assertEquals(expectedStatusCode, response.getStatus());
+    assertTrue(response.hasEntity());
+    String responseContent = (String) response.getEntity();
+    assertNotNull(responseContent);
+    assertFalse(responseContent.isEmpty());
+    Map<String, String> json = parseJSONResponse(responseContent);
+    boolean result = Boolean.valueOf(json.get("renewed"));
+    assertEquals(expectedResult, result);
+    assertEquals(expectedMessage, json.get("error"));
+  }
+
+  private static void validateSuccessfulRevocationResponse(final Response response) throws IOException {
+    validateRevocationResponse(response, 200, true, null);
+  }
+
+  private static void validateRevocationResponse(final Response response,
+                                                 final int      expectedStatusCode,
+                                                 final boolean  expectedResult,
+                                                 final String   expectedMessage) throws IOException {
+    assertEquals(expectedStatusCode, response.getStatus());
+    assertTrue(response.hasEntity());
+    String responseContent = (String) response.getEntity();
+    assertNotNull(responseContent);
+    assertFalse(responseContent.isEmpty());
+    Map<String, String> json = parseJSONResponse(responseContent);
+    boolean result = Boolean.valueOf(json.get("revoked"));
+    assertEquals(expectedResult, result);
+    assertEquals(expectedMessage, json.get("error"));
+  }
+
+
   private String getTagValue(String token, String tagName) {
     String searchString = tagName + "\":";
     String value = token.substring(token.indexOf(searchString) + searchString.length());
@@ -625,6 +919,94 @@ public class TokenServiceResourceTest {
     }
   }
 
+  /**
+   * Create a Subject for testing.
+   *
+   * @param username The user identifier
+   *
+   * @return A Subject
+   */
+  private Subject createTestSubject(final String username) {
+    Subject s = new Subject();
+
+    Set<Principal> principals = s.getPrincipals();
+    principals.add(new PrimaryPrincipal(username));
+
+    return s;
+  }
+
+  private static Map<String, String> parseJSONResponse(final String response) throws IOException {
+    return (new ObjectMapper()).readValue(response, new TypeReference<Map<String, String>>(){});
+  }
+
+
+  private static class TestTokenStateService implements TokenStateService {
+    @Override
+    public void addToken(JWTToken token, long issueTime) {
+      addToken(token.getPayload(), issueTime, token.getExpiresDate().getTime());
+    }
+
+    @Override
+    public void addToken(String token, long issueTime, long expiration) {
+    }
+
+    @Override
+    public boolean isExpired(JWTToken token) {
+      return isExpired(token.getPayload());
+    }
+
+    @Override
+    public boolean isExpired(String token) {
+      return false;
+    }
+
+    @Override
+    public void revokeToken(JWTToken token) {
+      revokeToken(token.getPayload());
+    }
+
+    @Override
+    public void revokeToken(String token) {
+    }
+
+    @Override
+    public long renewToken(JWTToken token) {
+      return renewToken(token.getPayload());
+    }
+
+    @Override
+    public long renewToken(String token) {
+      return renewToken(token, 0L);
+    }
+
+    @Override
+    public long renewToken(JWTToken token, Long renewInterval) {
+      return renewToken(token.getPayload());
+    }
+
+    @Override
+    public long renewToken(String token, Long renewInterval) {
+      return 0;
+    }
+
+    @Override
+    public long getTokenExpiration(String token) {
+      return 0;
+    }
+
+    @Override
+    public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void start() throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void stop() throws ServiceLifecycleException {
+    }
+  }
+
   private static class TestJWTokenAuthority implements JWTokenAuthority {
 
     private RSAPublicKey publicKey;
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
index 40d5433..736ecad 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
@@ -32,6 +32,7 @@ public enum ServiceType {
   SERVICE_REGISTRY_SERVICE("ServiceRegistryService"),
   SSL_SERVICE("SSLService"),
   TOKEN_SERVICE("TokenService"),
+  TOKEN_STATE_SERVICE("TokenStateService"),
   TOPOLOGY_SERVICE("TopologyService");
 
   private final String serviceTypeName;
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
new file mode 100644
index 0000000..2ab5721
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
@@ -0,0 +1,123 @@
+/*
+ * 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.security.token;
+
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+
+
+/**
+ * Service providing authentication token state management.
+ */
+public interface TokenStateService extends Service {
+
+  String CONFIG_SERVER_MANAGED = "knox.token.exp.server-managed";
+
+  /**
+   * 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.
+   *
+   * @param token      The token.
+   * @param issueTime  The time the token was issued.
+   * @param expiration The token expiration time.
+   */
+  void addToken(String token, long issueTime, long expiration);
+
+  /**
+   *
+   * @param token The token.
+   *
+   * @return true, if the token has expired; Otherwise, false.
+   */
+  boolean isExpired(JWTToken token);
+
+  /**
+   *
+   * @param token The token.
+   *
+   * @return true, if the token has expired; Otherwise, false.
+   */
+  boolean isExpired(String token);
+
+  /**
+   * Disable any subsequent use of the specified token.
+   *
+   * @param token The token.
+   */
+  void revokeToken(JWTToken token);
+
+  /**
+   * Disable any subsequent use of the specified token.
+   *
+   * @param token The token.
+   */
+  void revokeToken(String token);
+
+  /**
+   * Extend the lifetime of the specified token by the default amount of time.
+   *
+   * @param token The token.
+   *
+   * @return The token's updated expiration time in milliseconds.
+   */
+  long renewToken(JWTToken token);
+
+  /**
+   * Extend the lifetime of the specified token by the specified amount of time.
+   *
+   * @param token The token.
+   * @param renewInterval The amount of time that should be added to the token's lifetime.
+   *
+   * @return The token's updated expiration time in milliseconds.
+   */
+  long renewToken(JWTToken token, Long renewInterval);
+
+  /**
+   * Extend the lifetime of the specified token by the default amount of time.
+   *
+   * @param token The token.
+   *
+   * @return The token's updated expiration time in milliseconds.
+   */
+  long renewToken(String token);
+
+  /**
+   * Extend the lifetime of the specified token by the specified amount of time.
+   *
+   * @param token The token.
+   * @param renewInterval The amount of time that should be added to the token's lifetime.
+   *
+   * @return The token's updated expiration time in milliseconds.
+   */
+  long renewToken(String token, Long renewInterval);
+
+  /**
+   *
+   * @param token The token.
+   *
+   * @return The token's expiration time in milliseconds.
+   */
+  long getTokenExpiration(String token);
+
+}
diff --git a/pom.xml b/pom.xml
index 9a4f89c..883c779 100644
--- a/pom.xml
+++ b/pom.xml
@@ -192,6 +192,7 @@
         <jansi.version>1.18</jansi.version>
         <javax.activation.version>1.2.0</javax.activation.version>
         <javax.annotation-api.version>1.3.2</javax.annotation-api.version>
+        <javax.inject.version>2.2.0</javax.inject.version>
         <javax.json.version>1.1.3</javax.json.version>
         <javax.servlet-api.version>3.1.0</javax.servlet-api.version>
         <javax.ws.rs-api.version>2.0</javax.ws.rs-api.version>
@@ -1176,6 +1177,11 @@
                 <version>${javax.annotation-api.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.glassfish.hk2.external</groupId>
+                <artifactId>javax.inject</artifactId>
+                <version>${javax.inject.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>javax.servlet</groupId>
                 <artifactId>javax.servlet-api</artifactId>
                 <version>${javax.servlet-api.version}</version>


Mime
View raw message