You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2021/03/22 21:25:21 UTC

[knox] branch master updated: KNOX-2557 - Persisting token related metadata using Knox's alias service (#420)

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 9e2b831  KNOX-2557 - Persisting token related metadata using Knox's alias service (#420)
9e2b831 is described below

commit 9e2b831343974670057c24ea590cb942f2433be3
Author: Sandor Molnar <sm...@cloudera.com>
AuthorDate: Mon Mar 22 22:25:14 2021 +0100

    KNOX-2557 - Persisting token related metadata using Knox's alias service (#420)
---
 .../token/impl/AliasBasedTokenStateService.java    | 77 +++++++++++++++++++-
 .../token/impl/DefaultTokenStateService.java       | 13 ++++
 .../token/impl/JournalBasedTokenStateService.java  | 20 +++++-
 .../token/impl/ZookeeperTokenStateService.java     |  3 +
 .../token/impl/state/FileTokenStateJournal.java    | 46 +++++++++---
 .../impl/state/MultiFileTokenStateJournal.java     |  5 +-
 .../gateway/services/token/state/JournalEntry.java |  8 +++
 .../services/token/state/TokenStateJournal.java    |  5 +-
 .../impl/AliasBasedTokenStateServiceTest.java      | 28 ++++++--
 .../token/impl/DefaultTokenStateServiceTest.java   | 62 +++++++++++-----
 .../impl/JournalBasedTokenStateServiceTest.java    |  3 +-
 .../token/impl/ZookeeperTokenStateServiceTest.java |  8 +++
 .../state/AbstractFileTokenStateJournalTest.java   |  6 +-
 .../impl/state/FileTokenStateJournalTest.java      | 78 +++++++++++---------
 .../gateway/service/knoxtoken/TokenResource.java   |  2 +
 .../knoxtoken/TokenServiceResourceTest.java        | 10 +++
 .../services/security/token/TokenMetadata.java     | 82 ++++++++++++++++++++++
 .../services/security/token/TokenStateService.java | 18 +++++
 18 files changed, 392 insertions(+), 82 deletions(-)

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 545fa96..4ac128a 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
@@ -42,6 +42,7 @@ 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.impl.DefaultKeystoreService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.token.TokenStateServiceStatistics;
 import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory;
@@ -55,6 +56,7 @@ import org.apache.knox.gateway.util.ExecutorServiceUtils;
 public class AliasBasedTokenStateService extends DefaultTokenStateService implements TokenStatePeristerMonitorListener {
 
   static final String TOKEN_MAX_LIFETIME_POSTFIX = "--max";
+  static final String TOKEN_META_POSTFIX = "--meta";
 
   protected AliasService aliasService;
 
@@ -157,6 +159,9 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem
           super.updateExpiration(tokenId, expiration);
           super.setMaxLifetime(tokenId, maxLifeTime);
           count+=2;
+        } 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())));
         }
 
         // log some progress (it's very useful in case a huge amount of token related aliases in __gateway-credentials.jceks)
@@ -265,7 +270,7 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem
     }
 
     try {
-      journal.add(tokenId, issueTime, expiration, maxLifetimeDuration);
+      journal.add(tokenId, issueTime, expiration, maxLifetimeDuration, null);
     } catch (IOException e) {
       log.failedToAddJournalEntry(tokenId, e);
     }
@@ -419,8 +424,45 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem
     super.updateExpiration(tokenId, expiration);
   }
 
