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); }