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 <[email protected]>
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);
}