+  @Override
+  public void addMetadata(String tokenId, TokenMetadata metadata) {
+    addMetadataInMemory(tokenId, metadata);
+    try {
+      final JournalEntry entry = journal.get(tokenId);
+      if (entry != null) {
+        journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), Long.parseLong(entry.getExpiration()), Long.parseLong(entry.getMaxLifetime()), metadata);
+      }
+    } catch (IOException e) {
+      log.failedToAddJournalEntry(tokenId, e);
+    }
+
+    synchronized (unpersistedState) {
+      unpersistedState.add(new TokenMetadataState(tokenId, metadata));
+    }
+  }
+
+  protected void addMetadataInMemory(String tokenId, TokenMetadata metadata) {
+    super.addMetadata(tokenId, metadata);
+  }
+
+  @Override
+  public TokenMetadata getTokenMetadata(String tokenId) {
+    TokenMetadata tokenMetadata = super.getTokenMetadata(tokenId);
+    if (tokenMetadata == null) {
+      try {
+        final char[] tokenMetadataAliasValue = getPasswordUsingAliasService(tokenId + TOKEN_META_POSTFIX);
+        if (tokenMetadataAliasValue != null) {
+          tokenMetadata = TokenMetadata.fromJSON(new String(tokenMetadataAliasValue));
+        }
+      } catch (AliasServiceException e) {
+        log.errorAccessingTokenState(tokenId, e);
+      }
+    }
+    return tokenMetadata;
+  }
+
   enum TokenStateType {
-    EXP(1), MAX(2);
+    EXP(1), MAX(2), META(3);
 
     private final int id;
 
@@ -538,4 +580,35 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem
     }
   }
 
+  private static final class TokenMetadataState implements TokenState {
+
+    private final String tokenId;
+    private final TokenMetadata metadata;
+
+    TokenMetadataState(String tokenId, TokenMetadata metadata) {
+      this.tokenId = tokenId;
+      this.metadata = metadata;
+    }
+
+    @Override
+    public String getTokenId() {
+      return tokenId;
+    }
+
+    @Override
+    public String getAlias() {
+      return tokenId + TOKEN_META_POSTFIX;
+    }
+
+    @Override
+    public String getAliasValue() {
+      return metadata.toJSON();
+    }
+
+    @Override
+    public TokenStateType getType() {
+      return TokenStateType.META;
+    }
+  }
+
 }
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 93d8560..f08412d 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
@@ -38,6 +38,7 @@ import javax.management.ObjectName;
 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.token.TokenMetadata;
 import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
