This is an automated email from the ASF dual-hosted git repository.
pzampino 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 db8cc23 KNOX-2067 - KnoxToken service support for renewal and
revocation
db8cc23 is described below
commit db8cc2347070ce5956ad6e881224c285f825acb6
Author: pzampino <[email protected]>
AuthorDate: Thu Oct 24 11:52:06 2019 -0400
KNOX-2067 - KnoxToken service support for renewal and revocation
---
build.xml | 4 +-
.../federation/jwt/filter/AbstractJWTFilter.java | 19 +-
.../jwt/filter/AccessTokenFederationFilter.java | 26 +-
.../federation/jwt/filter/JWTFederationFilter.java | 2 +-
.../org/apache/knox/gateway/GatewayMessages.java | 9 +
.../gateway/services/DefaultGatewayServices.java | 7 +-
.../token/impl/AliasBasedTokenStateService.java | 140 ++++++++
.../token/impl/DefaultTokenStateService.java | 248 +++++++++++++
.../services/AbstractGatewayServicesTest.java | 1 +
.../impl/AliasBasedTokenStateServiceTest.java | 146 ++++++++
.../token/impl/DefaultTokenStateServiceTest.java | 158 +++++++++
gateway-service-knoxtoken/pom.xml | 14 +
.../gateway/service/knoxtoken/TokenResource.java | 118 ++++++-
.../service/knoxtoken/TokenServiceMessages.java | 4 +
.../knoxtoken/TokenServiceResourceTest.java | 382 +++++++++++++++++++++
.../apache/knox/gateway/services/ServiceType.java | 1 +
.../services/security/token/TokenStateService.java | 123 +++++++
pom.xml | 6 +
18 files changed, 1389 insertions(+), 19 deletions(-)
diff --git a/build.xml b/build.xml
index e69c8ef..9db6a17 100644
--- a/build.xml
+++ b/build.xml
@@ -24,8 +24,8 @@ Release build file for the Apache Knox Gateway
<property name="gateway-name" value="Apache Knox"/>
<property name="gateway-project" value="knox"/>
<property name="gateway-artifact" value="knox"/>
- <property name="knoxshell-artifact" value="knoxshell"/>
- <property name="gateway-version" value="1.4.0-SNAPSHOT"/>
+<property name="knoxshell-artifact" value="knoxshell"/>
+<property name="gateway-version" value="1.4.0-SNAPSHOT"/>
<property name="release-manager" value="kminder"/>
<property name="gateway-home" value="${gateway-artifact}-${gateway-version}"/>
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 58a34ed..6e92241 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
@@ -56,6 +56,7 @@ import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import com.nimbusds.jose.JWSHeader;
@@ -87,6 +88,8 @@ public abstract class AbstractJWTFilter implements Filter {
private String expectedIssuer;
private String expectedSigAlg;
+ private TokenStateService tokenStateService;
+
@Override
public abstract void doFilter(ServletRequest request, ServletResponse
response, FilterChain chain)
throws IOException, ServletException;
@@ -105,6 +108,9 @@ public abstract class AbstractJWTFilter implements Filter {
GatewayServices services = (GatewayServices)
context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
if (services != null) {
authority = services.getService(ServiceType.TOKEN_SERVICE);
+ if
(Boolean.valueOf(filterConfig.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED)))
{
+ tokenStateService =
services.getService(ServiceType.TOKEN_STATE_SERVICE);
+ }
}
}
}
@@ -136,10 +142,15 @@ public abstract class AbstractJWTFilter implements Filter
{
}
protected boolean tokenIsStillValid(JWT jwtToken) {
- // if there is no expiration date then the lifecycle is tied entirely to
- // the cookie validity - otherwise ensure that the current time is before
- // the designated expiration time
- Date expires = jwtToken.getExpiresDate();
+ Date expires;
+ if (tokenStateService != null) {
+ expires = new
Date(tokenStateService.getTokenExpiration(jwtToken.toString()));
+ } else {
+ // if there is no expiration date then the lifecycle is tied entirely to
+ // the cookie validity - otherwise ensure that the current time is before
+ // the designated expiration time
+ expires = jwtToken.getExpiresDate();
+ }
return expires == null || new Date().before(expires);
}
diff --git
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
index cf8d530..1b82fa0 100644
---
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
+++
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/AccessTokenFederationFilter.java
@@ -24,6 +24,7 @@ import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import javax.security.auth.Subject;
@@ -50,10 +51,16 @@ public class AccessTokenFederationFilter implements Filter {
private JWTokenAuthority authority;
+ private TokenStateService tokenStateService;
+
@Override
public void init( FilterConfig filterConfig ) throws ServletException {
GatewayServices services = (GatewayServices)
filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
authority = services.getService(ServiceType.TOKEN_SERVICE);
+
+ if
(Boolean.valueOf(filterConfig.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED)))
{
+ tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+ }
}
@Override
@@ -81,33 +88,32 @@ public class AccessTokenFederationFilter implements Filter {
log.unableToVerifyToken(e);
}
if (verified) {
- long expires = Long.parseLong(token.getExpires());
- if (expires > System.currentTimeMillis()) {
+ if (!isExpired(token)) {
if (((HttpServletRequest)
request).getRequestURL().indexOf(token.getAudience().toLowerCase(Locale.ROOT))
!= -1) {
Subject subject = createSubjectFromToken(token);
continueWithEstablishedSecurityContext(subject,
(HttpServletRequest)request, (HttpServletResponse)response, chain);
- }
- else {
+ } else {
log.failedToValidateAudience();
sendUnauthorized(response);
}
- }
- else {
+ } else {
log.tokenHasExpired();
sendUnauthorized(response);
}
- }
- else {
+ } else {
log.failedToVerifyTokenSignature();
sendUnauthorized(response);
}
- }
- else {
+ } else {
log.missingBearerToken();
sendUnauthorized(response);
}
}
+ private boolean isExpired(JWTToken token) {
+ return (tokenStateService != null) ?
tokenStateService.isExpired(token.toString()) :
(Long.parseLong(token.getExpires()) <= System.currentTimeMillis());
+ }
+
private void sendUnauthorized(ServletResponse response) throws IOException {
((HttpServletResponse)
response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
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 1f40d57..8d49f7f 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
@@ -43,7 +43,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
@Override
public void init( FilterConfig filterConfig ) throws ServletException {
- super.init(filterConfig);
+ super.init(filterConfig);
// expected audiences or null
String expectedAudiences =
filterConfig.getInitParameter(KNOX_TOKEN_AUDIENCES);
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index aa019da..0656842 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -653,6 +653,15 @@ public interface GatewayMessages {
@Message(level = MessageLevel.ERROR, text = "Failed to remove credential:
{1}")
void failedToRemoveCredential(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+ @Message(level = MessageLevel.ERROR, text = "Failed to save token state:
{0}")
+ void failedToSaveTokenState(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "Error accessing token state:
{0}")
+ void errorAccessingTokenState(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "Failed to update token
expiration: {0}")
+ void failedToUpdateTokenExpiration(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
@Message(level = MessageLevel.INFO, text = "Starting service: {0}")
void startingService(String serviceTypeName);
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 7ebc80e..1a638b5 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -29,6 +29,7 @@ import
org.apache.knox.gateway.services.registry.impl.DefaultServiceDefinitionRe
import org.apache.knox.gateway.services.metrics.impl.DefaultMetricsService;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.impl.RemoteAliasService;
+import org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService;
import
org.apache.knox.gateway.services.topology.impl.DefaultClusterConfigurationMonitorService;
import org.apache.knox.gateway.services.topology.impl.DefaultTopologyService;
import org.apache.knox.gateway.services.hostmap.impl.DefaultHostMapperService;
@@ -112,6 +113,11 @@ public class DefaultGatewayServices extends
AbstractGatewayServices {
// prolly should not allow the token service to be looked up?
addService(ServiceType.TOKEN_SERVICE, ts);
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(alias);
+ tss.init(config, options);
+ addService(ServiceType.TOKEN_STATE_SERVICE, tss);
+
DefaultServiceRegistryService sr = new DefaultServiceRegistryService();
sr.setCryptoService( crypto );
sr.init( config, options );
@@ -125,7 +131,6 @@ public class DefaultGatewayServices extends
AbstractGatewayServices {
sis.init( config, options );
addService(ServiceType.SERVER_INFO_SERVICE, sis );
-
DefaultClusterConfigurationMonitorService ccs = new
DefaultClusterConfigurationMonitorService();
ccs.setAliasService(alias);
ccs.init(config, options);
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
new file mode 100644
index 0000000..4da9ad6
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java
@@ -0,0 +1,140 @@
+/*
+ * 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 org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+
+import java.util.Map;
+
+/**
+ * A TokenStateService implementation based on the AliasService.
+ */
+public class AliasBasedTokenStateService extends DefaultTokenStateService {
+
+ private static final GatewayMessages LOG = MessagesFactory.get(
GatewayMessages.class);
+
+ private AliasService aliasService;
+
+ public void setAliasService(AliasService aliasService) {
+ this.aliasService = aliasService;
+ }
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws
ServiceLifecycleException {
+ super.init(config, options);
+ if (aliasService == null) {
+ throw new ServiceLifecycleException("The required AliasService reference
has not been set.");
+ }
+ }
+
+ @Override
+ public void addToken(final String token, final long issueTime, final long
expiration) {
+ isValidIdentifier(token);
+
+ try {
+ aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, token,
String.valueOf(expiration));
+ setMaxLifetime(token, issueTime);
+ } catch (AliasServiceException e) {
+ LOG.failedToSaveTokenState(e);
+ }
+ }
+
+ @Override
+ protected void setMaxLifetime(final String token, final long issueTime) {
+ try {
+ aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME,
+ token + "--max",
+ String.valueOf(issueTime +
getMaxLifetimeInterval()));
+ } catch (AliasServiceException e) {
+ LOG.failedToSaveTokenState(e);
+ }
+ }
+
+ @Override
+ protected long getMaxLifetime(String token) {
+ long result = 0;
+ try {
+ char[] maxLifetimeStr =
+
aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, token
+ "--max");
+ if (maxLifetimeStr != null) {
+ result = Long.parseLong(new String(maxLifetimeStr));
+ }
+ } catch (AliasServiceException e) {
+ LOG.errorAccessingTokenState(e);
+ }
+ return result;
+ }
+
+ @Override
+ public long getTokenExpiration(String token) {
+ long expiration = 0;
+
+ validateToken(token);
+
+ try {
+ char[] expStr =
aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME,
token);
+ if (expStr != null) {
+ expiration = Long.parseLong(new String(expStr));
+ }
+ } catch (Exception e) {
+ LOG.errorAccessingTokenState(e);
+ }
+
+ return expiration;
+ }
+
+ @Override
+ public void revokeToken(String token) {
+ // Record the revocation by setting the expiration to -1
+ updateExpiration(token, -1);
+ }
+
+ @Override
+ protected boolean isRevoked(String token) {
+ return (getTokenExpiration(token) < 0);
+ }
+
+ @Override
+ protected boolean isUnknown(String token) {
+ boolean isUnknown = false;
+ try {
+ isUnknown =
(aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME,
token) == null);
+ } catch (AliasServiceException e) {
+ LOG.errorAccessingTokenState(e);
+ }
+ return isUnknown;
+ }
+
+ @Override
+ protected void updateExpiration(String token, long expiration) {
+ if (isUnknown(token)) {
+ throw new IllegalArgumentException("Unknown token.");
+ }
+
+ try {
+ aliasService.removeAliasForCluster(AliasService.NO_CLUSTER_NAME, token);
+ aliasService.addAliasForCluster(AliasService.NO_CLUSTER_NAME, token,
String.valueOf(expiration));
+ } catch (AliasServiceException e) {
+ LOG.failedToUpdateTokenExpiration(e);
+ }
+ }
+}
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
new file mode 100644
index 0000000..6493450
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
@@ -0,0 +1,248 @@
+/*
+ * 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 org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * In-Memory authentication token state management implementation.
+ */
+public class DefaultTokenStateService implements TokenStateService {
+
+ protected static final long DEFAULT_RENEWAL_INTERVAL = 24 * 60 * 60 * 1000;
// 24 hours
+
+ protected static final int MAX_RENEWALS = 7;
+
+ protected static final long DEFAULT_MAX_LIFETIME = MAX_RENEWALS *
DEFAULT_RENEWAL_INTERVAL; // 7 days
+
+ private final Map<String, Long> tokenExpirations = new HashMap<>();
+
+ private final Set<String> revokedTokens = new HashSet<>();
+
+ private final Map<String, Long> maxTokenLifetimes = new HashMap<>();
+
+ private long maxLifetimeInterval = DEFAULT_MAX_LIFETIME;
+
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws
ServiceLifecycleException {
+// maxLifetimeInterval = ??; // TODO: PJZ: Honor gateway configuration for
this value, if specified ?
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void addToken(final JWTToken token, final long issueTime) {
+ if (token == null) {
+ throw new IllegalArgumentException("Token data cannot be null.");
+ }
+ addToken(token.getPayload(), issueTime, token.getExpiresDate().getTime());
+ }
+
+ @Override
+ public void addToken(final String token, final long issueTime, final long
expiration) {
+ if (!isValidIdentifier(token)) {
+ throw new IllegalArgumentException("Token data cannot be null.");
+ }
+ synchronized (tokenExpirations) {
+ tokenExpirations.put(token, expiration);
+ }
+ setMaxLifetime(token, issueTime);
+ }
+
+ @Override
+ public long getTokenExpiration(String token) {
+ long expiration;
+
+ validateToken(token);
+
+ synchronized (tokenExpirations) {
+ expiration = tokenExpirations.get(token);
+ }
+
+ return expiration;
+ }
+
+ @Override
+ public long renewToken(final JWTToken token) {
+ return renewToken(token, DEFAULT_RENEWAL_INTERVAL);
+ }
+
+ @Override
+ public long renewToken(final JWTToken token, final Long renewInterval) {
+ if (token == null) {
+ throw new IllegalArgumentException("Token data cannot be null.");
+ }
+ return renewToken(token.getPayload(), renewInterval);
+ }
+
+ @Override
+ public long renewToken(final String token) { // Should return new expiration?
+ return renewToken(token, DEFAULT_RENEWAL_INTERVAL);
+ }
+
+ @Override
+ public long renewToken(final String token, final Long renewInterval) { //
Should return new expiration?
+ long expiration;
+
+ validateToken(token, true);
+
+ // Make sure the maximum lifetime has not been (and will not be) exceeded
+ if (hasRemainingRenewals(token, (renewInterval != null ? renewInterval :
DEFAULT_RENEWAL_INTERVAL))) {
+ expiration = System.currentTimeMillis() + (renewInterval != null ?
renewInterval : DEFAULT_RENEWAL_INTERVAL);
+ updateExpiration(token, expiration);
+ } else {
+ throw new IllegalArgumentException("The renewal limit for the token has
been exceeded");
+ }
+
+ return expiration;
+ }
+
+ @Override
+ public void revokeToken(final JWTToken token) {
+ if (token == null) {
+ throw new IllegalArgumentException("Token data cannot be null.");
+ }
+
+ revokeToken(token.getPayload());
+ }
+
+ @Override
+ public void revokeToken(final String token) {
+ validateToken(token);
+ revokedTokens.add(token);
+ }
+
+ @Override
+ public boolean isExpired(final JWTToken token) {
+ return isExpired(token.getPayload());
+ }
+
+ @Override
+ public boolean isExpired(final String token) {
+ boolean isExpired;
+
+ isExpired = isRevoked(token); // Check if it has been revoked first
+ if (!isExpired) {
+ // If it has not been revoked, check its expiration
+ isExpired = (getTokenExpiration(token) <= System.currentTimeMillis());
+ }
+
+ return isExpired;
+ }
+
+ protected void setMaxLifetime(final String token, final long issueTime) {
+ synchronized (maxTokenLifetimes) {
+ maxTokenLifetimes.put(token, issueTime + maxLifetimeInterval);
+ }
+ }
+
+ /**
+ * @param token
+ * @return false, if the service has previously stored the specified token;
Otherwise, true.
+ */
+ protected boolean isUnknown(final String token) {
+ boolean isUnknown;
+
+ synchronized (tokenExpirations) {
+ isUnknown = !(tokenExpirations.containsKey(token));
+ }
+
+ return isUnknown;
+ }
+
+ protected void updateExpiration(final String token, long expiration) {
+ synchronized (tokenExpirations) {
+ tokenExpirations.replace(token, expiration);
+ }
+ }
+
+ protected boolean hasRemainingRenewals(final String token, final Long
renewInterval) {
+ // Is the current time + 30-second buffer + the renewal interval is less
than the max lifetime for the token?
+ return ((System.currentTimeMillis() + 30000 + renewInterval) <
getMaxLifetime(token));
+ }
+
+ protected long getMaxLifetime(final String token) {
+ long result;
+ synchronized (maxTokenLifetimes) {
+ result = maxTokenLifetimes.getOrDefault(token, 0L);
+ }
+ return result;
+ }
+
+ protected boolean isRevoked(final String token) {
+ return revokedTokens.contains(token);
+ }
+
+ protected long getMaxLifetimeInterval() {
+ return maxLifetimeInterval;
+ }
+
+ protected boolean isValidIdentifier(final String token) {
+ return token != null && !token.isEmpty();
+ }
+
+ /**
+ * Validate the specified token identifier.
+ *
+ * @param token The token identifier to validate.
+ *
+ * @throws IllegalArgumentException if the specified token in invalid.
+ */
+ protected void validateToken(final String token) throws
IllegalArgumentException {
+ validateToken(token, false);
+ }
+
+ /**
+ * Validate the specified token identifier.
+ *
+ * @param token The token identifier to validate.
+ * @param includeRevocation true, if the revocation status of the specified
token should be considered in the validation.
+ *
+ * @throws IllegalArgumentException if the specified token in invalid.
+ */
+ protected void validateToken(final String token, final boolean
includeRevocation) throws IllegalArgumentException {
+ if (!isValidIdentifier(token)) {
+ throw new IllegalArgumentException("Token data cannot be null.");
+ }
+
+ // First, make sure the token is one we know about
+ if (isUnknown(token)) {
+ throw new IllegalArgumentException("Unknown token");
+ }
+
+ // Then, make sure it has not been revoked
+ if (includeRevocation && isRevoked(token)) {
+ throw new IllegalArgumentException("The specified token has been
revoked");
+ }
+ }
+
+}
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
index e6c2ef1..1f0a352 100644
---
a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
@@ -52,6 +52,7 @@ public class AbstractGatewayServicesTest {
ServiceType.KEYSTORE_SERVICE,
ServiceType.ALIAS_SERVICE,
ServiceType.SSL_SERVICE,
+ ServiceType.TOKEN_STATE_SERVICE,
ServiceType.TOKEN_SERVICE,
ServiceType.SERVER_INFO_SERVICE,
ServiceType.REMOTE_REGISTRY_CLIENT_SERVICE,
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
new file mode 100644
index 0000000..d982186
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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 org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+
+import java.security.cert.Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTest {
+
+ @Override
+ protected TokenStateService createTokenStateService() {
+ AliasBasedTokenStateService tss = new AliasBasedTokenStateService();
+ tss.setAliasService(new TestAliasService());
+ initTokenStateService(tss);
+ return tss;
+ }
+
+ /**
+ * A dumbed-down AliasService implementation for testing purposes only.
+ */
+ private static final class TestAliasService implements AliasService {
+
+ private final Map<String, Map<String, String>> clusterAliases= new
HashMap<>();
+
+
+ @Override
+ public List<String> getAliasesForCluster(String clusterName) throws
AliasServiceException {
+ List<String> aliases = new ArrayList<>();
+
+ if (clusterAliases.containsKey(clusterName)) {
+ aliases.addAll(clusterAliases.get(clusterName).keySet());
+ }
+ return aliases;
+ }
+
+ @Override
+ public void addAliasForCluster(String clusterName, String alias, String
value) throws AliasServiceException {
+ Map<String, String> aliases = null;
+ if (clusterAliases.containsKey(clusterName)) {
+ aliases = clusterAliases.get(clusterName);
+ } else {
+ aliases = new HashMap<>();
+ clusterAliases.put(clusterName, aliases);
+ }
+ aliases.put(alias, value);
+ }
+
+ @Override
+ public void removeAliasForCluster(String clusterName, String alias) throws
AliasServiceException {
+ if (clusterAliases.containsKey(clusterName)) {
+ clusterAliases.get(clusterName).remove(alias);
+ }
+ }
+
+ @Override
+ public char[] getPasswordFromAliasForCluster(String clusterName, String
alias) throws AliasServiceException {
+ char[] value = null;
+ if (clusterAliases.containsKey(clusterName)) {
+ String valString = clusterAliases.get(clusterName).get(alias);
+ if (valString != null) {
+ value = valString.toCharArray();
+ }
+ }
+ return value;
+ }
+
+ @Override
+ public char[] getPasswordFromAliasForCluster(String clusterName, String
alias, boolean generate) throws AliasServiceException {
+ return new char[0];
+ }
+
+ @Override
+ public void generateAliasForCluster(String clusterName, String alias)
throws AliasServiceException {
+ }
+
+ @Override
+ public char[] getPasswordFromAliasForGateway(String alias) throws
AliasServiceException {
+ return getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME,
alias);
+ }
+
+ @Override
+ public char[] getGatewayIdentityPassphrase() throws AliasServiceException {
+ return new char[0];
+ }
+
+ @Override
+ public char[] getGatewayIdentityKeystorePassword() throws
AliasServiceException {
+ return new char[0];
+ }
+
+ @Override
+ public char[] getSigningKeyPassphrase() throws AliasServiceException {
+ return new char[0];
+ }
+
+ @Override
+ public char[] getSigningKeystorePassword() throws AliasServiceException {
+ return new char[0];
+ }
+
+ @Override
+ public void generateAliasForGateway(String alias) throws
AliasServiceException {
+ }
+
+ @Override
+ public Certificate getCertificateForGateway(String alias) throws
AliasServiceException {
+ return null;
+ }
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws
ServiceLifecycleException {
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..cb909d8
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class DefaultTokenStateServiceTest {
+
+ @Test
+ public void testGetExpiration() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+ final TokenStateService tss = createTokenStateService();
+
+ tss.addToken(token, System.currentTimeMillis());
+ long expiration = tss.getTokenExpiration(token.getPayload());
+ assertEquals(token.getExpiresDate().getTime(), expiration);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetExpiration_NullToken() {
+ // Expecting an IllegalArgumentException because the token is null
+ createTokenStateService().getTokenExpiration(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetExpiration_EmptyToken() {
+ // Expecting an IllegalArgumentException because the token is empty
+ createTokenStateService().getTokenExpiration("");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetExpiration_InvalidToken() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+
+ // Expecting an IllegalArgumentException because the token is not known to
the TokenStateService
+ createTokenStateService().getTokenExpiration(token.getPayload());
+ }
+
+ @Test
+ public void testGetExpiration_AfterRenewal() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+ final TokenStateService tss = createTokenStateService();
+
+ tss.addToken(token, System.currentTimeMillis());
+ long expiration = tss.getTokenExpiration(token.getPayload());
+ assertEquals(token.getExpiresDate().getTime(), expiration);
+
+ long newExpiration = tss.renewToken(token);
+ assertTrue(newExpiration > token.getExpiresDate().getTime());
+ assertTrue(tss.getTokenExpiration(token.getPayload()) >
token.getExpiresDate().getTime());
+ }
+
+ @Test
+ public void testIsExpired_Negative() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+ final TokenStateService tss = createTokenStateService();
+
+ tss.addToken(token, System.currentTimeMillis());
+ assertFalse(tss.isExpired(token));
+ }
+
+ @Test
+ public void testIsExpired_Positive() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() - 60000);
+ final TokenStateService tss = createTokenStateService();
+
+ tss.addToken(token, System.currentTimeMillis());
+ assertTrue(tss.isExpired(token));
+ }
+
+
+ @Test
+ public void testIsExpired_Revoked() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() + 60000);
+ final TokenStateService tss = createTokenStateService();
+
+ tss.addToken(token, System.currentTimeMillis());
+ assertFalse("Expected the token to be valid.", tss.isExpired(token));
+
+ tss.revokeToken(token);
+ assertTrue("Expected the token to have been marked as revoked.",
tss.isExpired(token));
+ }
+
+
+ @Test
+ public void testRenewal() {
+ final JWTToken token = createMockToken(System.currentTimeMillis() - 60000);
+ final TokenStateService tss = createTokenStateService();
+
+ // Add the expired token
+ tss.addToken(token, System.currentTimeMillis());
+ assertTrue("Expected the token to have expired.", tss.isExpired(token));
+
+ tss.renewToken(token);
+ assertFalse("Expected the token to have been renewed.",
tss.isExpired(token));
+ }
+
+
+ protected static JWTToken createMockToken(final long expiration) {
+ return createMockToken("ABCD1234", expiration);
+ }
+
+ protected static JWTToken createMockToken(final String payload, final long
expiration) {
+ JWTToken token = EasyMock.createNiceMock(JWTToken.class);
+ EasyMock.expect(token.getPayload()).andReturn(payload).anyTimes();
+ EasyMock.expect(token.getExpiresDate()).andReturn(new
Date(expiration)).anyTimes();
+ EasyMock.replay(token);
+ return token;
+ }
+
+ protected static GatewayConfig createMockGatewayConfig() {
+ GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+ EasyMock.replay(config);
+ return config;
+ }
+
+ protected void initTokenStateService(TokenStateService tss) {
+ try {
+ tss.init(createMockGatewayConfig(), Collections.emptyMap());
+ } catch (ServiceLifecycleException e) {
+ fail("Error creating TokenStateService: " + e.getMessage());
+ }
+ }
+
+ protected TokenStateService createTokenStateService() {
+ TokenStateService tss = new DefaultTokenStateService();
+ initTokenStateService(tss);
+ return tss;
+ }
+
+}
diff --git a/gateway-service-knoxtoken/pom.xml
b/gateway-service-knoxtoken/pom.xml
index 61c758d..5a4d2f1 100644
--- a/gateway-service-knoxtoken/pom.xml
+++ b/gateway-service-knoxtoken/pom.xml
@@ -60,6 +60,10 @@
<artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
+ <groupId>org.glassfish.hk2.external</groupId>
+ <artifactId>javax.inject</artifactId>
+ </dependency>
+ <dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
@@ -78,5 +82,15 @@
<artifactId>gateway-test-utils</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
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 0d2c0e9..fd01428 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
@@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.List;
import javax.annotation.PostConstruct;
+import javax.inject.Singleton;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
@@ -39,18 +40,21 @@ import javax.ws.rs.core.Response;
import org.apache.commons.codec.binary.Base64;
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.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.util.JsonUtils;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+@Singleton
@Path(TokenResource.RESOURCE_PATH)
public class TokenResource {
private static final String EXPIRES_IN = "expires_in";
@@ -66,8 +70,12 @@ public class TokenResource {
private static final String TOKEN_CLIENT_CERT_REQUIRED =
"knox.token.client.cert.required";
private static final String TOKEN_ALLOWED_PRINCIPALS =
"knox.token.allowed.principals";
private static final String TOKEN_SIG_ALG = "knox.token.sigalg";
+ private static final String TOKEN_EXP_RENEWAL_INTERVAL =
"knox.token.exp.renew-interval";
+ private static final String TOKEN_RENEWER_WHITELIST =
"knox.token.renewer.whitelist";
private static final long TOKEN_TTL_DEFAULT = 30000L;
static final String RESOURCE_PATH = "knoxtoken/api/v1/token";
+ static final String RENEW_PATH = "/renew";
+ static final String REVOKE_PATH = "/revoke";
private static final String TARGET_ENDPOINT_PULIC_CERT_PEM =
"knox.token.target.endpoint.cert.pem";
private static TokenServiceMessages log =
MessagesFactory.get(TokenServiceMessages.class);
private long tokenTTL = TOKEN_TTL_DEFAULT;
@@ -79,6 +87,13 @@ public class TokenResource {
private String signatureAlgorithm = "RS256";
private String endpointPublicCert;
+ // Optional token store service
+ private TokenStateService tokenStateService;
+
+ private Long renewInterval;
+
+ private List<String> allowedRenewers;
+
@Context
HttpServletRequest request;
@@ -138,6 +153,29 @@ public class TokenResource {
if (targetEndpointPublicCert != null) {
endpointPublicCert = targetEndpointPublicCert;
}
+
+ // If server-managed token expiration is configured, set the token store
service
+ if
(Boolean.valueOf(context.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED)))
{
+ GatewayServices services = (GatewayServices)
context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE);
+
+ String renewIntervalValue =
context.getInitParameter(TOKEN_EXP_RENEWAL_INTERVAL);
+ if (renewIntervalValue != null && !renewIntervalValue.isEmpty()) {
+ try {
+ renewInterval = Long.parseLong(renewIntervalValue);
+ } catch (NumberFormatException e) {
+ log.invalidConfigValue(TOKEN_EXP_RENEWAL_INTERVAL,
renewIntervalValue, e);
+ }
+ }
+
+ allowedRenewers = new ArrayList<>();
+ String renewerList = context.getInitParameter(TOKEN_RENEWER_WHITELIST);
+ if (renewerList != null && !renewerList.isEmpty()) {
+ for (String renewer : renewerList.split(",")) {
+ allowedRenewers.add(renewer.trim());
+ }
+ }
+ }
}
@GET
@@ -152,6 +190,80 @@ public class TokenResource {
return getAuthenticationToken();
}
+ @POST
+ @Path(RENEW_PATH)
+ @Produces({APPLICATION_JSON})
+ public Response renew(String token) {
+ Response resp;
+
+ long expiration = 0;
+ String error = "";
+
+ if (tokenStateService == null) {
+ error = "Token renewal support is not configured";
+ } else {
+ String renewer = SubjectUtils.getCurrentEffectivePrincipalName();
+ if (allowedRenewers.contains(renewer)) {
+ try {
+ // If renewal fails, it should be an exception
+ expiration = tokenStateService.renewToken(token, renewInterval);
+ } catch (Exception e) {
+ error = e.getMessage();
+ }
+ } else {
+ error = "Caller (" + renewer + ") not authorized to renew tokens.";
+ }
+ }
+
+ if(error.isEmpty()) {
+ resp = Response.status(Response.Status.OK)
+ .entity("{\n \"renewed\": \"true\",\n \"expires\": \""
+ expiration + "\"\n}\n")
+ .build();
+ } else {
+ resp = Response.status(Response.Status.BAD_REQUEST)
+ .entity("{\n \"renewed\": \"false\",\n \"error\": \"" +
error + "\"\n}\n")
+ .build();
+ }
+
+ return resp;
+ }
+
+ @POST
+ @Path(REVOKE_PATH)
+ @Produces({APPLICATION_JSON})
+ public Response revoke(String token) {
+ Response resp;
+
+ String error = "";
+
+ if (tokenStateService == null) {
+ error = "Token revocation support is not configured";
+ } else {
+ String renewer = SubjectUtils.getCurrentEffectivePrincipalName();
+ if (allowedRenewers.contains(renewer)) {
+ try {
+ tokenStateService.revokeToken(token);
+ } catch (IllegalArgumentException e) {
+ error = e.getMessage();
+ }
+ } else {
+ error = "Caller (" + renewer + ") not authorized to revoke tokens.";
+ }
+ }
+
+ if(error.isEmpty()) {
+ resp = Response.status(Response.Status.OK)
+ .entity("{\n \"revoked\": \"true\"\n}\n")
+ .build();
+ } else {
+ resp = Response.status(Response.Status.BAD_REQUEST)
+ .entity("{\n \"revoked\": \"false\",\n \"error\": \"" +
error + "\"\n}\n")
+ .build();
+ }
+
+ return resp;
+ }
+
private X509Certificate extractCertificate(HttpServletRequest req) {
X509Certificate[] certs = (X509Certificate[])
req.getAttribute("javax.servlet.request.X509Certificate");
if (null != certs && certs.length > 0) {
@@ -204,7 +316,6 @@ public class TokenResource {
if (token != null) {
String accessToken = token.toString();
-
HashMap<String, Object> map = new HashMap<>();
map.put(ACCESS_TOKEN, accessToken);
map.put(TOKEN_TYPE, BEARER);
@@ -221,6 +332,11 @@ public class TokenResource {
String jsonResponse = JsonUtils.renderAsJsonString(map);
+ // Optional token store service persistence
+ if (tokenStateService != null) {
+ tokenStateService.addToken(accessToken, System.currentTimeMillis(),
expires);
+ }
+
return Response.ok().entity(jsonResponse).build();
} else {
return Response.serverError().build();
diff --git
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
index f9d97ad..c0c2e52 100644
---
a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
+++
b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceMessages.java
@@ -66,4 +66,8 @@ public interface TokenServiceMessages {
@Message( level = MessageLevel.WARN, text = "Unable to acquire cert for
endpoint clients - assume trust will be provisioned separately: {0}.")
void unableToAcquireCertForEndpointClients(@StackTrace( level =
MessageLevel.DEBUG ) Exception e);
+
+ @Message( level = MessageLevel.ERROR, text = "The specified value for the
{0} configuration property is not valid: {1}")
+ void invalidConfigValue(String name, String value, @StackTrace( level =
MessageLevel.DEBUG ) Exception e);
+
}
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 e6c106e..e78ae1b 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
@@ -17,14 +17,19 @@
*/
package org.apache.knox.gateway.service.knoxtoken;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
+import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
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.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import org.easymock.EasyMock;
@@ -36,9 +41,11 @@ import javax.security.auth.Subject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
+import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Principal;
+import java.security.PrivilegedAction;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
@@ -49,8 +56,10 @@ import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -62,6 +71,11 @@ public class TokenServiceResourceTest {
private static RSAPublicKey publicKey;
private static RSAPrivateKey privateKey;
+ private enum TokenLifecycleOperation {
+ Renew,
+ Revoke
+ }
+
@BeforeClass
public static void setUpBeforeClass() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
@@ -610,6 +624,286 @@ public class TokenServiceResourceTest {
assertTrue((expiresDate.getTime() - now.getTime()) < 30000L);
}
+ @Test
+ public void testTokenRenewal_ServerManagedStateNotConfigured() throws
Exception {
+ Response renewalResponse = doTestTokenRenewal(null, null, null);
+ validateRenewalResponse(renewalResponse, 400, false, "Token renewal
support is not configured");
+ }
+
+ @Test
+ public void testTokenRenewal_Disabled() throws Exception {
+ Response renewalResponse = doTestTokenRenewal(false, null, null);
+ validateRenewalResponse(renewalResponse, 400, false, "Token renewal
support is not configured");
+ }
+
+ @Test
+ public void testTokenRenewal_Enabled_NoRenewersNoSubject() throws Exception {
+ Response renewalResponse = doTestTokenRenewal(true, null, null);
+ validateRenewalResponse(renewalResponse, 400, false, "Caller (null) not
authorized to renew tokens.");
+ }
+
+ @Test
+ public void testTokenRenewal_Enabled_NoRenewersWithSubject() throws
Exception {
+ final String caller = "yarn";
+ Response renewalResponse = doTestTokenRenewal(true, null,
createTestSubject(caller));
+ validateRenewalResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (" + caller + ") not authorized to renew
tokens.");
+ }
+
+ @Test
+ public void testTokenRenewal_Enabled_WithRenewersNoSubject() throws
Exception {
+ Response renewalResponse = doTestTokenRenewal(true, "larry, moe, curly ",
null);
+ validateRenewalResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (null) not authorized to renew tokens.");
+ }
+
+ @Test
+ public void testTokenRenewal_Enabled_WithRenewersWithInvalidSubject() throws
Exception {
+ final String caller = "shemp";
+ Response renewalResponse = doTestTokenRenewal(true, "larry, moe, curly ",
createTestSubject(caller));
+ validateRenewalResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (" + caller + ") not authorized to renew
tokens.");
+ }
+
+ @Test
+ public void testTokenRenewal_Enabled_WithRenewersWithValidSubject() throws
Exception {
+ final String caller = "shemp";
+ Response renewalResponse =
+ doTestTokenRenewal(true, ("larry, moe, curly ," +
caller), createTestSubject(caller));
+ validateSuccessfulRenewalResponse(renewalResponse);
+ }
+
+ @Test
+ public void testTokenRevocation_ServerManagedStateNotConfigured() throws
Exception {
+ Response renewalResponse = doTestTokenRevocation(null, null, null);
+ validateRevocationResponse(renewalResponse,
+ 400,
+ false,
+ "Token revocation support is not configured");
+ }
+
+ @Test
+ public void testTokenRevocation_Disabled() throws Exception {
+ Response renewalResponse = doTestTokenRevocation(false, null, null);
+ validateRevocationResponse(renewalResponse,
+ 400,
+ false,
+ "Token revocation support is not configured");
+ }
+
+ @Test
+ public void testTokenRevocation_Enabled_NoRenewersNoSubject() throws
Exception {
+ Response renewalResponse = doTestTokenRevocation(true, null, null);
+ validateRevocationResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (null) not authorized to revoke
tokens.");
+ }
+
+ @Test
+ public void testTokenRevocation_Enabled_NoRenewersWithSubject() throws
Exception {
+ final String caller = "yarn";
+ Response renewalResponse = doTestTokenRevocation(true, null,
createTestSubject(caller));
+ validateRevocationResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (" + caller + ") not authorized to
revoke tokens.");
+ }
+
+ @Test
+ public void testTokenRevocation_Enabled_WithRenewersNoSubject() throws
Exception {
+ Response renewalResponse = doTestTokenRevocation(true, "larry, moe, curly
", null);
+ validateRevocationResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (null) not authorized to revoke
tokens.");
+ }
+
+ @Test
+ public void testTokenRevocation_Enabled_WithRenewersWithInvalidSubject()
throws Exception {
+ final String caller = "shemp";
+ Response renewalResponse = doTestTokenRevocation(true, "larry, moe, curly
", createTestSubject(caller));
+ validateRevocationResponse(renewalResponse,
+ 400,
+ false,
+ "Caller (" + caller + ") not authorized to
revoke tokens.");
+ }
+
+ @Test
+ public void testTokenRevocation_Enabled_WithRenewersWithValidSubject()
throws Exception {
+ final String caller = "shemp";
+ Response renewalResponse =
+ doTestTokenRevocation(true, ("larry, moe, curly ," + caller),
createTestSubject(caller));
+ validateSuccessfulRevocationResponse(renewalResponse);
+ }
+
+ /**
+ *
+ * @param isTokenStateServerManaged true, if server-side token state
management should be enabled; Otherwise, false or null.
+ * @param renewers A comma-delimited list of permitted renewer user names
+ * @param caller The user name making the request
+ *
+ * @return The Response from the token renewal request
+ *
+ * @throws Exception
+ */
+ private Response doTestTokenRenewal(final Boolean isTokenStateServerManaged,
+ final String renewers,
+ final Subject caller) throws Exception {
+ return doTestTokenLifecyle(TokenLifecycleOperation.Renew,
isTokenStateServerManaged, renewers, caller);
+ }
+
+ /**
+ *
+ * @param isTokenStateServerManaged true, if server-side token state
management should be enabled; Otherwise, false or null.
+ * @param renewers A comma-delimited list of permitted renewer user names
+ * @param caller The user name making the request
+ *
+ * @return The Response from the token revocation request
+ *
+ * @throws Exception
+ */
+ private Response doTestTokenRevocation(final Boolean
isTokenStateServerManaged,
+ final String renewers,
+ final Subject caller) throws
Exception {
+ return doTestTokenLifecyle(TokenLifecycleOperation.Revoke,
isTokenStateServerManaged, renewers, caller);
+ }
+
+ /**
+ * @param operation A TokenLifecycleOperation
+ * @param isTokenStateServerManaged true, if server-side token state
management should be enabled; Otherwise, false or null.
+ * @param renewers A comma-delimited list of permitted renewer user names
+ * @param caller The user name making the request
+ *
+ * @return The Response from the token revocation request
+ *
+ * @throws Exception
+ */
+ private Response doTestTokenLifecyle(final TokenLifecycleOperation operation,
+ final Boolean isTokenStateServerManaged,
+ final String renewers,
+ final Subject caller) throws Exception {
+ ServletContext context = EasyMock.createNiceMock(ServletContext.class);
+
EasyMock.expect(context.getInitParameter("knox.token.audiences")).andReturn("recipient1,recipient2");
+
EasyMock.expect(context.getInitParameter("knox.token.ttl")).andReturn(String.valueOf(Long.MAX_VALUE));
+
EasyMock.expect(context.getInitParameter("knox.token.target.url")).andReturn(null);
+
EasyMock.expect(context.getInitParameter("knox.token.client.data")).andReturn(null);
+ if (isTokenStateServerManaged != null) {
+
EasyMock.expect(context.getInitParameter("knox.token.exp.server-managed"))
+ .andReturn(String.valueOf(isTokenStateServerManaged));
+ }
+
EasyMock.expect(context.getInitParameter("knox.token.renewer.whitelist")).andReturn(renewers);
+
+ HttpServletRequest request =
EasyMock.createNiceMock(HttpServletRequest.class);
+ EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes();
+ Principal principal = EasyMock.createNiceMock(Principal.class);
+ EasyMock.expect(principal.getName()).andReturn("alice").anyTimes();
+
EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes();
+
+ GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
+
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes();
+
+ JWTokenAuthority authority = new TestJWTokenAuthority(publicKey,
privateKey);
+
EasyMock.expect(services.getService(ServiceType.TOKEN_SERVICE)).andReturn(authority).anyTimes();
+
+ TokenStateService tss = new TestTokenStateService();
+
EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
+
+ EasyMock.replay(principal, services, context, request);
+
+ TokenResource tr = new TokenResource();
+ tr.request = request;
+ tr.context = context;
+ tr.init();
+
+ // Request a token
+ Response retResponse = tr.doGet();
+ assertEquals(200, retResponse.getStatus());
+
+ // Parse the response
+ String retString = retResponse.getEntity().toString();
+ String accessToken = getTagValue(retString, "access_token");
+ assertNotNull(accessToken);
+
+ Response response;
+ switch (operation) {
+ case Renew:
+ response = requestTokenRenewal(tr, accessToken, caller);
+ break;
+ case Revoke:
+ response = requestTokenRevocation(tr, accessToken, caller);
+ break;
+ default:
+ throw new Exception("Invalid operation: " + operation);
+ }
+ return response;
+ }
+
+ private static Response requestTokenRenewal(final TokenResource tr, final
String tokenData, final Subject caller) {
+ Response response;
+ if (caller != null) {
+ response = Subject.doAs(caller, (PrivilegedAction<Response>) () ->
tr.renew(tokenData));
+ } else {
+ response = tr.renew(tokenData);
+ }
+ return response;
+ }
+
+ private static Response requestTokenRevocation(final TokenResource tr, final
String tokenData, final Subject caller) {
+ Response response;
+ if (caller != null) {
+ response = Subject.doAs(caller, (PrivilegedAction<Response>) () ->
tr.revoke(tokenData));
+ } else {
+ response = tr.revoke(tokenData);
+ }
+ return response;
+ }
+
+ private static void validateSuccessfulRenewalResponse(final Response
response) throws IOException {
+ validateRenewalResponse(response, 200, true, null);
+ }
+
+ private static void validateRenewalResponse(final Response response,
+ final int
expectedStatusCode,
+ final boolean expectedResult,
+ final String expectedMessage)
throws IOException {
+ assertEquals(expectedStatusCode, response.getStatus());
+ assertTrue(response.hasEntity());
+ String responseContent = (String) response.getEntity();
+ assertNotNull(responseContent);
+ assertFalse(responseContent.isEmpty());
+ Map<String, String> json = parseJSONResponse(responseContent);
+ boolean result = Boolean.valueOf(json.get("renewed"));
+ assertEquals(expectedResult, result);
+ assertEquals(expectedMessage, json.get("error"));
+ }
+
+ private static void validateSuccessfulRevocationResponse(final Response
response) throws IOException {
+ validateRevocationResponse(response, 200, true, null);
+ }
+
+ private static void validateRevocationResponse(final Response response,
+ final int
expectedStatusCode,
+ final boolean expectedResult,
+ final String
expectedMessage) throws IOException {
+ assertEquals(expectedStatusCode, response.getStatus());
+ assertTrue(response.hasEntity());
+ String responseContent = (String) response.getEntity();
+ assertNotNull(responseContent);
+ assertFalse(responseContent.isEmpty());
+ Map<String, String> json = parseJSONResponse(responseContent);
+ boolean result = Boolean.valueOf(json.get("revoked"));
+ assertEquals(expectedResult, result);
+ assertEquals(expectedMessage, json.get("error"));
+ }
+
+
private String getTagValue(String token, String tagName) {
String searchString = tagName + "\":";
String value = token.substring(token.indexOf(searchString) +
searchString.length());
@@ -625,6 +919,94 @@ public class TokenServiceResourceTest {
}
}
+ /**
+ * Create a Subject for testing.
+ *
+ * @param username The user identifier
+ *
+ * @return A Subject
+ */
+ private Subject createTestSubject(final String username) {
+ Subject s = new Subject();
+
+ Set<Principal> principals = s.getPrincipals();
+ principals.add(new PrimaryPrincipal(username));
+
+ return s;
+ }
+
+ private static Map<String, String> parseJSONResponse(final String response)
throws IOException {
+ return (new ObjectMapper()).readValue(response, new
TypeReference<Map<String, String>>(){});
+ }
+
+
+ private static class TestTokenStateService implements TokenStateService {
+ @Override
+ public void addToken(JWTToken token, long issueTime) {
+ addToken(token.getPayload(), issueTime,
token.getExpiresDate().getTime());
+ }
+
+ @Override
+ public void addToken(String token, long issueTime, long expiration) {
+ }
+
+ @Override
+ public boolean isExpired(JWTToken token) {
+ return isExpired(token.getPayload());
+ }
+
+ @Override
+ public boolean isExpired(String token) {
+ return false;
+ }
+
+ @Override
+ public void revokeToken(JWTToken token) {
+ revokeToken(token.getPayload());
+ }
+
+ @Override
+ public void revokeToken(String token) {
+ }
+
+ @Override
+ public long renewToken(JWTToken token) {
+ return renewToken(token.getPayload());
+ }
+
+ @Override
+ public long renewToken(String token) {
+ return renewToken(token, 0L);
+ }
+
+ @Override
+ public long renewToken(JWTToken token, Long renewInterval) {
+ return renewToken(token.getPayload());
+ }
+
+ @Override
+ public long renewToken(String token, Long renewInterval) {
+ return 0;
+ }
+
+ @Override
+ public long getTokenExpiration(String token) {
+ return 0;
+ }
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws
ServiceLifecycleException {
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+ }
+
private static class TestJWTokenAuthority implements JWTokenAuthority {
private RSAPublicKey publicKey;
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
index 40d5433..736ecad 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
@@ -32,6 +32,7 @@ public enum ServiceType {
SERVICE_REGISTRY_SERVICE("ServiceRegistryService"),
SSL_SERVICE("SSLService"),
TOKEN_SERVICE("TokenService"),
+ TOKEN_STATE_SERVICE("TokenStateService"),
TOPOLOGY_SERVICE("TopologyService");
private final String serviceTypeName;
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
new file mode 100644
index 0000000..2ab5721
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java
@@ -0,0 +1,123 @@
+/*
+ * 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;
+
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+
+
+/**
+ * Service providing authentication token state management.
+ */
+public interface TokenStateService extends Service {
+
+ String CONFIG_SERVER_MANAGED = "knox.token.exp.server-managed";
+
+ /**
+ * Add state for the specified token.
+ *
+ * @param token The token.
+ * @param issueTime The time the token was issued.
+ */
+ void addToken(JWTToken token, long issueTime);
+
+ /**
+ * Add state for the specified token.
+ *
+ * @param token The token.
+ * @param issueTime The time the token was issued.
+ * @param expiration The token expiration time.
+ */
+ void addToken(String token, long issueTime, long expiration);
+
+ /**
+ *
+ * @param token The token.
+ *
+ * @return true, if the token has expired; Otherwise, false.
+ */
+ boolean isExpired(JWTToken token);
+
+ /**
+ *
+ * @param token The token.
+ *
+ * @return true, if the token has expired; Otherwise, false.
+ */
+ boolean isExpired(String token);
+
+ /**
+ * Disable any subsequent use of the specified token.
+ *
+ * @param token The token.
+ */
+ void revokeToken(JWTToken token);
+
+ /**
+ * Disable any subsequent use of the specified token.
+ *
+ * @param token The token.
+ */
+ void revokeToken(String token);
+
+ /**
+ * Extend the lifetime of the specified token by the default amount of time.
+ *
+ * @param token The token.
+ *
+ * @return The token's updated expiration time in milliseconds.
+ */
+ long renewToken(JWTToken token);
+
+ /**
+ * Extend the lifetime of the specified token by the specified amount of
time.
+ *
+ * @param token The token.
+ * @param renewInterval The amount of time that should be added to the
token's lifetime.
+ *
+ * @return The token's updated expiration time in milliseconds.
+ */
+ long renewToken(JWTToken token, Long renewInterval);
+
+ /**
+ * Extend the lifetime of the specified token by the default amount of time.
+ *
+ * @param token The token.
+ *
+ * @return The token's updated expiration time in milliseconds.
+ */
+ long renewToken(String token);
+
+ /**
+ * Extend the lifetime of the specified token by the specified amount of
time.
+ *
+ * @param token The token.
+ * @param renewInterval The amount of time that should be added to the
token's lifetime.
+ *
+ * @return The token's updated expiration time in milliseconds.
+ */
+ long renewToken(String token, Long renewInterval);
+
+ /**
+ *
+ * @param token The token.
+ *
+ * @return The token's expiration time in milliseconds.
+ */
+ long getTokenExpiration(String token);
+
+}
diff --git a/pom.xml b/pom.xml
index 9a4f89c..883c779 100644
--- a/pom.xml
+++ b/pom.xml
@@ -192,6 +192,7 @@
<jansi.version>1.18</jansi.version>
<javax.activation.version>1.2.0</javax.activation.version>
<javax.annotation-api.version>1.3.2</javax.annotation-api.version>
+ <javax.inject.version>2.2.0</javax.inject.version>
<javax.json.version>1.1.3</javax.json.version>
<javax.servlet-api.version>3.1.0</javax.servlet-api.version>
<javax.ws.rs-api.version>2.0</javax.ws.rs-api.version>
@@ -1176,6 +1177,11 @@
<version>${javax.annotation-api.version}</version>
</dependency>
<dependency>
+ <groupId>org.glassfish.hk2.external</groupId>
+ <artifactId>javax.inject</artifactId>
+ <version>${javax.inject.version}</version>
+ </dependency>
+ <dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet-api.version}</version>