You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by mo...@apache.org on 2020/02/29 01:40:32 UTC

[knox] branch master updated: KNOX-2212 - Token permissiveness (#274)

This is an automated email from the ASF dual-hosted git repository.

more 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 04ec47e  KNOX-2212 - Token permissiveness (#274)
04ec47e is described below

commit 04ec47e085d09326d8908d7b2c1ebecc28c9d8a9
Author: Sandeep Moré <mo...@gmail.com>
AuthorDate: Fri Feb 28 20:40:25 2020 -0500

    KNOX-2212 - Token permissiveness (#274)
---
 .../gateway/config/impl/GatewayConfigImpl.java     | 11 ++++
 .../token/impl/AliasBasedTokenStateService.java    | 13 ++++-
 .../token/impl/DefaultTokenStateService.java       | 40 +++++++++++++-
 .../token/impl/TokenStateServiceMessages.java      |  6 ++
 .../token/impl/DefaultTokenStateServiceTest.java   | 64 +++++++++++++++++++++-
 .../apache/knox/gateway/config/GatewayConfig.java  |  5 ++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  8 +++
 7 files changed, 141 insertions(+), 6 deletions(-)

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 117165a..5a4031a 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
@@ -249,8 +249,10 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
 
   private static final String KNOX_TOKEN_EVICTION_INTERVAL = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.eviction.interval";
   private static final String KNOX_TOKEN_EVICTION_GRACE_PERIOD = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.eviction.grace.period";
+  private static final String KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED = GATEWAY_CONFIG_FILE_PREFIX + ".knox.token.permissive.validation";
   private static final long KNOX_TOKEN_EVICTION_INTERVAL_DEFAULT = TimeUnit.MINUTES.toSeconds(5);
   private static final long KNOX_TOKEN_EVICTION_GRACE_PERIOD_DEFAULT = TimeUnit.MINUTES.toSeconds(5);
+  private static final boolean KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED_DEFAULT = false;
 
   private static final String KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES =  "knox.homepage.hidden.topologies";
   private static final Set<String> KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES_DEFAULT = new HashSet<>(Arrays.asList("admin", "manager", "knoxsso", "metadata"));
@@ -1141,4 +1143,13 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig {
     final Set<String> hiddenTopologies = new HashSet<>(getStringCollection(KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES));
     return hiddenTopologies == null || hiddenTopologies.isEmpty() ? KNOX_HOMEPAGE_HIDDEN_TOPOLOGIES_DEFAULT : hiddenTopologies;
   }
+
+  /**
+   * @return returns whether know token permissive failure is enabled
+   */
+  @Override
+  public boolean isKnoxTokenPermissiveValidationEnabled() {
+    return getBoolean(KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED,
+        KNOX_TOKEN_PERMISSIVE_VALIDATION_ENABLED_DEFAULT);
+  }
 }
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 9f6fa1a..ab900e5 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
@@ -92,9 +92,16 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService {
   @Override
   public long getTokenExpiration(final String token) throws UnknownTokenException {
     long expiration = 0;
-
-    validateToken(token);
-
+    try {
+      validateToken(token);
+    } catch (final UnknownTokenException e) {
+      /* if token permissiveness is enabled we check JWT token expiration when the token state is unknown */
+      if (permissiveValidationEnabled &&  getJWTTokenExpiration(token).isPresent()) {
+        return getJWTTokenExpiration(token).getAsLong();
+      } else {
+        throw e;
+      }
+    }
     try {
       char[] expStr = aliasService.getPasswordFromAliasForCluster(AliasService.NO_CLUSTER_NAME, token);
       if (expStr != null) {
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 13b728c..f0f5ca7 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
@@ -22,12 +22,16 @@ import org.apache.knox.gateway.services.ServiceLifecycleException;
 import org.apache.knox.gateway.services.security.token.TokenStateService;
 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 java.text.ParseException;
 import java.time.Instant;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalLong;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -54,6 +58,8 @@ public class DefaultTokenStateService implements TokenStateService {
   private long tokenEvictionInterval;
   /* grace period (in seconds) after which an expired token should be evicted */
   private long tokenEvictionGracePeriod;
+  /* should knox token fail permissively */
+  protected boolean permissiveValidationEnabled;
 
   private final ScheduledExecutorService evictionScheduler = Executors.newScheduledThreadPool(1);
 
@@ -62,6 +68,7 @@ public class DefaultTokenStateService implements TokenStateService {
   public void init(final GatewayConfig config, final Map<String, String> options) throws ServiceLifecycleException {
     tokenEvictionInterval = config.getKnoxTokenEvictionInterval();
     tokenEvictionGracePeriod = config.getKnoxTokenEvictionGracePeriod();
+    permissiveValidationEnabled = config.isKnoxTokenPermissiveValidationEnabled();
   }
 
   @Override
@@ -119,8 +126,17 @@ public class DefaultTokenStateService implements TokenStateService {
   public long getTokenExpiration(final String token) throws UnknownTokenException {
     long expiration;
 
-    validateToken(token);
 
+    try {
+      validateToken(token);
+    } catch (final UnknownTokenException e) {
+      /* if token permissiveness is enabled we check JWT token expiration when the token state is unknown */
+      if (permissiveValidationEnabled && getJWTTokenExpiration(token).isPresent()) {
+        return getJWTTokenExpiration(token).getAsLong();
+      } else {
+        throw e;
+      }
+    }
     synchronized (tokenExpirations) {
       expiration = tokenExpirations.get(token);
     }
@@ -310,4 +326,26 @@ public class DefaultTokenStateService implements TokenStateService {
     return tokenExpirations.keySet().stream().collect(Collectors.toList());
   }
 
+  /**
+   * A function that returns the JWT token expiration. This is only called when
+   * gateway.knox.token.permissive.validation property is set to true.
+   * @param token token to be verified and saved
+   */
+  protected OptionalLong getJWTTokenExpiration(final String token) {
+    JWT jwt;
+    try {
+      jwt = new JWTToken(token);
+    } catch (final ParseException e) {
+      log.errorParsingToken(e.toString());
+      return OptionalLong.empty();
+    }
+    final Date expires = jwt.getExpiresDate();
+    if (expires == null) {
+      log.jwtTokenExpiry(TokenUtils.getTokenDisplayText(token), "-1");
+      return OptionalLong.of(-1);
+    }
+    log.jwtTokenExpiry(TokenUtils.getTokenDisplayText(token), expires.toString());
+    return OptionalLong.of(expires.getTime());
+  }
+
 }
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 c7702d7..318cced 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
@@ -63,4 +63,10 @@ public interface TokenStateServiceMessages {
   @Message(level = MessageLevel.ERROR, text = "Error occurred evicting token {0}")
   void errorEvictingTokens(@StackTrace(level = MessageLevel.DEBUG) Exception e);
 
+  @Message(level = MessageLevel.ERROR, text = "Error occurred while parsing JWT token, cause: {0}")
+  void errorParsingToken(String cause);
+
+  @Message(level = MessageLevel.DEBUG, text = "Permissive validation for token is enabled, expiration for token {0} is {1}")
+  void jwtTokenExpiry(String tokenDisplayText, String expiration);
+
 }
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 44eaa19..28ac7b8 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
@@ -16,15 +16,22 @@
  */
 package org.apache.knox.gateway.services.token.impl;
 
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
 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.JWT;
 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.JWTToken;
 import org.easymock.EasyMock;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPrivateKey;
 import java.util.Collections;
 import java.util.Date;
 import java.util.concurrent.TimeUnit;
@@ -38,6 +45,16 @@ import static org.junit.Assert.fail;
 public class DefaultTokenStateServiceTest {
 
   private static long EVICTION_INTERVAL = 2L;
+  private static RSAPrivateKey privateKey;
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+    KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+    kpg.initialize(2048);
+
+    KeyPair kp = kpg.genKeyPair();
+    privateKey = (RSAPrivateKey) kp.getPrivate();
+  }
 
   @Test
   public void testGetExpiration() throws Exception {
@@ -185,6 +202,32 @@ public class DefaultTokenStateServiceTest {
     }
   }
 
+  @Test
+  public void testTokenPermissiveness() throws UnknownTokenException {
+    final long expiry = System.currentTimeMillis() + 300000;
+    final JWT token = getJWTToken(expiry);
+    TokenStateService tss = new DefaultTokenStateService();
+    try {
+      tss.init(createMockGatewayConfig(true), Collections.emptyMap());
+    } catch (ServiceLifecycleException e) {
+      fail("Error creating TokenStateService: " + e.getMessage());
+    }
+    assertEquals(expiry/1000, tss.getTokenExpiration(token.toString())/1000);
+  }
+
+  @Test
+  public void testTokenPermissivenessNoExpiry() throws UnknownTokenException {
+    final JWT token = getJWTToken(-1);
+    TokenStateService tss = new DefaultTokenStateService();
+    try {
+      tss.init(createMockGatewayConfig(true), Collections.emptyMap());
+    } catch (ServiceLifecycleException e) {
+      fail("Error creating TokenStateService: " + e.getMessage());
+    }
+
+    assertEquals(-1L, tss.getTokenExpiration(token.toString()));
+  }
+
   protected static JWTToken createMockToken(final long expiration) {
     return createMockToken("abcD1234eFGHIJKLmnoPQRSTUVwXYz", expiration);
   }
@@ -197,18 +240,19 @@ public class DefaultTokenStateServiceTest {
     return token;
   }
 
-  protected static GatewayConfig createMockGatewayConfig() {
+  protected static GatewayConfig createMockGatewayConfig(boolean tokenPermissiveness) {
     GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
     /* configure token eviction time to be 5 secs for test */
     EasyMock.expect(config.getKnoxTokenEvictionInterval()).andReturn(EVICTION_INTERVAL).anyTimes();
     EasyMock.expect(config.getKnoxTokenEvictionGracePeriod()).andReturn(0L).anyTimes();
+    EasyMock.expect(config.isKnoxTokenPermissiveValidationEnabled()).andReturn(tokenPermissiveness).anyTimes();
     EasyMock.replay(config);
     return config;
   }
 
   protected void initTokenStateService(TokenStateService tss) {
     try {
-      tss.init(createMockGatewayConfig(), Collections.emptyMap());
+      tss.init(createMockGatewayConfig(false), Collections.emptyMap());
     } catch (ServiceLifecycleException e) {
       fail("Error creating TokenStateService: " + e.getMessage());
     }
@@ -220,4 +264,20 @@ public class DefaultTokenStateServiceTest {
     return tss;
   }
 
+  /* create a test JWT token */
+  protected JWT getJWTToken(final long expiry) {
+    String[] claims = new String[4];
+    claims[0] = "KNOXSSO";
+    claims[1] = "john.doe@example.com";
+    claims[2] = "https://login.example.com";
+    if(expiry > 0) {
+      claims[3] = Long.toString(expiry);
+    }
+    JWT token = new JWTToken("RS256", claims);
+    // Sign the token
+    JWSSigner signer = new RSASSASigner(privateKey);
+    token.sign(signer);
+    return token;
+  }
+
 }
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 d6e40a0..48330e3 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
@@ -675,4 +675,9 @@ public interface GatewayConfig {
    * @return the list of topologies that should be hidden on Knox homepage
    */
   Set<String> getHiddenTopologiesOnHomepage();
+
+  /**
+   * @return returns whether know token permissive validation is enabled
+   */
+  boolean isKnoxTokenPermissiveValidationEnabled();
 }
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 a7774bc..77206e4 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
@@ -794,4 +794,12 @@ public class GatewayTestConfig extends Configuration implements GatewayConfig {
   public Set<String> getHiddenTopologiesOnHomepage() {
     return Collections.emptySet();
   }
+
+  /**
+   * @return returns whether know token permissive failure is enabled
+   */
+  @Override
+  public boolean isKnoxTokenPermissiveValidationEnabled() {
+    return false;
+  }
 }