@@ -62,6 +63,8 @@ public class DefaultTokenStateService implements TokenStateService {
 
   private final Map<String, Long> maxTokenLifetimes = new ConcurrentHashMap<>();
 
+  private final Map<String, TokenMetadata> metadataMap = new ConcurrentHashMap<>();
+
   // Token eviction interval (in seconds)
   private long tokenEvictionInterval;
 
@@ -282,6 +285,7 @@ public class DefaultTokenStateService implements TokenStateService {
   private void removeTokenState(final Set<String> tokenIds) {
     tokenExpirations.keySet().removeAll(tokenIds);
     maxTokenLifetimes.keySet().removeAll(tokenIds);
+    metadataMap.keySet().removeAll(tokenIds);
     log.removedTokenState(String.join(", ", tokenIds));
   }
 
@@ -375,4 +379,13 @@ public class DefaultTokenStateService implements TokenStateService {
     return tokenExpirations.keySet().stream().collect(Collectors.toList());
   }
 
+  @Override
+  public void addMetadata(String tokenId, TokenMetadata metadata) {
+    metadataMap.put(tokenId, metadata);
+  }
+
+  @Override
+  public TokenMetadata getTokenMetadata(String tokenId) {
+    return metadataMap.get(tokenId);
+  }
 }
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 abee0e5..25597e2 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
@@ -20,6 +20,7 @@ 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.TokenMetadata;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
 import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory;
 import org.apache.knox.gateway.services.token.state.JournalEntry;
@@ -68,7 +69,7 @@ public class JournalBasedTokenStateService extends DefaultTokenStateService {
         super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration);
 
         try {
-            journal.add(tokenId, issueTime, expiration, maxLifetimeDuration);
+            journal.add(tokenId, issueTime, expiration, maxLifetimeDuration, null);
         } catch (IOException e) {
             log.failedToAddJournalEntry(tokenId, e);
         }
@@ -151,7 +152,8 @@ public class JournalBasedTokenStateService extends DefaultTokenStateService {
                 journal.add(entry.getTokenId(),
                             Long.parseLong(entry.getIssueTime()),
                             expiration,
-                            Long.parseLong(entry.getMaxLifetime()));
+                            Long.parseLong(entry.getMaxLifetime()),
+                            entry.getTokenMetadata());
             }
         } catch (IOException e) {
             log.errorAccessingTokenState(e);
@@ -170,4 +172,18 @@ public class JournalBasedTokenStateService extends DefaultTokenStateService {
         return (entry == null);
     }
 
+  @Override
+  public void addMetadata(String tokenId, TokenMetadata metadata) {
+    super.addMetadata(tokenId, metadata);
+    try {
+      JournalEntry entry = journal.get(tokenId);
+      if (entry == null) {
+        log.journalEntryNotFound(tokenId);
+      } else {
+        journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), Long.parseLong(entry.getExpiration()), Long.parseLong(entry.getMaxLifetime()), metadata);
+      }
+    } catch (IOException e) {
+      log.errorAccessingTokenState(e);
+    }
+  }
 }
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 161bfc3..60d0300 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
@@ -30,6 +30,7 @@ import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.factory.AliasServiceFactory;
 import org.apache.knox.gateway.services.security.AliasServiceException;
 import org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.token.RemoteTokenStateChangeListener;
 
 /**
@@ -133,6 +134,8 @@ public class ZookeeperTokenStateService extends AliasBasedTokenStateService impl
         if (alias.endsWith(TOKEN_MAX_LIFETIME_POSTFIX)) {
           final long maxLifeTime = Long.parseLong(value);
           setMaxLifetime(tokenId, maxLifeTime);
+        } else if (alias.endsWith(TOKEN_META_POSTFIX)) {
+          addMetadataInMemory(tokenId, TokenMetadata.fromJSON(value));
         } else {
           final long expiration = Long.parseLong(value);
           updateExpirationInMemory(tokenId, expiration);
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java
index 11dd702..966766d 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java
@@ -22,6 +22,7 @@ import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.services.token.state.JournalEntry;
 import org.apache.knox.gateway.services.token.state.TokenStateJournal;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.token.impl.TokenStateServiceMessages;
 
 import java.io.BufferedReader;
@@ -49,6 +50,8 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
     protected static final int INDEX_ISSUE_TIME   = 1;
     protected static final int INDEX_EXPIRATION   = 2;
     protected static final int INDEX_MAX_LIFETIME = 3;
+    protected static final int INDEX_USERNAME     = 4;
+    protected static final int INDEX_COMMENT      = 5;
 
     protected static final TokenStateServiceMessages log = MessagesFactory.get(TokenStateServiceMessages.class);
 
@@ -68,7 +71,7 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
     }
 
     @Override
-    public abstract void add(String tokenId, long issueTime, long expiration, long maxLifetime) throws IOException;
+    public abstract void add(String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) throws IOException;
 
     @Override
     public void add(JournalEntry entry) throws IOException {
@@ -133,24 +136,31 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
     /**
      * A JournalEntry implementation for File-based TokenStateJournal implementations
      */
