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 53849ac KNOX-2579 - Saving token passcode securely in the DB as
additional token metadata (#437)
53849ac is described below
commit 53849ac45757a8b915dec3ff912a5e7e7665f18a
Author: Sandor Molnar <[email protected]>
AuthorDate: Tue May 11 21:00:32 2021 +0200
KNOX-2579 - Saving token passcode securely in the DB as additional token
metadata (#437)
---
.../applications/tokengen/app/js/tokengen.js | 2 +-
.../provider/federation/jwt/JWTMessages.java | 6 ++
.../federation/jwt/filter/AbstractJWTFilter.java | 34 ++++++-
.../federation/jwt/filter/JWTFederationFilter.java | 59 +++++++-----
.../JWTAsHTTPBasicCredsFederationFilterTest.java | 2 +-
.../federation/TestJWTFederationFilter.java | 7 ++
...okenIDAsHTTPBasicCredsFederationFilterTest.java | 58 ++++++++++--
.../gateway/config/impl/GatewayConfigImpl.java | 7 ++
.../token/impl/AliasBasedTokenStateService.java | 103 ++++++++++++++++++++-
.../token/impl/DefaultTokenStateService.java | 18 ++++
.../services/token/impl/JDBCTokenStateService.java | 30 +++++-
.../token/impl/JournalBasedTokenStateService.java | 27 ++++++
.../services/token/impl/TokenStateDatabase.java | 18 +++-
.../token/impl/TokenStateServiceMessages.java | 6 ++
.../token/impl/ZookeeperTokenStateService.java | 2 +
.../impl/AliasBasedTokenStateServiceTest.java | 23 ++++-
.../token/impl/DefaultTokenStateServiceTest.java | 7 ++
.../token/impl/JDBCTokenStateServiceTest.java | 15 +++
.../gateway/services/token/impl/TokenMACTest.java | 43 +++++++++
.../gateway/service/knoxtoken/TokenResource.java | 27 +++++-
.../knoxtoken/TokenServiceResourceTest.java | 11 ++-
.../apache/knox/gateway/config/GatewayConfig.java | 5 +
.../services/security/token/TokenMetadata.java | 9 ++
.../services/security/token/TokenStateService.java | 9 ++
.../services/security/token/impl/TokenMAC.java | 66 +++++++++++++
.../org/apache/knox/gateway/GatewayTestConfig.java | 5 +
26 files changed, 546 insertions(+), 53 deletions(-)
diff --git
a/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
b/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
index 1e263b7..adfe297 100644
---
a/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
+++
b/gateway-applications/src/main/resources/applications/tokengen/app/js/tokengen.js
@@ -142,7 +142,7 @@ var gen = function() {
$('#accessToken').text(accessToken);
var decodedToken =
b64DecodeUnicode(accessToken.split(".")[1]);
var jwtjson = JSON.parse(decodedToken);
- $('#accessPasscode').text(jwtjson["knox.id"]);
+ $('#accessPasscode').text(resp.passcode);
var date = new Date(resp.expires_in);
$('#expiry').text(date.toLocaleString());
$('#user').text(jwtjson.sub);
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 a6e7a43..b17b4e9 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
@@ -35,6 +35,9 @@ public interface JWTMessages {
@Message( level = MessageLevel.INFO, text = "Access token {0} has expired; a
new one must be acquired." )
void tokenHasExpired(String tokenId);
+ @Message(level = MessageLevel.ERROR, text = "Received wrong passcode token
for {0}")
+ void wrongPasscodeToken(String tokenId);
+
@Message( level = MessageLevel.INFO, text = "The NotBefore check failed." )
void notBeforeCheckFailed();
@@ -79,4 +82,7 @@ public interface JWTMessages {
@Message( level = MessageLevel.INFO, text = "Initialized token signature
verification cache for the {0} topology." )
void initializedSignatureVerificationCache(String topology);
+ @Message( level = MessageLevel.ERROR, text = "Failed to parse passcode
token: {0}" )
+ void failedToParsePasscodeToken(@StackTrace( level = MessageLevel.ERROR)
Exception e);
+
}
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 6eeaf37..82b39c0 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
@@ -17,6 +17,8 @@
*/
package org.apache.knox.gateway.provider.federation.jwt.filter;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
@@ -24,6 +26,7 @@ import java.security.PrivilegedExceptionAction;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -49,12 +52,16 @@ import
org.apache.knox.gateway.audit.api.AuditServiceFactory;
import org.apache.knox.gateway.audit.api.Auditor;
import org.apache.knox.gateway.audit.api.ResourceType;
import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.filter.AbstractGatewayFilter;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
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.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenMetadata;
import org.apache.knox.gateway.services.security.token.TokenServiceException;
@@ -63,6 +70,7 @@ import
org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
import com.nimbusds.jose.JWSHeader;
import org.apache.knox.gateway.util.Tokens;
@@ -99,6 +107,7 @@ public abstract class AbstractJWTFilter implements Filter {
protected String expectedJWKSUrl;
private TokenStateService tokenStateService;
+ private TokenMAC tokenMAC;
@Override
public abstract void doFilter(ServletRequest request, ServletResponse
response, FilterChain chain)
@@ -120,6 +129,13 @@ public abstract class AbstractJWTFilter implements Filter {
authority = services.getService(ServiceType.TOKEN_SERVICE);
if (TokenUtils.isServerManagedTokenStateEnabled(filterConfig)) {
tokenStateService =
services.getService(ServiceType.TOKEN_STATE_SERVICE);
+ try {
+ final GatewayConfig config = (GatewayConfig)
context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+ final AliasService aliasService =
services.getService(ServiceType.ALIAS_SERVICE);
+ tokenMAC = new TokenMAC(config.getKnoxTokenHashAlgorithm(),
aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME));
+ } catch (ServiceLifecycleException | AliasServiceException e) {
+ throw new ServletException("Error while initializing Knox token
MAC generator", e);
+ }
}
}
}
@@ -358,14 +374,20 @@ public abstract class AbstractJWTFilter implements Filter
{
protected boolean validateToken(final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain chain,
- final String tokenId)
+ final String tokenId,
+ final String passcode)
throws IOException, ServletException {
if (tokenStateService != null) {
try {
if (tokenId != null) {
if (tokenIsStillValid(tokenId)) {
- return true;
+ if (validatePasscode(tokenId, passcode)) {
+ return true;
+ } else {
+ log.wrongPasscodeToken(tokenId);
+ handleValidationError(request, response,
HttpServletResponse.SC_BAD_REQUEST, "Bad request: wrong passcode");
+ }
} else {
log.tokenHasExpired(Tokens.getTokenIDDisplayText(tokenId));
handleValidationError(request, response,
HttpServletResponse.SC_BAD_REQUEST,
@@ -385,6 +407,14 @@ public abstract class AbstractJWTFilter implements Filter {
return false;
}
+ private boolean validatePasscode(String tokenId, String passcode) throws
UnknownTokenException {
+ final long issueTime = tokenStateService.getTokenIssueTime(tokenId);
+ final TokenMetadata tokenMetadata =
tokenStateService.getTokenMetadata(tokenId);
+ final String userName = tokenMetadata == null ? "" :
tokenMetadata.getUserName();
+ final byte[] storedPasscode = tokenMetadata == null ? null :
tokenMetadata.getPasscode().getBytes(UTF_8);
+ return Arrays.equals(tokenMAC.hash(tokenId, issueTime, userName,
passcode).getBytes(UTF_8), storedPasscode);
+ }
+
protected boolean verifyTokenSignature(final JWT token) {
boolean verified;
diff --git
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
index 7e4e9d0..b5d34b7 100644
---
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
+++
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
@@ -17,15 +17,15 @@
*/
package org.apache.knox.gateway.provider.federation.jwt.filter;
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
-import org.apache.knox.gateway.security.PrimaryPrincipal;
-import org.apache.knox.gateway.services.security.token.UnknownTokenException;
-import org.apache.knox.gateway.services.security.token.impl.JWT;
-import org.apache.knox.gateway.services.security.token.impl.JWTToken;
-import org.apache.knox.gateway.util.AuthFilterUtils;
-import org.apache.knox.gateway.util.CertificateUtils;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static
org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Base64;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
import javax.security.auth.Subject;
import javax.servlet.FilterChain;
@@ -35,15 +35,16 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.text.ParseException;
-import java.util.Base64;
-import java.util.HashSet;
-import java.util.Locale;
-import java.util.Set;
-import static
org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
+import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.util.AuthFilterUtils;
+import org.apache.knox.gateway.util.CertificateUtils;
public class JWTFederationFilter extends AbstractJWTFilter {
@@ -65,7 +66,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
public static final String TOKEN = "Token";
public static final String PASSCODE = "Passcode";
private String paramName;
- private Set<String> unAuthenticatedPaths = new HashSet(20);
+ private Set<String> unAuthenticatedPaths = new HashSet<>(20);
@Override
public void init( FilterConfig filterConfig ) throws ServletException {
@@ -141,9 +142,21 @@ public class JWTFederationFilter extends AbstractJWTFilter
{
}
} else if (TokenType.Passcode.equals(tokenType)) {
// Validate the token based on the server-managed metadata
- if (validateToken((HttpServletRequest) request, (HttpServletResponse)
response, chain, tokenValue)) {
+ // The received token value must be a Base64 encoded value of
Base64(tokenId)::Base64(rawPasscode)
+ String tokenId = null, passcode = null;
+ try {
+ final String[] base64DecodedTokenIdAndPasscode =
decodeBase64(tokenValue).split("::");
+ tokenId = decodeBase64(base64DecodedTokenIdAndPasscode[0]);
+ passcode = decodeBase64(base64DecodedTokenIdAndPasscode[1]);
+ } catch (Exception e) {
+ log.failedToParsePasscodeToken(e);
+ handleValidationError((HttpServletRequest) request,
(HttpServletResponse) response, HttpServletResponse.SC_UNAUTHORIZED,
+ "Error while parsing the received passcode token");
+ }
+
+ if (validateToken((HttpServletRequest) request, (HttpServletResponse)
response, chain, tokenId, passcode)) {
try {
- Subject subject = createSubjectFromTokenIdentifier(tokenValue);
+ Subject subject = createSubjectFromTokenIdentifier(tokenId);
continueWithEstablishedSecurityContext(subject,
(HttpServletRequest) request, (HttpServletResponse) response, chain);
} catch (UnknownTokenException e) {
((HttpServletResponse)
response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
@@ -156,6 +169,10 @@ public class JWTFederationFilter extends AbstractJWTFilter
{
}
}
+ private String decodeBase64(String toBeDecoded) {
+ return new String(Base64.getDecoder().decode(toBeDecoded.getBytes(UTF_8)),
UTF_8);
+ }
+
public Pair<TokenType, String> getWireToken(final ServletRequest request) {
Pair<TokenType, String> parsed = null;
String token = null;
@@ -187,7 +204,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
Pair<TokenType, String> parsed = null;
final String base64Credentials = header.substring(BASIC.length()).trim();
final byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
- final String credentials = new String(credDecoded,
StandardCharsets.UTF_8);
+ final String credentials = new String(credDecoded, UTF_8);
final String[] values = credentials.split(":", 2);
String username = values[0];
String passcode = values[1].isEmpty() ? null : values[1];
diff --git
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
index a7830cc..a5549ad 100644
---
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
+++
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/JWTAsHTTPBasicCredsFederationFilterTest.java
@@ -41,7 +41,7 @@ import static org.junit.Assert.fail;
public class JWTAsHTTPBasicCredsFederationFilterTest extends
AbstractJWTFilterTest {
@Before
- public void setUp() {
+ public void setUp() throws Exception{
handler = new TestJWTFederationFilter();
((TestJWTFederationFilter) handler).setTokenService(new
TestJWTokenAuthority(publicKey));
}
diff --git
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
index e895ba0..8eccdac 100644
---
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
+++
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TestJWTFederationFilter.java
@@ -21,6 +21,7 @@ package org.apache.knox.gateway.provider.federation;
import
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
import java.lang.reflect.Field;
@@ -42,6 +43,12 @@ public class TestJWTFederationFilter extends
JWTFederationFilter
}
}
+ void setTokenMac(TokenMAC tokenMAC) throws Exception {
+ final Field tokenMacField =
getClass().getSuperclass().getSuperclass().getDeclaredField("tokenMAC");
+ tokenMacField.setAccessible(true);
+ tokenMacField.set(this, tokenMAC);
+ }
+
@Override
protected void recordSignatureVerification(String tokenId) {
super.recordSignatureVerification(tokenId);
diff --git
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
index 1d33e1c..cbd28b3 100644
---
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
+++
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/TokenIDAsHTTPBasicCredsFederationFilterTest.java
@@ -19,6 +19,9 @@
package org.apache.knox.gateway.provider.federation;
import com.nimbusds.jwt.SignedJWT;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.knox.gateway.config.GatewayConfig;
import
org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
import
org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
@@ -29,6 +32,7 @@ import
org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
import org.easymock.EasyMock;
import org.junit.Assert;
import org.junit.Test;
@@ -36,12 +40,15 @@ import org.junit.Test;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+
+import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.time.Instant;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@@ -51,18 +58,33 @@ import static org.junit.Assert.fail;
public class TokenIDAsHTTPBasicCredsFederationFilterTest extends
JWTAsHTTPBasicCredsFederationFilterTest {
TestTokenStateService tss;
+ TokenMAC tokenMAC;
@Override
- public void setUp() {
+ public void setUp() throws Exception {
super.setUp();
tss = new TestTokenStateService();
((TestJWTFederationFilter) handler).setTokenStateService(tss);
+ tokenMAC = new TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(),
"sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray());
+ ((TestJWTFederationFilter) handler).setTokenMac(tokenMAC);
}
@Override
protected void setTokenOnRequest(final HttpServletRequest request, final
SignedJWT jwt) {
- addTokenState(jwt);
- setTokenOnRequest(request, TestJWTFederationFilter.PASSCODE,
getTokenId(jwt));
+ try {
+ final long issueTime = System.currentTimeMillis() -
TimeUnit.MINUTES.toMillis(5);
+ final String subject = (String)
jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT);
+ final String passcode = UUID.randomUUID().toString();
+ addTokenState(jwt, issueTime, subject, passcode);
+ setTokenOnRequest(request, TestJWTFederationFilter.PASSCODE,
generatePasscodeField(getTokenId(jwt), passcode));
+ } catch(ParseException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ private String generatePasscodeField(String tokenId, String passcode) {
+ final String base64TokenIdPasscode =
Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" +
Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8));
+ return
Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8));
}
/**
@@ -76,8 +98,15 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest
extends JWTAsHTTPBasicC
protected void setTokenOnRequest(final HttpServletRequest request,
final SignedJWT jwt,
final String authUsername) {
- addTokenState(jwt);
- setTokenOnRequest(request, authUsername, getTokenId(jwt));
+ try {
+ final long issueTime = System.currentTimeMillis() -
TimeUnit.MINUTES.toMillis(5);
+ final String subject = (String)
jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT);
+ final String passcode = UUID.randomUUID().toString();
+ addTokenState(jwt, issueTime, subject, passcode);
+ setTokenOnRequest(request, authUsername,
generatePasscodeField(getTokenId(jwt), passcode));
+ } catch(ParseException e) {
+ Assert.fail(e.getMessage());
+ }
}
@Override
@@ -95,13 +124,14 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest
extends JWTAsHTTPBasicC
return tokenId;
}
- private void addTokenState(final SignedJWT jwt) {
+ private void addTokenState(final SignedJWT jwt, long issueTime, String
subject, String passcode) {
try {
JWTToken token = new JWTToken(jwt.serialize());
- tss.addToken(token, System.currentTimeMillis() -
TimeUnit.MINUTES.toMillis(5));
+ tss.addToken(token, issueTime);
- String subject = (String)
jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT);
- tss.addMetadata(TokenUtils.getTokenId(token), new
TokenMetadata(subject));
+ final TokenMetadata metadata = new TokenMetadata(subject);
+ metadata.setPasscode(tokenMAC.hash(TokenUtils.getTokenId(token),
issueTime, subject, passcode));
+ tss.addMetadata(TokenUtils.getTokenId(token), metadata);
} catch (ParseException e) {
Assert.fail(e.getMessage());
}
@@ -328,6 +358,7 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest
extends JWTAsHTTPBasicC
*/
private static class TestTokenStateService implements TokenStateService {
+ private final Map<String, Long> tokenIssueTimes = new
ConcurrentHashMap<>();
private final Map<String, Long> tokenExpirations = new
ConcurrentHashMap<>();
private final Map<String, TokenMetadata> tokenMetadata = new
ConcurrentHashMap<>();
@@ -359,7 +390,7 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest
extends JWTAsHTTPBasicC
if (expiration == null || expiration.isEmpty()) {
expiration = "0";
}
- addToken(TokenUtils.getTokenId(token),
Instant.now().toEpochMilli(), Long.parseLong(expiration));
+ addToken(TokenUtils.getTokenId(token), issueTime,
Long.parseLong(expiration));
}
private void addToken(String tokenId, long expiration) {
@@ -369,11 +400,18 @@ public class TokenIDAsHTTPBasicCredsFederationFilterTest
extends JWTAsHTTPBasicC
@Override
public void addToken(String tokenId, long issueTime, long expiration) {
addToken(tokenId, expiration);
+ tokenIssueTimes.put(tokenId, issueTime);
}
@Override
public void addToken(String tokenId, long issueTime, long expiration,
long maxLifetimeDuration) {
addToken(tokenId, expiration);
+ tokenIssueTimes.put(tokenId, issueTime);
+ }
+
+ @Override
+ public long getTokenIssueTime(String tokenId) throws
UnknownTokenException {
+ return tokenIssueTimes.getOrDefault(tokenId, 0L);
}
@Override
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 486e7dd..43895e9 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
@@ -39,6 +39,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.conf.Configuration;
@@ -260,6 +261,7 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
private static final String KNOX_TOKEN_EVICTION_GRACE_PERIOD =
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.eviction.grace.period";
private static final String KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL =
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.state.alias.persistence.interval";
private static final String KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED =
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.permissive.validation";
+ private static final String KNOX_TOKEN_HASH_ALGORITHM =
GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.hash.algorithm";
private static final long KNOX_TOKEN_EVICTION_INTERVAL_DEFAULT =
TimeUnit.MINUTES.toSeconds(5);
private static final long KNOX_TOKEN_EVICTION_GRACE_PERIOD_DEFAULT =
TimeUnit.HOURS.toSeconds(24);
private static final long KNOX_TOKEN_ALIAS_PERSISTENCE_INTERVAL_DEFAULT =
TimeUnit.SECONDS.toSeconds(15);
@@ -1179,6 +1181,11 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
}
@Override
+ public String getKnoxTokenHashAlgorithm() {
+ return get(KNOX_TOKEN_HASH_ALGORITHM,
HmacAlgorithms.HMAC_SHA_256.getName());
+ }
+
+ @Override
public Set<String> getHiddenTopologiesOnHomepage() {
final Set<String> hiddenTopologies = new
HashSet<>(getTrimmedStringCollection(KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES));
return hiddenTopologies == null || hiddenTopologies.isEmpty() ?
KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES_DEFAULT : hiddenTopologies;
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
index 9c38303..1e76662 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
@@ -57,6 +57,7 @@ import org.apache.knox.gateway.util.Tokens;
public class AliasBasedTokenStateService extends DefaultTokenStateService
implements TokenStatePeristerMonitorListener {
static final String TOKEN_ALIAS_SUFFIX_DELIM = "--";
+ static final String TOKEN_ISSUE_TIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM +
"iss";
static final String TOKEN_MAX_LIFETIME_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM +
"max";
static final String TOKEN_META_POSTFIX = TOKEN_ALIAS_SUFFIX_DELIM +
"meta";
@@ -148,9 +149,11 @@ public class AliasBasedTokenStateService extends
DefaultTokenStateService implem
for (Map.Entry<String, char[]> passwordAliasMapEntry :
passwordAliasMap.entrySet()) {
alias = passwordAliasMapEntry.getKey();
if (alias.endsWith(TOKEN_MAX_LIFETIME_POSTFIX)) {
- // This token state service implementation persists two aliases in
__gateway-credentials.jceks (see persistTokenState below):
+ // This token state service implementation persists 4 aliases in
__gateway-credentials.jceks (see persistTokenState below):
// - an alias which maps a token ID to its expiration time
- // - another alias with '--max' postfix which maps the maximum
lifetime of the token identified by the 1st alias
+ // - an alias with '--max' postfix which maps the maximum lifetime
of the token identified by the 1st alias
+ // - an alias with '--iss' postfix which maps the issue time of the
token
+ // - an alias with '-meta' postfix which maps an arbitrary metadata
of the token
// Given this, we should check aliases ending with '--max' and
calculate the token ID from this alias.
// If all aliases were blindly processed we would end-up handling
aliases that were not persisted via this token state service
// implementation -> facing error(s) when trying to parse the
expiration/maxLifeTime values and irrelevant data would be loaded in the
@@ -164,6 +167,9 @@ public class AliasBasedTokenStateService extends
DefaultTokenStateService implem
} else if (alias.endsWith(TOKEN_META_POSTFIX)) {
tokenId = alias.substring(0, alias.indexOf(TOKEN_META_POSTFIX));
super.addMetadata(tokenId, TokenMetadata.fromJSON(new
String(passwordAliasMapEntry.getValue())));
+ } else if (alias.endsWith(TOKEN_ISSUE_TIME_POSTFIX)) {
+ tokenId = alias.substring(0,
alias.indexOf(TOKEN_ISSUE_TIME_POSTFIX));
+ setIssueTimeInMemory(tokenId,
convertCharArrayToLong(passwordAliasMapEntry.getValue()));
}
// log some progress (it's very useful in case a huge amount of token
related aliases in __gateway-credentials.jceks)
@@ -279,6 +285,18 @@ public class AliasBasedTokenStateService extends
DefaultTokenStateService implem
}
@Override
+ protected void setIssueTime(String tokenId, long issueTime) {
+ synchronized (unpersistedState) {
+ unpersistedState.add(new TokenIssueTime(tokenId, issueTime));
+ }
+ setIssueTimeInMemory(tokenId, issueTime);
+ }
+
+ protected void setIssueTimeInMemory(String tokenId, long issueTime) {
+ super.setIssueTime(tokenId, issueTime);
+ }
+
+ @Override
protected void setMaxLifetime(final String tokenId, long issueTime, long
maxLifetimeDuration) {
super.setMaxLifetime(tokenId, issueTime, maxLifetimeDuration);
synchronized (unpersistedState) {
@@ -317,6 +335,35 @@ public class AliasBasedTokenStateService extends
DefaultTokenStateService implem
}
@Override
+ public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
+ // Check the in-memory collection first, to avoid costly keystore access
when possible
+ try {
+ // check the in-memory cache first
+ return super.getTokenIssueTime(tokenId);
+ } catch (UnknownTokenException e) {
+ // It's not in memory
+ }
+
+ // If there is no associated state in the in-memory cache, proceed to
check the alias service
+ long issueTime = 0;
+ try {
+ char[] issueTimeStr = getPasswordUsingAliasService(tokenId +
TOKEN_ISSUE_TIME_POSTFIX);
+ if (issueTimeStr == null) {
+ throw new UnknownTokenException(tokenId);
+ }
+ issueTime = convertCharArrayToLong(issueTimeStr);
+ // Update the in-memory cache to avoid subsequent keystore look-ups for
the same state
+ super.setIssueTime(tokenId, issueTime);
+ } catch (UnknownTokenException e) {
+ throw e;
+ } catch (Exception e) {
+ log.errorAccessingTokenState(Tokens.getTokenIDDisplayText(tokenId), e);
+ }
+
+ return issueTime;
+ }
+
+ @Override
public long getTokenExpiration(String tokenId, boolean validate) throws
UnknownTokenException {
// Check the in-memory collection first, to avoid costly keystore access
when possible
try {
@@ -473,7 +520,7 @@ public class AliasBasedTokenStateService extends
DefaultTokenStateService implem
}
enum TokenStateType {
- EXP(1), MAX(2), META(3);
+ EXP(1), MAX(2), META(3), ISS(4);
private final int id;
@@ -591,6 +638,56 @@ public class AliasBasedTokenStateService extends
DefaultTokenStateService implem
}
}
+ private static final class TokenIssueTime implements TokenState {
+ private String tokenId;
+ private long issueTime;
+
+ TokenIssueTime(String tokenId, long issueTime) {
+ this.tokenId = tokenId;
+ this.issueTime = issueTime;
+ }
+
+ @Override
+ public String getTokenId() {
+ return tokenId;
+ }
+
+ @Override
+ public String getAlias() {
+ return tokenId + TOKEN_ISSUE_TIME_POSTFIX;
+ }
+
+ @Override
+ public String getAliasValue() {
+ return String.valueOf(issueTime);
+ }
+
+ @Override
+ public TokenStateType getType() {
+ return TokenStateType.ISS;
+ }
+
+ @Override
+ public int hashCode() {
+ return new
HashCodeBuilder().append(tokenId).append(getType().id).toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (obj.getClass() != getClass()) {
+ return false;
+ }
+ final TokenIssueTime rhs = (TokenIssueTime) obj;
+ return new EqualsBuilder().append(this.tokenId,
rhs.tokenId).append(this.getType().id, rhs.getType().id).isEquals();
+ }
+ }
+
private static final class TokenMetadataState implements TokenState {
private final String tokenId;
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 9b44dfd..e51aebf 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
@@ -62,6 +62,8 @@ public class DefaultTokenStateService implements
TokenStateService {
private final Map<String, Long> tokenExpirations = new ConcurrentHashMap<>();
+ private final Map<String, Long> tokenIssueTimes = new ConcurrentHashMap<>();
+
private final Map<String, Long> maxTokenLifetimes = new
ConcurrentHashMap<>();
private final Map<String, TokenMetadata> metadataMap = new
ConcurrentHashMap<>();
@@ -139,6 +141,7 @@ public class DefaultTokenStateService implements
TokenStateService {
long expiration,
long maxLifetimeDuration) {
validateTokenIdentifier(tokenId);
+ setIssueTime(tokenId, issueTime);
tokenExpirations.put(tokenId, expiration);
setMaxLifetime(tokenId, issueTime, maxLifetimeDuration);
log.addedToken(Tokens.getTokenIDDisplayText(tokenId),
getTimestampDisplay(expiration));
@@ -147,6 +150,20 @@ public class DefaultTokenStateService implements
TokenStateService {
}
}
+ protected void setIssueTime(String tokenId, long issueTime) {
+ tokenIssueTimes.put(tokenId, issueTime);
+ }
+
+ @Override
+ public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
+ validateToken(tokenId);
+ final Long issueTime = tokenIssueTimes.get(tokenId);
+ if (issueTime == null) {
+ throw new UnknownTokenException(tokenId);
+ }
+ return issueTime.longValue();
+ }
+
@Override
public long getTokenExpiration(final JWT token) throws UnknownTokenException
{
long expiration = -1;
@@ -284,6 +301,7 @@ public class DefaultTokenStateService implements
TokenStateService {
}
private void removeTokenState(final Set<String> tokenIds) {
+ tokenIssueTimes.keySet().removeAll(tokenIds);
tokenExpirations.keySet().removeAll(tokenIds);
maxTokenLifetimes.keySet().removeAll(tokenIds);
metadataMap.keySet().removeAll(tokenIds);
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
index 4907188..ecb648d 100644
---
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
@@ -35,7 +35,7 @@ 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 AliasService aliasService; // connection username/pw and passcode
HMAC secret are stored here
private TokenStateDatabase tokenDatabase;
private AtomicBoolean initialized = new AtomicBoolean(false);
private Lock initLock = new ReentrantLock(true);
@@ -86,9 +86,35 @@ public class JDBCTokenStateService extends
DefaultTokenStateService {
}
@Override
+ public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
+ try {
+ // check the in-memory cache first
+ return super.getTokenIssueTime(tokenId);
+ } catch (UnknownTokenException e) {
+ // It's not in memory
+ }
+
+ long issueTime = 0;
+ try {
+ issueTime = tokenDatabase.getTokenIssueTime(tokenId);
+ if (issueTime > 0) {
+
log.fetchedIssueTimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
issueTime);
+
+ // Update the in-memory cache to avoid subsequent DB look-ups for the
same state
+ super.setIssueTime(tokenId, issueTime);
+ } else {
+ throw new UnknownTokenException(tokenId);
+ }
+ } catch (SQLException e) {
+
log.errorFetchingIssueTimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ }
+ return issueTime;
+ }
+
+ @Override
public long getTokenExpiration(String tokenId, boolean validate) throws
UnknownTokenException {
try {
- // check the in-memory cache, then
+ // check the in-memory cache first
return super.getTokenExpiration(tokenId, validate);
} catch (UnknownTokenException e) {
// It's not in memory
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
index 5a5c1dd..ee21c6a 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java
@@ -77,6 +77,33 @@ public class JournalBasedTokenStateService extends
DefaultTokenStateService {
}
@Override
+ public long getTokenIssueTime(String tokenId) throws UnknownTokenException
{
+ try {
+ // Check the in-memory collection first, to avoid file access when
possible
+ return super.getTokenIssueTime(tokenId);
+ } catch (UnknownTokenException e) {
+ // It's not in memory
+ }
+
+ validateToken(tokenId);
+
+ // If there is no associated state in the in-memory cache, proceed to
check the journal
+ long issueTime = 0;
+ try {
+ JournalEntry entry = journal.get(tokenId);
+ if (entry == null) {
+ throw new UnknownTokenException(tokenId);
+ }
+
+ issueTime = Long.parseLong(entry.getIssueTime());
+ } catch (IOException e) {
+ log.failedToLoadJournalEntry(e);
+ }
+
+ return issueTime;
+ }
+
+ @Override
public long getTokenExpiration(final String tokenId, boolean validate)
throws UnknownTokenException {
// Check the in-memory collection first, to avoid file access when
possible
try {
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
index 88bd68d..695e62b 100644
---
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
@@ -32,6 +32,7 @@ import java.util.Map;
import javax.sql.DataSource;
+import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.knox.gateway.services.security.token.TokenMetadata;
@@ -43,6 +44,7 @@ public class TokenStateDatabase {
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_ISSUE_TIME_SQL = "SELECT issue_time FROM " +
TOKENS_TABLE_NAME + " WHERE token_id = ?";
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 = ?";
@@ -101,6 +103,15 @@ public class TokenStateDatabase {
}
}
+ long getTokenIssueTime(String tokenId) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
getTokenExpirationStatement =
connection.prepareStatement(GET_TOKEN_ISSUE_TIME_SQL)) {
+ getTokenExpirationStatement.setString(1, tokenId);
+ try (ResultSet rs = getTokenExpirationStatement.executeQuery()) {
+ return rs.next() ? rs.getLong(1) : -1;
+ }
+ }
+ }
+
long getTokenExpiration(String tokenId) throws SQLException {
try (Connection connection = dataSource.getConnection(); PreparedStatement
getTokenExpirationStatement =
connection.prepareStatement(GET_TOKEN_EXPIRATION_SQL)) {
getTokenExpirationStatement.setString(1, tokenId);
@@ -136,7 +147,7 @@ public class TokenStateDatabase {
boolean updateMetadata(String tokenId, String metadataName, String
metadataValue) throws SQLException {
try (Connection connection = dataSource.getConnection(); PreparedStatement
updateMetadataStatement = connection.prepareStatement(UPDATE_METADATA_SQL)) {
- updateMetadataStatement.setString(1, metadataValue);
+ updateMetadataStatement.setString(1,
metadataName.equals(TokenMetadata.PASSCODE) ?
Base64.encodeBase64String(metadataValue.getBytes(UTF_8)) : metadataValue);
updateMetadataStatement.setString(2, tokenId);
updateMetadataStatement.setString(3, metadataName);
return updateMetadataStatement.executeUpdate() == 1;
@@ -147,7 +158,7 @@ public class TokenStateDatabase {
try (Connection connection = dataSource.getConnection(); PreparedStatement
addMetadataStatement = connection.prepareStatement(ADD_METADATA_SQL)) {
addMetadataStatement.setString(1, tokenId);
addMetadataStatement.setString(2, metadataName);
- addMetadataStatement.setString(3, metadataValue);
+ addMetadataStatement.setString(3,
metadataName.equals(TokenMetadata.PASSCODE) ?
Base64.encodeBase64String(metadataValue.getBytes(UTF_8)) : metadataValue);
return addMetadataStatement.executeUpdate() == 1;
}
}
@@ -158,7 +169,8 @@ public class TokenStateDatabase {
try (ResultSet rs = getMaxLifetimeStatement.executeQuery()) {
final Map<String, String> metadataMap = new HashMap<>();
while (rs.next()) {
- metadataMap.put(rs.getString(1), rs.getString(2));
+ String metadataName = rs.getString(1);
+ metadataMap.put(metadataName,
metadataName.equals(TokenMetadata.PASSCODE) ? new
String(Base64.decodeBase64(rs.getString(2).getBytes(UTF_8)), UTF_8) :
rs.getString(2));
}
return metadataMap.isEmpty() ? null : new TokenMetadata(metadataMap);
}
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 101c3cc..e5d0707 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
@@ -199,6 +199,12 @@ public interface TokenStateServiceMessages {
@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 issue time for {0} from
the database : {1}")
+ void fetchedIssueTimeFromDatabase(String tokenId, long issueTime);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
fetching issue time for {0} from the database : {1}")
+ void errorFetchingIssueTimeFromDatabase(String tokenId, 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);
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
index f5450a7..1fce481 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java
@@ -137,6 +137,8 @@ public class ZookeeperTokenStateService extends
AliasBasedTokenStateService impl
setMaxLifetime(tokenId, maxLifeTime);
} else if (alias.endsWith(TOKEN_META_POSTFIX)) {
addMetadataInMemory(tokenId, TokenMetadata.fromJSON(value));
+ } else if (alias.endsWith(TOKEN_ISSUE_TIME_POSTFIX)) {
+ setIssueTimeInMemory(tokenId, Long.parseLong(value));
} else {
final long expiration = Long.parseLong(value);
updateExpirationInMemory(tokenId, expiration);
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
index fc9f316..96c5d38 100644
---
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
@@ -524,6 +524,7 @@ public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
Map<String, Long> tokenExpirations = getTokenExpirationsField(tss);
Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss);
+ Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, true);
Set<AliasBasedTokenStateService.TokenState> unpersistedState =
getUnpersistedStateField(tss);
@@ -535,8 +536,12 @@ public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
TOKEN_COUNT,
maxTokenLifetimes.size());
+ assertEquals("Expected the tokens issue times to have been added in the
base class cache.",
+ TOKEN_COUNT,
+ tokenIssueTimes.size());
+
assertEquals("Expected the unpersisted state to have been added.",
- (TOKEN_COUNT * 2), // Two TokenState entries per token
(expiration, max lifetime)
+ (TOKEN_COUNT * 3), // Two TokenState entries per token
(expiration, max lifetime, issue time)
unpersistedState.size());
// Verify that the expected methods were invoked
@@ -605,6 +610,7 @@ public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
Map<String, Long> tokenExpirations = getTokenExpirationsField(tss);
Map<String, Long> maxTokenLifetimes = getMaxTokenLifetimesField(tss);
+ Map<String, Long> tokenIssueTimes = getTokenIssueTimesField(tss, true);
Set<AliasBasedTokenStateService.TokenState> unpersistedState =
getUnpersistedStateField(tss);
@@ -616,8 +622,12 @@ public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
TOKEN_COUNT,
maxTokenLifetimes.size());
+ assertEquals("Expected the tokens issue times to have been added in the
base class cache.",
+ TOKEN_COUNT,
+ tokenIssueTimes.size());
+
assertEquals("Expected the unpersisted state to have been added.",
- (TOKEN_COUNT * 2), // Two TokenState entries per token
(expiration, max lifetime)
+ (TOKEN_COUNT * 3), // Two TokenState entries per token
(expiration, max lifetime, issue time)
unpersistedState.size());
// Verify that the expected methods were invoked
@@ -641,7 +651,7 @@ public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
}
final List<AliasBasedTokenStateService.TokenState> unpersistedTokenStates
= new ArrayList<>(getUnpersistedStateField(tss, false));
- final int expectedAliasCount = 2 * tokenCount; //expiration + max for each
token
+ final int expectedAliasCount = 3 * tokenCount; //expiration + max + issue
time for each token
assertEquals(expectedAliasCount, unpersistedTokenStates.size());
for (JWTToken token : testTokens) {
String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM);
@@ -835,6 +845,13 @@ public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
return (Map<String, Long>) maxTokenLifetimesField.get(tss);
}
+ private static Map<String, Long> getTokenIssueTimesField(TokenStateService
tss, boolean fromGrandParent) throws Exception {
+ final Class<TokenStateService> clazz = (Class<TokenStateService>)
(fromGrandParent ? tss.getClass().getSuperclass().getSuperclass() :
tss.getClass().getSuperclass());
+ Field tokenIssueTimesField = clazz.getDeclaredField("tokenIssueTimes");
+ tokenIssueTimesField.setAccessible(true);
+ return (Map<String, Long>) tokenIssueTimesField.get(tss);
+ }
+
private static Set<AliasBasedTokenStateService.TokenState>
getUnpersistedStateField(TokenStateService tss) throws Exception {
return getUnpersistedStateField(tss, true);
}
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
index 8d9836d..ab9103c 100644
---
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
@@ -300,6 +300,13 @@ public class DefaultTokenStateServiceTest {
assertNotNull(tss.getTokenMetadata(tokenId));
assertEquals(tss.getTokenMetadata(tokenId).getComment(), comment);
assertTrue(tss.getTokenMetadata(tokenId).isEnabled());
+
+ final String passcode = "myPasscode";
+ final TokenMetadata metadata = new TokenMetadata(userName, comment, true);
+ metadata.setPasscode(passcode);
+ tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), metadata);
+ assertNotNull(tss.getTokenMetadata(tokenId));
+ assertEquals(tss.getTokenMetadata(tokenId).getPasscode(), passcode);
}
protected static JWTToken createMockToken(final long expiration) {
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
index 7658e4f..2ae6408 100644
---
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
@@ -17,6 +17,7 @@
*/
package org.apache.knox.gateway.services.token.impl;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -30,11 +31,14 @@ import java.sql.SQLException;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.HmacAlgorithms;
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.services.security.token.impl.TokenMAC;
import org.apache.knox.gateway.shell.jdbc.Database;
import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
import org.apache.knox.gateway.util.JDBCUtils;
@@ -59,6 +63,7 @@ public class JDBCTokenStateServiceTest {
private static NetworkServerControl derbyNetworkServerControl;
private static Database derbyDatabase;
private static JDBCTokenStateService jdbcTokenStateService;
+ private static TokenMAC tokenMAC;
@SuppressWarnings("PMD.JUnit4TestShouldUseBeforeAnnotation")
@BeforeClass
@@ -85,7 +90,10 @@ public class JDBCTokenStateServiceTest {
jdbcTokenStateService = new JDBCTokenStateService();
jdbcTokenStateService.setAliasService(aliasService);
jdbcTokenStateService.init(gatewayConfig, null);
+
assertTrue(derbyDatabase.hasTable(TokenStateDatabase.TOKENS_TABLE_NAME));
+
+ tokenMAC = new TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(),
"sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray());
}
private static Database prepareDerbyDatabase(Path derbyDatabaseFolder)
throws SQLException {
@@ -143,17 +151,24 @@ public class JDBCTokenStateServiceTest {
@Test(expected = UnknownTokenException.class)
public void testAddMetadata() throws Exception {
final String tokenId = UUID.randomUUID().toString();
+ final String passcode = UUID.randomUUID().toString();
+ final String passcodeMac = tokenMAC.hash(tokenId, 1, "sampleUser",
passcode);
final TokenMetadata tokenMetadata = new TokenMetadata("sampleUser", "my
test comment", false);
+ tokenMetadata.setPasscode(passcodeMac);
jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
jdbcTokenStateService.addMetadata(tokenId, tokenMetadata);
assertEquals("sampleUser",
jdbcTokenStateService.getTokenMetadata(tokenId).getUserName());
assertEquals("my test comment",
jdbcTokenStateService.getTokenMetadata(tokenId).getComment());
assertFalse(jdbcTokenStateService.getTokenMetadata(tokenId).isEnabled());
+ final String storedPasscode =
jdbcTokenStateService.getTokenMetadata(tokenId).getPasscode();
+ assertEquals(passcodeMac, storedPasscode);
assertEquals("sampleUser", getStringTokenAttributeFromDatabase(tokenId,
getSelectMetadataSql(TokenMetadata.USER_NAME)));
assertEquals("my test comment",
getStringTokenAttributeFromDatabase(tokenId,
getSelectMetadataSql(TokenMetadata.COMMENT)));
assertEquals("false", getStringTokenAttributeFromDatabase(tokenId,
getSelectMetadataSql(TokenMetadata.ENABLED)));
+ final String storedPasscodeInDb = new
String(Base64.decodeBase64(getStringTokenAttributeFromDatabase(tokenId,
getSelectMetadataSql(TokenMetadata.PASSCODE))), UTF_8);
+ assertEquals(passcodeMac, storedPasscodeInDb);
//enable the token (it was disabled)
tokenMetadata.setEnabled(true);
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/TokenMACTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/TokenMACTest.java
new file mode 100644
index 0000000..368e343
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/TokenMACTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.util.UUID;
+
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
+import org.junit.Test;
+
+public class TokenMACTest {
+
+ @Test
+ public void testHash() throws Exception {
+ final TokenMAC tokenMAC = new
TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(),
"sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray());
+ final String toBeHashed = "sampleTokenContent";
+ final String tokenId = UUID.randomUUID().toString();
+ final long issueTime = 123L;
+ final String userName = "smolnar";
+ final String hashed = tokenMAC.hash(tokenId, issueTime, userName,
toBeHashed);
+ assertNotEquals(toBeHashed, hashed);
+ assertEquals(hashed, tokenMAC.hash(tokenId, issueTime, userName,
toBeHashed));
+ }
+
+}
diff --git
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index e484f56..41ea7eb 100644
---
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -17,6 +17,7 @@
*/
package org.apache.knox.gateway.service.knoxtoken;
+import java.nio.charset.StandardCharsets;
import java.security.KeyStoreException;
import java.security.Principal;
import java.security.cert.Certificate;
@@ -30,6 +31,7 @@ import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
import javax.annotation.PostConstruct;
import javax.inject.Singleton;
@@ -49,6 +51,7 @@ import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.security.SubjectUtils;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.services.security.KeystoreService;
@@ -63,6 +66,7 @@ import
org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
import org.apache.knox.gateway.util.JsonUtils;
import org.apache.knox.gateway.util.Tokens;
@@ -78,6 +82,7 @@ public class TokenResource {
private static final String TOKEN_TYPE = "token_type";
private static final String ACCESS_TOKEN = "access_token";
private static final String TOKEN_ID = "token_id";
+ private static final String PASSCODE = "passcode";
private static final String MANAGED_TOKEN = "managed";
private static final String TARGET_URL = "target_url";
private static final String ENDPOINT_PUBLIC_CERT = "endpoint_public_cert";
@@ -118,6 +123,7 @@ public class TokenResource {
// Optional token store service
private TokenStateService tokenStateService;
+ private TokenMAC tokenMAC;
private final Map<String, String> tokenStateServiceStatusMap = new
HashMap<>();
private Optional<Long> renewInterval = Optional.empty();
@@ -133,7 +139,7 @@ public class TokenResource {
ServletContext context;
@PostConstruct
- public void init() throws AliasServiceException {
+ public void init() throws AliasServiceException, ServiceLifecycleException {
String audiences = context.getInitParameter(TOKEN_AUDIENCES_PARAM);
if (audiences != null) {
@@ -190,6 +196,9 @@ public class TokenResource {
GatewayServices services = (GatewayServices)
context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+ final GatewayConfig gatewayConfig = (GatewayConfig)
context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+ final AliasService aliasService =
services.getService(ServiceType.ALIAS_SERVICE);
+ tokenMAC = new TokenMAC(gatewayConfig.getKnoxTokenHashAlgorithm(),
aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME));
String renewIntervalValue =
context.getInitParameter(TOKEN_EXP_RENEWAL_INTERVAL);
if (renewIntervalValue != null && !renewIntervalValue.isEmpty()) {
@@ -528,7 +537,7 @@ public class TokenResource {
String tokenId = TokenUtils.getTokenId(token);
log.issuedToken(getTopologyName(),
Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
- HashMap<String, Object> map = new HashMap<>();
+ final HashMap<String, Object> map = new HashMap<>();
map.put(ACCESS_TOKEN, accessToken);
map.put(TOKEN_ID, tokenId);
map.put(MANAGED_TOKEN, String.valueOf(managedToken));
@@ -543,17 +552,22 @@ public class TokenResource {
if (endpointPublicCert != null) {
map.put(ENDPOINT_PUBLIC_CERT, endpointPublicCert);
}
+ final String passcode = UUID.randomUUID().toString();
+ map.put(PASSCODE, generatePasscodeField(tokenId, passcode));
String jsonResponse = JsonUtils.renderAsJsonString(map);
// Optional token store service persistence
if (tokenStateService != null) {
+ final long issueTime = System.currentTimeMillis();
tokenStateService.addToken(tokenId,
- System.currentTimeMillis(),
+ issueTime,
expires,
maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
final String comment = request.getParameter(COMMENT);
- tokenStateService.addMetadata(tokenId, new
TokenMetadata(p.getName(), StringUtils.isBlank(comment) ? null : comment));
+ final TokenMetadata tokenMetadata = new TokenMetadata(p.getName(),
StringUtils.isBlank(comment) ? null : comment);
+ tokenMetadata.setPasscode(tokenMAC.hash(tokenId, issueTime,
p.getName(), passcode));
+ tokenStateService.addMetadata(tokenId, tokenMetadata);
log.storedToken(getTopologyName(),
Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
}
@@ -567,6 +581,11 @@ public class TokenResource {
return Response.ok().entity("{ \"Unable to acquire token.\" }").build();
}
+ private String generatePasscodeField(String tokenId, String passcode) {
+ final String base64TokenIdPasscode =
Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" +
Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8));
+ return
Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8));
+ }
+
void addClientDataToMap(String[] tokenClientData,
Map<String,Object> map) {
String[] kv;
diff --git
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index ba4d338..9438a79 100644
---
a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++
b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -30,6 +30,7 @@ import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
+import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.security.PrimaryPrincipal;
@@ -45,6 +46,7 @@ import
org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
import org.apache.knox.gateway.services.token.impl.JDBCTokenStateService;
import org.apache.knox.gateway.util.JsonUtils;
import org.easymock.EasyMock;
@@ -154,12 +156,14 @@ public class TokenServiceResourceTest {
if (contextExpectations.containsKey(tokenStateServiceType)) {
EasyMock.expect(config.getServiceParameter(tokenStateServiceType,
"impl")).andReturn(contextExpectations.get(tokenStateServiceType)).anyTimes();
}
+
EasyMock.expect(config.getKnoxTokenHashAlgorithm()).andReturn(HmacAlgorithms.HMAC_SHA_256.getName()).anyTimes();
tss = new TestTokenStateService();
EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
EasyMock.expect(services.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TokenUtils.SIGNING_HMAC_SECRET_ALIAS)).andReturn(null).anyTimes();
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME)).andReturn("sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray()).anyTimes();
authority = new TestJWTokenAuthority(publicKey, privateKey);
EasyMock.expect(services.getService(ServiceType.TOKEN_SERVICE)).andReturn(authority).anyTimes();
@@ -1220,8 +1224,9 @@ public class TokenServiceResourceTest {
return maxLifetimes.get(token);
}
- long getExpiration(final String token) {
- return expirationData.get(token);
+ @Override
+ public long getTokenIssueTime(String tokenId) throws UnknownTokenException
{
+ return issueTimes.getOrDefault(tokenId, 0L);
}
@Override
@@ -1292,7 +1297,7 @@ public class TokenServiceResourceTest {
@Override
public long getTokenExpiration(String tokenId) {
- return 0;
+ return expirationData.getOrDefault(tokenId, 0L);
}
@Override
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 38d866e..4474c33 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
@@ -698,6 +698,11 @@ public interface GatewayConfig {
long getKnoxTokenStateAliasPersistenceInterval();
/**
+ * @return the HMAC algorithm name to be used to sign generated Knox Token
content (e.g. the token.id claim)
+ */
+ String getKnoxTokenHashAlgorithm();
+
+ /**
* @return the list of topologies that should be hidden on Knox homepage
*/
Set<String> getHiddenTopologiesOnHomepage();
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 97455a5..6fcaf03 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
@@ -30,6 +30,7 @@ public class TokenMetadata {
public static final String USER_NAME = "userName";
public static final String COMMENT = "comment";
public static final String ENABLED = "enabled";
+ public static final String PASSCODE = "passcode";
private final Map<String, String> metadataMap = new HashMap<>();
@@ -78,6 +79,14 @@ public class TokenMetadata {
return Boolean.parseBoolean(metadataMap.get(ENABLED));
}
+ public void setPasscode(String passcode) {
+ saveMetadata(PASSCODE, passcode);
+ }
+
+ public String getPasscode() {
+ return metadataMap.get(PASSCODE);
+ }
+
public String toJSON() {
return JsonUtils.renderAsJsonString(metadataMap);
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
index ff1e3a9..c6d22e7 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
@@ -66,6 +66,15 @@ public interface TokenStateService extends Service {
void addToken(String tokenId, long issueTime, long expiration, long
maxLifetimeDuration);
/**
+ * @param tokenId
+ * The token unique identifier.
+ * @return The time the token was issued.
+ * @throws UnknownTokenException
+ * if token is not found.
+ */
+ long getTokenIssueTime(String tokenId) throws UnknownTokenException;
+
+ /**
* Checks if the token is expired.
*
* @param token The token.
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/TokenMAC.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/TokenMAC.java
new file mode 100644
index 0000000..2a632bc
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/TokenMAC.java
@@ -0,0 +1,66 @@
+/*
+ * 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.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+
+public class TokenMAC {
+ public static final String KNOX_TOKEN_HASH_KEY_ALIAS_NAME =
"knox.token.hash.key";
+
+ private final Mac mac;
+ private final Lock hashLock = new ReentrantLock(true);
+
+ public TokenMAC(String algorithm, char[] knoxTokenHashKey) throws
ServiceLifecycleException {
+ try {
+ if (knoxTokenHashKey != null) {
+ final SecretKey key = new SecretKeySpec(new
String(knoxTokenHashKey).getBytes(StandardCharsets.UTF_8), algorithm);
+ mac = Mac.getInstance(algorithm);
+ mac.init(key);
+ } else {
+ throw new ServiceLifecycleException("Missing " +
KNOX_TOKEN_HASH_KEY_ALIAS_NAME + " alias from Gateway's credential store");
+ }
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new ServiceLifecycleException("Error while initiating Knox Token
MAC: " + e, e);
+ }
+ }
+
+ public String hash(String tokenId, long issueTime, String userName, String
toBeHashed) {
+ hashLock.lock();
+ try {
+ mac.update(getSalt(tokenId, issueTime, userName));
+ return new
String(mac.doFinal(toBeHashed.getBytes(StandardCharsets.UTF_8)),
StandardCharsets.UTF_8);
+ } finally {
+ hashLock.unlock();
+ }
+ }
+
+ private byte[] getSalt(String tokenId, long issueTime, String userName) {
+ return (tokenId + issueTime + userName).getBytes(StandardCharsets.UTF_8);
+ }
+
+}
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 ab8df65..c65fb66 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
@@ -807,6 +807,11 @@ public class GatewayTestConfig extends Configuration
implements GatewayConfig {
}
@Override
+ public String getKnoxTokenHashAlgorithm() {
+ return null;
+ }
+
+ @Override
public Set<String> getHiddenTopologiesOnHomepage() {
return Collections.emptySet();
}