This is an automated email from the ASF dual-hosted git repository.

smolnar 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 53849ac  KNOX-2579 - Saving token passcode securely in the DB as 
additional token metadata (#437)
53849ac is described below

commit 53849ac45757a8b915dec3ff912a5e7e7665f18a
Author: Sandor Molnar <[email protected]>
AuthorDate: Tue May 11 21:00:32 2021 +0200

    KNOX-2579 - Saving token passcode securely in the DB as additional token 
metadata (#437)
---
 .../applications/tokengen/app/js/tokengen.js       |   2 +-
 .../provider/federation/jwt/JWTMessages.java       |   6 ++
 .../federation/jwt/filter/AbstractJWTFilter.java   |  34 ++++++-
 .../federation/jwt/filter/JWTFederationFilter.java |  59 +++++++-----
 .../JWTAsHTTPBasicCredsFederationFilterTest.java   |   2 +-
 .../federation/TestJWTFederationFilter.java        |   7 ++
 ...okenIDAsHTTPBasicCredsFederationFilterTest.java |  58 ++++++++++--
 .../gateway/config/impl/GatewayConfigImpl.java     |   7 ++
 .../token/impl/AliasBasedTokenStateService.java    | 103 ++++++++++++++++++++-
 .../token/impl/DefaultTokenStateService.java       |  18 ++++
 .../services/token/impl/JDBCTokenStateService.java |  30 +++++-
 .../token/impl/JournalBasedTokenStateService.java  |  27 ++++++
 .../services/token/impl/TokenStateDatabase.java    |  18 +++-
 .../token/impl/TokenStateServiceMessages.java      |   6 ++
 .../token/impl/ZookeeperTokenStateService.java     |   2 +
 .../impl/AliasBasedTokenStateServiceTest.java      |  23 ++++-
 .../token/impl/DefaultTokenStateServiceTest.java   |   7 ++
 .../token/impl/JDBCTokenStateServiceTest.java      |  15 +++
 .../gateway/services/token/impl/TokenMACTest.java  |  43 +++++++++
 .../gateway/service/knoxtoken/TokenResource.java   |  27 +++++-
 .../knoxtoken/TokenServiceResourceTest.java        |  11 ++-
 .../apache/knox/gateway/config/GatewayConfig.java  |   5 +
 .../services/security/token/TokenMetadata.java     |   9 ++
 .../services/security/token/TokenStateService.java |   9 ++
 .../services/security/token/impl/TokenMAC.java     |  66 +++++++++++++
 .../org/apache/knox/gateway/GatewayTestConfig.java |   5 +
 26 files changed, 546 insertions(+), 53 deletions(-)

diff --git 
a/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
 
b/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
index 1e263b7..adfe297 100644
--- 
a/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
+++ 
b/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
@@ -142,7 +142,7 @@ var gen = function() {
                     $('#accessToken').text(accessToken);
                     var decodedToken = 
b64DecodeUnicode(accessToken.split(".")[1]);
                     var jwtjson = JSON.parse(decodedToken);
-                    $('#accessPasscode').text(jwtjson["knox.id"]);
+                    $('#accessPasscode').text(resp.passcode);
                     var date = new Date(resp.expires_in);
                     $('#expiry').text(date.toLocaleString());
                     $('#user').text(jwtjson.sub);
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
index a6e7a43..b17b4e9 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
@@ -35,6 +35,9 @@ public interface JWTMessages {
   @Message( level = MessageLevel.INFO, text = "Access token {0} has expired; a 
new one must be acquired." )
   void tokenHasExpired(String tokenId);
 
+  @Message(level = MessageLevel.ERROR, text = "Received wrong passcode token 
for {0}")
+  void wrongPasscodeToken(String tokenId);
+
   @Message( level = MessageLevel.INFO, text = "The NotBefore check failed." )
   void notBeforeCheckFailed();
 
@@ -79,4 +82,7 @@ public interface JWTMessages {
   @Message( level = MessageLevel.INFO, text = "Initialized token signature 
verification cache for the {0} topology." )
   void initializedSignatureVerificationCache(String topology);
 
+  @Message( level = MessageLevel.ERROR, text = "Failed to parse passcode 
token: {0}" )
+  void failedToParsePasscodeToken(@StackTrace( level = MessageLevel.ERROR) 
Exception e);
+
 }
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 6eeaf37..82b39c0 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
@@ -17,6 +17,8 @@
  */
 package org.apache.knox.gateway.provider.federation.jwt.filter;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.IOException;
 import java.security.Principal;
 import java.security.PrivilegedActionException;
@@ -24,6 +26,7 @@ import java.security.PrivilegedExceptionAction;
 import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
@@ -49,12 +52,16 @@ import 
org.apache.knox.gateway.audit.api.AuditServiceFactory;
 import org.apache.knox.gateway.audit.api.Auditor;
 import org.apache.knox.gateway.audit.api.ResourceType;
 import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.filter.AbstractGatewayFilter;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
 import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
 import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
 import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.security.token.TokenServiceException;
@@ -63,6 +70,7 @@ import 
org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
 
 import com.nimbusds.jose.JWSHeader;
 import org.apache.knox.gateway.util.Tokens;
@@ -99,6 +107,7 @@ public abstract class AbstractJWTFilter implements Filter {
   protected String expectedJWKSUrl;
 
   private TokenStateService tokenStateService;
+  private TokenMAC tokenMAC;
 
   @Override
   public abstract void doFilter(ServletRequest request, ServletResponse 
response, FilterChain chain)
@@ -120,6 +129,13 @@ public abstract class AbstractJWTFilter implements Filter {
         authority = services.getService(ServiceType.TOKEN_SERVICE);
         if (TokenUtils.isServerManagedTokenStateEnabled(filterConfig)) {
           tokenStateService = 
services.getService(ServiceType.TOKEN_STATE_SERVICE);
+          try {
+            final GatewayConfig config = (GatewayConfig) 
context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+            final AliasService aliasService =  
services.getService(ServiceType.ALIAS_SERVICE);
+            tokenMAC = new TokenMAC(config.getKnoxTokenHashAlgorithm(), 
aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME));
+          } catch (ServiceLifecycleException | AliasServiceException e) {
+            throw new ServletException("Error while initializing Knox token 
MAC generator", e);
+          }
         }
       }
     }
@@ -358,14 +374,20 @@ public abstract class AbstractJWTFilter implements Filter 
{
   protected boolean validateToken(final HttpServletRequest request,
                                   final HttpServletResponse response,
                                   final FilterChain chain,
-                                  final String tokenId)
+                                  final String tokenId,
+                                  final String passcode)
           throws IOException, ServletException {
 
     if (tokenStateService != null) {
       try {
         if (tokenId != null) {
           if (tokenIsStillValid(tokenId)) {
-            return true;
+            if (validatePasscode(tokenId, passcode)) {
+              return true;
+            } else {
+              log.wrongPasscodeToken(tokenId);
+              handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST, "Bad request: wrong passcode");
+            }
           } else {
             log.tokenHasExpired(Tokens.getTokenIDDisplayText(tokenId));
             handleValidationError(request, response, 
HttpServletResponse.SC_BAD_REQUEST,
@@ -385,6 +407,14 @@ public abstract class AbstractJWTFilter implements Filter {
     return false;
   }
 
+  private boolean validatePasscode(String tokenId, String passcode) throws 
UnknownTokenException {
+    final long issueTime = tokenStateService.getTokenIssueTime(tokenId);
+    final TokenMetadata tokenMetadata = 
tokenStateService.getTokenMetadata(tokenId);
+    final String userName = tokenMetadata == null ? "" : 
tokenMetadata.getUserName();
+    final byte[] storedPasscode = tokenMetadata == null ? null : 
tokenMetadata.getPasscode().getBytes(UTF_8);
+    return Arrays.equals(tokenMAC.hash(tokenId, issueTime, userName, 
passcode).getBytes(UTF_8), storedPasscode);
+  }
+
   protected boolean verifyTokenSignature(final JWT token) {
     boolean verified;
 
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 7e4e9d0..b5d34b7 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
@@ -17,15 +17,15 @@
  */
 package org.apache.knox.gateway.provider.federation.jwt.filter;
 
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
-import org.apache.knox.gateway.security.PrimaryPrincipal;
-import org.apache.knox.gateway.services.security.token.UnknownTokenException;
-import org.apache.knox.gateway.services.security.token.impl.JWT;
-import org.apache.knox.gateway.services.security.token.impl.JWTToken;
-import org.apache.knox.gateway.util.AuthFilterUtils;
-import org.apache.knox.gateway.util.CertificateUtils;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static 
org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Base64;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
 
 import javax.security.auth.Subject;
 import javax.servlet.FilterChain;
@@ -35,15 +35,16 @@ import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.text.ParseException;
-import java.util.Base64;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
 
-import static 
org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.util.AuthFilterUtils;
+import org.apache.knox.gateway.util.CertificateUtils;
 
 public class JWTFederationFilter extends AbstractJWTFilter {
 
@@ -65,7 +66,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
   public static final String TOKEN    = "Token";
   public static final String PASSCODE = "Passcode";
   private String paramName;
-  private Set<String> unAuthenticatedPaths = new HashSet(20);
+  private Set<String> unAuthenticatedPaths = new HashSet<>(20);
 
   @Override
   public void init( FilterConfig filterConfig ) throws ServletException {
@@ -141,9 +142,21 @@ public class JWTFederationFilter extends AbstractJWTFilter 
{
         }
       } else if (TokenType.Passcode.equals(tokenType)) {
         // Validate the token based on the server-managed metadata
-        if (validateToken((HttpServletRequest) request, (HttpServletResponse) 
response, chain, tokenValue)) {
+        // The received token value must be a Base64 encoded value of 
Base64(tokenId)::Base64(rawPasscode)
+        String tokenId = null, passcode = null;
+        try {
+          final String[] base64DecodedTokenIdAndPasscode = 
decodeBase64(tokenValue).split("::");
+          tokenId = decodeBase64(base64DecodedTokenIdAndPasscode[0]);
+          passcode = decodeBase64(base64DecodedTokenIdAndPasscode[1]);
+        } catch (Exception e) {
+          log.failedToParsePasscodeToken(e);
+          handleValidationError((HttpServletRequest) request, 
(HttpServletResponse) response, HttpServletResponse.SC_UNAUTHORIZED,
+              "Error while parsing the received passcode token");
+        }
+
+        if (validateToken((HttpServletRequest) request, (HttpServletResponse) 
response, chain, tokenId, passcode)) {
           try {
-            Subject subject = createSubjectFromTokenIdentifier(tokenValue);
+            Subject subject = createSubjectFromTokenIdentifier(tokenId);
             continueWithEstablishedSecurityContext(subject, 
(HttpServletRequest) request, (HttpServletResponse) response, chain);
           } catch (UnknownTokenException e) {
             ((HttpServletResponse) 
response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
@@ -156,6 +169,10 @@ public class JWTFederationFilter extends AbstractJWTFilter 
{
     }
   }
 
+  private String decodeBase64(String toBeDecoded) {
+    return new String(Base64.getDecoder().decode(toBeDecoded.getBytes(UTF_8)), 
UTF_8);
+  }
+
   public Pair<TokenType, String> getWireToken(final ServletRequest request) {
       Pair<TokenType, String> parsed = null;
       String token = null;
@@ -187,7 +204,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
       Pair<TokenType, String> parsed = null;
       final String base64Credentials = header.substring(BASIC.length()).trim();
       final byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
-      final String credentials = new String(credDecoded, 
StandardCharsets.UTF_8);
+      final String credentials = new String(credDecoded, UTF_8);
       final String[] values = credentials.split(":", 2);
       String username = values[0];
       String passcode = values[1].isEmpty() ? null : values[1];
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
index a7830cc..a5549ad 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
@@ -41,7 +41,7 @@ import static org.junit.Assert.fail;
 public class JWTAsHTTPBasicCredsFederationFilterTest extends 
AbstractJWTFilterTest {
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception{
       handler = new TestJWTFederationFilter();
       ((TestJWTFederationFilter) handler).setTokenService(new 
TestJWTokenAuthority(publicKey));
     }
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
index e895ba0..8eccdac 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
@@ -21,6 +21,7 @@ package org.apache.knox.gateway.provider.federation;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
 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.TokenMAC;
 
 import java.lang.reflect.Field;
 
@@ -42,6 +43,12 @@ public class TestJWTFederationFilter extends 
JWTFederationFilter
         }
     }
 
+    void setTokenMac(TokenMAC tokenMAC) throws Exception {
+      final Field tokenMacField = 
getClass().getSuperclass().getSuperclass().getDeclaredField("tokenMAC");
+      tokenMacField.setAccessible(true);
+      tokenMacField.set(this, tokenMAC);
+    }
+
     @Override
     protected void recordSignatureVerification(String tokenId) {
         super.recordSignatureVerification(tokenId);
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
index 1d33e1c..cbd28b3 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
@@ -19,6 +19,9 @@
 package org.apache.knox.gateway.provider.federation;
 
 import com.nimbusds.jwt.SignedJWT;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.HmacAlgorithms;
 import org.apache.knox.gateway.config.GatewayConfig;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
@@ -29,6 +32,7 @@ import 
org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
 import org.easymock.EasyMock;
 import org.junit.Assert;
 import org.junit.Test;
@@ -36,12 +40,15 @@ import org.junit.Test;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+
+import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.time.Instant;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
+import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
@@ -51,18 +58,33 @@ import static org.junit.Assert.fail;
 public class TokenIDAsHTTPBasicCredsFederationFilterTest extends 
JWTAsHTTPBasicCredsFederationFilterTest {
 
     TestTokenStateService tss;
+    TokenMAC tokenMAC;
 
     @Override
-    public void setUp() {
+    public void setUp() throws Exception {
         super.setUp();
         tss = new TestTokenStateService();
         ((TestJWTFederationFilter) handler).setTokenStateService(tss);
+        tokenMAC = new TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(), 
"sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray());
+        ((TestJWTFederationFilter) handler).setTokenMac(tokenMAC);
     }
 
     @Override
     protected void setTokenOnRequest(final HttpServletRequest request, final 
SignedJWT jwt) {
-        addTokenState(jwt);
-        setTokenOnRequest(request, TestJWTFederationFilter.PASSCODE, 
getTokenId(jwt));
+      try {
+        final long issueTime = System.currentTimeMillis() - 
TimeUnit.MINUTES.toMillis(5);
+        final String subject = (String) 
jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT);
+        final String passcode = UUID.randomUUID().toString();
+        addTokenState(jwt, issueTime, subject, passcode);
+        setTokenOnRequest(request, TestJWTFederationFilter.PASSCODE, 
generatePasscodeField(getTokenId(jwt), passcode));
+      } catch(ParseException e) {
+        Assert.fail(e.getMessage());
+      }
+    }
+
+    private String generatePasscodeField(String tokenId, String passcode) {
+      final String base64TokenIdPasscode = 
Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" + 
Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8));
+      return 
Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8));
     }
 
     /**
@@ -76,8 +98,15 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
     protected void setTokenOnRequest(final HttpServletRequest request,
                                      final SignedJWT          jwt,
                                      final String             authUsername) {
-        addTokenState(jwt);
-        setTokenOnRequest(request, authUsername, getTokenId(jwt));
+      try {
+        final long issueTime = System.currentTimeMillis() - 
TimeUnit.MINUTES.toMillis(5);
+        final String subject = (String) 
jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT);
+        final String passcode = UUID.randomUUID().toString();
+        addTokenState(jwt, issueTime, subject, passcode);
+        setTokenOnRequest(request, authUsername, 
generatePasscodeField(getTokenId(jwt), passcode));
+      } catch(ParseException e) {
+        Assert.fail(e.getMessage());
+      }
     }
 
     @Override
@@ -95,13 +124,14 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
         return tokenId;
     }
 
-    private void addTokenState(final SignedJWT jwt) {
+    private void addTokenState(final SignedJWT jwt, long issueTime, String 
subject, String passcode) {
         try {
             JWTToken token = new JWTToken(jwt.serialize());
-            tss.addToken(token, System.currentTimeMillis() - 
TimeUnit.MINUTES.toMillis(5));
+            tss.addToken(token, issueTime);
 
-            String subject = (String) 
jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT);
-            tss.addMetadata(TokenUtils.getTokenId(token), new 
TokenMetadata(subject));
+            final TokenMetadata metadata = new TokenMetadata(subject);
+            metadata.setPasscode(tokenMAC.hash(TokenUtils.getTokenId(token), 
issueTime, subject, passcode));
+            tss.addMetadata(TokenUtils.getTokenId(token), metadata);
         } catch (ParseException e) {
             Assert.fail(e.getMessage());
         }
@@ -328,6 +358,7 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
      */
     private static class TestTokenStateService implements TokenStateService {
 
+        private final Map<String, Long> tokenIssueTimes = new 
ConcurrentHashMap<>();
         private final Map<String, Long> tokenExpirations = new 
ConcurrentHashMap<>();
         private final Map<String, TokenMetadata> tokenMetadata = new 
ConcurrentHashMap<>();
 
@@ -359,7 +390,7 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
             if (expiration == null || expiration.isEmpty()) {
                 expiration = "0";
             }
-            addToken(TokenUtils.getTokenId(token), 
Instant.now().toEpochMilli(), Long.parseLong(expiration));
+            addToken(TokenUtils.getTokenId(token), issueTime, 
Long.parseLong(expiration));
         }
 
         private void addToken(String tokenId, long expiration) {
@@ -369,11 +400,18 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest 
extends JWTAsHTTPBasicC
         @Override
         public void addToken(String tokenId, long issueTime, long expiration) {
             addToken(tokenId, expiration);
+            tokenIssueTimes.put(tokenId, issueTime);
         }
 
         @Override
         public void addToken(String tokenId, long issueTime, long expiration, 
long maxLifetimeDuration) {
             addToken(tokenId, expiration);
+            tokenIssueTimes.put(tokenId, issueTime);
+        }
+
+        @Override
+        public long getTokenIssueTime(String tokenId) throws 
UnknownTokenException {
+        return tokenIssueTimes.getOrDefault(tokenId, 0L);
         }
 
         @Override
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index 486e7dd..43895e9 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -39,6 +39,7 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.commons.codec.digest.HmacAlgorithms;
 import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hadoop.conf.Configuration;
@@ -260,6 +261,7 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   private static final String KNOX_TOKEN_EVICTION_GRACE_PERIOD = 
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.eviction.grace.period";
   private static final String KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL = 
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.state.alias.persistence.interval";
   private static final String KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED = 
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.permissive.validation";
+  private static final String KNOX_TOKEN_HASH_ALGORITHM = 
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.hash.algorithm";
   private static final long KNOX_TOKEN_EVICTION_INTERVAL_DEFAULT = 
TimeUnit.MINUTES.toSeconds(5);
   private static final long KNOX_TOKEN_EVICTION_GRACE_PERIOD_DEFAULT = 
TimeUnit.HOURS.toSeconds(24);
   private static final long KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL_DEFAULT = 
TimeUnit.SECONDS.toSeconds(15);
@@ -1179,6 +1181,11 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   }
 
   @Override
+  public String getKnoxTokenHashAlgorithm() {
+    return get(KNOX_TOKEN_HASH_ALGORITHM, 
HmacAlgorithms.HMAC_SHA_256.getName());
+  }
+
+  @Override
   public Set<String> getHiddenTopologiesOnHomepage() {
     final Set<String> hiddenTopologies = new 
HashSet<>(getTrimmedStringCollection(KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES));
     return hiddenTopologies == null || hiddenTopologies.isEmpty() ? 
KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES_DEFAULT : hiddenTopologies;
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 9c38303..1e76662 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
@@ -57,6 +57,7 @@ import org.apache.knox.gateway.util.Tokens;
 public class AliasBasedTokenStateService extends DefaultTokenStateService 
implements TokenStatePeristerMonitorListener {
 
   static final String TOKEN_ALIAS_SUFFIX_DELIM   = "--";
+  static final String TOKEN_ISSUE_TIME_POSTFIX   = TOKEN_ALIAS_SUFFIX_DELIM + 
"iss";
   static final String TOKEN_MAX_LIFETIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM + 
"max";
   static final String TOKEN_META_POSTFIX         = TOKEN_ALIAS_SUFFIX_DELIM + 
"meta";
 
@@ -148,9 +149,11 @@ public class AliasBasedTokenStateService extends 
DefaultTokenStateService implem
       for (Map.Entry<String, char[]> passwordAliasMapEntry : 
passwordAliasMap.entrySet()) {
         alias = passwordAliasMapEntry.getKey();
         if (alias.endsWith(TOKEN_MAX_LIFETIME_POSTFIX)) {
-          // This token state service implementation persists two aliases in 
__gateway-credentials.jceks (see persistTokenState below):
+          // This token state service implementation persists 4 aliases in 
__gateway-credentials.jceks (see persistTokenState below):
           // - an alias which maps a token ID to its expiration time
-          // - another alias with '--max' postfix which maps the maximum 
lifetime of the token identified by the 1st alias
+          // - an alias with '--max' postfix which maps the maximum lifetime 
of the token identified by the 1st alias
+          // - an alias with '--iss' postfix which maps the issue time of the 
token
+          // - an alias with '-meta' postfix which maps an arbitrary metadata 
of the token
           // Given this, we should check aliases ending with '--max' and 
calculate the token ID from this alias.
           // If all aliases were blindly processed we would end-up handling 
aliases that were not persisted via this token state service
           // implementation -> facing error(s) when trying to parse the 
expiration/maxLifeTime values and irrelevant data would be loaded in the
@@ -164,6 +167,9 @@ public class AliasBasedTokenStateService extends 
DefaultTokenStateService implem
         } else if (alias.endsWith(TOKEN_META_POSTFIX)) {
           tokenId = alias.substring(0, alias.indexOf(TOKEN_META_POSTFIX));
           super.addMetadata(tokenId, TokenMetadata.fromJSON(new 
String(passwordAliasMapEntry.getValue())));
+        } else if (alias.endsWith(TOKEN_ISSUE_TIME_POSTFIX)) {
+          tokenId = alias.substring(0, 
alias.indexOf(TOKEN_ISSUE_TIME_POSTFIX));
+          setIssueTimeInMemory(tokenId, 
convertCharArrayToLong(passwordAliasMapEntry.getValue()));
         }
 
         // log some progress (it's very useful in case a huge amount of token 
related aliases in __gateway-credentials.jceks)
@@ -279,6 +285,18 @@ public class AliasBasedTokenStateService extends 
DefaultTokenStateService implem
   }
 
   @Override
+  protected void setIssueTime(String tokenId, long issueTime) {
+    synchronized (unpersistedState) {
+      unpersistedState.add(new TokenIssueTime(tokenId, issueTime));
+    }
+    setIssueTimeInMemory(tokenId, issueTime);
+  }
+
+  protected void setIssueTimeInMemory(String tokenId, long issueTime) {
+    super.setIssueTime(tokenId, issueTime);
+  }
+
+  @Override
   protected void setMaxLifetime(final String tokenId, long issueTime, long 
maxLifetimeDuration) {
     super.setMaxLifetime(tokenId, issueTime, maxLifetimeDuration);
     synchronized (unpersistedState) {
@@ -317,6 +335,35 @@ public class AliasBasedTokenStateService extends 
DefaultTokenStateService implem
   }
 
   @Override
+  public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
+    // Check the in-memory collection first, to avoid costly keystore access 
when possible
+    try {
+      // check the in-memory cache first
+      return super.getTokenIssueTime(tokenId);
+    } catch (UnknownTokenException e) {
+      // It's not in memory
+    }
+
+    // If there is no associated state in the in-memory cache, proceed to 
check the alias service
+    long issueTime = 0;
+    try {
+      char[] issueTimeStr = getPasswordUsingAliasService(tokenId + 
TOKEN_ISSUE_TIME_POSTFIX);
+      if (issueTimeStr == null) {
+        throw new UnknownTokenException(tokenId);
+      }
+      issueTime = convertCharArrayToLong(issueTimeStr);
+      // Update the in-memory cache to avoid subsequent keystore look-ups for 
the same state
+      super.setIssueTime(tokenId, issueTime);
+    } catch (UnknownTokenException e) {
+      throw e;
+    } catch (Exception e) {
+      log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e);
+    }
+
+    return issueTime;
+  }
+
+  @Override
   public long getTokenExpiration(String tokenId, boolean validate) throws 
UnknownTokenException {
     // Check the in-memory collection first, to avoid costly keystore access 
when possible
     try {
@@ -473,7 +520,7 @@ public class AliasBasedTokenStateService extends 
DefaultTokenStateService implem
   }
 
   enum TokenStateType {
-    EXP(1), MAX(2), META(3);
+    EXP(1), MAX(2), META(3), ISS(4);
 
     private final int id;
 
@@ -591,6 +638,56 @@ public class AliasBasedTokenStateService extends 
DefaultTokenStateService implem
     }
   }
 
+  private static final class TokenIssueTime implements TokenState {
+    private String tokenId;
+    private long   issueTime;
+
+    TokenIssueTime(String tokenId, long issueTime) {
+      this.tokenId    = tokenId;
+      this.issueTime = issueTime;
+    }
+
+    @Override
+    public String getTokenId() {
+      return tokenId;
+    }
+
+    @Override
+    public String getAlias() {
+      return tokenId + TOKEN_ISSUE_TIME_POSTFIX;
+    }
+
+    @Override
+    public String getAliasValue() {
+      return String.valueOf(issueTime);
+    }
+
+    @Override
+    public TokenStateType getType() {
+      return TokenStateType.ISS;
+    }
+
+    @Override
+    public int hashCode() {
+      return new 
HashCodeBuilder().append(tokenId).append(getType().id).toHashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == null) {
+        return false;
+      }
+      if (obj == this) {
+        return true;
+      }
+      if (obj.getClass() != getClass()) {
+        return false;
+      }
+      final TokenIssueTime rhs = (TokenIssueTime) obj;
+      return new EqualsBuilder().append(this.tokenId, 
rhs.tokenId).append(this.getType().id, rhs.getType().id).isEquals();
+    }
+  }
+
   private static final class TokenMetadataState implements TokenState {
 
     private final String tokenId;
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 9b44dfd..e51aebf 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
@@ -62,6 +62,8 @@ public class DefaultTokenStateService implements 
TokenStateService {
 
   private final Map<String, Long> tokenExpirations = new ConcurrentHashMap<>();
 
+  private final Map<String, Long> tokenIssueTimes = new ConcurrentHashMap<>();
+
   private final Map<String, Long> maxTokenLifetimes = new 
ConcurrentHashMap<>();
 
   private final Map<String, TokenMetadata> metadataMap = new 
ConcurrentHashMap<>();
@@ -139,6 +141,7 @@ public class DefaultTokenStateService implements 
TokenStateService {
                              long   expiration,
                              long   maxLifetimeDuration) {
     validateTokenIdentifier(tokenId);
+    setIssueTime(tokenId, issueTime);
     tokenExpirations.put(tokenId, expiration);
     setMaxLifetime(tokenId, issueTime, maxLifetimeDuration);
     log.addedToken(Tokens.getTokenIDDisplayText(tokenId), 
getTimestampDisplay(expiration));
@@ -147,6 +150,20 @@ public class DefaultTokenStateService implements 
TokenStateService {
     }
   }
 
+  protected void setIssueTime(String tokenId, long issueTime) {
+    tokenIssueTimes.put(tokenId, issueTime);
+  }
+
+  @Override
+  public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
+    validateToken(tokenId);
+    final Long issueTime = tokenIssueTimes.get(tokenId);
+    if (issueTime == null) {
+      throw new UnknownTokenException(tokenId);
+    }
+    return issueTime.longValue();
+  }
+
   @Override
   public long getTokenExpiration(final JWT token) throws UnknownTokenException 
{
     long expiration = -1;
@@ -284,6 +301,7 @@ public class DefaultTokenStateService implements 
TokenStateService {
   }
 
   private void removeTokenState(final Set<String> tokenIds) {
+    tokenIssueTimes.keySet().removeAll(tokenIds);
     tokenExpirations.keySet().removeAll(tokenIds);
     maxTokenLifetimes.keySet().removeAll(tokenIds);
     metadataMap.keySet().removeAll(tokenIds);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
index 4907188..ecb648d 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
@@ -35,7 +35,7 @@ import org.apache.knox.gateway.util.JDBCUtils;
 import org.apache.knox.gateway.util.Tokens;
 
 public class JDBCTokenStateService extends DefaultTokenStateService {
-  private AliasService aliasService; // connection username/pw is stored here
+  private AliasService aliasService; // connection username/pw and passcode 
HMAC secret are stored here
   private TokenStateDatabase tokenDatabase;
   private AtomicBoolean initialized = new AtomicBoolean(false);
   private Lock initLock = new ReentrantLock(true);
@@ -86,9 +86,35 @@ public class JDBCTokenStateService extends 
DefaultTokenStateService {
   }
 
   @Override
+  public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
+    try {
+      // check the in-memory cache first
+      return super.getTokenIssueTime(tokenId);
+    } catch (UnknownTokenException e) {
+      // It's not in memory
+    }
+
+    long issueTime = 0;
+    try {
+      issueTime = tokenDatabase.getTokenIssueTime(tokenId);
+      if (issueTime > 0) {
+        
log.fetchedIssueTimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId), 
issueTime);
+
+        // Update the in-memory cache to avoid subsequent DB look-ups for the 
same state
+        super.setIssueTime(tokenId, issueTime);
+      } else {
+        throw new UnknownTokenException(tokenId);
+      }
+    } catch (SQLException e) {
+      
log.errorFetchingIssueTimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId), 
e.getMessage(), e);
+    }
+    return issueTime;
+  }
+
+  @Override
   public long getTokenExpiration(String tokenId, boolean validate) throws 
UnknownTokenException {
     try {
-      // check the in-memory cache, then
+      // check the in-memory cache first
       return super.getTokenExpiration(tokenId, validate);
     } catch (UnknownTokenException e) {
       // It's not in memory
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
index 5a5c1dd..ee21c6a 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
@@ -77,6 +77,33 @@ public class JournalBasedTokenStateService extends 
DefaultTokenStateService {
     }
 
     @Override
+    public long getTokenIssueTime(String tokenId) throws UnknownTokenException 
{
+      try {
+        // Check the in-memory collection first, to avoid file access when 
possible
+        return super.getTokenIssueTime(tokenId);
+      } catch (UnknownTokenException e) {
+        // It's not in memory
+      }
+
+      validateToken(tokenId);
+
+      // If there is no associated state in the in-memory cache, proceed to 
check the journal
+      long issueTime = 0;
+      try {
+        JournalEntry entry = journal.get(tokenId);
+        if (entry == null) {
+          throw new UnknownTokenException(tokenId);
+        }
+
+        issueTime = Long.parseLong(entry.getIssueTime());
+      } catch (IOException e) {
+        log.failedToLoadJournalEntry(e);
+      }
+
+      return issueTime;
+    }
+
+    @Override
     public long getTokenExpiration(final String tokenId, boolean validate) 
throws UnknownTokenException {
         // Check the in-memory collection first, to avoid file access when 
possible
         try {
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
index 88bd68d..695e62b 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
@@ -32,6 +32,7 @@ import java.util.Map;
 
 import javax.sql.DataSource;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.io.IOUtils;
 import org.apache.knox.gateway.services.security.token.TokenMetadata;
 
@@ -43,6 +44,7 @@ public class TokenStateDatabase {
   private static final String ADD_TOKEN_SQL = "INSERT INTO " + 
TOKENS_TABLE_NAME + "(token_id, issue_time, expiration, max_lifetime) VALUES(?, 
?, ?, ?)";
   private static final String REMOVE_TOKEN_SQL = "DELETE FROM " + 
TOKENS_TABLE_NAME + " WHERE token_id = ?";
   private static final String REMOVE_EXPIRED_TOKENS_SQL = "DELETE FROM " + 
TOKENS_TABLE_NAME + " WHERE expiration < ?";
+  static final String GET_TOKEN_ISSUE_TIME_SQL = "SELECT issue_time FROM " + 
TOKENS_TABLE_NAME + " WHERE token_id = ?";
   static final String GET_TOKEN_EXPIRATION_SQL = "SELECT expiration FROM " + 
TOKENS_TABLE_NAME + " WHERE token_id = ?";
   private static final String UPDATE_TOKEN_EXPIRATION_SQL = "UPDATE " + 
TOKENS_TABLE_NAME + " SET expiration = ? WHERE token_id = ?";
   static final String GET_MAX_LIFETIME_SQL = "SELECT max_lifetime FROM " + 
TOKENS_TABLE_NAME + " WHERE token_id = ?";
@@ -101,6 +103,15 @@ public class TokenStateDatabase {
     }
   }
 
+  long getTokenIssueTime(String tokenId) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement 
getTokenExpirationStatement = 
connection.prepareStatement(GET_TOKEN_ISSUE_TIME_SQL)) {
+      getTokenExpirationStatement.setString(1, tokenId);
+      try (ResultSet rs = getTokenExpirationStatement.executeQuery()) {
+        return rs.next() ? rs.getLong(1) : -1;
+      }
+    }
+  }
+
   long getTokenExpiration(String tokenId) throws SQLException {
     try (Connection connection = dataSource.getConnection(); PreparedStatement 
getTokenExpirationStatement = 
connection.prepareStatement(GET_TOKEN_EXPIRATION_SQL)) {
       getTokenExpirationStatement.setString(1, tokenId);
@@ -136,7 +147,7 @@ public class TokenStateDatabase {
 
   boolean updateMetadata(String tokenId, String metadataName, String 
metadataValue) throws SQLException {
     try (Connection connection = dataSource.getConnection(); PreparedStatement 
updateMetadataStatement = connection.prepareStatement(UPDATE_METADATA_SQL)) {
-      updateMetadataStatement.setString(1, metadataValue);
+      updateMetadataStatement.setString(1, 
metadataName.equals(TokenMetadata.PASSCODE) ? 
Base64.encodeBase64String(metadataValue.getBytes(UTF_8)) : metadataValue);
       updateMetadataStatement.setString(2, tokenId);
       updateMetadataStatement.setString(3, metadataName);
       return updateMetadataStatement.executeUpdate() == 1;
@@ -147,7 +158,7 @@ public class TokenStateDatabase {
     try (Connection connection = dataSource.getConnection(); PreparedStatement 
addMetadataStatement = connection.prepareStatement(ADD_METADATA_SQL)) {
       addMetadataStatement.setString(1, tokenId);
       addMetadataStatement.setString(2, metadataName);
-      addMetadataStatement.setString(3, metadataValue);
+      addMetadataStatement.setString(3, 
metadataName.equals(TokenMetadata.PASSCODE) ? 
Base64.encodeBase64String(metadataValue.getBytes(UTF_8)) : metadataValue);
       return addMetadataStatement.executeUpdate() == 1;
     }
   }
@@ -158,7 +169,8 @@ public class TokenStateDatabase {
       try (ResultSet rs = getMaxLifetimeStatement.executeQuery()) {
         final Map<String, String> metadataMap = new HashMap<>();
         while (rs.next()) {
-          metadataMap.put(rs.getString(1), rs.getString(2));
+          String metadataName = rs.getString(1);
+          metadataMap.put(metadataName, 
metadataName.equals(TokenMetadata.PASSCODE) ? new 
String(Base64.decodeBase64(rs.getString(2).getBytes(UTF_8)), UTF_8) : 
rs.getString(2));
         }
         return metadataMap.isEmpty() ? null : new TokenMetadata(metadataMap);
       }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
index 101c3cc..e5d0707 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
@@ -199,6 +199,12 @@ public interface TokenStateServiceMessages {
   @Message(level = MessageLevel.ERROR, text = "An error occurred while 
removing expired tokens from the database : {1}")
   void errorRemovingTokensFromDatabase(String errorMessage, @StackTrace(level 
= MessageLevel.DEBUG) Exception e);
 
+  @Message(level = MessageLevel.DEBUG, text = "Fetched issue time for {0} from 
the database : {1}")
+  void fetchedIssueTimeFromDatabase(String tokenId, long issueTime);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while 
fetching issue time for {0} from the database : {1}")
+  void errorFetchingIssueTimeFromDatabase(String tokenId, String errorMessage, 
@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
   @Message(level = MessageLevel.DEBUG, text = "Fetched expiration for {0} from 
the database : {1}")
   void fetchedExpirationFromDatabase(String tokenId, long expiration);
 
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
index f5450a7..1fce481 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
@@ -137,6 +137,8 @@ public class ZookeeperTokenStateService extends 
AliasBasedTokenStateService impl
           setMaxLifetime(tokenId, maxLifeTime);
         } else if (alias.endsWith(TOKEN_META_POSTFIX)) {
           addMetadataInMemory(tokenId, TokenMetadata.fromJSON(value));
+        } else if (alias.endsWith(TOKEN_ISSUE_TIME_POSTFIX)) {
+          setIssueTimeInMemory(tokenId, Long.parseLong(value));
         } else {
           final long expiration = Long.parseLong(value);
           updateExpirationInMemory(tokenId, expiration);
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
index fc9f316..96c5d38 100644
--- 
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
@@ -524,6 +524,7 @@ public class AliasBasedTokenStateServiceTest extends 
DefaultTokenStateServiceTes
 
     Map<String, Long> tokenExpirations = getTokenExpirationsField(tss);
     Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss);
+    Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, true);
 
     Set<AliasBasedTokenStateService.TokenState> unpersistedState = 
getUnpersistedStateField(tss);
 
@@ -535,8 +536,12 @@ public class AliasBasedTokenStateServiceTest extends 
DefaultTokenStateServiceTes
                  TOKEN_COUNT,
                  maxTokenLifetimes.size());
 
+    assertEquals("Expected the tokens issue times to have been added in the 
base class cache.",
+                 TOKEN_COUNT,
+                 tokenIssueTimes.size());
+
     assertEquals("Expected the unpersisted state to have been added.",
-                 (TOKEN_COUNT * 2), // Two TokenState entries per token 
(expiration, max lifetime)
+                 (TOKEN_COUNT * 3), // Two TokenState entries per token 
(expiration, max lifetime, issue time)
                  unpersistedState.size());
 
     // Verify that the expected methods were invoked
@@ -605,6 +610,7 @@ public class AliasBasedTokenStateServiceTest extends 
DefaultTokenStateServiceTes
 
     Map<String, Long> tokenExpirations = getTokenExpirationsField(tss);
     Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss);
+    Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, true);
 
     Set<AliasBasedTokenStateService.TokenState> unpersistedState = 
getUnpersistedStateField(tss);
 
@@ -616,8 +622,12 @@ public class AliasBasedTokenStateServiceTest extends 
DefaultTokenStateServiceTes
                  TOKEN_COUNT,
                  maxTokenLifetimes.size());
 
+    assertEquals("Expected the tokens issue times to have been added in the 
base class cache.",
+                 TOKEN_COUNT,
+                 tokenIssueTimes.size());
+
     assertEquals("Expected the unpersisted state to have been added.",
-                 (TOKEN_COUNT * 2), // Two TokenState entries per token 
(expiration, max lifetime)
+                 (TOKEN_COUNT * 3), // Two TokenState entries per token 
(expiration, max lifetime, issue time)
                  unpersistedState.size());
 
     // Verify that the expected methods were invoked
@@ -641,7 +651,7 @@ public class AliasBasedTokenStateServiceTest extends 
DefaultTokenStateServiceTes
     }
 
     final List<AliasBasedTokenStateService.TokenState> unpersistedTokenStates 
= new ArrayList<>(getUnpersistedStateField(tss, false));
-    final int expectedAliasCount = 2 * tokenCount; //expiration + max for each 
token
+    final int expectedAliasCount = 3 * tokenCount; //expiration + max + issue 
time for each token
     assertEquals(expectedAliasCount, unpersistedTokenStates.size());
     for (JWTToken token : testTokens) {
       String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
@@ -835,6 +845,13 @@ public class AliasBasedTokenStateServiceTest extends 
DefaultTokenStateServiceTes
     return (Map<String, Long>) maxTokenLifetimesField.get(tss);
   }
 
+  private static Map<String, Long> getTokenIssueTimesField(TokenStateService 
tss, boolean fromGrandParent) throws Exception {
+    final Class<TokenStateService> clazz = (Class<TokenStateService>) 
(fromGrandParent ? tss.getClass().getSuperclass().getSuperclass() : 
tss.getClass().getSuperclass());
+    Field tokenIssueTimesField = clazz.getDeclaredField("tokenIssueTimes");
+    tokenIssueTimesField.setAccessible(true);
+    return (Map<String, Long>) tokenIssueTimesField.get(tss);
+  }
+
   private static Set<AliasBasedTokenStateService.TokenState> 
getUnpersistedStateField(TokenStateService tss) throws Exception {
     return getUnpersistedStateField(tss, true);
   }
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 8d9836d..ab9103c 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
@@ -300,6 +300,13 @@ public class DefaultTokenStateServiceTest {
     assertNotNull(tss.getTokenMetadata(tokenId));
     assertEquals(tss.getTokenMetadata(tokenId).getComment(), comment);
     assertTrue(tss.getTokenMetadata(tokenId).isEnabled());
+
+    final String passcode = "myPasscode";
+    final TokenMetadata metadata = new TokenMetadata(userName, comment, true);
+    metadata.setPasscode(passcode);
+    tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), metadata);
+    assertNotNull(tss.getTokenMetadata(tokenId));
+    assertEquals(tss.getTokenMetadata(tokenId).getPasscode(), passcode);
   }
 
   protected static JWTToken createMockToken(final long expiration) {
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
index 7658e4f..2ae6408 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
@@ -17,6 +17,7 @@
  */
 package org.apache.knox.gateway.services.token.impl;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -30,11 +31,14 @@ import java.sql.SQLException;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.HmacAlgorithms;
 import org.apache.derby.drda.NetworkServerControl;
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
 import org.apache.knox.gateway.shell.jdbc.Database;
 import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
 import org.apache.knox.gateway.util.JDBCUtils;
@@ -59,6 +63,7 @@ public class JDBCTokenStateServiceTest {
   private static NetworkServerControl derbyNetworkServerControl;
   private static Database derbyDatabase;
   private static JDBCTokenStateService jdbcTokenStateService;
+  private static TokenMAC tokenMAC;
 
   @SuppressWarnings("PMD.JUnit4TestShouldUseBeforeAnnotation")
   @BeforeClass
@@ -85,7 +90,10 @@ public class JDBCTokenStateServiceTest {
     jdbcTokenStateService = new JDBCTokenStateService();
     jdbcTokenStateService.setAliasService(aliasService);
     jdbcTokenStateService.init(gatewayConfig, null);
+
     assertTrue(derbyDatabase.hasTable(TokenStateDatabase.TOKENS_TABLE_NAME));
+
+    tokenMAC = new TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(), 
"sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray());
   }
 
   private static Database prepareDerbyDatabase(Path derbyDatabaseFolder) 
throws SQLException {
@@ -143,17 +151,24 @@ public class JDBCTokenStateServiceTest {
   @Test(expected = UnknownTokenException.class)
   public void testAddMetadata() throws Exception {
     final String tokenId = UUID.randomUUID().toString();
+    final String passcode = UUID.randomUUID().toString();
+    final String passcodeMac = tokenMAC.hash(tokenId, 1, "sampleUser", 
passcode);
     final TokenMetadata tokenMetadata = new TokenMetadata("sampleUser", "my 
test comment", false);
+    tokenMetadata.setPasscode(passcodeMac);
     jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
     jdbcTokenStateService.addMetadata(tokenId, tokenMetadata);
 
     assertEquals("sampleUser", 
jdbcTokenStateService.getTokenMetadata(tokenId).getUserName());
     assertEquals("my test comment", 
jdbcTokenStateService.getTokenMetadata(tokenId).getComment());
     assertFalse(jdbcTokenStateService.getTokenMetadata(tokenId).isEnabled());
+    final String storedPasscode = 
jdbcTokenStateService.getTokenMetadata(tokenId).getPasscode();
+    assertEquals(passcodeMac, storedPasscode);
 
     assertEquals("sampleUser", getStringTokenAttributeFromDatabase(tokenId, 
getSelectMetadataSql(TokenMetadata.USER_NAME)));
     assertEquals("my test comment", 
getStringTokenAttributeFromDatabase(tokenId, 
getSelectMetadataSql(TokenMetadata.COMMENT)));
     assertEquals("false", getStringTokenAttributeFromDatabase(tokenId, 
getSelectMetadataSql(TokenMetadata.ENABLED)));
+    final String storedPasscodeInDb = new 
String(Base64.decodeBase64(getStringTokenAttributeFromDatabase(tokenId, 
getSelectMetadataSql(TokenMetadata.PASSCODE))), UTF_8);
+    assertEquals(passcodeMac, storedPasscodeInDb);
 
     //enable the token (it was disabled)
     tokenMetadata.setEnabled(true);
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/TokenMACTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/TokenMACTest.java
new file mode 100644
index 0000000..368e343
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/TokenMACTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.util.UUID;
+
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
+import org.junit.Test;
+
+public class TokenMACTest {
+
+  @Test
+  public void testHash() throws Exception {
+    final TokenMAC tokenMAC = new 
TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(), 
"sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray());
+    final String toBeHashed = "sampleTokenContent";
+    final String tokenId = UUID.randomUUID().toString();
+    final long issueTime = 123L;
+    final String userName = "smolnar";
+    final String hashed = tokenMAC.hash(tokenId, issueTime, userName, 
toBeHashed);
+    assertNotEquals(toBeHashed, hashed);
+    assertEquals(hashed, tokenMAC.hash(tokenId, issueTime, userName, 
toBeHashed));
+  }
+
+}
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 e484f56..41ea7eb 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
@@ -17,6 +17,7 @@
  */
 package org.apache.knox.gateway.service.knoxtoken;
 
+import java.nio.charset.StandardCharsets;
 import java.security.KeyStoreException;
 import java.security.Principal;
 import java.security.cert.Certificate;
@@ -30,6 +31,7 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 import javax.annotation.PostConstruct;
 import javax.inject.Singleton;
@@ -49,6 +51,7 @@ 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.ServiceLifecycleException;
 import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.AliasServiceException;
 import org.apache.knox.gateway.services.security.KeystoreService;
@@ -63,6 +66,7 @@ import 
org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
 import org.apache.knox.gateway.util.JsonUtils;
 import org.apache.knox.gateway.util.Tokens;
 
@@ -78,6 +82,7 @@ public class TokenResource {
   private static final String TOKEN_TYPE = "token_type";
   private static final String ACCESS_TOKEN = "access_token";
   private static final String TOKEN_ID = "token_id";
+  private static final String PASSCODE = "passcode";
   private static final String MANAGED_TOKEN = "managed";
   private static final String TARGET_URL = "target_url";
   private static final String ENDPOINT_PUBLIC_CERT = "endpoint_public_cert";
@@ -118,6 +123,7 @@ public class TokenResource {
 
   // Optional token store service
   private TokenStateService tokenStateService;
+  private TokenMAC tokenMAC;
   private final Map<String, String> tokenStateServiceStatusMap = new 
HashMap<>();
 
   private Optional<Long> renewInterval = Optional.empty();
@@ -133,7 +139,7 @@ public class TokenResource {
   ServletContext context;
 
   @PostConstruct
-  public void init() throws AliasServiceException {
+  public void init() throws AliasServiceException, ServiceLifecycleException {
 
     String audiences = context.getInitParameter(TOKEN_AUDIENCES_PARAM);
     if (audiences != null) {
@@ -190,6 +196,9 @@ public class TokenResource {
 
       GatewayServices services = (GatewayServices) 
context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
       tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+      final GatewayConfig gatewayConfig = (GatewayConfig) 
context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+      final AliasService aliasService = 
services.getService(ServiceType.ALIAS_SERVICE);
+      tokenMAC = new TokenMAC(gatewayConfig.getKnoxTokenHashAlgorithm(), 
aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME));
 
       String renewIntervalValue = 
context.getInitParameter(TOKEN_EXP_RENEWAL_INTERVAL);
       if (renewIntervalValue != null && !renewIntervalValue.isEmpty()) {
@@ -528,7 +537,7 @@ public class TokenResource {
         String tokenId = TokenUtils.getTokenId(token);
         log.issuedToken(getTopologyName(), 
Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
 
-        HashMap<String, Object> map = new HashMap<>();
+        final HashMap<String, Object> map = new HashMap<>();
         map.put(ACCESS_TOKEN, accessToken);
         map.put(TOKEN_ID, tokenId);
         map.put(MANAGED_TOKEN, String.valueOf(managedToken));
@@ -543,17 +552,22 @@ public class TokenResource {
         if (endpointPublicCert != null) {
           map.put(ENDPOINT_PUBLIC_CERT, endpointPublicCert);
         }
+        final String passcode = UUID.randomUUID().toString();
+        map.put(PASSCODE, generatePasscodeField(tokenId, passcode));
 
         String jsonResponse = JsonUtils.renderAsJsonString(map);
 
         // Optional token store service persistence
         if (tokenStateService != null) {
+          final long issueTime = System.currentTimeMillis();
           tokenStateService.addToken(tokenId,
-                                     System.currentTimeMillis(),
+                                     issueTime,
                                      expires,
                                      
maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
           final String comment = request.getParameter(COMMENT);
-          tokenStateService.addMetadata(tokenId, new 
TokenMetadata(p.getName(), StringUtils.isBlank(comment) ? null : comment));
+          final TokenMetadata tokenMetadata = new TokenMetadata(p.getName(), 
StringUtils.isBlank(comment) ? null : comment);
+          tokenMetadata.setPasscode(tokenMAC.hash(tokenId, issueTime, 
p.getName(), passcode));
+          tokenStateService.addMetadata(tokenId, tokenMetadata);
           log.storedToken(getTopologyName(), 
Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
         }
 
@@ -567,6 +581,11 @@ public class TokenResource {
     return Response.ok().entity("{ \"Unable to acquire token.\" }").build();
   }
 
+  private String generatePasscodeField(String tokenId, String passcode) {
+    final String base64TokenIdPasscode = 
Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" + 
Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8));
+    return 
Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8));
+  }
+
   void addClientDataToMap(String[] tokenClientData,
       Map<String,Object> map) {
     String[] kv;
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 ba4d338..9438a79 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
@@ -30,6 +30,7 @@ import com.nimbusds.jose.JWSVerifier;
 import com.nimbusds.jose.crypto.RSASSASigner;
 import com.nimbusds.jose.crypto.RSASSAVerifier;
 
+import org.apache.commons.codec.digest.HmacAlgorithms;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
@@ -45,6 +46,7 @@ import 
org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
 import org.apache.knox.gateway.services.token.impl.JDBCTokenStateService;
 import org.apache.knox.gateway.util.JsonUtils;
 import org.easymock.EasyMock;
@@ -154,12 +156,14 @@ public class TokenServiceResourceTest {
     if (contextExpectations.containsKey(tokenStateServiceType)) {
       EasyMock.expect(config.getServiceParameter(tokenStateServiceType, 
"impl")).andReturn(contextExpectations.get(tokenStateServiceType)).anyTimes();
     }
+    
EasyMock.expect(config.getKnoxTokenHashAlgorithm()).andReturn(HmacAlgorithms.HMAC_SHA_256.getName()).anyTimes();
     tss = new TestTokenStateService();
     
EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
 
     AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
     
EasyMock.expect(services.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
     
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TokenUtils.SIGNING_HMAC_SECRET_ALIAS)).andReturn(null).anyTimes();
+    
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME)).andReturn("sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray()).anyTimes();
 
     authority = new TestJWTokenAuthority(publicKey, privateKey);
     
EasyMock.expect(services.getService(ServiceType.TOKEN_SERVICE)).andReturn(authority).anyTimes();
@@ -1220,8 +1224,9 @@ public class TokenServiceResourceTest {
       return maxLifetimes.get(token);
     }
 
-    long getExpiration(final String token) {
-      return expirationData.get(token);
+    @Override
+    public long getTokenIssueTime(String tokenId) throws UnknownTokenException 
{
+      return issueTimes.getOrDefault(tokenId, 0L);
     }
 
     @Override
@@ -1292,7 +1297,7 @@ public class TokenServiceResourceTest {
 
     @Override
     public long getTokenExpiration(String tokenId) {
-      return 0;
+      return expirationData.getOrDefault(tokenId, 0L);
     }
 
     @Override
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 38d866e..4474c33 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -698,6 +698,11 @@ public interface GatewayConfig {
   long getKnoxTokenStateAliasPersistenceInterval();
 
   /**
+   * @return the HMAC algorithm name to be used to sign generated Knox Token 
content (e.g. the token.id claim)
+   */
+  String getKnoxTokenHashAlgorithm();
+
+  /**
    * @return the list of topologies that should be hidden on Knox homepage
    */
   Set<String> getHiddenTopologiesOnHomepage();
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
index 97455a5..6fcaf03 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
@@ -30,6 +30,7 @@ public class TokenMetadata {
   public static final String USER_NAME = "userName";
   public static final String COMMENT = "comment";
   public static final String ENABLED = "enabled";
+  public static final String PASSCODE = "passcode";
 
   private final Map<String, String> metadataMap = new HashMap<>();
 
@@ -78,6 +79,14 @@ public class TokenMetadata {
     return Boolean.parseBoolean(metadataMap.get(ENABLED));
   }
 
+  public void setPasscode(String passcode) {
+    saveMetadata(PASSCODE, passcode);
+  }
+
+  public String getPasscode() {
+    return metadataMap.get(PASSCODE);
+  }
+
   public String toJSON() {
     return JsonUtils.renderAsJsonString(metadataMap);
   }
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 ff1e3a9..c6d22e7 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
@@ -66,6 +66,15 @@ public interface TokenStateService extends Service {
   void addToken(String tokenId, long issueTime, long expiration, long 
maxLifetimeDuration);
 
   /**
+   * @param tokenId
+   *          The token unique identifier.
+   * @return The time the token was issued.
+   * @throws UnknownTokenException
+   *           if token is not found.
+   */
+  long getTokenIssueTime(String tokenId) throws UnknownTokenException;
+
+  /**
    * Checks if the token is expired.
    *
    * @param token The token.
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/TokenMAC.java
 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/TokenMAC.java
new file mode 100644
index 0000000..2a632bc
--- /dev/null
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/TokenMAC.java
@@ -0,0 +1,66 @@
+/*
+ * 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.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+
+public class TokenMAC {
+  public static final String KNOX_TOKEN_HASH_KEY_ALIAS_NAME = 
"knox.token.hash.key";
+
+  private final Mac mac;
+  private final Lock hashLock = new ReentrantLock(true);
+
+  public TokenMAC(String algorithm, char[] knoxTokenHashKey) throws 
ServiceLifecycleException {
+    try {
+      if (knoxTokenHashKey != null) {
+        final SecretKey key = new SecretKeySpec(new 
String(knoxTokenHashKey).getBytes(StandardCharsets.UTF_8), algorithm);
+        mac = Mac.getInstance(algorithm);
+        mac.init(key);
+      } else {
+        throw new ServiceLifecycleException("Missing " + 
KNOX_TOKEN_HASH_KEY_ALIAS_NAME + " alias from Gateway's credential store");
+      }
+    } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+      throw new ServiceLifecycleException("Error while initiating Knox Token 
MAC: " + e, e);
+    }
+  }
+
+  public String hash(String tokenId, long issueTime, String userName, String 
toBeHashed) {
+    hashLock.lock();
+    try {
+      mac.update(getSalt(tokenId, issueTime, userName));
+      return new 
String(mac.doFinal(toBeHashed.getBytes(StandardCharsets.UTF_8)), 
StandardCharsets.UTF_8);
+    } finally {
+      hashLock.unlock();
+    }
+  }
+
+  private byte[] getSalt(String tokenId, long issueTime, String userName) {
+    return (tokenId + issueTime + userName).getBytes(StandardCharsets.UTF_8);
+  }
+
+}
diff --git 
a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
 
b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index ab8df65..c65fb66 100644
--- 
a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ 
b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -807,6 +807,11 @@ public class GatewayTestConfig extends Configuration 
implements GatewayConfig {
   }
 
   @Override
+  public String getKnoxTokenHashAlgorithm() {
+    return null;
+  }
+
+  @Override
   public Set<String> getHiddenTopologiesOnHomepage() {
     return Collections.emptySet();
   }

Reply via email to