-    static final class FileJournalEntry implements JournalEntry {
+    public static final class FileJournalEntry implements JournalEntry {
         private final String tokenId;
         private final String issueTime;
         private final String expiration;
         private final String maxLifetime;
+        private final TokenMetadata tokenMetadata;
 
         FileJournalEntry(final String tokenId, long issueTime, long expiration, long maxLifetime) {
-            this(tokenId, String.valueOf(issueTime), String.valueOf(expiration), String.valueOf(maxLifetime));
+          this(tokenId, String.valueOf(issueTime), String.valueOf(expiration), String.valueOf(maxLifetime), null);
+        }
+
+        FileJournalEntry(final String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) {
+            this(tokenId, String.valueOf(issueTime), String.valueOf(expiration), String.valueOf(maxLifetime), tokenMetadata);
         }
 
         FileJournalEntry(final String tokenId,
                          final String issueTime,
                          final String expiration,
-                         final String maxLifetime) {
+                         final String maxLifetime,
+                         final TokenMetadata tokenMetadata) {
             this.tokenId = tokenId;
             this.issueTime = issueTime;
             this.expiration = expiration;
             this.maxLifetime = maxLifetime;
+            this.tokenMetadata = tokenMetadata;
         }
 
         @Override
@@ -174,8 +184,13 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
         }
 
         @Override
+        public TokenMetadata getTokenMetadata() {
+          return tokenMetadata;
+        }
+
+        @Override
         public String toString() {
-            String[] elements = new String[4];
+            String[] elements = new String[6];
 
             elements[INDEX_TOKEN_ID] = getTokenId();
 
@@ -188,12 +203,20 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
             String maxLifetime = getMaxLifetime();
             elements[INDEX_MAX_LIFETIME] = (maxLifetime != null) ? maxLifetime : "";
 
+            String userName = getTokenMetadata() == null ? "" : (getTokenMetadata().getUserName() == null ? "" : getTokenMetadata().getUserName());
+            elements[INDEX_USERNAME] = userName;
+
+            String comment = getTokenMetadata() == null ? "" : (getTokenMetadata().getComment() == null ? "" : getTokenMetadata().getComment());
+            elements[INDEX_COMMENT] = comment;
+
             return String.format(Locale.ROOT,
-                                 "%s,%s,%s,%s",
+                                 "%s,%s,%s,%s,%s,%s",
                                  elements[INDEX_TOKEN_ID],
                                  elements[INDEX_ISSUE_TIME],
                                  elements[INDEX_EXPIRATION],
-                                 elements[INDEX_MAX_LIFETIME]);
+                                 elements[INDEX_MAX_LIFETIME],
+                                 elements[INDEX_USERNAME],
+                                 elements[INDEX_COMMENT]);
         }
 
         /**
@@ -204,8 +227,8 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
           * @return A FileJournalEntry object created from the specified entry.
           */
         static FileJournalEntry parse(final String entry) {
-            String[] elements = entry.split(",");
-            if (elements.length < 4) {
+            String[] elements = entry.split(",", -1);
+            if (elements.length < 6) {
                 throw new IllegalArgumentException("Invalid journal entry: " + entry);
             }
 
@@ -213,11 +236,14 @@ abstract class FileTokenStateJournal implements TokenStateJournal {
             String issueTime   = elements[INDEX_ISSUE_TIME].trim();
             String expiration  = elements[INDEX_EXPIRATION].trim();
             String maxLifetime = elements[INDEX_MAX_LIFETIME].trim();
+            String userName    = elements[INDEX_USERNAME].trim();
+            String comment     = elements[INDEX_COMMENT].trim();
 
             return new FileJournalEntry(tokenId.isEmpty() ? null : tokenId,
                                         issueTime.isEmpty() ? null : issueTime,
                                         expiration.isEmpty() ? null : expiration,
-                                        maxLifetime.isEmpty() ? null : maxLifetime);
+                                        maxLifetime.isEmpty() ? null : maxLifetime,
+                                        new TokenMetadata(userName.isEmpty() ? null : userName, comment.isEmpty() ? null : comment));
         }
 
     }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java
index dfdd1e1..6fccd60 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java
@@ -19,6 +19,7 @@
 package org.apache.knox.gateway.services.token.impl.state;
 
 import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.token.state.JournalEntry;
 
 import java.io.BufferedWriter;
@@ -53,8 +54,8 @@ class MultiFileTokenStateJournal extends FileTokenStateJournal {
     }
 
     @Override
