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 d3f5a567a KNOX-3005 - Implemented KnoxSSO idle timeout (#839)
d3f5a567a is described below

commit d3f5a567ac25cf9f5045866cf14db03151e9f978
Author: Sandor Molnar <smol...@apache.org>
AuthorDate: Tue Feb 6 08:28:22 2024 +0100

    KNOX-3005 - Implemented KnoxSSO idle timeout (#839)
---
 .../provider/federation/jwt/JWTMessages.java       |  7 +++
 .../federation/jwt/filter/AbstractJWTFilter.java   | 69 +++++++++++++++++-----
 .../jwt/filter/SSOCookieFederationFilter.java      | 13 +++-
 .../provider/federation/AbstractJWTFilterTest.java | 14 +++++
 .../provider/federation/SSOCookieProviderTest.java | 41 +++++++++++++
 .../provider/federation/TestFilterConfig.java      | 38 +++++++++++-
 .../gateway/service/knoxsso/WebSSOResource.java    |  1 +
 .../services/security/token/TokenMetadata.java     | 17 +++++-
 8 files changed, 180 insertions(+), 20 deletions(-)

diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
index b188539f2..967e56466 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
@@ -104,4 +104,11 @@ public interface JWTMessages {
 
   @Message( level = MessageLevel.WARN, text = "Invalid SSO cookie found! 
Cleaning up..." )
   void invalidSsoCookie();
+
+  @Message( level = MessageLevel.WARN, text = "User {0} with SSO token {1} 
exceeded the configured idle timeout of {2} seconds." )
+  void idleTimoutExceeded(String principal, String tokenId, long idleTimeout);
+
+  @Message( level = MessageLevel.INFO, text = "Idle timeout has been 
configured to {0} seconds in {1}" )
+  void configuredIdleTimeout(long idleTimeout, String topology);
+
 }
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
index 5f0a2a512..07ce39030 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AbstractJWTFilter.java
@@ -25,6 +25,7 @@ import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
 import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
@@ -85,6 +86,10 @@ public abstract class AbstractJWTFilter implements Filter {
   public static final String JWT_EXPECTED_ISSUER = "jwt.expected.issuer";
   public static final String JWT_DEFAULT_ISSUER = "KNOXSSO";
 
+  public static final String TOKEN_PREFIX = "Token ";
+  public static final String DISABLED_POSTFIX = " is disabled";
+  public static final String IDLE_TIMEOUT_POSTFIX = " exceeded idle timeout";
+
   /**
    * If specified, this configuration property refers to the signature 
algorithm which a received
    * token must match. Otherwise, the default value "RS256" is used
@@ -111,6 +116,8 @@ public abstract class AbstractJWTFilter implements Filter {
 
   private TokenStateService tokenStateService;
   private TokenMAC tokenMAC;
+  protected long idleTimeoutSeconds = -1;
+  protected String topologyName;
 
   @Override
   public abstract void doFilter(ServletRequest request, ServletResponse 
response, FilterChain chain)
@@ -144,8 +151,7 @@ public abstract class AbstractJWTFilter implements Filter {
     }
 
     // Setup the verified tokens cache
-    String topologyName =
-              (context != null) ? (String) 
context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE) : null;
+    topologyName = context != null ? (String) 
context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE) : null;
     signatureVerificationCache = 
SignatureVerificationCache.getInstance(topologyName, filterConfig);
   }
 
@@ -338,16 +344,23 @@ public abstract class AbstractJWTFilter implements Filter 
{
           if (audValid) {
             Date nbf = token.getNotBeforeDate();
             if (nbf == null || new Date().after(nbf)) {
-              if (isTokenEnabled(tokenId)) {
-                if (verifyTokenSignature(token)) {
-                  return true;
+              final TokenMetadata tokenMetadata = tokenStateService == null ? 
null : tokenStateService.getTokenMetadata(tokenId);
+              if (isTokenEnabled(tokenMetadata)) {
+                if (isIdleTimeoutLimitNotExceeded(tokenMetadata)) {
+                  if (verifyTokenSignature(token)) {
+                    markLastUsedAt(tokenId, tokenMetadata);
+                    return true;
+                  } else {
+                    log.failedToVerifyTokenSignature(displayableToken, 
displayableTokenId);
+                    handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, null);
+                  }
                 } else {
-                  log.failedToVerifyTokenSignature(displayableToken, 
displayableTokenId);
-                  handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, null);
+                  log.idleTimoutExceeded(token.getSubject(), 
displayableTokenId, idleTimeoutSeconds);
+                  handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, TOKEN_PREFIX + displayableTokenId + 
IDLE_TIMEOUT_POSTFIX);
                 }
               } else {
                 log.disabledToken(displayableTokenId);
-                handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, "Token " + displayableTokenId + " is 
disabled");
+                handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, TOKEN_PREFIX + displayableTokenId + 
DISABLED_POSTFIX);
               }
             } else {
               log.notBeforeCheckFailed();
@@ -381,11 +394,29 @@ public abstract class AbstractJWTFilter implements Filter 
{
     return false;
   }
 
-  private boolean isTokenEnabled(String tokenId) throws UnknownTokenException {
-    final TokenMetadata tokenMetadata = tokenStateService == null ? null : 
tokenStateService.getTokenMetadata(tokenId);
+  private boolean isTokenEnabled(TokenMetadata tokenMetadata) throws 
UnknownTokenException {
     return tokenMetadata == null ? true : tokenMetadata.isEnabled();
   }
 
+  private boolean isIdleTimeoutLimitNotExceeded(TokenMetadata tokenMetadata) 
throws UnknownTokenException {
+    if (idleTimeoutSeconds > 0) {
+      final Instant lastUsedAt = tokenMetadata == null ? null : 
tokenMetadata.getLastUsedAt();
+      final Instant idleTimeoutLimit = lastUsedAt == null ? null : 
lastUsedAt.plusSeconds(idleTimeoutSeconds);
+      return idleTimeoutLimit == null ? true : 
(tokenMetadata.isKnoxSsoCookie() && idleTimeoutLimit.isAfter(Instant.now()));
+    }
+    return true; // no idle timeout is configured -> ignore idleness check
+  }
+
+  private void markLastUsedAt(String tokenId, TokenMetadata tokenMetadata) 
throws UnknownTokenException {
+    if (tokenMetadata != null && tokenMetadata.isKnoxSsoCookie()) {
+      // to avoid updating every single metadata value, we create a new token 
metadata
+      // instance only with the updated "LAST_USED_AT" information
+      final TokenMetadata updatedTokenMetadata = new TokenMetadata();
+      updatedTokenMetadata.useTokenNow();
+      tokenStateService.addMetadata(tokenId, updatedTokenMetadata);
+    }
+  }
+
   protected boolean validateToken(final HttpServletRequest request,
                                   final HttpServletResponse response,
                                   final FilterChain chain,
@@ -398,12 +429,20 @@ public abstract class AbstractJWTFilter implements Filter 
{
         if (tokenId != null) {
           final String displayableTokenId = 
Tokens.getTokenIDDisplayText(tokenId);
           if (tokenIsStillValid(tokenId)) {
-            if (isTokenEnabled(tokenId)) {
-              if (hasSignatureBeenVerified(passcode) || 
validatePasscode(tokenId, passcode)) {
-                return true;
+            final TokenMetadata tokenMetadata = tokenStateService == null ? 
null : tokenStateService.getTokenMetadata(tokenId);
+            if (isTokenEnabled(tokenMetadata)) {
+              if (isIdleTimeoutLimitNotExceeded(tokenMetadata)) {
+                if (hasSignatureBeenVerified(passcode) || 
validatePasscode(tokenId, passcode)) {
+                  markLastUsedAt(tokenId, tokenMetadata);
+                  return true;
+                } else {
+                  log.wrongPasscodeToken(tokenId);
+                  handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, "Invalid passcode");
+                }
               } else {
-                log.wrongPasscodeToken(tokenId);
-                handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, "Invalid passcode");
+                // tokenMetadata at this point cannot be null (see 
isIdleTimeoutLimitNotExceeded(...))
+                log.idleTimoutExceeded(tokenMetadata.getUserName(), 
displayableTokenId, idleTimeoutSeconds);
+                handleValidationError(request, response, 
HttpServletResponse.SC_UNAUTHORIZED, "Token " + displayableTokenId + " exceeded 
idle timeout");
               }
             } else {
               log.disabledToken(displayableTokenId);
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
index e329037de..314fda9e2 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
@@ -62,6 +62,7 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
   public static final String SSO_EXPECTED_AUDIENCES = "sso.expected.audiences";
   public static final String SSO_AUTHENTICATION_PROVIDER_URL = 
"sso.authentication.provider.url";
   public static final String SSO_VERIFICATION_PEM = 
"sso.token.verification.pem";
+  public static final String SSO_IDLE_TIMEOUT_SECONDS = 
"sso.idle.timeout.seconds";
   public static final String X_FORWARDED_HOST = "X-Forwarded-Host";
   public static final String X_FORWARDED_PORT = "X-Forwarded-Port";
   public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
@@ -114,6 +115,12 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
     // gateway path for deriving an idp url when missing
     setGatewayPath(filterConfig);
 
+    final String ssoIdleTimeoutSeconds = 
filterConfig.getInitParameter(SSO_IDLE_TIMEOUT_SECONDS);
+    if (ssoIdleTimeoutSeconds != null) {
+      idleTimeoutSeconds = Long.parseLong(ssoIdleTimeoutSeconds);
+      LOGGER.configuredIdleTimeout(idleTimeoutSeconds, topologyName);
+    }
+
     configureExpectedParameters(filterConfig);
   }
 
@@ -197,7 +204,7 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
 
   @Override
   protected void handleValidationError(HttpServletRequest request, 
HttpServletResponse response, int status, String error) throws IOException {
-    if (error != null && error.startsWith("Token") && 
error.endsWith("disabled")) {
+    if (isInvalidSsoCookie(error)) {
       LOGGER.invalidSsoCookie();
       response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
       removeAuthenticationToken(request, response);
@@ -229,6 +236,10 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
     }
   }
 
+  private boolean isInvalidSsoCookie(String error) {
+    return error != null && error.startsWith(TOKEN_PREFIX) && 
(error.endsWith(DISABLED_POSTFIX) || error.endsWith(IDLE_TIMEOUT_POSTFIX));
+  }
+
   private String constructGlobalLogoutUrl(HttpServletRequest request) {
     final StringBuilder logoutUrlBuilder = new 
StringBuilder(deriveDefaultAuthenticationProviderUrl(request, true));
     
logoutUrlBuilder.append('&').append(ORIGINAL_URL_QUERY_PARAM).append(deriveDefaultAuthenticationProviderUrl(request,
 false)); //orignalUrl=WebSSO login
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
index 7219a6a73..ffd99444b 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/AbstractJWTFilterTest.java
@@ -51,6 +51,8 @@ import javax.servlet.ServletResponse;
 import javax.servlet.WriteListener;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
 import java.lang.reflect.Field;
 import java.net.InetAddress;
 import java.nio.charset.StandardCharsets;
@@ -1084,6 +1086,14 @@ public abstract class AbstractJWTFilterTest  {
   }
 
   static class DummyServletOutputStream extends ServletOutputStream {
+
+      byte[] data;
+
+      @Override
+      public void write(byte[] b) throws IOException {
+          data = b;
+      }
+
       @Override
       public void write(int b) {
       }
@@ -1096,6 +1106,10 @@ public abstract class AbstractJWTFilterTest  {
       public boolean isReady() {
         return false;
       }
+
+      public byte[] getData() {
+        return data;
+      }
   }
 
 }
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
index 679bf333c..e38e6a871 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
@@ -21,7 +21,9 @@ import static 
org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFe
 import static 
org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_VALUE;
 import static org.junit.Assert.fail;
 
+import java.nio.charset.StandardCharsets;
 import java.security.Principal;
+import java.time.Instant;
 import java.util.Properties;
 import java.util.Date;
 import java.util.Set;
@@ -36,6 +38,8 @@ import 
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
 import 
org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
 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.easymock.EasyMock;
 import org.junit.Assert;
 import org.junit.Before;
@@ -326,6 +330,43 @@ public class SSOCookieProviderTest extends 
AbstractJWTFilterTest {
     Assert.assertEquals(loginURL, 
"https://remotehost/notgateway/knoxsso/api/v1/websso?originalUrl="; + 
"https://remotehost/resource";);
   }
 
+  @Test
+  public void testIdleTimoutExceeded() throws Exception {
+    final TokenStateService tokenStateService = 
EasyMock.createNiceMock(TokenStateService.class);
+    final TokenMetadata tokenMetadata = 
EasyMock.createNiceMock(TokenMetadata.class);
+    EasyMock.expect(tokenMetadata.isEnabled()).andReturn(true).anyTimes();
+    
EasyMock.expect(tokenMetadata.getLastUsedAt()).andReturn(Instant.now().minusSeconds(10));
+    
EasyMock.expect(tokenStateService.getTokenMetadata(EasyMock.anyString())).andReturn(tokenMetadata).anyTimes();
+
+    final Properties filterConfig = new Properties();
+    
filterConfig.setProperty(SSOCookieFederationFilter.SSO_IDLE_TIMEOUT_SECONDS, 
"1");
+    filterConfig.setProperty(TokenStateService.CONFIG_SERVER_MANAGED, "true");
+    handler.init(new TestFilterConfig(filterConfig, tokenStateService));
+    ((TestSSOCookieFederationProvider) handler).setTokenService(new 
TestJWTokenAuthority(publicKey));
+
+    final SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, 
"alice", new Date(new Date().getTime() + 5000), privateKey);
+    final Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize());
+    final HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getCookies()).andReturn(new Cookie[] { cookie 
}).anyTimes();
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader(XHR_HEADER)).andReturn(XHR_VALUE).anyTimes();
+
+    final HttpServletResponse response = 
EasyMock.createNiceMock(HttpServletResponse.class);
+    
EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL);
+    final DummyServletOutputStream reponseOutputStream = new 
DummyServletOutputStream();
+    
EasyMock.expect(response.getOutputStream()).andReturn(reponseOutputStream).anyTimes();
+
+    EasyMock.replay(request, response, tokenStateService, tokenMetadata);
+
+    final TestFilterChain chain = new TestFilterChain();
+    handler.doFilter(request, response, chain);
+    Assert.assertNotNull(reponseOutputStream.getData());
+    final String errorResponse = new String(reponseOutputStream.getData(), 
StandardCharsets.UTF_8);
+    
Assert.assertTrue(errorResponse.startsWith(SSOCookieFederationFilter.TOKEN_PREFIX));
+    
Assert.assertTrue(errorResponse.endsWith(SSOCookieFederationFilter.IDLE_TIMEOUT_POSTFIX));
+  }
+
   @Override
   protected String getVerificationPemProperty() {
     return SSOCookieFederationFilter.SSO_VERIFICATION_PEM;
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
index a44c35953..8cb3449a0 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestFilterConfig.java
@@ -17,7 +17,13 @@
  */
 package org.apache.knox.gateway.provider.federation;
 
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
 import org.easymock.EasyMock;
 
 import javax.servlet.FilterConfig;
@@ -28,14 +34,20 @@ import java.util.Properties;
 public class TestFilterConfig implements FilterConfig {
     public static final String TOPOLOGY_NAME_PROP = "test-topology-name";
 
-    Properties props;
+    private final Properties props;
+    private final TokenStateService tokenStateService;
 
     public TestFilterConfig() {
-        this.props = new Properties();
+      this(new Properties());
     }
 
     public TestFilterConfig(Properties props) {
-        this.props = props;
+      this(props, null);
+    }
+
+    public TestFilterConfig(Properties props, TokenStateService 
tokenStateService) {
+      this.props = props;
+      this.tokenStateService = tokenStateService;
     }
 
     @Override
@@ -51,6 +63,26 @@ public class TestFilterConfig implements FilterConfig {
         }
         ServletContext context = EasyMock.createNiceMock(ServletContext.class);
         
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE)).andReturn(topologyName).anyTimes();
+        if (tokenStateService != null) {
+          final GatewayServices gatewayServices = 
EasyMock.createNiceMock(GatewayServices.class);
+          
EasyMock.expect(gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tokenStateService).anyTimes();
+
+          // tests with TSS need GatewayConfig and AliasService too for 
TokenMAC calculation
+          final AliasService aliasService  = 
EasyMock.createNiceMock(AliasService.class);
+          try {
+            
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString())).andReturn("supersecretpassword".toCharArray()).anyTimes();
+          } catch (AliasServiceException e) {
+            // NOP - if the mock initialization failed there will be errors 
down the line
+          }
+          
EasyMock.expect(gatewayServices.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
+
+          final GatewayConfig gatewayConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+          
EasyMock.expect(gatewayConfig.getKnoxTokenHashAlgorithm()).andReturn(HmacAlgorithms.HMAC_SHA_256.getName()).anyTimes();
+          
EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gatewayConfig).anyTimes();
+
+          
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gatewayServices).anyTimes();
+          EasyMock.replay(gatewayConfig, gatewayServices, aliasService);
+        }
         EasyMock.replay(context);
         return context;
     }
diff --git 
a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
 
b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
index 1a3515662..56e72b317 100644
--- 
a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
+++ 
b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
@@ -450,6 +450,7 @@ public class WebSSOResource {
       tokenStateService.addToken(tokenId, issueTime, 
token.getExpiresDate().getTime(), 
tokenStateService.getDefaultMaxLifetimeDuration());
       final TokenMetadata tokenMetadata = new 
TokenMetadata(token.getSubject());
       tokenMetadata.setKnoxSsoCookie(true);
+      tokenMetadata.useTokenNow();
       tokenStateService.addMetadata(tokenId, tokenMetadata);
       LOGGER.storedToken(getTopologyName(), 
Tokens.getTokenDisplayText(token.toString()), 
Tokens.getTokenIDDisplayText(tokenId));
     }
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
index a3f7d29ad..a48d38d05 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
@@ -16,6 +16,7 @@
  */
 package org.apache.knox.gateway.services.security.token;
 
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -37,10 +38,15 @@ public class TokenMetadata {
   public static final String PASSCODE = "passcode";
   public static final String CREATED_BY = "createdBy";
   public static final String KNOX_SSO_COOKIE = "knoxSSOCookie";
-  private static final List<String> KNOWN_MD_NAMES = Arrays.asList(USER_NAME, 
COMMENT, ENABLED, PASSCODE, CREATED_BY, KNOX_SSO_COOKIE);
+  public static final String LAST_USED_AT = "lastUsedAt";
+  private static final List<String> KNOWN_MD_NAMES = Arrays.asList(USER_NAME, 
COMMENT, ENABLED, PASSCODE, CREATED_BY, KNOX_SSO_COOKIE, LAST_USED_AT);
 
   private final Map<String, String> metadataMap = new HashMap<>();
 
+  public TokenMetadata() {
+    //creates an instance with an empty metadata map
+  }
+
   public TokenMetadata(String userName) {
     this(userName, null);
   }
@@ -127,6 +133,15 @@ public class TokenMetadata {
     return Boolean.parseBoolean(getMetadata(KNOX_SSO_COOKIE));
   }
 
+  public void useTokenNow() {
+    saveMetadata(LAST_USED_AT, Instant.now().toString());
+  }
+
+  public Instant getLastUsedAt() {
+    final String lastUsedAt = getMetadata(LAST_USED_AT);
+    return lastUsedAt == null ? null : Instant.parse(lastUsedAt);
+  }
+
   public String toJSON() {
     return JsonUtils.renderAsJsonString(metadataMap);
   }

Reply via email to