You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2022/08/18 10:38:17 UTC

[knox] branch master updated: KNOX-2778 - Enforce concurrent session limit in KnoxSSO (#615)

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 78a058900 KNOX-2778 - Enforce concurrent session limit in KnoxSSO (#615)
78a058900 is described below

commit 78a058900f17078ea3278cc41bf8cc9540ed4415
Author: MrtnBalazs <77...@users.noreply.github.com>
AuthorDate: Thu Aug 18 12:38:11 2022 +0200

    KNOX-2778 - Enforce concurrent session limit in KnoxSSO (#615)
---
 .../org/apache/knox/gateway/GatewayMessages.java   |   3 +
 .../gateway/services/DefaultGatewayServices.java   |   2 +
 .../factory/ConcurrentSessionVerifierFactory.java  |  46 +++
 .../control/InMemoryConcurrentSessionVerifier.java | 172 +++++++++++
 ...org.apache.knox.gateway.services.ServiceFactory |   3 +-
 .../services/AbstractGatewayServicesTest.java      |   3 +-
 .../InMemoryConcurrentSessionVerifierTest.java     | 323 +++++++++++++++++++++
 .../gateway/service/knoxsso/WebSSOResource.java    |  26 +-
 .../service/knoxsso/WebSSOResourceTest.java        |  31 +-
 .../gateway/service/knoxsso/KnoxSSOutMessages.java |   3 +
 .../gateway/service/knoxsso/WebSSOutResource.java  |  27 ++
 .../service/knoxsso/WebSSOutResourceTest.java      |   6 +-
 .../session/control/ConcurrentSessionVerifier.java | 106 -------
 .../control/ConcurrentSessionVerifierTest.java     | 161 ----------
 .../apache/knox/gateway/services/ServiceType.java  |   3 +-
 .../session/control/ConcurrentSessionVerifier.java |  25 +-
 16 files changed, 645 insertions(+), 295 deletions(-)

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 574ff388b..c88b467e8 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
@@ -718,4 +718,7 @@ public interface GatewayMessages {
 
   @Message(level = MessageLevel.DEBUG, text = "Jetty's maxFormKeys is set to {0}")
   void setMaxFormKeys(int maxFormKeys);
+
+  @Message(level = MessageLevel.ERROR, text = "ConcurrentSessionVerifier got blank username for verification.")
+  void errorVerifyingUserBlankUsername();
 }
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 051776334..994025e5e 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
@@ -78,6 +78,8 @@ public class DefaultGatewayServices extends AbstractGatewayServices {
     addService(ServiceType.SERVICE_DEFINITION_REGISTRY, gatewayServiceFactory.create(this, ServiceType.SERVICE_DEFINITION_REGISTRY, config, options));
 
     addService(ServiceType.METRICS_SERVICE, gatewayServiceFactory.create(this, ServiceType.METRICS_SERVICE, config, options));
+
+    addService(ServiceType.CONCURRENT_SESSION_VERIFIER, gatewayServiceFactory.create(this, ServiceType.CONCURRENT_SESSION_VERIFIER, config, options));
   }
 
   @Override
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/ConcurrentSessionVerifierFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/ConcurrentSessionVerifierFactory.java
new file mode 100644
index 000000000..0e84dc2ff
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/ConcurrentSessionVerifierFactory.java
@@ -0,0 +1,46 @@
+/*
+ * 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.factory;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.session.control.InMemoryConcurrentSessionVerifier;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+public class ConcurrentSessionVerifierFactory extends AbstractServiceFactory {
+  @Override
+  protected Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> options, String implementation) throws ServiceLifecycleException {
+    return shouldCreateService(implementation) ? new InMemoryConcurrentSessionVerifier() : null;
+  }
+
+  @Override
+  protected ServiceType getServiceType() {
+    return ServiceType.CONCURRENT_SESSION_VERIFIER;
+  }
+
+  @Override
+  protected Collection<String> getKnownImplementations() {
+    return Collections.singleton(InMemoryConcurrentSessionVerifier.class.getName());
+  }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifier.java b/gateway-server/src/main/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifier.java
new file mode 100644
index 000000000..bf0ec4df4
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifier.java
@@ -0,0 +1,172 @@
+/*
+ * 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.session.control;
+
+
+import org.apache.commons.lang3.StringUtils;
+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.token.impl.JWT;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class InMemoryConcurrentSessionVerifier implements ConcurrentSessionVerifier {
+  private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
+
+  private Set<String> privilegedUsers;
+  private Set<String> nonPrivilegedUsers;
+  private int privilegedUserConcurrentSessionLimit;
+  private int nonPrivilegedUserConcurrentSessionLimit;
+  private Map<String, Set<SessionJWT>> concurrentSessionCounter;
+  private final Lock sessionCountModifyLock = new ReentrantLock();
+
+  @Override
+  public boolean verifySessionForUser(String username, JWT jwtToken) {
+    if (StringUtils.isBlank(username)) {
+      LOG.errorVerifyingUserBlankUsername();
+      return false;
+    }
+    if (!privilegedUsers.contains(username) && !nonPrivilegedUsers.contains(username)) {
+      return true;
+    }
+
+    sessionCountModifyLock.lock();
+    try {
+      int validTokenNumber = countValidTokensForUser(username);
+      if (privilegedUserCheckLimitReached(username, validTokenNumber) || nonPrivilegedUserCheckLimitReached(username, validTokenNumber)) {
+        return false;
+      }
+      concurrentSessionCounter.putIfAbsent(username, new HashSet<>());
+      concurrentSessionCounter.compute(username, (key, sessionTokenSet) -> addTokenForUser(sessionTokenSet, jwtToken));
+    } finally {
+      sessionCountModifyLock.unlock();
+    }
+    return true;
+  }
+
+  int countValidTokensForUser(String username) {
+    return (int) concurrentSessionCounter
+            .getOrDefault(username, Collections.emptySet())
+            .stream()
+            .filter(each -> !each.hasExpired())
+            .count();
+  }
+
+  private boolean privilegedUserCheckLimitReached(String username, int validTokenNumber) {
+    if (privilegedUserConcurrentSessionLimit < 0) {
+      return false;
+    }
+    return privilegedUsers.contains(username) && (validTokenNumber >= privilegedUserConcurrentSessionLimit);
+  }
+
+  private boolean nonPrivilegedUserCheckLimitReached(String username, int validTokenNumber) {
+    if (nonPrivilegedUserConcurrentSessionLimit < 0) {
+      return false;
+    }
+    return nonPrivilegedUsers.contains(username) && (validTokenNumber >= nonPrivilegedUserConcurrentSessionLimit);
+  }
+
+  @Override
+  public void sessionEndedForUser(String username, String token) {
+    if (StringUtils.isNotBlank(token) && StringUtils.isNotBlank(username)) {
+      sessionCountModifyLock.lock();
+      try {
+        concurrentSessionCounter.computeIfPresent(username, (key, sessionTokenSet) -> removeTokenFromUser(sessionTokenSet, token));
+      } finally {
+        sessionCountModifyLock.unlock();
+      }
+    }
+  }
+
+  private Set<SessionJWT> removeTokenFromUser(Set<SessionJWT> sessionTokenSet, String token) {
+    sessionTokenSet.removeIf(sessionToken -> sessionToken.getToken().equals(token));
+    if (sessionTokenSet.isEmpty()) {
+      return null;
+    }
+    return sessionTokenSet;
+  }
+
+  private Set<SessionJWT> addTokenForUser(Set<SessionJWT> sessionTokenSet, JWT jwtToken) {
+    sessionTokenSet.add(new SessionJWT(jwtToken));
+    return sessionTokenSet;
+  }
+
+  @Override
+  public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    this.privilegedUsers = config.getPrivilegedUsers();
+    this.nonPrivilegedUsers = config.getNonPrivilegedUsers();
+    this.privilegedUserConcurrentSessionLimit = config.getPrivilegedUsersConcurrentSessionLimit();
+    this.nonPrivilegedUserConcurrentSessionLimit = config.getNonPrivilegedUsersConcurrentSessionLimit();
+    this.concurrentSessionCounter = new ConcurrentHashMap<>();
+  }
+
+  @Override
+  public void start() throws ServiceLifecycleException {
+
+  }
+
+  @Override
+  public void stop() throws ServiceLifecycleException {
+
+  }
+
+  public static class SessionJWT {
+    private final Date expiry;
+    private final String token;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SessionJWT that = (SessionJWT) o;
+      return Objects.equals(token, that.token);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(token);
+    }
+
+    public SessionJWT(JWT token) {
+      this.expiry = token.getExpiresDate();
+      this.token = token.toString();
+    }
+
+    public String getToken() {
+      return token;
+    }
+
+    public boolean hasExpired() {
+      return expiry != null && expiry.before(new Date());
+    }
+  }
+}
diff --git a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
index 02275eafb..b8a4fc49d 100644
--- a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
+++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
@@ -30,4 +30,5 @@ org.apache.knox.gateway.services.factory.ServiceRegistryServiceFactory
 org.apache.knox.gateway.services.factory.SslServiceFactory
 org.apache.knox.gateway.services.factory.TokenServiceFactory
 org.apache.knox.gateway.services.factory.TokenStateServiceFactory
-org.apache.knox.gateway.services.factory.TopologyServiceFactory
\ No newline at end of file
+org.apache.knox.gateway.services.factory.TopologyServiceFactory
+org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory
\ No newline at end of file
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 1f0a35258..8ecb72684 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
@@ -62,7 +62,8 @@ public class AbstractGatewayServicesTest {
         ServiceType.CRYPTO_SERVICE,
         ServiceType.HOST_MAPPING_SERVICE,
         ServiceType.SERVICE_DEFINITION_REGISTRY,
-        ServiceType.SERVICE_REGISTRY_SERVICE
+        ServiceType.SERVICE_REGISTRY_SERVICE,
+        ServiceType.CONCURRENT_SESSION_VERIFIER
     };
 
     assertNotEquals(ServiceType.values(), orderedServiceTypes);
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifierTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifierTest.java
new file mode 100644
index 000000000..4cf1f2c75
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifierTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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.session.control;
+
+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.MasterService;
+import org.apache.knox.gateway.services.security.impl.DefaultKeystoreService;
+import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
+import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
+import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.token.impl.DefaultTokenAuthorityService;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class InMemoryConcurrentSessionVerifierTest {
+  private InMemoryConcurrentSessionVerifier verifier;
+  private Map<String, String> options;
+  private DefaultTokenAuthorityService tokenAuthority;
+  private JWTokenAttributes jwtAttributesForAdmin;
+  private JWTokenAttributes jwtAttributesForTom;
+  private JWT adminToken1;
+  private JWT adminToken2;
+  private JWT adminToken3;
+  private JWT adminToken4;
+  private JWT adminToken5;
+  private JWT adminToken6;
+  private JWT tomToken1;
+  private JWT tomToken2;
+  private JWT tomToken3;
+  private JWT tomToken4;
+  private JWT tomToken5;
+  private JWT tomToken6;
+
+  @Before
+  public void setUp() throws AliasServiceException, IOException, ServiceLifecycleException {
+    verifier = new InMemoryConcurrentSessionVerifier();
+    options = Collections.emptyMap();
+
+    GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    String basedir = System.getProperty("basedir");
+    if (basedir == null) {
+      basedir = new File(".").getCanonicalPath();
+    }
+    EasyMock.expect(config.getGatewaySecurityDir()).andReturn(basedir + "/target/test-classes").anyTimes();
+    EasyMock.expect(config.getGatewayKeystoreDir()).andReturn(basedir + "/target/test-classes/keystores").anyTimes();
+    EasyMock.expect(config.getSigningKeystoreName()).andReturn("server-keystore.jks").anyTimes();
+    EasyMock.expect(config.getSigningKeystorePath()).andReturn(basedir + "/target/test-classes/keystores/server-keystore.jks").anyTimes();
+    EasyMock.expect(config.getSigningKeystorePasswordAlias()).andReturn(GatewayConfig.DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS).anyTimes();
+    EasyMock.expect(config.getSigningKeyPassphraseAlias()).andReturn(GatewayConfig.DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS).anyTimes();
+    EasyMock.expect(config.getSigningKeystoreType()).andReturn("jks").anyTimes();
+    EasyMock.expect(config.getSigningKeyAlias()).andReturn("server").anyTimes();
+    EasyMock.expect(config.getCredentialStoreType()).andReturn(GatewayConfig.DEFAULT_CREDENTIAL_STORE_TYPE).anyTimes();
+    EasyMock.expect(config.getCredentialStoreAlgorithm()).andReturn(GatewayConfig.DEFAULT_CREDENTIAL_STORE_ALG).anyTimes();
+
+    MasterService ms = EasyMock.createNiceMock(MasterService.class);
+    EasyMock.expect(ms.getMasterSecret()).andReturn("horton".toCharArray());
+
+    AliasService as = EasyMock.createNiceMock(AliasService.class);
+    EasyMock.expect(as.getSigningKeyPassphrase()).andReturn("horton".toCharArray()).anyTimes();
+
+    EasyMock.replay(config, ms, as);
+
+    DefaultKeystoreService ks = new DefaultKeystoreService();
+    ks.setMasterService(ms);
+    ks.init(config, new HashMap<>());
+
+    tokenAuthority = new DefaultTokenAuthorityService();
+    tokenAuthority.setAliasService(as);
+    tokenAuthority.setKeystoreService(ks);
+    tokenAuthority.init(config, new HashMap<>());
+    tokenAuthority.start();
+
+    jwtAttributesForAdmin = new JWTokenAttributesBuilder()
+            .setIssuer("KNOXSSO")
+            .setUserName("admin")
+            .setAudiences(new ArrayList<>())
+            .setAlgorithm("RS256")
+            .setExpires(-1)
+            .setSigningKeystoreName(null)
+            .setSigningKeystoreAlias(null)
+            .setSigningKeystorePassphrase(null)
+            .build();
+    jwtAttributesForTom = new JWTokenAttributesBuilder()
+            .setIssuer("KNOXSSO")
+            .setUserName("tom")
+            .setAudiences(new ArrayList<>())
+            .setAlgorithm("RS256")
+            .setExpires(-1)
+            .setSigningKeystoreName(null)
+            .setSigningKeystoreAlias(null)
+            .setSigningKeystorePassphrase(null)
+            .build();
+    try {
+      adminToken1 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+      adminToken2 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+      adminToken3 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+      adminToken4 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+      adminToken5 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+      adminToken6 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+      tomToken1 = tokenAuthority.issueToken(jwtAttributesForTom);
+      tomToken2 = tokenAuthority.issueToken(jwtAttributesForTom);
+      tomToken3 = tokenAuthority.issueToken(jwtAttributesForTom);
+      tomToken4 = tokenAuthority.issueToken(jwtAttributesForTom);
+      tomToken5 = tokenAuthority.issueToken(jwtAttributesForTom);
+      tomToken6 = tokenAuthority.issueToken(jwtAttributesForTom);
+    } catch (TokenServiceException ignored) {
+    }
+  }
+
+  private GatewayConfig mockConfig(Set<String> privilegedUsers, Set<String> nonPrivilegedUsers, int privilegedUsersLimit, int nonPrivilegedUsersLimit) {
+    GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(config.getPrivilegedUsers()).andReturn(privilegedUsers);
+    EasyMock.expect(config.getNonPrivilegedUsers()).andReturn(nonPrivilegedUsers);
+    EasyMock.expect(config.getPrivilegedUsersConcurrentSessionLimit()).andReturn(privilegedUsersLimit);
+    EasyMock.expect(config.getNonPrivilegedUsersConcurrentSessionLimit()).andReturn(nonPrivilegedUsersLimit);
+    EasyMock.replay(config);
+    return config;
+  }
+
+  /**
+   * The goal for this test is to prove that if the user is not configured for either of the groups then
+   * neither of the limits apply to him, he can have unlimited sessions.
+   */
+  @Test
+  public void testUserIsInNeitherOfTheGroupsCanBeLoggedInUnlimitedTimes() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("guest")), 3, 2);
+    verifier.init(config, options);
+
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken3));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken4));
+  }
+
+  @Test
+  public void testUserIsInBothOfTheGroupsLowerLimitApplies() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
+    verifier.init(config, options);
+
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+    Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken3));
+
+    config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 4);
+    verifier.init(config, options);
+
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken3));
+    Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken4));
+  }
+
+  @Test
+  public void testUserIsPrivileged() throws ServiceLifecycleException, TokenServiceException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
+    verifier.init(config, options);
+
+    Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken1));
+    Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken2));
+    Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken3));
+    Assert.assertFalse(verifier.verifySessionForUser("admin", adminToken4));
+    verifier.sessionEndedForUser("admin", adminToken1.toString());
+    Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken5));
+    Assert.assertFalse(verifier.verifySessionForUser("admin", adminToken6));
+  }
+
+  @Test
+  public void testUserIsNotPrivileged() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
+    verifier.init(config, options);
+
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+    Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken3));
+    Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken4));
+    verifier.sessionEndedForUser("tom", tomToken1.toString());
+    Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken5));
+    Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken6));
+  }
+
+  @Test
+  public void testPrivilegedLimitIsZero() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 0, 2);
+    verifier.init(config, options);
+
+    Assert.assertFalse(verifier.verifySessionForUser("admin", adminToken1));
+  }
+
+  @Test
+  public void testNonPrivilegedLimitIsZero() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 0);
+    verifier.init(config, options);
+
+    Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken1));
+  }
+
+  @Test
+  public void testSessionsDoNotGoToNegative() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 2, 2);
+    verifier.init(config, options);
+
+    Assert.assertEquals(0, verifier.countValidTokensForUser("admin"));
+    verifier.verifySessionForUser("admin", adminToken1);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+    verifier.sessionEndedForUser("admin", adminToken1.toString());
+    Assert.assertEquals(0, verifier.countValidTokensForUser("admin"));
+    verifier.sessionEndedForUser("admin", adminToken1.toString());
+    Assert.assertEquals(0, verifier.countValidTokensForUser("admin"));
+    verifier.verifySessionForUser("admin", adminToken2);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+
+    Assert.assertEquals(0, verifier.countValidTokensForUser("tom"));
+    verifier.verifySessionForUser("tom", tomToken1);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+    verifier.sessionEndedForUser("tom", tomToken1.toString());
+    Assert.assertEquals(0, verifier.countValidTokensForUser("tom"));
+    verifier.sessionEndedForUser("tom", tomToken1.toString());
+    Assert.assertEquals(0, verifier.countValidTokensForUser("tom"));
+    verifier.verifySessionForUser("tom", tomToken2);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+  }
+
+  @Test
+  public void testNegativeLimitMeansUnlimited() throws ServiceLifecycleException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), -2, -2);
+    verifier.init(config, options);
+
+    for (int i = 0; i < 10; i++) {
+      try {
+        JWT token = tokenAuthority.issueToken(jwtAttributesForAdmin);
+        Assert.assertTrue(verifier.verifySessionForUser("admin", token));
+        token = tokenAuthority.issueToken(jwtAttributesForTom);
+        Assert.assertTrue(verifier.verifySessionForUser("tom", token));
+      } catch (TokenServiceException ignored) {
+      }
+    }
+  }
+
+  @Test
+  public void testExpiredTokensAreNotCounted() throws ServiceLifecycleException, TokenServiceException, InterruptedException {
+    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 3);
+    verifier.init(config, options);
+
+    JWTokenAttributes expiringJwtAttributesForTom = new JWTokenAttributesBuilder()
+            .setIssuer("KNOXSSO")
+            .setUserName("tom")
+            .setAudiences(new ArrayList<>())
+            .setAlgorithm("RS256")
+            .setExpires(System.currentTimeMillis() + 1000)
+            .setSigningKeystoreName(null)
+            .setSigningKeystoreAlias(null)
+            .setSigningKeystorePassphrase(null)
+            .build();
+
+    JWT tomToken = tokenAuthority.issueToken(jwtAttributesForTom);
+    verifier.verifySessionForUser("tom", tomToken);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+    tomToken = tokenAuthority.issueToken(expiringJwtAttributesForTom);
+    verifier.verifySessionForUser("tom", tomToken);
+    Assert.assertEquals(2, verifier.countValidTokensForUser("tom"));
+    tomToken = tokenAuthority.issueToken(expiringJwtAttributesForTom);
+    verifier.verifySessionForUser("tom", tomToken);
+    Assert.assertEquals(3, verifier.countValidTokensForUser("tom"));
+    Thread.sleep(1000L);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+
+    JWTokenAttributes expiringJwtAttributesForAdmin = new JWTokenAttributesBuilder()
+            .setIssuer("KNOXSSO")
+            .setUserName("admin")
+            .setAudiences(new ArrayList<>())
+            .setAlgorithm("RS256")
+            .setExpires(System.currentTimeMillis() + 1000)
+            .setSigningKeystoreName(null)
+            .setSigningKeystoreAlias(null)
+            .setSigningKeystorePassphrase(null)
+            .build();
+
+    JWT adminToken = tokenAuthority.issueToken(jwtAttributesForAdmin);
+    verifier.verifySessionForUser("admin", adminToken);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+    adminToken = tokenAuthority.issueToken(expiringJwtAttributesForAdmin);
+    verifier.verifySessionForUser("admin", adminToken);
+    Assert.assertEquals(2, verifier.countValidTokensForUser("admin"));
+    adminToken = tokenAuthority.issueToken(expiringJwtAttributesForAdmin);
+    verifier.verifySessionForUser("admin", adminToken);
+    Assert.assertEquals(3, verifier.countValidTokensForUser("admin"));
+    Thread.sleep(1000L);
+    Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+  }
+}
+
+
diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
index e48509a76..59af1c6d3 100644
--- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
+++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
@@ -58,6 +58,7 @@ 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.TokenUtils;
 import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.session.control.ConcurrentSessionVerifier;
 import org.apache.knox.gateway.util.CookieUtils;
 import org.apache.knox.gateway.util.RegExUtils;
 import org.apache.knox.gateway.util.Urls;