-    public void add(final String tokenId, long issueTime, long expiration, long maxLifetime) throws IOException {
-        add(Collections.singletonList(new FileJournalEntry(tokenId, issueTime, expiration, maxLifetime)));
+    public void add(final String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) throws IOException {
+        add(Collections.singletonList(new FileJournalEntry(tokenId, issueTime, expiration, maxLifetime, tokenMetadata)));
     }
 
     @Override
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java
index d520f45..ccd7833 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java
@@ -18,6 +18,8 @@
  */
 package org.apache.knox.gateway.services.token.state;
 
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+
 /**
  * An entry in the TokenStateJournal
  */
@@ -48,4 +50,10 @@ public interface JournalEntry {
      * @return The token's maximum allowed lifetime
      */
     String getMaxLifetime();
+
+    /**
+     * @return The metadata belongs to this token
+     */
+    TokenMetadata getTokenMetadata();
+
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java
index a51d162..f29378c 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java
@@ -22,6 +22,8 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+
 /**
  *
  */
@@ -34,10 +36,11 @@ public interface TokenStateJournal {
      * @param issueTime   The issue timestamp
      * @param expiration  The expiration time
      * @param maxLifetime The maximum allowed lifetime
+     * @param tokenMetafata The associated token metadata
      *
      * @throws IOException exception on error
      */
-    void add(String tokenId, long issueTime, long expiration, long maxLifetime)
+    void add(String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata)
         throws IOException;
 
     /**
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 5da2f5d..fc9f316 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
@@ -21,6 +21,7 @@ import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.security.AbstractAliasService;
 import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.impl.JWTToken;
 import org.apache.knox.gateway.services.token.state.JournalEntry;
@@ -511,7 +512,8 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes
       journal.add(token.getClaim(JWTToken.KNOX_ID_CLAIM),
                   System.currentTimeMillis(),
                   token.getExpiresDate().getTime(),
-                  System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24));
+                  System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24),
+                  null);
     }
 
     AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService();
@@ -563,32 +565,37 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes
       journal.add(token.getClaim(JWTToken.KNOX_ID_CLAIM),
                   System.currentTimeMillis(),
                   token.getExpiresDate().getTime(),
-                  System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24));
+                  System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24),
+                  null);
     }
 
     // Add an entry with an invalid token identifier
     journal.add("   ",
                 System.currentTimeMillis(),
                 System.currentTimeMillis(),
-                System.currentTimeMillis());
+                System.currentTimeMillis(),
+                null);
 
     // Add an entry with an invalid issue time
     journal.add(new TestJournalEntry(UUID.randomUUID().toString(),
                 "invalidLongValue",
                 String.valueOf(System.currentTimeMillis()),
-                String.valueOf(System.currentTimeMillis())));
+                String.valueOf(System.currentTimeMillis()),
+                new TokenMetadata("testUser")));
 
     // Add an entry with an invalid expiration time
     journal.add(new TestJournalEntry(UUID.randomUUID().toString(),
                 String.valueOf(System.currentTimeMillis()),
                 "invalidLongValue",
-                String.valueOf(System.currentTimeMillis())));
+                String.valueOf(System.currentTimeMillis()),
+                new TokenMetadata("testUser")));
 
     // Add an entry with an invalid max lifetime
     journal.add(new TestJournalEntry(UUID.randomUUID().toString(),
                                      String.valueOf(System.currentTimeMillis()),
                                      String.valueOf(System.currentTimeMillis()),
-                                     "invalidLongValue"));
+                                     "invalidLongValue",
+                                     new TokenMetadata("testUser")));
 
     AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService();
     tss.setAliasService(aliasService);
@@ -844,12 +851,14 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes
     private String issueTime;
     private String expiration;
     private String maxLifetime;
+    private TokenMetadata tokenMetadata;
 
-    TestJournalEntry(String tokenId, String issueTime, String expiration, String maxLifetime) {
+    TestJournalEntry(String tokenId, String issueTime, String expiration, String maxLifetime, TokenMetadata tokenMetadata) {
       this.tokenId     = tokenId;
       this.issueTime   = issueTime;
       this.expiration  = expiration;
       this.maxLifetime = maxLifetime;
+      this.tokenMetadata = tokenMetadata;
     }
 
     @Override
@@ -873,6 +882,11 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes
     }
 
     @Override
+    public TokenMetadata getTokenMetadata() {
+      return tokenMetadata;
+    }
+
+    @Override
     public String toString() {
       return tokenId + "," + issueTime + "," + expiration + "," + maxLifetime;
     }
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 6eaff90..1ed01ec 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
@@ -16,20 +16,13 @@
  */
 package org.apache.knox.gateway.services.token.impl;
 
-import com.nimbusds.jose.JWSSigner;
-import com.nimbusds.jose.crypto.RSASSASigner;
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.services.ServiceLifecycleException;
-import org.apache.knox.gateway.services.security.token.TokenStateService;
-import org.apache.knox.gateway.services.security.token.TokenUtils;
-import org.apache.knox.gateway.services.security.token.impl.JWT;
-import org.apache.knox.gateway.services.security.token.UnknownTokenException;
-import org.apache.knox.gateway.services.security.token.impl.JWTToken;
-import org.easymock.EasyMock;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.IOException;
 import java.nio.file.Files;
@@ -42,11 +35,22 @@ import java.util.Date;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.easymock.EasyMock;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
 
 public class DefaultTokenStateServiceTest {
 
@@ -262,6 +266,26 @@ public class DefaultTokenStateServiceTest {
     tss.getTokenExpiration(token);
   }
 
+  @Test
+  public void testAddTokenMetadata() throws Exception {
+    final JWT token = getJWTToken(System.currentTimeMillis());
+    final String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
+    final TokenStateService tss = new DefaultTokenStateService();
+    tss.addToken((JWTToken) token, System.currentTimeMillis());
+    assertNull(tss.getTokenMetadata(tokenId));
+
+    final String userName = "testUser";
+    tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata(userName));
+    assertNotNull(tss.getTokenMetadata(tokenId));
+    assertEquals(tss.getTokenMetadata(tokenId).getUserName(), userName);
+    assertTrue(tss.getTokenMetadata(tokenId).getComment().isEmpty());
+
+    final String comment = "this is my test comment";
+    tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata(userName, comment));
+    assertNotNull(tss.getTokenMetadata(tokenId));
+    assertEquals(tss.getTokenMetadata(tokenId).getComment(), comment);
+  }
+
   protected static JWTToken createMockToken(final long expiration) {
     return createMockToken("abcD1234eFGHIJKLmnoPQRSTUVwXYz", expiration);
   }
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java
index bcfaec3..bb5c99f 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java
@@ -157,7 +157,8 @@ public class JournalBasedTokenStateServiceTest extends DefaultTokenStateServiceT
         testJournal.add(uncachedTokenId,
                         System.currentTimeMillis(),
                         uncachedToken.getExpiresDate().getTime(),
-                        maxTokenLifetime);
+                        maxTokenLifetime,
+                        null);
         assertEquals("Expected the uncached journal entry", 1, testJournal.get().size());
 
         // Create and initialize the TokenStateService
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java
index 443d409..a31aa47 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java
@@ -48,6 +48,7 @@ import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.KeystoreService;
 import org.apache.knox.gateway.services.security.KeystoreServiceException;
 import org.apache.knox.gateway.services.security.MasterService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
 import org.easymock.EasyMock;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -115,6 +116,13 @@ public class ZookeeperTokenStateServiceTest {
     final long expiration = zktokenStateServiceNode2.getTokenExpiration("node1Token");
     Thread.sleep(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000);
     assertEquals(2000L, expiration);
+
+    final String userName = "testUser";
+    final String comment = "This is my test comment";
+    zktokenStateServiceNode1.addMetadata("node1Token", new TokenMetadata(userName, comment));
+    Thread.sleep(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000);
+    assertEquals(userName, zktokenStateServiceNode2.getTokenMetadata("node1Token").getUserName());
+    assertEquals(comment, zktokenStateServiceNode2.getTokenMetadata("node1Token").getComment());
   }
 
   @Test
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java
index 5d64f71..1c5e65f 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java
@@ -79,7 +79,7 @@ public abstract class AbstractFileTokenStateJournalTest {
         long issueTime = System.currentTimeMillis();
         long expiration = issueTime + TimeUnit.MINUTES.toMillis(5);
         long maxLifetime = issueTime + (5 * TimeUnit.MINUTES.toMillis(5));
-        journal.add(tokenId, issueTime, expiration, maxLifetime);
+        journal.add(tokenId, issueTime, expiration, maxLifetime, null);
 
         // Get the token state from the journal, and validate its contents
         JournalEntry entry = journal.get(tokenId);
@@ -109,7 +109,7 @@ public abstract class AbstractFileTokenStateJournalTest {
         long issueTime = System.currentTimeMillis();
         long expiration = issueTime + TimeUnit.MINUTES.toMillis(5);
         long maxLifetime = issueTime + (5 * TimeUnit.MINUTES.toMillis(5));
-        journal.add(tokenId, issueTime, expiration, maxLifetime);
+        journal.add(tokenId, issueTime, expiration, maxLifetime, null);
 
         // Get the token state from the journal, and validate its contents
         JournalEntry entry = journal.get(tokenId);
@@ -120,7 +120,7 @@ public abstract class AbstractFileTokenStateJournalTest {
         assertEquals(maxLifetime, Long.parseLong(entry.getMaxLifetime()));
 
         long updatedExpiration = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5);
-        journal.add(tokenId, issueTime, updatedExpiration, maxLifetime);
+        journal.add(tokenId, issueTime, updatedExpiration, maxLifetime, null);
 
         // Get and validate the updated token state
         entry = journal.get(tokenId);
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java
index da79a4c..2fe601d 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java
@@ -18,15 +18,15 @@
  */
 package org.apache.knox.gateway.services.token.impl.state;
 
-import org.apache.knox.gateway.services.token.state.JournalEntry;
-import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
+import org.apache.knox.gateway.services.token.state.JournalEntry;
+import org.junit.Test;
 
 public class FileTokenStateJournalTest {
 
@@ -40,14 +40,14 @@ public class FileTokenStateJournalTest {
         doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime);
     }
 
-    @Test(expected = IllegalArgumentException.class)
+    @Test
     public void testParseJournalEntry_MissingMaxLifetime() {
         final String tokenId     = UUID.randomUUID().toString();
         final Long   issueTime   = System.currentTimeMillis();
         final Long   expiration  = issueTime + TimeUnit.HOURS.toMillis(1);
         final Long   maxLifetime = null;
 
-        doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime);
+        doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime, "user", null);
     }
 
     @Test
@@ -76,57 +76,65 @@ public class FileTokenStateJournalTest {
     }
 
     @Test
+    public void tesParseTokenMetadata() throws Exception {
+      doTestParseJournalEntry("", "", "", "", "userName", "");
+      doTestParseJournalEntry("", "", "", "", "", "comment");
+    }
+
+    @Test
     public void testParseJournalEntry_AllMissing() {
-        doTestParseJournalEntry(null, null, null, " ");
+        doTestParseJournalEntry(null, null, null, " ", null, null);
+    }
+
+    private void doTestParseJournalEntry(final String tokenId, final Long issueTime, final Long expiration, final Long maxLifetime) {
+      doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime, null, null);
     }
 
     private void doTestParseJournalEntry(final String tokenId,
                                          final Long   issueTime,
                                          final Long   expiration,
-                                         final Long   maxLifetime) {
+                                         final Long   maxLifetime,
+                                         final String userName,
+                                         final String comment) {
         doTestParseJournalEntry(tokenId,
                                 (issueTime != null ? issueTime.toString() : null),
                                 (expiration != null ? expiration.toString() : null),
-                                (maxLifetime != null ? maxLifetime.toString() : null));
+                                (maxLifetime != null ? maxLifetime.toString() : null),
+                                userName, comment);
     }
 
     private void doTestParseJournalEntry(final String tokenId,
                                          final String issueTime,
                                          final String expiration,
-                                         final String maxLifetime) {
+                                         final String maxLifetime,
+                                         final String userName,
+                                         final String comment) {
         StringBuilder entryStringBuilder =
             new StringBuilder(tokenId != null ? tokenId : "").append(',')
                                                              .append(issueTime != null ? issueTime : "")
                                                              .append(',')
                                                              .append(expiration != null ? expiration : "")
                                                              .append(',')
-                                                             .append(maxLifetime != null ? maxLifetime : "");
+                                                             .append(maxLifetime != null ? maxLifetime : "")
+                                                             .append(",").append(userName == null ? "" : userName)
+                                                             .append(",").append(comment == null ? "" : comment);
 
         JournalEntry entry = FileTokenStateJournal.FileJournalEntry.parse(entryStringBuilder.toString());
         assertNotNull(entry);
-        if (tokenId != null && !tokenId.trim().isEmpty()) {
-            assertEquals(tokenId, entry.getTokenId());
-        } else {
-            assertNull(entry.getTokenId());
-        }
-
-        if (issueTime != null && !issueTime.trim().isEmpty()) {
-            assertEquals(issueTime, entry.getIssueTime());
-        } else {
-            assertNull(entry.getIssueTime());
-        }
-
-        if (expiration != null && !expiration.trim().isEmpty()) {
-            assertEquals(expiration, entry.getExpiration());
-        } else {
-            assertNull(entry.getExpiration());
-        }
-
-        if (maxLifetime != null && !maxLifetime.trim().isEmpty()) {
-            assertEquals(maxLifetime, entry.getMaxLifetime());
-        } else {
-            assertNull(entry.getMaxLifetime());
-        }
+        assertJournalEntryField(tokenId, entry.getTokenId());
+        assertJournalEntryField(issueTime, entry.getIssueTime());
+        assertJournalEntryField(expiration, entry.getExpiration());
+        assertJournalEntryField(maxLifetime, entry.getMaxLifetime());
+        assertJournalEntryField(userName, entry.getTokenMetadata().getUserName());
+        assertJournalEntryField(comment, entry.getTokenMetadata().getComment());
+    }
+
+    private void assertJournalEntryField(String received, String parsed) {
+      if (received != null && !received.trim().isEmpty()) {
+        assertEquals(received, parsed);
+      } else {
+        assertNull(parsed);
+      }
     }
 
 
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 d260f58..9f285ee 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
@@ -53,6 +53,7 @@ import org.apache.knox.gateway.services.security.KeystoreServiceException;
 import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
 import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
 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;
 import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.apache.knox.gateway.services.security.token.TokenUtils;
