knox-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From smol...@apache.org
Subject [knox] branch master updated: KNOX-2554 - Implemented JDBC Token State Service (#433)
Date Tue, 20 Apr 2021 06:38:02 GMT
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 b07dbee  KNOX-2554 - Implemented JDBC Token State Service (#433)
b07dbee is described below

commit b07dbee0b9bccf877d351d6e596554fb76e894ca
Author: Sandor Molnar <smolnar@apache.org>
AuthorDate: Tue Apr 20 08:37:53 2021 +0200

    KNOX-2554 - Implemented JDBC Token State Service (#433)
---
 gateway-server/pom.xml                             |  26 +++
 .../gateway/config/impl/GatewayConfigImpl.java     |  53 +++--
 .../services/factory/TokenStateServiceFactory.java |   6 +-
 .../token/impl/DefaultTokenStateService.java       |  30 +--
 .../services/token/impl/JDBCTokenStateService.java | 227 +++++++++++++++++++++
 .../services/token/impl/TokenStateDatabase.java    | 114 +++++++++++
 .../token/impl/TokenStateServiceMessages.java      |  57 ++++++
 .../org/apache/knox/gateway/util/JDBCUtils.java    |  76 +++++++
 .../resources/createKnoxTokenDatabaseTable.sql     |  24 +++
 .../token/impl/JDBCTokenStateServiceTest.java      | 207 +++++++++++++++++++
 .../apache/knox/gateway/util/JDBCUtilsTest.java    |  90 ++++++++
 .../gateway/shell/jdbc/derby/DerbyDatabase.java    |  25 ++-
 .../gateway/shell/table/KnoxShellTableTest.java    |   2 +-
 .../apache/knox/gateway/config/GatewayConfig.java  |   9 +
 .../security/token/TokenStateServiceException.java |  31 +++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  21 ++
 pom.xml                                            |  25 ++-
 17 files changed, 985 insertions(+), 38 deletions(-)

diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index 3a8f7a6..a393cf4 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -397,6 +397,14 @@
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.derby</groupId>
+            <artifactId>derbyclient</artifactId>
+        </dependency>
 
         <!-- ********** ********** ********** ********** ********** ********** -->
         <!-- ********** Test Dependencies                           ********** -->
@@ -415,6 +423,24 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-shell</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.derby</groupId>
+            <artifactId>derby</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.derby</groupId>
+            <artifactId>derbynet</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity</artifactId>
             <scope>test</scope>
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 458026b..6ae40fe 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
@@ -17,19 +17,6 @@
  */
 package org.apache.knox.gateway.config.impl;
 
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.Path;
-import org.apache.knox.gateway.GatewayMessages;
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.dto.HomePageProfile;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService;
-import org.joda.time.Period;
-import org.joda.time.format.PeriodFormatter;
-import org.joda.time.format.PeriodFormatterBuilder;
-
 import static org.apache.knox.gateway.services.security.impl.RemoteAliasService.REMOTE_ALIAS_SERVICE_TYPE;
 
 import java.io.File;
@@ -52,6 +39,20 @@ import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.dto.HomePageProfile;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService;
+import org.joda.time.Period;
+import org.joda.time.format.PeriodFormatter;
+import org.joda.time.format.PeriodFormatterBuilder;
+
+
 /**
  * The configuration for the Gateway.
  *
@@ -271,6 +272,12 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
   private static final String KNOX_HOMEPAGE_LOGOUT_ENABLED =  "knox.homepage.logout.enabled";
   private static final String KNOX_INCOMING_XFORWARDED_ENABLED = "gateway.incoming.xforwarded.enabled";
 
+  //Gateway Database related properties
+  private static final String GATEWAY_DATABASE_TYPE = GATEWAY_CONFIG_FILE_PREFIX + ".database.type";
+  private static final String GATEWAY_DATABASE_HOST =  GATEWAY_CONFIG_FILE_PREFIX + ".database.host";
+  private static final String GATEWAY_DATABASE_PORT =  GATEWAY_CONFIG_FILE_PREFIX + ".database.port";
+  private static final String GATEWAY_DATABASE_NAME =  GATEWAY_CONFIG_FILE_PREFIX + ".database.name";
+
   public GatewayConfigImpl() {
     init();
   }
@@ -1231,4 +1238,24 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
     profiles.put("token", HomePageProfile.getTokenProfileElements());
     return profiles;
   }
+
+  @Override
+  public String getDatabaseType() {
+    return get(GATEWAY_DATABASE_TYPE, "none");
+  }
+
+  @Override
+  public String getDatabaseHost() {
+    return get(GATEWAY_DATABASE_HOST);
+  }
+
+  @Override
+  public int getDatabasePort() {
+    return getInt(GATEWAY_DATABASE_PORT, 0);
+  }
+
+  @Override
+  public String getDatabaseName() {
+    return get(GATEWAY_DATABASE_NAME, "GATEWAY_DATABASE");
+  }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
index 7d8be07..e9c141e 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
@@ -30,6 +30,7 @@ import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.ServiceType;
 import org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService;
 import org.apache.knox.gateway.services.token.impl.DefaultTokenStateService;
+import org.apache.knox.gateway.services.token.impl.JDBCTokenStateService;
 import org.apache.knox.gateway.services.token.impl.JournalBasedTokenStateService;
 import org.apache.knox.gateway.services.token.impl.ZookeeperTokenStateService;
 
@@ -49,6 +50,9 @@ public class TokenStateServiceFactory extends AbstractServiceFactory {
         service = new JournalBasedTokenStateService();
       } else if (matchesImplementation(implementation, ZookeeperTokenStateService.class)) {
         service = new ZookeeperTokenStateService(gatewayServices);
+      } else if (matchesImplementation(implementation, JDBCTokenStateService.class)) {
+        service = new JDBCTokenStateService();
+       ((JDBCTokenStateService) service).setAliasService(getAliasService(gatewayServices));
       }
 
       logServiceUsage(isEmptyDefaultImplementation(implementation) ? AliasBasedTokenStateService.class.getName() : implementation, serviceType);
@@ -65,6 +69,6 @@ public class TokenStateServiceFactory extends AbstractServiceFactory {
   @Override
   protected Collection<String> getKnownImplementations() {
     return unmodifiableList(asList(DefaultTokenStateService.class.getName(), AliasBasedTokenStateService.class.getName(), JournalBasedTokenStateService.class.getName(),
-        ZookeeperTokenStateService.class.getName()));
+        ZookeeperTokenStateService.class.getName(), JDBCTokenStateService.class.getName()));
   }
 }
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 8ffc0dc..9b44dfd 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
@@ -70,7 +70,7 @@ public class DefaultTokenStateService implements TokenStateService {
   private long tokenEvictionInterval;
 
   // Grace period (in seconds) after which an expired token should be evicted
-  private long tokenEvictionGracePeriod;
+  protected long tokenEvictionGracePeriod;
 
   // Knox token validation permissiveness
   protected boolean permissiveValidationEnabled;
@@ -332,18 +332,7 @@ public class DefaultTokenStateService implements TokenStateService {
    */
   protected void evictExpiredTokens() {
     if (readyForEviction()) {
-      final Set<String> tokensToEvict = new HashSet<>();
-
-      for (final String tokenId : getTokenIds()) {
-        try {
-          if (needsEviction(tokenId)) {
-            log.evictToken(Tokens.getTokenIDDisplayText(tokenId));
-            tokensToEvict.add(tokenId); // Add the token to the set of tokens to evict
-          }
-        } catch (final Exception e) {
-          log.failedExpiredTokenEviction(Tokens.getTokenIDDisplayText(tokenId), e);
-        }
-      }
+      final Set<String> tokensToEvict = getExpiredTokens();
 
       if (!tokensToEvict.isEmpty()) {
         removeTokens(tokensToEvict);
@@ -357,6 +346,21 @@ public class DefaultTokenStateService implements TokenStateService {
     return true;
   }
 
+  protected Set<String> getExpiredTokens() {
+    final Set<String> expiredTokens = new HashSet<>();
+    for (final String tokenId : getTokenIds()) {
+      try {
+        if (needsEviction(tokenId)) {
+          log.evictToken(Tokens.getTokenIDDisplayText(tokenId));
+          expiredTokens.add(tokenId); // Add the token to the set of tokens to evict
+        }
+      } catch (final Exception e) {
+        log.failedExpiredTokenEviction(Tokens.getTokenIDDisplayText(tokenId), e);
+      }
+    }
+    return expiredTokens;
+  }
+
   /**
    * Method that checks if a token's state is a candidate for eviction.
    *
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
new file mode 100644
index 0000000..4a3e56a
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
@@ -0,0 +1,227 @@
+/*
+ * 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 java.sql.SQLException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenStateServiceException;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+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 TokenStateDatabase tokenDatabase;
+
+  public void setAliasService(AliasService aliasService) {
+    this.aliasService = aliasService;
+  }
+
+  @Override
+  public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    super.init(config, options);
+    if (aliasService == null) {
+      throw new ServiceLifecycleException("The required AliasService reference has not been set.");
+    }
+    try {
+      this.tokenDatabase = new TokenStateDatabase(JDBCUtils.getDataSource(config, aliasService));
+    } catch (Exception e) {
+      throw new ServiceLifecycleException("Error while initiating JDBCTokenStateService: " + e, e);
+    }
+  }
+
+  @Override
+  public void addToken(String tokenId, long issueTime, long expiration, long maxLifetimeDuration) {
+    try {
+      final boolean added = tokenDatabase.addToken(tokenId, issueTime, expiration, maxLifetimeDuration);
+      if (added) {
+        log.savedTokenInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+
+        // add in-memory
+        super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration);
+      } else {
+        log.failedToSaveTokenInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+        throw new TokenStateServiceException("Failed to save token " + Tokens.getTokenIDDisplayText(tokenId) + " in the database");
+      }
+    } catch (SQLException e) {
+      log.errorSavingTokenInDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+      throw new TokenStateServiceException("An error occurred while saving token " + Tokens.getTokenIDDisplayText(tokenId) + " in the database", e);
+    }
+  }
+
+  @Override
+  public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException {
+    try {
+      // check the in-memory cache, then
+      return super.getTokenExpiration(tokenId, validate);
+    } catch (UnknownTokenException e) {
+      // It's not in memory
+    }
+
+    if (validate) {
+      validateToken(tokenId);
+    }
+
+    long expiration = 0;
+    try {
+      expiration = tokenDatabase.getTokenExpiration(tokenId);
+      if (expiration > 0) {
+        log.fetchedExpirationFromDatabase(Tokens.getTokenIDDisplayText(tokenId), expiration);
+
+        // Update the in-memory cache to avoid subsequent DB look-ups for the same state
+        super.updateExpiration(tokenId, expiration);
+      } else {
+        throw new UnknownTokenException(tokenId);
+      }
+    } catch (SQLException e) {
+      log.errorFetchingExpirationFromDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+    }
+    return expiration;
+  }
+
+  @Override
+  protected void updateExpiration(String tokenId, long expiration) {
+    try {
+      final boolean updated = tokenDatabase.updateExpiration(tokenId, expiration);
+      if (updated) {
+        log.updatedExpirationInDatabase(Tokens.getTokenIDDisplayText(tokenId), expiration);
+
+        // Update in-memory
+        super.updateExpiration(tokenId, expiration);
+      } else {
+        log.failedToUpdateExpirationInDatabase(Tokens.getTokenIDDisplayText(tokenId), expiration);
+        throw new TokenStateServiceException("Failed to updated expiration for " + Tokens.getTokenIDDisplayText(tokenId) + " in the database");
+      }
+    } catch (SQLException e) {
+      log.errorUpdatingExpirationInDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+      throw new TokenStateServiceException("An error occurred while updating expiration for " + Tokens.getTokenIDDisplayText(tokenId) + " in the database", e);
+    }
+  }
+
+  @Override
+  protected long getMaxLifetime(String tokenId) {
+    long maxLifetime = super.getMaxLifetime(tokenId);
+
+    // If there is no result from the in-memory collection, proceed to check the Database
+    if (maxLifetime < 1L) {
+      try {
+        maxLifetime = tokenDatabase.getMaxLifetime(tokenId);
+        log.fetchedMaxLifetimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId), maxLifetime);
+      } catch (SQLException e) {
+        log.errorFetchingMaxLifetimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+      }
+    }
+    return maxLifetime;
+  }
+
+  @Override
+  protected boolean isUnknown(String tokenId) {
+    boolean isUnknown = super.isUnknown(tokenId);
+
+    // If it's not in the cache, then check in the Database
+    if (isUnknown) {
+      try {
+        isUnknown = tokenDatabase.getMaxLifetime(tokenId) < 0;
+      } catch (SQLException e) {
+        log.errorFetchingMaxLifetimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+      }
+    }
+    return isUnknown;
+  }
+
+  @Override
+  protected void removeToken(String tokenId) throws UnknownTokenException {
+    try {
+      final boolean removed = tokenDatabase.removeToken(tokenId);
+      if (removed) {
+        super.removeToken(tokenId);
+        log.removedTokenFromDatabase(Tokens.getTokenIDDisplayText(tokenId));
+      } else {
+        throw new UnknownTokenException(tokenId);
+      }
+    } catch (SQLException e) {
+      log.errorRemovingTokenFromDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+    }
+  }
+
+  @Override
+  protected void evictExpiredTokens() {
+    try {
+      int numOfExpiredTokens = tokenDatabase.deleteExpiredTokens(TimeUnit.SECONDS.toMillis(tokenEvictionGracePeriod));
+      log.removedTokensFromDatabase(numOfExpiredTokens);
+
+      // remove from in-memory collections
+      super.evictExpiredTokens();
+    } catch (SQLException e) {
+      log.errorRemovingTokensFromDatabase(e.getMessage(), e);
+    }
+  }
+
+  @Override
+  public void addMetadata(String tokenId, TokenMetadata metadata) {
+    try {
+      final boolean added = tokenDatabase.addMetadata(tokenId, metadata);
+      if (added) {
+        log.updatedMetadataInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+
+        // Update in-memory
+        super.addMetadata(tokenId, metadata);
+      } else {
+        log.failedToUpdateMetadataInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+        throw new TokenStateServiceException("Failed to update metadata for " + Tokens.getTokenIDDisplayText(tokenId) + " in the database");
+      }
+    } catch (SQLException e) {
+      log.errorUpdatingMetadataInDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+      throw new TokenStateServiceException("An error occurred while updating metadata for " + Tokens.getTokenIDDisplayText(tokenId) + " in the database", e);
+    }
+  }
+
+  @Override
+  public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenException {
+    TokenMetadata tokenMetadata = null;
+    try {
+      tokenMetadata = super.getTokenMetadata(tokenId);
+    } catch (UnknownTokenException e) {
+      // This is expected if the metadata is not yet part of the in-memory record. In this case, the metadata will
+      // be retrieved from the database.
+    }
+
+    if (tokenMetadata == null) {
+      try {
+        tokenMetadata = tokenDatabase.getTokenMetadata(tokenId);
+
+        if (tokenMetadata != null) {
+          log.fetchedMetadataFromDatabase(Tokens.getTokenIDDisplayText(tokenId));
+          // Update the in-memory cache to avoid subsequent DB look-ups for the same state
+          super.addMetadata(tokenId, tokenMetadata);
+        } else {
+          throw new UnknownTokenException(tokenId);
+        }
+      } catch (SQLException e) {
+        log.errorFetchingMetadataFromDatabase(Tokens.getTokenIDDisplayText(tokenId), e.getMessage(), e);
+      }
+    }
+    return tokenMetadata;
+  }
+}
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
new file mode 100644
index 0000000..3bb2882
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
@@ -0,0 +1,114 @@
+/*
+ * 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 java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+
+public class TokenStateDatabase {
+  static final String TOKENS_TABLE_NAME = "KNOX_TOKENS";
+  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_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 = ?";
+  private static final String ADD_METADATA_SQL = "UPDATE " + TOKENS_TABLE_NAME + " SET username = ?, comment = ? WHERE token_id = ?";
+  private static final String GET_METADATA_SQL = "SELECT username, comment FROM " + TOKENS_TABLE_NAME + " WHERE token_id = ?";
+
+  private final DataSource dataSource;
+
+  TokenStateDatabase(DataSource dataSource) throws Exception {
+    this.dataSource = dataSource;
+  }
+
+  boolean addToken(String tokenId, long issueTime, long expiration, long maxLifetimeDuration) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement addTokenStatement = connection.prepareStatement(ADD_TOKEN_SQL)) {
+      addTokenStatement.setString(1, tokenId);
+      addTokenStatement.setLong(2, issueTime);
+      addTokenStatement.setLong(3, expiration);
+      addTokenStatement.setLong(4, issueTime + maxLifetimeDuration);
+      return addTokenStatement.executeUpdate() == 1;
+    }
+  }
+
+  boolean removeToken(String tokenId) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement addTokenStatement = connection.prepareStatement(REMOVE_TOKEN_SQL)) {
+      addTokenStatement.setString(1, tokenId);
+      return addTokenStatement.executeUpdate() == 1;
+    }
+  }
+
+  long getTokenExpiration(String tokenId) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement getTokenExpirationStatement = connection.prepareStatement(GET_TOKEN_EXPIRATION_SQL)) {
+      getTokenExpirationStatement.setString(1, tokenId);
+      try (ResultSet rs = getTokenExpirationStatement.executeQuery()) {
+        return rs.next() ? rs.getLong(1) : -1;
+      }
+    }
+  }
+
+  boolean updateExpiration(final String tokenId, long expiration) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement updateTokenExpirationStatement = connection.prepareStatement(UPDATE_TOKEN_EXPIRATION_SQL)) {
+      updateTokenExpirationStatement.setLong(1, expiration);
+      updateTokenExpirationStatement.setString(2, tokenId);
+      return updateTokenExpirationStatement.executeUpdate() == 1;
+    }
+  }
+
+  long getMaxLifetime(String tokenId) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement getMaxLifetimeStatement = connection.prepareStatement(GET_MAX_LIFETIME_SQL)) {
+      getMaxLifetimeStatement.setString(1, tokenId);
+      try (ResultSet rs = getMaxLifetimeStatement.executeQuery()) {
+        return rs.next() ? rs.getLong(1) : -1;
+      }
+    }
+  }
+
+  int deleteExpiredTokens(long tokenEvictionGracePeriod) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement deleteExpiredTokensStatement = connection.prepareStatement(REMOVE_EXPIRED_TOKENS_SQL)) {
+      deleteExpiredTokensStatement.setLong(1, System.currentTimeMillis() - tokenEvictionGracePeriod);
+      return deleteExpiredTokensStatement.executeUpdate();
+    }
+  }
+
+  boolean addMetadata(String tokenId, TokenMetadata metadata) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement addMetadataStatement = connection.prepareStatement(ADD_METADATA_SQL)) {
+      addMetadataStatement.setString(1, metadata.getUserName());
+      addMetadataStatement.setString(2, metadata.getComment());
+      addMetadataStatement.setString(3, tokenId);
+      return addMetadataStatement.executeUpdate() == 1;
+    }
+  }
+
+  TokenMetadata getTokenMetadata(String tokenId) throws SQLException {
+    try (Connection connection = dataSource.getConnection(); PreparedStatement getMaxLifetimeStatement = connection.prepareStatement(GET_METADATA_SQL)) {
+      getMaxLifetimeStatement.setString(1, tokenId);
+      try (ResultSet rs = getMaxLifetimeStatement.executeQuery()) {
+        return rs.next() ? new TokenMetadata(rs.getString(1), rs.getString(2)) : null;
+      }
+    }
+  }
+
+}
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 ceb11a2..a5eeece 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
@@ -177,4 +177,61 @@ public interface TokenStateServiceMessages {
 
   @Message(level = MessageLevel.INFO, text = "Removed related token alias {0} on receiving signal from Zookeeper ")
   void onRemoteTokenStateRemoval(String alias);
+
+  @Message(level = MessageLevel.DEBUG, text = "Token {0} has been saved in the database")
+  void savedTokenInDatabase(String tokenId);
+
+  @Message(level = MessageLevel.ERROR, text = "Failed to save token {0} in the database")
+  void failedToSaveTokenInDatabase(String tokenId);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while saving token {0} in the database : {1}")
+  void errorSavingTokenInDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.DEBUG, text = "Token {0} has been removed from the database")
+  void removedTokenFromDatabase(String tokenId);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while removing token {0} from the database : {1}")
+  void errorRemovingTokenFromDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.DEBUG, text = "{0} expired tokens have been removed from the database")
+  void removedTokensFromDatabase(int size);
+
+  @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 expiration for {0} from the database : {1}")
+  void fetchedExpirationFromDatabase(String tokenId, long expiration);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching expiration for {0} from the database : {1}")
+  void errorFetchingExpirationFromDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.DEBUG, text = "Updated expiration for {0} in the database to {1}")
+  void updatedExpirationInDatabase(String tokenId, long expiration);
+
+  @Message(level = MessageLevel.DEBUG, text = "Failed to updated expiration for {0} in the database to {1}")
+  void failedToUpdateExpirationInDatabase(String tokenId, long expiration);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while updating expiration for {0} in the database : {1}")
+  void errorUpdatingExpirationInDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.DEBUG, text = "Fetched max lifetime for {0} from the database : {1}")
+  void fetchedMaxLifetimeFromDatabase(String tokenId, long maxLifetime);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching max lifetime for {0} from the database : {1}")
+  void errorFetchingMaxLifetimeFromDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.DEBUG, text = "Updated metadata for {0} in the database")
+  void updatedMetadataInDatabase(String tokenId);
+
+  @Message(level = MessageLevel.DEBUG, text = "Failed to update metadata for {0} in the database")
+  void failedToUpdateMetadataInDatabase(String tokenId);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while updating metadata for {0} in the database : {1}")
+  void errorUpdatingMetadataInDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+  @Message(level = MessageLevel.DEBUG, text = "Fetched metadata for {0} from the database")
+  void fetchedMetadataFromDatabase(String tokenId);
+
+  @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching metadata for {0} from the database : {1}")
+  void errorFetchingMetadataFromDatabase(String tokenId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/util/JDBCUtils.java b/gateway-server/src/main/java/org/apache/knox/gateway/util/JDBCUtils.java
new file mode 100644
index 0000000..04b6cd6
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/util/JDBCUtils.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util;
+
+import javax.sql.DataSource;
+
+import org.apache.derby.jdbc.ClientDataSource;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.postgresql.ds.PGSimpleDataSource;
+
+public class JDBCUtils {
+  public static final String POSTGRES_DB_TYPE = "postgres";
+  public static final String DERBY_DB_TYPE = "derbydb";
+  public static final String DATABASE_USER_ALIAS_NAME = "gateway_database_user";
+  public static final String DATABASE_PASSWORD_ALIAS_NAME = "gateway_database_password";
+
+  public static DataSource getDataSource(GatewayConfig gatewayConfig, AliasService aliasService) throws AliasServiceException {
+    if (POSTGRES_DB_TYPE.equalsIgnoreCase(gatewayConfig.getDatabaseType())) {
+      return createPostgresDataSource(gatewayConfig, aliasService);
+    } else if (DERBY_DB_TYPE.equalsIgnoreCase(gatewayConfig.getDatabaseType())) {
+      return createDerbyDatasource(gatewayConfig, aliasService);
+    }
+    throw new IllegalArgumentException("Invalid database type: " + gatewayConfig.getDatabaseType());
+  }
+
+  private static DataSource createPostgresDataSource(GatewayConfig gatewayConfig, AliasService aliasService) throws AliasServiceException {
+    final PGSimpleDataSource postgresDataSource = new PGSimpleDataSource();
+    postgresDataSource.setDatabaseName(gatewayConfig.getDatabaseName());
+    postgresDataSource.setServerNames(new String[] { gatewayConfig.getDatabaseHost() });
+    postgresDataSource.setPortNumbers(new int[] { gatewayConfig.getDatabasePort() });
+    postgresDataSource.setUser(getDatabaseUser(aliasService));
+    postgresDataSource.setPassword(getDatabasePassword(aliasService));
+    return postgresDataSource;
+  }
+
+  private static DataSource createDerbyDatasource(GatewayConfig gatewayConfig, AliasService aliasService) throws AliasServiceException {
+    final ClientDataSource derbyDatasource = new ClientDataSource();
+    derbyDatasource.setDatabaseName(gatewayConfig.getDatabaseName());
+    derbyDatasource.setServerName(gatewayConfig.getDatabaseHost());
+    derbyDatasource.setPortNumber(gatewayConfig.getDatabasePort());
+    derbyDatasource.setUser(getDatabaseUser(aliasService));
+    derbyDatasource.setPassword(getDatabasePassword(aliasService));
+    return derbyDatasource;
+  }
+
+  private static String getDatabaseUser(AliasService aliasService) throws AliasServiceException {
+    return getDatabaseAlias(aliasService, DATABASE_USER_ALIAS_NAME);
+  }
+
+  private static String getDatabasePassword(AliasService aliasService) throws AliasServiceException {
+    return getDatabaseAlias(aliasService, DATABASE_PASSWORD_ALIAS_NAME);
+  }
+
+  private static String getDatabaseAlias(AliasService aliasService, String aliasName) throws AliasServiceException {
+    final char[] value = aliasService.getPasswordFromAliasForGateway(aliasName);
+    return value == null ? null : new String(value);
+  }
+
+}
diff --git a/gateway-server/src/main/resources/createKnoxTokenDatabaseTable.sql b/gateway-server/src/main/resources/createKnoxTokenDatabaseTable.sql
new file mode 100644
index 0000000..faeca79
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxTokenDatabaseTable.sql
@@ -0,0 +1,24 @@
+--  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.
+
+CREATE TABLE KNOX_TOKENS (
+   token_id varchar(128) NOT NULL,
+   issue_time bigint NOT NULL,
+   expiration bigint NOT NULL,
+   max_lifetime bigint NOT NULL,
+   username varchar(128),
+   comment varchar(256),
+   PRIMARY KEY (token_id)
+)
\ No newline at end of file
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
new file mode 100644
index 0000000..a5c750b
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.io.FileUtils.readFileToString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+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.shell.jdbc.Database;
+import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
+import org.apache.knox.gateway.util.JDBCUtils;
+import org.easymock.EasyMock;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class JDBCTokenStateServiceTest {
+
+  private static final String GET_TOKENS_COUNT_SQL = "SELECT count(*) FROM " + TokenStateDatabase.TOKENS_TABLE_NAME;
+  private static final String GET_USERNAME_SQL = "SELECT username FROM " + TokenStateDatabase.TOKENS_TABLE_NAME + " WHERE token_id = ?";
+  private static final String GET_COMMENT_SQL = "SELECT comment FROM " + TokenStateDatabase.TOKENS_TABLE_NAME + " WHERE token_id = ?";
+  private static final String TRUNCATE_KNOX_TOKENS_SQL = "TRUNCATE TABLE " + TokenStateDatabase.TOKENS_TABLE_NAME;
+
+  @ClassRule
+  public static final TemporaryFolder testFolder = new TemporaryFolder();
+
+  private static final String SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE = "derby.stream.error.file";
+  private static final String SAMPLE_DERBY_DATABASE_NAME = "sampleDerbyDatabase";
+  private static NetworkServerControl derbyNetworkServerControl;
+  private static Database derbyDatabase;
+  private static JDBCTokenStateService jdbcTokenStateService;
+
+  @SuppressWarnings("PMD.JUnit4TestShouldUseBeforeAnnotation")
+  @BeforeClass
+  public static void setUp() throws Exception {
+    final String username = "app";
+    final String password = "P4ssW0rd!";
+    System.setProperty(SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE, "/dev/null");
+    derbyNetworkServerControl = new NetworkServerControl(username, password);
+    derbyNetworkServerControl.start(null);
+    TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
+    final Path derbyDatabaseFolder = Paths.get(testFolder.newFolder().toPath().toString(), SAMPLE_DERBY_DATABASE_NAME);
+    final GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.DERBY_DB_TYPE).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseHost()).andReturn("localhost").anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabasePort()).andReturn(NetworkServerControl.DEFAULT_PORTNUMBER).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn(derbyDatabaseFolder.toString()).anyTimes();
+    final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_USER_ALIAS_NAME)).andReturn(username.toCharArray()).anyTimes();
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_PASSWORD_ALIAS_NAME)).andReturn(password.toCharArray()).anyTimes();
+    EasyMock.replay(gatewayConfig, aliasService);
+
+    derbyDatabase = prepareDerbyDatabase(derbyDatabaseFolder);
+    assertTrue(derbyDatabase.hasTable(TokenStateDatabase.TOKENS_TABLE_NAME));
+
+    jdbcTokenStateService = new JDBCTokenStateService();
+    jdbcTokenStateService.setAliasService(aliasService);
+    jdbcTokenStateService.init(gatewayConfig, null);
+  }
+
+  private static Database prepareDerbyDatabase(Path derbyDatabaseFolder) throws SQLException, IOException {
+    final Database derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString(), true);
+    derbyDatabase.create();
+    final String createTableSql = readFileToString(new File(JDBCTokenStateServiceTest.class.getClassLoader().getResource("createKnoxTokenDatabaseTable.sql").getFile()), UTF_8);
+    try (Connection connection = derbyDatabase.getConnection(); Statement createTableStatment = connection.createStatement();) {
+      createTableStatment.execute(createTableSql);
+    }
+    return derbyDatabase;
+  }
+
+  @SuppressWarnings("PMD.JUnit4TestShouldUseAfterAnnotation")
+  @AfterClass
+  public static void tearDown() throws Exception {
+    if (derbyDatabase != null) {
+      derbyDatabase.shutdown();
+    }
+    derbyNetworkServerControl.shutdown();
+    System.clearProperty(SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE);
+  }
+
+  @Test
+  public void testAddToken() throws Exception {
+    final String tokenId = UUID.randomUUID().toString();
+    long issueTime = System.currentTimeMillis();
+    long maxLifetimeDuration = 1000;
+    long expiration = issueTime + maxLifetimeDuration;
+    jdbcTokenStateService.addToken(tokenId, issueTime, expiration, maxLifetimeDuration);
+
+    assertEquals(expiration, jdbcTokenStateService.getTokenExpiration(tokenId));
+    assertEquals(issueTime + maxLifetimeDuration, jdbcTokenStateService.getMaxLifetime(tokenId));
+
+    assertEquals(expiration, getLongTokenAttributeFromDatabase(tokenId, TokenStateDatabase.GET_TOKEN_EXPIRATION_SQL));
+    assertEquals(issueTime + maxLifetimeDuration, getLongTokenAttributeFromDatabase(tokenId, TokenStateDatabase.GET_MAX_LIFETIME_SQL));
+  }
+
+  @Test(expected = UnknownTokenException.class)
+  public void testRemoveToken() throws Exception {
+    truncateDatabase();
+    final String tokenId = UUID.randomUUID().toString();
+    jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+    assertEquals(1, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
+    jdbcTokenStateService.removeToken(tokenId);
+    assertEquals(0, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
+    jdbcTokenStateService.getTokenExpiration(tokenId);
+  }
+
+  @Test
+  public void testUpdateExpiration() throws Exception {
+    final String tokenId = UUID.randomUUID().toString();
+    jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+    jdbcTokenStateService.updateExpiration(tokenId, 2);
+
+    assertEquals(2, jdbcTokenStateService.getTokenExpiration(tokenId));
+    assertEquals(2, getLongTokenAttributeFromDatabase(tokenId, TokenStateDatabase.GET_TOKEN_EXPIRATION_SQL));
+  }
+
+  @Test(expected = UnknownTokenException.class)
+  public void testAddMetadata() throws Exception {
+    final String tokenId = UUID.randomUUID().toString();
+    jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+    jdbcTokenStateService.addMetadata(tokenId, new TokenMetadata("sampleUser", "my test comment"));
+
+    assertEquals("sampleUser", jdbcTokenStateService.getTokenMetadata(tokenId).getUserName());
+    assertEquals("my test comment", jdbcTokenStateService.getTokenMetadata(tokenId).getComment());
+
+    assertEquals("sampleUser", getStringTokenAttributeFromDatabase(tokenId, GET_USERNAME_SQL));
+    assertEquals("my test comment", getStringTokenAttributeFromDatabase(tokenId, GET_COMMENT_SQL));
+
+    jdbcTokenStateService.removeToken(tokenId);
+    jdbcTokenStateService.getTokenMetadata(tokenId);
+  }
+
+  @Test
+  public void testEvictExpiredTokens() throws Exception {
+    truncateDatabase();
+    final int tokenCount = 1000;
+    for (int i = 0; i < tokenCount; i++) {
+      final String tokenId = UUID.randomUUID().toString();
+      jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+    }
+    assertEquals(tokenCount, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
+    jdbcTokenStateService.evictExpiredTokens();
+    assertEquals(0, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL));
+  }
+
+  private long getLongTokenAttributeFromDatabase(String tokenId, String sql) throws SQLException {
+    try (Connection conn = derbyDatabase.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) {
+      if (tokenId != null) {
+        stmt.setString(1, tokenId);
+      }
+      try (ResultSet rs = stmt.executeQuery()) {
+        return rs.next() ? rs.getLong(1) : 0;
+      }
+    }
+  }
+
+  private String getStringTokenAttributeFromDatabase(String tokenId, String sql) throws SQLException {
+    try (Connection conn = derbyDatabase.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) {
+      stmt.setString(1, tokenId);
+      try (ResultSet rs = stmt.executeQuery()) {
+        return rs.next() ? rs.getString(1) : null;
+      }
+    }
+  }
+
+  private void truncateDatabase() throws SQLException {
+    try (Connection conn = derbyDatabase.getConnection(); PreparedStatement stmt = conn.prepareStatement(TRUNCATE_KNOX_TOKENS_SQL)) {
+      stmt.executeUpdate();
+    }
+  }
+
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/util/JDBCUtilsTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/util/JDBCUtilsTest.java
new file mode 100644
index 0000000..0e37e55
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/util/JDBCUtilsTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.derby.jdbc.ClientDataSource;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.easymock.EasyMock;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+public class JDBCUtilsTest {
+
+  @Test
+  public void shouldReturnPostgresDataSource() throws Exception {
+    final GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.POSTGRES_DB_TYPE).anyTimes();
+    final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString())).andReturn(null).anyTimes();
+    EasyMock.replay(gatewayConfig, aliasService);
+    assertTrue(JDBCUtils.getDataSource(gatewayConfig, aliasService) instanceof PGSimpleDataSource);
+  }
+
+  @Test
+  public void postgresDataSourceShouldHaveProperConnectionProperties() throws AliasServiceException {
+    final GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.POSTGRES_DB_TYPE).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseHost()).andReturn("localhost").anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabasePort()).andReturn(5432).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn("sampleDatabase");
+    final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_USER_ALIAS_NAME)).andReturn("user".toCharArray()).anyTimes();
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_PASSWORD_ALIAS_NAME)).andReturn("password".toCharArray()).anyTimes();
+    EasyMock.replay(gatewayConfig, aliasService);
+    final PGSimpleDataSource dataSource = (PGSimpleDataSource) JDBCUtils.getDataSource(gatewayConfig, aliasService);
+    assertEquals("localhost", dataSource.getServerNames()[0]);
+    assertEquals(5432, dataSource.getPortNumbers()[0]);
+    assertEquals("sampleDatabase", dataSource.getDatabaseName());
+    assertEquals("user", dataSource.getUser());
+    assertEquals("password", dataSource.getPassword());
+  }
+
+  @Test
+  public void shouldReturnDerbyDataSource() throws Exception {
+    final GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.DERBY_DB_TYPE).anyTimes();
+    final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString())).andReturn(null).anyTimes();
+    EasyMock.replay(gatewayConfig, aliasService);
+    assertTrue(JDBCUtils.getDataSource(gatewayConfig, aliasService) instanceof ClientDataSource);
+  }
+
+  @Test
+  public void derbyDataSourceShouldHaveProperConnectionProperties() throws AliasServiceException {
+    final GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.DERBY_DB_TYPE).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseHost()).andReturn("localhost").anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabasePort()).andReturn(1527).anyTimes();
+    EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn("sampleDatabase");
+    final AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_USER_ALIAS_NAME)).andReturn("user".toCharArray()).anyTimes();
+    EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_PASSWORD_ALIAS_NAME)).andReturn("password".toCharArray()).anyTimes();
+    EasyMock.replay(gatewayConfig, aliasService);
+    final ClientDataSource dataSource = (ClientDataSource) JDBCUtils.getDataSource(gatewayConfig, aliasService);
+    assertEquals("localhost", dataSource.getServerName());
+    assertEquals(1527, dataSource.getPortNumber());
+    assertEquals("sampleDatabase", dataSource.getDatabaseName());
+    assertEquals("user", dataSource.getUser());
+    assertEquals("password", dataSource.getPassword());
+  }
+}
diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
index 61edfc5..88405b6 100644
--- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
+++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
@@ -27,10 +27,12 @@ import org.apache.knox.gateway.shell.jdbc.Database;
 
 public class DerbyDatabase implements Database {
 
-  public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";
+  public static final String EMBEDDED_DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";
+  public static final String NETWORK_SERVER_DRIVER = "org.apache.derby.jdbc.ClientDriver";
   public static final String PROTOCOL = "jdbc:derby:";
   private static final String CREATE_ATTRIBUTE = ";create=true";
   private static final String SHUTDOWN_ATTRIBUTE = ";shutdown=true";
+  private static final String DEFAULT_SCHEMA_NAME = "APP";
 
   private final String dbUri;
 
@@ -43,8 +45,12 @@ public class DerbyDatabase implements Database {
    *           if the database engine can not be load for some reasons
    */
   public DerbyDatabase(String directory) throws DerbyDatabaseException {
-    this.dbUri = PROTOCOL + directory;
-    loadDriver();
+    this(directory, false);
+  }
+
+  public DerbyDatabase(String directory, boolean networkServer) throws DerbyDatabaseException {
+    this.dbUri = PROTOCOL + (networkServer ? "//localhost:1527/" : "") + directory;
+    loadDriver(networkServer);
   }
 
   @Override
@@ -87,7 +93,7 @@ public class DerbyDatabase implements Database {
 
   @Override
   public boolean hasTable(String tableName) throws SQLException {
-    return hasTable(null, tableName);
+    return hasTable(DEFAULT_SCHEMA_NAME, tableName);
   }
 
   @Override
@@ -103,15 +109,16 @@ public class DerbyDatabase implements Database {
     return result;
   }
 
-  private void loadDriver() throws DerbyDatabaseException {
+  private void loadDriver(boolean networkServer) throws DerbyDatabaseException {
+    final String driverToLoad = networkServer ? NETWORK_SERVER_DRIVER : EMBEDDED_DRIVER;
     try {
-      Class.forName(DRIVER).newInstance();
+      Class.forName(driverToLoad).newInstance();
     } catch (ClassNotFoundException e) {
-      throw new DerbyDatabaseException("Unable to load the JDBC driver " + DRIVER + ". Check your CLASSPATH.", e);
+      throw new DerbyDatabaseException("Unable to load the JDBC driver " + driverToLoad + ". Check your CLASSPATH.", e);
     } catch (InstantiationException e) {
-      throw new DerbyDatabaseException("Unable to instantiate the JDBC driver " + DRIVER, e);
+      throw new DerbyDatabaseException("Unable to instantiate the JDBC driver " + driverToLoad, e);
     } catch (IllegalAccessException e) {
-      throw new DerbyDatabaseException("Not allowed to access the JDBC driver " + DRIVER, e);
+      throw new DerbyDatabaseException("Not allowed to access the JDBC driver " + driverToLoad, e);
     }
   }
 
diff --git a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
index e44e6a1..b46bc9f 100644
--- a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
+++ b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
@@ -479,7 +479,7 @@ public class KnoxShellTableTest {
     try {
       derbyDatabase = prepareDerbyDatabase(derbyDatabaseFolder);
       assertTrue(derbyDatabase.hasTable("BOOKS"));
-      final KnoxShellTable table = KnoxShellTable.builder().jdbc().driver(DerbyDatabase.DRIVER).connectTo(DerbyDatabase.PROTOCOL + derbyDatabaseFolder.toString())
+      final KnoxShellTable table = KnoxShellTable.builder().jdbc().driver(DerbyDatabase.EMBEDDED_DRIVER).connectTo(DerbyDatabase.PROTOCOL + derbyDatabaseFolder.toString())
           .sql("select * from books");
       assertEquals(2, table.getRows().size());
       assertTrue(table.values("TITLE").containsAll(Arrays.asList("Apache Knox: The Definitive Guide", "Apache Knox: The Definitive Guide 2nd Edition")));
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 020eb63..4d87061 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
@@ -747,4 +747,13 @@ public interface GatewayConfig {
    * It's important that keys in the returned map are converted to lowercase strings.
    */
   Map<String, Collection<String>> getHomePageProfiles();
+
+  String getDatabaseType();
+
+  String getDatabaseHost();
+
+  int getDatabasePort();
+
+  String getDatabaseName();
+
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateServiceException.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateServiceException.java
new file mode 100644
index 0000000..6c8c6f0
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateServiceException.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+@SuppressWarnings("serial")
+public class TokenStateServiceException extends RuntimeException {
+
+  public TokenStateServiceException(String message) {
+    super(message);
+  }
+
+  public TokenStateServiceException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+}
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 b84834d..6291e70 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
@@ -853,4 +853,25 @@ public class GatewayTestConfig extends Configuration implements GatewayConfig {
   public Map<String, Collection<String>> getHomePageProfiles() {
     return null;
   }
+
+  @Override
+  public String getDatabaseType() {
+    return null;
+  }
+
+  @Override
+  public String getDatabaseHost() {
+    return null;
+  }
+
+  @Override
+  public int getDatabasePort() {
+    return 0;
+  }
+
+  @Override
+  public String getDatabaseName() {
+    return null;
+  }
+
 }
diff --git a/pom.xml b/pom.xml
index 4eadaa5..9e546e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -250,6 +250,7 @@
         <okhttp.version>2.7.5</okhttp.version>
         <opensaml.version>3.4.5</opensaml.version>
         <pac4j.version>4.3.0</pac4j.version>
+        <postgresql.version>42.2.19</postgresql.version>
         <protobuf.version>3.14.0</protobuf.version>
         <rest-assured.version>4.3.3</rest-assured.version>
         <shiro.version>1.7.0</shiro.version>
@@ -2041,6 +2042,17 @@
                 <artifactId>swagger-annotations</artifactId>
                 <version>${swagger-annotations.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.postgresql</groupId>
+                <artifactId>postgresql</artifactId>
+                <version>${postgresql.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.checkerframework</groupId>
+                        <artifactId>checker-qual</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
 
             <!-- pac4j Dependencies -->
             <dependency>
@@ -2328,7 +2340,18 @@
                 <groupId>org.apache.derby</groupId>
                 <artifactId>derby</artifactId>
                 <version>${derby.db.version}</version>
-                <scope>test</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.derby</groupId>
+                <artifactId>derbyclient</artifactId>
+                <version>${derby.db.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.derby</groupId>
+                <artifactId>derbynet</artifactId>
+                <version>${derby.db.version}</version>
             </dependency>
 
             <dependency>

Mime
View raw message