@@ -215,6 +216,14 @@ public class WebSSOResource {
   }
 
   private Response getAuthenticationToken(int statusCode) {
+    if (!enableSession) {
+      // invalidate the session to avoid autologin
+      // Coverity CID 1352857
+      HttpSession session = request.getSession(false);
+      if (session != null) {
+        session.invalidate();
+      }
+    }
     GatewayServices services =
                 (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
     boolean removeOriginalUrlCookie = true;
@@ -281,8 +290,12 @@ public class WebSSOResource {
       JWT token = tokenAuthority.issueToken(jwtAttributes);
 
       // Coverity CID 1327959
-      if( token != null ) {
-        addJWTHadoopCookie( original, token );
+      if (token != null) {
+        ConcurrentSessionVerifier verifier = services.getService(ServiceType.CONCURRENT_SESSION_VERIFIER);
+        if (verifier != null && !verifier.verifySessionForUser(p.getName(), token)) {
+          throw new WebApplicationException("Too many sessions for user: " + request.getUserPrincipal().getName(), Response.Status.FORBIDDEN);
+        }
+        addJWTHadoopCookie(original, token);
       }
 
       if (removeOriginalUrlCookie) {
@@ -308,14 +321,7 @@ public class WebSSOResource {
       // todo log return error response
     }
 
-    if (!enableSession) {
-      // invalidate the session to avoid autologin
-      // Coverity CID 1352857
-      HttpSession session = request.getSession(false);
-      if( session != null ) {
-        session.invalidate();
-      }
-    }
+
 
     return Response.seeOther(location).entity("{ \"redirectTo\" : " + original + " }").build();
   }
diff --git a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
index 407813a21..a3774fd77 100644
--- a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
+++ b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
@@ -18,6 +18,8 @@
 package org.apache.knox.gateway.service.knoxsso;
 
 import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -69,6 +71,7 @@ import org.apache.knox.gateway.services.security.token.TokenServiceException;
 import org.apache.knox.gateway.services.security.token.TokenUtils;
 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.session.control.ConcurrentSessionVerifier;
 import org.apache.knox.gateway.util.RegExUtils;
 import org.easymock.EasyMock;
 import org.junit.Assert;
@@ -86,6 +89,7 @@ public class WebSSOResourceTest {
   private ServletContext context;
   private HttpServletRequest request;
   private JWTokenAuthority authority;
+  private ConcurrentSessionVerifier verifier;
   CookieResponseWrapper responseWrapper;
 
   @BeforeClass
@@ -142,14 +146,14 @@ public class WebSSOResourceTest {
   }
 
   private void configureCommonExpectations(Map<String, String> contextExpectations) throws Exception {
-    configureCommonExpectations(contextExpectations, false, false);
+    configureCommonExpectations(contextExpectations, false, false, true);
   }
 
   private void configureCommonExpectations(Map<String, String> contextExpectations, boolean sslEnabled) throws Exception {
-    configureCommonExpectations(contextExpectations, false, sslEnabled);
+    configureCommonExpectations(contextExpectations, false, sslEnabled, true);
   }
 
-  private void configureCommonExpectations(Map<String, String> contextExpectations, boolean useHmac, boolean sslEnabled) throws Exception {
+  private void configureCommonExpectations(Map<String, String> contextExpectations, boolean useHmac, boolean sslEnabled, boolean concurrentSessionVerifyResult) throws Exception {
     context = EasyMock.createNiceMock(ServletContext.class);
     contextExpectations.forEach((key, value) -> EasyMock.expect(context.getInitParameter(key)).andReturn(value).anyTimes());
 
@@ -178,7 +182,11 @@ public class WebSSOResourceTest {
     ServletOutputStream outputStream = EasyMock.createNiceMock(ServletOutputStream.class);
     responseWrapper = new CookieResponseWrapper(response, outputStream);
 
-    EasyMock.replay(principal, services, context, request);
+    verifier = EasyMock.createNiceMock(ConcurrentSessionVerifier.class);
+    EasyMock.expect(verifier.verifySessionForUser(anyString(), anyObject())).andReturn(concurrentSessionVerifyResult).anyTimes();
+    EasyMock.expect(services.getService(ServiceType.CONCURRENT_SESSION_VERIFIER)).andReturn(verifier).anyTimes();
+
+    EasyMock.replay(principal, services, context, request, verifier);
   }
 
   @Test
@@ -271,7 +279,7 @@ public class WebSSOResourceTest {
 
   private void testSignatureAlgorithm(boolean useHMAC) throws Exception {
     final String algorithm = useHMAC ? "HS256" : "RS512";
-    configureCommonExpectations(Collections.singletonMap("knoxsso.token.sigalg", algorithm), useHMAC, false);
+    configureCommonExpectations(Collections.singletonMap("knoxsso.token.sigalg", algorithm), useHMAC, false, true);
 
     WebSSOResource webSSOResponse = new WebSSOResource();
     webSSOResponse.request = request;
@@ -683,6 +691,19 @@ public class WebSSOResourceTest {
     assertTrue(authority.verifyToken(parsedToken, customPublicKey));
   }
 
+  @Test
+  public void testConcurrentSessionLimitHit() throws Exception {
+    configureCommonExpectations(Collections.emptyMap(), false, false, false);
+
+    WebSSOResource webSSOResponse = new WebSSOResource();
+    webSSOResponse.request = request;
+    webSSOResponse.response = responseWrapper;
+    webSSOResponse.context = context;
+    webSSOResponse.init();
+
+    Assert.assertThrows(WebApplicationException.class, webSSOResponse::doPost);
+  }
+
   /**
    * A wrapper for HttpServletResponseWrapper to store the cookies
    */
diff --git a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
index 768cf6d70..e94dab897 100644
--- a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
+++ b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
@@ -25,4 +25,7 @@ import org.apache.knox.gateway.i18n.messages.Messages;
 public interface KnoxSSOutMessages {
   @Message( level = MessageLevel.INFO, text = "There was a problem determining the SSO cookie domain - using default domain.")
   void problemWithCookieDomainUsingDefault();
+
+  @Message(level = MessageLevel.WARN, text = "Could not find cookie with the name: {0} in the request to be removed from the concurrent session counter for user: {1}. ")
+  void couldNotFindCookieWithTokenToRemove(String cookieName, String username);
 }
\ No newline at end of file
diff --git a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java
index 623274ada..134972c15 100644
--- a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java
+++ b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java
@@ -18,6 +18,9 @@
 package org.apache.knox.gateway.service.knoxsso;
 
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.session.control.ConcurrentSessionVerifier;
 import org.apache.knox.gateway.util.Urls;
 
 import javax.annotation.PostConstruct;
@@ -32,6 +35,8 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.Optional;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static javax.ws.rs.core.MediaType.APPLICATION_XML;
@@ -106,6 +111,28 @@ public class WebSSOutResource {
     }
     response.addCookie(c);
 
+    Optional<Cookie> ssoCookie = findCookie(cookieName);
+    if (ssoCookie.isPresent()) {
+      GatewayServices gwServices =
+              (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+      if (gwServices != null) {
+        ConcurrentSessionVerifier verifier = gwServices.getService(ServiceType.CONCURRENT_SESSION_VERIFIER);
+        if (verifier != null) {
+          verifier.sessionEndedForUser(request.getUserPrincipal().getName(), ssoCookie.get().getValue());
+        }
+      }
+    } else {
+      log.couldNotFindCookieWithTokenToRemove(cookieName, request.getUserPrincipal().getName());
+    }
     return rc;
   }
+
+  private Optional<Cookie> findCookie(String cookieName) {
+    Cookie[] cookies = request.getCookies();
+    if (cookies != null) {
+      return Arrays.stream(cookies).filter(cookie -> cookie.getName().equals(cookieName)).findFirst();
+    } else {
+      return Optional.empty();
+    }
+  }
 }
diff --git a/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java b/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java
index 0e973b161..8d262f315 100644
--- a/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java
+++ b/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java
@@ -26,6 +26,7 @@ import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponseWrapper;
+import java.security.Principal;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
@@ -47,12 +48,15 @@ public class WebSSOutResourceTest {
 
     HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
     EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(""));
+    Principal mockPrincipal = EasyMock.createNiceMock(Principal.class);
+    EasyMock.expect(mockPrincipal.getName()).andReturn("admin").anyTimes();
+    EasyMock.expect(request.getUserPrincipal()).andReturn(mockPrincipal).anyTimes();
 
     HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class);
     ServletOutputStream outputStream = EasyMock.createNiceMock(ServletOutputStream.class);
     CookieResponseWrapper responseWrapper = new CookieResponseWrapper(response, outputStream);
 
-    EasyMock.replay(context, request);
+    EasyMock.replay(context, request, mockPrincipal);
 
     WebSSOutResource webSSOutResponse = new WebSSOutResource();
     webSSOutResponse.request = request;
diff --git a/gateway-spi-common/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java b/gateway-spi-common/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
deleted file mode 100644
index e06633ff6..000000000
--- a/gateway-spi-common/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.session.control;
-
-
-import org.apache.knox.gateway.config.GatewayConfig;
-
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-public class ConcurrentSessionVerifier {
-  public static final ConcurrentSessionVerifier INSTANCE = new ConcurrentSessionVerifier();
-  private Set<String> privilegedUsers;
-  private Set<String> nonPrivilegedUsers;
-  private int privilegedUserConcurrentSessionLimit;
-  private int nonPrivilegedUserConcurrentSessionLimit;
-  private Map<String, Integer> concurrentSessionCounter;
-  private final Lock sessionCountModifyLock = new ReentrantLock();
-
-  private ConcurrentSessionVerifier() {
-  }
-
-  public static ConcurrentSessionVerifier getInstance() {
-    return INSTANCE;
-  }
-
-  public void init(GatewayConfig config) {
-    this.privilegedUsers = config.getPrivilegedUsers();
-    this.nonPrivilegedUsers = config.getNonPrivilegedUsers();
-    this.privilegedUserConcurrentSessionLimit = config.getPrivilegedUsersConcurrentSessionLimit();
-    this.nonPrivilegedUserConcurrentSessionLimit = config.getNonPrivilegedUsersConcurrentSessionLimit();
-    this.concurrentSessionCounter = new ConcurrentHashMap<>();
-  }
-
-  public boolean verifySessionForUser(String username) {
-    if (!privilegedUsers.contains(username) && !nonPrivilegedUsers.contains(username)) {
-      return true;
-    }
-
-    sessionCountModifyLock.lock();
-    try {
-      concurrentSessionCounter.putIfAbsent(username, 0);
-      if (privilegedUserCheckLimitReached(username) || nonPrivilegedUserCheckLimitReached(username)) {
-        return false;
-      }
-      concurrentSessionCounter.compute(username, (key, value) -> value + 1);
-    } finally {
-      sessionCountModifyLock.unlock();
-    }
-    return true;
-  }
-
-  private boolean privilegedUserCheckLimitReached(String username) {
-    if (privilegedUserConcurrentSessionLimit < 0) {
-      return false;
-    }
-    return privilegedUsers.contains(username) && (concurrentSessionCounter.get(username) >= privilegedUserConcurrentSessionLimit);
-  }
-
-  private boolean nonPrivilegedUserCheckLimitReached(String username) {
-    if (nonPrivilegedUserConcurrentSessionLimit < 0) {
-      return false;
-    }
-    return nonPrivilegedUsers.contains(username) && (concurrentSessionCounter.get(username) >= nonPrivilegedUserConcurrentSessionLimit);
-  }
-
-  public void sessionEndedForUser(String username) {
-    sessionCountModifyLock.lock();
-    try {
-      concurrentSessionCounter.computeIfPresent(username, (key, counter) -> decreaseCounter(counter));
-    } finally {
-      sessionCountModifyLock.unlock();
-    }
-  }
-
-  private Integer decreaseCounter(Integer counter) {
-    counter--;
-    if (counter < 1) {
-      return null;
-    } else {
-      return counter;
-    }
-  }
-
-  Integer getUserConcurrentSessionCount(String username) {
-    return concurrentSessionCounter.get(username);
-  }
-}
diff --git a/gateway-spi-common/src/test/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifierTest.java b/gateway-spi-common/src/test/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifierTest.java
deleted file mode 100644
index d70eca356..000000000
--- a/gateway-spi-common/src/test/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifierTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.session.control;
-
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.easymock.EasyMock;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-public class ConcurrentSessionVerifierTest {
-
-  private ConcurrentSessionVerifier verifier;
-
-  @Before
-  public void setUp() {
-    verifier = ConcurrentSessionVerifier.getInstance();
-  }
-
-  private GatewayConfig mockConfig(Set<String> privilegedUsers, Set<String> nonPrivilegedUsers, int privilegedUsersLimit, int nonPrivilegedUsersLimit) {
-    GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
-    EasyMock.expect(config.getPrivilegedUsers()).andReturn(privilegedUsers);
-    EasyMock.expect(config.getNonPrivilegedUsers()).andReturn(nonPrivilegedUsers);
-    EasyMock.expect(config.getPrivilegedUsersConcurrentSessionLimit()).andReturn(privilegedUsersLimit);
-    EasyMock.expect(config.getNonPrivilegedUsersConcurrentSessionLimit()).andReturn(nonPrivilegedUsersLimit);
-    EasyMock.replay(config);
-    return config;
-  }
-
-
-  @Test
-  public void userIsInNeitherOfTheGroups() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
-    verifier.init(config);
-    for (int i = 0; i < 4; i++) {
-      Assert.assertTrue(verifier.verifySessionForUser("sam"));
-    }
-  }
-
-  @Test
-  public void userIsInBothOfTheGroups() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
-    verifier.init(config);
-
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-
-    config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 4);
-    verifier.init(config);
-
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-  }
-
-  @Test
-  public void userIsPrivileged() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
-    verifier.init(config);
-
-    Assert.assertTrue(verifier.verifySessionForUser("admin"));
-    Assert.assertTrue(verifier.verifySessionForUser("admin"));
-    Assert.assertTrue(verifier.verifySessionForUser("admin"));
-    Assert.assertFalse(verifier.verifySessionForUser("admin"));
-    Assert.assertFalse(verifier.verifySessionForUser("admin"));
-    verifier.sessionEndedForUser("admin");
-    Assert.assertTrue(verifier.verifySessionForUser("admin"));
-    Assert.assertFalse(verifier.verifySessionForUser("admin"));
-    Assert.assertFalse(verifier.verifySessionForUser("admin"));
-  }
-
-  @Test
-  public void userIsNotPrivileged() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
-    verifier.init(config);
-
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-    verifier.sessionEndedForUser("tom");
-    Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-  }
-
-  @Test
-  public void privilegedLimitIsZero() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 0, 2);
-    verifier.init(config);
-
-    Assert.assertFalse(verifier.verifySessionForUser("admin"));
-  }
-
-  @Test
-  public void nonPrivilegedLimitIsZero() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 0);
-    verifier.init(config);
-
-    Assert.assertFalse(verifier.verifySessionForUser("tom"));
-  }
-
-  @Test
-  public void sessionsDoNotGoToNegative() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 2, 2);
-    verifier.init(config);
-
-    Assert.assertNull(verifier.getUserConcurrentSessionCount("admin"));
-    verifier.verifySessionForUser("admin");
-    Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("admin").intValue());
-    verifier.sessionEndedForUser("admin");
-    Assert.assertNull(verifier.getUserConcurrentSessionCount("admin"));
-    verifier.sessionEndedForUser("admin");
-    Assert.assertNull(verifier.getUserConcurrentSessionCount("admin"));
-    verifier.verifySessionForUser("admin");
-    Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("admin").intValue());
-
-    Assert.assertNull(verifier.getUserConcurrentSessionCount("tom"));
-    verifier.verifySessionForUser("tom");
-    Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("tom").intValue());
-    verifier.sessionEndedForUser("tom");
-    Assert.assertNull(verifier.getUserConcurrentSessionCount("tom"));
-    verifier.sessionEndedForUser("tom");
-    Assert.assertNull(verifier.getUserConcurrentSessionCount("tom"));
-    verifier.verifySessionForUser("tom");
-    Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("tom").intValue());
-  }
-
-  @Test
-  public void negativeLimitMeansUnlimited() {
-    GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), -2, -2);
-    verifier.init(config);
-
-    for (int i = 0; i < 10; i++) {
-      Assert.assertTrue(verifier.verifySessionForUser("admin"));
-      Assert.assertTrue(verifier.verifySessionForUser("tom"));
-    }
-  }
-}
-
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 38767f86e..14a1da5d8 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
@@ -35,7 +35,8 @@ public enum ServiceType {
   SSL_SERVICE("SSLService"),
   TOKEN_SERVICE("TokenService"),
   TOKEN_STATE_SERVICE("TokenStateService"),
-  TOPOLOGY_SERVICE("TopologyService");
+  TOPOLOGY_SERVICE("TopologyService"),
+  CONCURRENT_SESSION_VERIFIER("ConcurrentSessionCounter");
 
   private final String serviceTypeName;
   private final String shortName;
diff --git a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java b/gateway-spi/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
similarity index 53%
copy from gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
copy to gateway-spi/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
index 768cf6d70..f421f9c2c 100644
--- a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
@@ -15,14 +15,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.knox.gateway.service.knoxsso;
+package org.apache.knox.gateway.session.control;
 
-import org.apache.knox.gateway.i18n.messages.Message;
-import org.apache.knox.gateway.i18n.messages.MessageLevel;
-import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
 
-@Messages(logger="org.apache.knox.gateway.service.knoxsso")
-public interface KnoxSSOutMessages {
-  @Message( level = MessageLevel.INFO, text = "There was a problem determining the SSO cookie domain - using default domain.")
-  void problemWithCookieDomainUsingDefault();
-}
\ No newline at end of file
+public interface ConcurrentSessionVerifier extends Service {
+  /**
+   * Verifies whether the given user is permitted to have a[nother] session or not.
+   *
+   * @param username the user who needs verification
+   * @param JWToken  the token which the user will use in the session
+   * @return true if the user is allowed to have a[nother] session, false if the user is not allowed to have a[nother] session
+   */
+  boolean verifySessionForUser(String username, JWT JWToken);
+
+  void sessionEndedForUser(String username, String token);
+
+}