@@ -441,6 +442,7 @@ public class TokenResource {
                                      System.currentTimeMillis(),
                                      expires,
                                      maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
+          tokenStateService.addMetadata(tokenId, new TokenMetadata(p.getName()));
           log.storedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), tokenId);
         }
 
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 54e92a4..b9d57b0 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
@@ -33,6 +33,7 @@ import org.apache.knox.gateway.services.GatewayServices;
 import org.apache.knox.gateway.services.security.AliasService;
 import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
 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.TokenStateService;
 import org.apache.knox.gateway.services.security.token.TokenUtils;
 import org.apache.knox.gateway.services.security.token.UnknownTokenException;
@@ -1130,6 +1131,15 @@ public class TokenServiceResourceTest {
     }
 
     @Override
+    public void addMetadata(String tokenId, TokenMetadata metadata) {
+    }
+
+    @Override
+    public TokenMetadata getTokenMetadata(String tokenId) {
+      return null;
+    }
+
+    @Override
     public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
     }
 
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
new file mode 100644
index 0000000..f8a6bd3
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
@@ -0,0 +1,82 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.knox.gateway.util.JsonUtils;
+
+public class TokenMetadata {
+  private static final String JSON_ELEMENT_USER_NAME = "userName";
+  private static final String JSON_ELEMENT_COMMENT = "comment";
+  private static final String EMPTY_COMMENT = "";
+
+  private final String userName;
+  private final String comment;
+
+  public TokenMetadata(String userName) {
+    this(userName, EMPTY_COMMENT);
+  }
+
+  public TokenMetadata(String userName, String comment) {
+    this.userName = userName;
+    this.comment = comment;
+  }
+
+  public String getUserName() {
+    return userName;
+  }
+
+  public String getComment() {
+    return comment;
+  }
+
+  public String toJSON() {
+    final Map<String, String> metadataMap = new HashMap<>();
+    metadataMap.put(JSON_ELEMENT_USER_NAME, getUserName());
+    metadataMap.put(JSON_ELEMENT_COMMENT, getComment() == null ? EMPTY_COMMENT : getComment());
+    return JsonUtils.renderAsJsonString(metadataMap);
+  }
+
+  public static TokenMetadata fromJSON(String json) {
+    final Map<String, String> metadataMap = JsonUtils.getMapFromJsonString(json);
+    if (metadataMap != null) {
+      return new TokenMetadata(metadataMap.get(JSON_ELEMENT_USER_NAME), metadataMap.get(JSON_ELEMENT_COMMENT));
+    }
+    throw new IllegalArgumentException("Invalid metadata JSON: " + json);
+  }
+
+  @Override
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return EqualsBuilder.reflectionEquals(this, obj);
+  }
+
+  @Override
+  public int hashCode() {
+    return HashCodeBuilder.reflectionHashCode(this);
+  }
+}
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 007c2de..736a272 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
@@ -166,4 +166,22 @@ public interface TokenStateService extends Service {
    */
   long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException;
 
+  /**
+   * Adds metadata to the token identified by the given ID
+   *
+   * @param tokenId
+   *          The token's unique identifier.
+   * @param metadata
+   *          The metadata to be added
+   */
+  void addMetadata(String tokenId, TokenMetadata metadata);
+
+  /**
+   *
+   * @param tokenId
+   *          The token's unique identifier.
+   * @return The associated token metadata
+   */
+  TokenMetadata getTokenMetadata(String tokenId);
+
 }