You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by al...@apache.org on 2020/07/24 20:28:22 UTC

[nifi] branch main updated: NIFI-7568 - Applied Kerberos mappings to authentication requests. Kerberos mappings should now be applied correctly in H2 database for username/password based login. Added tests. Logout now deletes signing key by key ID rather than identity. Validate token expiration now uses mapped identity instead, which allows logging of the mapped identity. Updated delete key to expect only 0 or 1 keys deleted.

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

alopresto pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new d846a74  NIFI-7568 - Applied Kerberos mappings to authentication requests. Kerberos mappings should now be applied correctly in H2 database for username/password based login. Added tests. Logout now deletes signing key by key ID rather than identity. Validate token expiration now uses mapped identity instead, which allows logging of the mapped identity. Updated delete key to expect only 0 or 1 keys deleted.
d846a74 is described below

commit d846a74730edb4d142e0a3051c60e9fec33f1f09
Author: Nathan Gough <th...@gmail.com>
AuthorDate: Mon Jun 29 19:18:22 2020 -0400

    NIFI-7568 - Applied Kerberos mappings to authentication requests. Kerberos mappings should now be applied correctly in H2 database for username/password based login. Added tests.
    Logout now deletes signing key by key ID rather than identity.
    Validate token expiration now uses mapped identity instead, which allows logging of the mapped identity.
    Updated delete key to expect only 0 or 1 keys deleted.
    
    This closes #4416.
    
    Signed-off-by: Andy LoPresto <al...@apache.org>
---
 .../java/org/apache/nifi/admin/dao/KeyDAO.java     |   6 +-
 .../apache/nifi/admin/dao/impl/StandardKeyDAO.java |   8 +-
 .../org/apache/nifi/admin/service/KeyService.java  |   4 +-
 ...{DeleteKeysAction.java => DeleteKeyAction.java} |  17 +-
 .../admin/service/impl/StandardKeyService.java     |  26 +--
 .../src/main/java/org/apache/nifi/key/Key.java     |   9 +
 .../org/apache/nifi/web/api/AccessResource.java    |  19 +-
 .../accesscontrol/ITAccessTokenEndpoint.java       |   9 +-
 .../nifi/integration/util/NiFiTestAuthorizer.java  |   4 +-
 .../util/NiFiTestLoginIdentityProvider.java        |   1 +
 .../apache/nifi/integration/util/NiFiTestUser.java |   8 +
 .../nifi-mapped-identities.properties              | 144 +++++++++++++++
 .../web/security/jwt/JwtAuthenticationFilter.java  |   2 +-
 .../apache/nifi/web/security/jwt/JwtService.java   |  27 ++-
 .../jwt/JwtAuthenticationProviderTest.java         | 132 ++++++++++++++
 .../nifi/web/security/jwt/JwtServiceTest.java      | 202 ++++++++++++++++++---
 .../nifi/web/security/jwt/TestKeyService.java      |  73 ++++++++
 17 files changed, 613 insertions(+), 78 deletions(-)

diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java
index 9626445..3cfaf2f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java
@@ -48,9 +48,9 @@ public interface KeyDAO {
     Key createKey(String identity);
 
     /**
-     * Deletes all keys for the specified user identity.
+     * Deletes a key using the key ID.
      *
-     * @param identity The user identity
+     * @param keyId The key ID
      */
-    void deleteKeys(String identity);
+    Integer deleteKey(Integer keyId);
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
index 44d9716..28d090d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
@@ -47,7 +47,7 @@ public class StandardKeyDAO implements KeyDAO {
             + ")";
 
     private static final String DELETE_KEYS = "DELETE FROM KEY "
-            + "WHERE IDENTITY = ?";
+            + "WHERE ID = ?";
 
     private final Connection connection;
 
@@ -156,13 +156,13 @@ public class StandardKeyDAO implements KeyDAO {
     }
 
     @Override
-    public void deleteKeys(String identity) {
+    public Integer deleteKey(Integer keyId) {
         PreparedStatement statement = null;
         try {
             // add each authority for the specified user
             statement = connection.prepareStatement(DELETE_KEYS);
-            statement.setString(1, identity);
-            statement.executeUpdate();
+            statement.setInt(1, keyId);
+            return statement.executeUpdate();
         } catch (SQLException sqle) {
             throw new DataAccessException(sqle);
         } catch (DataAccessException dae) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java
index 4543475..5ac10cb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java
@@ -43,7 +43,7 @@ public interface KeyService {
     /**
      * Deletes keys for the specified identity.
      *
-     * @param identity The user identity
+     * @param keyId The user's key ID
      */
-    void deleteKey(String identity);
+    void deleteKey(Integer keyId);
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeysAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java
similarity index 72%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeysAction.java
rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java
index 6b8a2d5..c72d2c3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeysAction.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java
@@ -23,23 +23,22 @@ import org.apache.nifi.admin.dao.KeyDAO;
 /**
  *
  */
-public class DeleteKeysAction implements AdministrationAction<Void> {
+public class DeleteKeyAction implements AdministrationAction<Integer> {
 
-    private final String identity;
+    private final Integer keyId;
 
     /**
-     * Creates a new transactions for deleting keys for specified user.
+     * Creates a new transactions for deleting keys for a specified user based on their keyId.
      *
-     * @param identity user identity
+     * @param keyId user identity
      */
-    public DeleteKeysAction(String identity) {
-        this.identity = identity;
+    public DeleteKeyAction(Integer keyId) {
+        this.keyId = keyId;
     }
 
     @Override
-    public Void execute(DAOFactory daoFactory) throws DataAccessException {
+    public Integer execute(DAOFactory daoFactory) throws DataAccessException {
         final KeyDAO keyDao = daoFactory.getKeyDAO();
-        keyDao.deleteKeys(identity);
-        return null;
+        return keyDao.deleteKey(keyId);
     }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
index 7a7f62d..8f4198a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
@@ -19,7 +19,7 @@ package org.apache.nifi.admin.service.impl;
 import org.apache.nifi.admin.dao.DataAccessException;
 import org.apache.nifi.admin.service.AdministrationException;
 import org.apache.nifi.admin.service.KeyService;
-import org.apache.nifi.admin.service.action.DeleteKeysAction;
+import org.apache.nifi.admin.service.action.DeleteKeyAction;
 import org.apache.nifi.admin.service.action.GetKeyByIdAction;
 import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
 import org.apache.nifi.admin.service.transaction.Transaction;
@@ -35,7 +35,7 @@ import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 /**
- *
+ * This key service manages user JWT signing keys in the H2 user database.
  */
 public class StandardKeyService implements KeyService {
 
@@ -51,7 +51,7 @@ public class StandardKeyService implements KeyService {
     @Override
     public Key getKey(int id) {
         Transaction transaction = null;
-        Key key = null;
+        Key key;
 
         readLock.lock();
         try {
@@ -81,7 +81,7 @@ public class StandardKeyService implements KeyService {
     @Override
     public Key getOrCreateKey(String identity) {
         Transaction transaction = null;
-        Key key = null;
+        Key key;
 
         writeLock.lock();
         try {
@@ -109,7 +109,7 @@ public class StandardKeyService implements KeyService {
     }
 
     @Override
-    public void deleteKey(String identity) {
+    public void deleteKey(Integer keyId) {
         Transaction transaction = null;
 
         writeLock.lock();
@@ -118,11 +118,16 @@ public class StandardKeyService implements KeyService {
             transaction = transactionBuilder.start();
 
             // delete the keys
-            DeleteKeysAction deleteKeys = new DeleteKeysAction(identity);
-            transaction.execute(deleteKeys);
-
-            // commit the transaction
-            transaction.commit();
+            DeleteKeyAction deleteKey = new DeleteKeyAction(keyId);
+            Integer rowsDeleted = transaction.execute(deleteKey);
+
+            // commit the transaction if we found one and only one matching keyId/user identity
+            if (rowsDeleted == 1) {
+                transaction.commit();
+            } else {
+                rollback(transaction);
+                throw new AdministrationException("Unable to find user key for key ID " + keyId + " to remove token.");
+            }
         } catch (TransactionException | DataAccessException te) {
             rollback(transaction);
             throw new AdministrationException(te);
@@ -157,5 +162,4 @@ public class StandardKeyService implements KeyService {
     public void setProperties(NiFiProperties properties) {
         this.properties = properties;
     }
-
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java
index 9ce7a9a..ce1c6d5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java
@@ -66,4 +66,13 @@ public class Key implements Serializable {
         this.key = key;
     }
 
+    public Key(int id, String identity, String key) {
+        this.id = id;
+        this.identity = identity;
+        this.key = key;
+    }
+
+    public Key() {
+    }
+
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index 0e3e70f..5ee98cd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -60,6 +60,7 @@ import org.apache.nifi.authorization.AccessDeniedException;
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.authorization.user.NiFiUserDetails;
 import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.authorization.util.IdentityMappingUtil;
 import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
 import org.apache.nifi.web.api.dto.AccessStatusDTO;
@@ -659,11 +660,12 @@ public class AccessResource extends ApplicationResource {
 
                 final String expirationFromProperties = properties.getKerberosAuthenticationExpiration();
                 long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS);
-                final String identity = authentication.getName();
-                expiration = validateTokenExpiration(expiration, identity);
+                final String rawIdentity = authentication.getName();
+                String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties));
+                expiration = validateTokenExpiration(expiration, mappedIdentity);
 
                 // create the authentication token
-                final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(identity, expiration, "KerberosService");
+                final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, "KerberosService");
 
                 // generate JWT for response
                 final String token = jwtService.generateSignedToken(loginAuthenticationToken);
@@ -729,10 +731,12 @@ public class AccessResource extends ApplicationResource {
         try {
             // attempt to authenticate
             final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password));
-            long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), authenticationResponse.getIdentity());
+            final String rawIdentity = authenticationResponse.getIdentity();
+            String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties));
+            long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), mappedIdentity);
 
             // create the authentication token
-            loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, authenticationResponse.getIssuer());
+            loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, authenticationResponse.getIssuer());
         } catch (final InvalidLoginCredentialsException ilce) {
             throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
         } catch (final IdentityAccessException iae) {
@@ -769,10 +773,11 @@ public class AccessResource extends ApplicationResource {
 
         String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
 
-        if(userIdentity != null && !userIdentity.isEmpty()) {
+        if (userIdentity != null && !userIdentity.isEmpty()) {
             try {
                 logger.info("Logging out user " + userIdentity);
-                jwtService.logOut(userIdentity);
+                jwtService.logOutUsingAuthHeader(httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION));
+                logger.info("Successfully logged out user" + userIdentity);
                 return generateOkResponse().build();
             } catch (final JwtException e) {
                 logger.error("Logout of user " + userIdentity + " failed due to: " + e.getMessage());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
index 9f1ae29..a97a744 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
@@ -44,13 +44,13 @@ public class ITAccessTokenEndpoint {
 
     private static OneWaySslAccessControlHelper helper;
 
-    private final String user = "unregistered-user@nifi";
+    private final String user = "nifiadmin@nifi.apache.org";
     private final String password = "password";
     private static final String CLIENT_ID = "token-endpoint-id";
 
     @BeforeClass
     public static void setup() throws Exception {
-        helper = new OneWaySslAccessControlHelper();
+        helper = new OneWaySslAccessControlHelper("src/test/resources/access-control/nifi-mapped-identities.properties");
     }
 
     // -----------
@@ -92,7 +92,7 @@ public class ITAccessTokenEndpoint {
     public void testCreateProcessorUsingToken() throws Exception {
         String url = helper.getBaseUrl() + "/access/token";
 
-        Response response = helper.getUser().testCreateToken(url, "user@nifi", "whatever");
+        Response response = helper.getUser().testCreateToken(url, user, password);
 
         // ensure the request is successful
         Assert.assertEquals(201, response.getStatus());
@@ -154,7 +154,7 @@ public class ITAccessTokenEndpoint {
 
         Response response = helper.getUser().testCreateToken(url, "user@nifi", "not a real password");
 
-        // ensure the request is successful
+        // ensure the request is not successful
         Assert.assertEquals(400, response.getStatus());
     }
 
@@ -262,7 +262,6 @@ public class ITAccessTokenEndpoint {
         // verify unregistered
         Assert.assertEquals("ACTIVE", accessStatus.getStatus());
 
-
         // log out
         response = helper.getUser().testDeleteWithHeaders(logoutUrl, headers);
         Assert.assertEquals(200, response.getStatus());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java
index 3d56591..956eeec 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java
@@ -44,7 +44,7 @@ public class NiFiTestAuthorizer implements Authorizer {
     public static final String PRIVILEGED_USER_DN = "privileged@nifi";
     public static final String EXECUTED_CODE_USER_DN = "executecode@nifi";
 
-    public static final String TOKEN_USER = "user@nifi";
+    public static final String MAPPED_TOKEN_USER = "nifiadmin";
 
     /**
      * Creates a new FileAuthorizationProvider.
@@ -83,7 +83,7 @@ public class NiFiTestAuthorizer implements Authorizer {
         }
 
         // allow the token user
-        if (TOKEN_USER.equals(request.getIdentity())) {
+        if (MAPPED_TOKEN_USER.equals(request.getIdentity())) {
             return AuthorizationResult.approved();
         }
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java
index 508a0d1..a244c19 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java
@@ -43,6 +43,7 @@ public class NiFiTestLoginIdentityProvider implements LoginIdentityProvider {
         users = new HashMap<>();
         users.put("user@nifi", "whatever");
         users.put("unregistered-user@nifi", "password");
+        users.put("nifiadmin@nifi.apache.org", "password");
     }
 
     private void checkUser(final String user, final String password) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java
index 72d59ba..c99895a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java
@@ -17,6 +17,8 @@
 package org.apache.nifi.integration.util;
 
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.Entity;
@@ -25,6 +27,7 @@ import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedHashMap;
 import javax.ws.rs.core.Response;
+import java.util.Arrays;
 import java.util.Map;
 
 /**
@@ -34,8 +37,11 @@ public class NiFiTestUser {
 
     private final Client client;
     private final String proxyDn;
+    private final Logger logger;
 
     public NiFiTestUser(Client client, String proxyDn) {
+        logger = LoggerFactory.getLogger(NiFiTestUser.class);
+
         this.client = client;
         if (proxyDn != null) {
             this.proxyDn = ProxiedEntitiesUtils.formatProxyDn(proxyDn);
@@ -156,6 +162,8 @@ public class NiFiTestUser {
             }
         }
 
+        logger.info("POST Request to URL: " + url + " with headers: " + Arrays.toString(headers.entrySet().toArray()));
+
         // perform the request
         return resourceBuilder.post(Entity.json(entity));
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/access-control/nifi-mapped-identities.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/access-control/nifi-mapped-identities.properties
new file mode 100644
index 0000000..858ea8e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/access-control/nifi-mapped-identities.properties
@@ -0,0 +1,144 @@
+# 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.
+
+
+
+# Core Properties #
+nifi.flow.configuration.file=target/test-classes/access-control/flow.xml.gz
+nifi.flow.configuration.archive.dir=target/archive
+nifi.flowcontroller.autoResumeState=true
+nifi.flowcontroller.graceful.shutdown.period=10 sec
+nifi.flowservice.writedelay.interval=2 sec
+
+# Mapped Identities #
+nifi.security.identity.mapping.pattern.kerb=^(.*?)@(.*?)$
+nifi.security.identity.mapping.value.kerb=$1
+
+nifi.authorizer.configuration.file=target/test-classes/access-control/authorizers.xml
+nifi.login.identity.provider.configuration.file=target/test-classes/access-control/login-identity-providers.xml
+nifi.templates.directory=target/test-classes/access-control/templates
+nifi.ui.banner.text=TEST BANNER
+nifi.ui.autorefresh.interval=30 sec
+nifi.nar.library.directory=target/test-classes/access-control/lib
+nifi.nar.working.directory=target/test-classes/access-control/nar
+
+nifi.state.management.configuration.file=target/test-classes/access-control/state-management.xml
+nifi.state.management.embedded.zookeeper.start=false
+nifi.state.management.embedded.zookeeper.properties=
+nifi.state.management.embedded.zookeeper.max.instances=3
+nifi.state.management.provider.local=local-provider
+nifi.state.management.provider.cluster=
+
+# H2 Settings
+nifi.database.directory=target/test-classes/database_repository
+nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE
+
+# FlowFile Repository
+nifi.provenance.repository.implementation=org.apache.nifi.provenance.VolatileProvenanceRepository
+nifi.flowfile.repository.directory=target/test-classes/flowfile_repository
+nifi.flowfile.repository.partitions=256
+nifi.flowfile.repository.checkpoint.interval=2 mins
+nifi.queue.swap.threshold=20000
+nifi.swap.storage.directory=target/test-classes/flowfile_repository/swap
+nifi.swap.in.period=5 sec
+nifi.swap.in.threads=1
+nifi.swap.out.period=5 sec
+nifi.swap.out.threads=4
+
+# Content Repository
+nifi.content.claim.max.appendable.size=10 MB
+nifi.content.claim.max.flow.files=100
+nifi.content.repository.directory.default=target/test-classes/content_repository
+nifi.content.repository.archive.enabled=false
+
+# Provenance Repository Properties
+nifi.provenance.repository.directory.default=./target/provenance_repository
+nifi.provenance.repository.query.threads=2
+nifi.provenance.repository.max.storage.time=24 hours
+nifi.provenance.repository.max.storage.size=1 GB
+nifi.provenance.repository.rollover.time=30 secs
+nifi.provenance.repository.rollover.size=100 MB
+
+# Component Status Repository
+nifi.components.status.repository.implementation=org.apache.nifi.controller.status.history.VolatileComponentStatusRepository
+nifi.components.status.repository.buffer.size=288
+nifi.components.status.snapshot.frequency=10 secs
+
+# Site to Site properties
+#For testing purposes. Default value should actually be empty!
+nifi.remote.input.host=
+nifi.remote.input.socket.port=
+nifi.remote.input.secure=false
+
+# web properties #
+nifi.web.war.directory=target/test-classes/lib
+nifi.web.http.host=
+nifi.web.http.port=
+nifi.web.https.host=
+nifi.web.https.port=8443
+nifi.web.jetty.working.directory=target/test-classes/access-control/jetty
+
+# security properties #
+nifi.sensitive.props.key=REPLACE_ME
+nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL
+nifi.sensitive.props.provider=BC
+
+nifi.security.keystore=target/test-classes/access-control/keystore.jks
+nifi.security.keystoreType=JKS
+nifi.security.keystorePasswd=passwordpassword
+nifi.security.keyPasswd=
+nifi.security.truststore=target/test-classes/access-control/truststore.jks
+nifi.security.truststoreType=JKS
+nifi.security.truststorePasswd=passwordpassword
+nifi.security.user.login.identity.provider=test-provider
+nifi.security.user.authorizer=test-provider
+
+# cluster common properties (cluster manager and nodes must have same values) #
+nifi.cluster.protocol.heartbeat.interval=5 sec
+nifi.cluster.protocol.is.secure=false
+nifi.cluster.protocol.socket.timeout=30 sec
+nifi.cluster.protocol.connection.handshake.timeout=45 sec
+# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured #
+nifi.cluster.protocol.use.multicast=false
+nifi.cluster.protocol.multicast.address=
+nifi.cluster.protocol.multicast.port=
+nifi.cluster.protocol.multicast.service.broadcast.delay=500 ms
+nifi.cluster.protocol.multicast.service.locator.attempts=3
+nifi.cluster.protocol.multicast.service.locator.attempts.delay=1 sec
+
+# cluster node properties (only configure for cluster nodes) #
+nifi.cluster.is.node=false
+nifi.cluster.node.address=
+nifi.cluster.node.protocol.port=
+nifi.cluster.node.protocol.threads=2
+# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx #
+nifi.cluster.node.unicast.manager.address=
+nifi.cluster.node.unicast.manager.protocol.port=
+nifi.cluster.node.unicast.manager.authority.provider.port=
+
+# cluster manager properties (only configure for cluster manager) #
+nifi.cluster.is.manager=false
+nifi.cluster.manager.address=
+nifi.cluster.manager.protocol.port=
+nifi.cluster.manager.authority.provider.port=
+nifi.cluster.manager.authority.provider.threads=10
+nifi.cluster.manager.node.firewall.file=
+nifi.cluster.manager.node.event.history.size=10
+nifi.cluster.manager.node.api.connection.timeout=30 sec
+nifi.cluster.manager.node.api.read.timeout=30 sec
+nifi.cluster.manager.node.api.request.threads=10
+nifi.cluster.manager.flow.retrieval.delay=5 sec
+nifi.cluster.manager.protocol.threads=10
+nifi.cluster.manager.safemode.duration=0 sec
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
index 0d21a8a..9a25ff3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
@@ -63,7 +63,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
         return matcher.matches();
     }
 
-    private String getTokenFromHeader(String authenticationHeader) {
+    public static String getTokenFromHeader(String authenticationHeader) {
         Matcher matcher = tokenPattern.matcher(authenticationHeader);
         if(matcher.matches()) {
             return matcher.group(1);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
index ecbfc67..9569631 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
@@ -169,16 +169,33 @@ public class JwtService {
         }
     }
 
-    public void logOut(String userIdentity) {
-        if (userIdentity == null || userIdentity.isEmpty()) {
-            throw new JwtException("Log out failed: The user identity was not present in the request token to log out user.");
+    /**
+     * Log out the authenticated user using the 'kid' (Key ID) claim from the base64 encoded JWT
+     *
+     * @param token a signed, base64 encoded, JSON Web Token in form HEADER.PAYLOAD.SIGNATURE
+     * @throws JwtException if there is a problem with the token input
+     * @throws Exception if there is an issue logging the user out
+     */
+    public void logOut(String token) {
+        Jws<Claims> claims = parseTokenFromBase64EncodedString(token);
+
+        // Get the key ID from the claims
+        final Integer keyId = claims.getBody().get(KEY_ID_CLAIM, Integer.class);
+
+        if (keyId == null) {
+            throw new JwtException("The key claim (kid) was not present in the request token to log out user.");
         }
 
         try {
-            keyService.deleteKey(userIdentity);
+            keyService.deleteKey(keyId);
         } catch (Exception e) {
-            logger.error("Unable to log out user: " + userIdentity + ". Failed to remove their token from database.");
+            logger.error("The key with key ID: " + keyId + " failed to be removed from the user database.");
             throw e;
         }
     }
+
+    public void logOutUsingAuthHeader(String authorizationHeader) {
+        String base64EncodedToken = JwtAuthenticationFilter.getTokenFromHeader(authorizationHeader);
+        logOut(base64EncodedToken);
+    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java
new file mode 100644
index 0000000..8a1dbd4
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.nifi.web.security.jwt;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.properties.StandardNiFiProperties;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+
+public class JwtAuthenticationProviderTest {
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    private final static int EXPIRATION_MILLIS = 60000;
+    private final static String CLIENT_ADDRESS = "127.0.0.1";
+    private final static String ADMIN_IDENTITY = "nifiadmin";
+    private final static String REALMED_ADMIN_KERBEROS_IDENTITY = "nifiadmin@nifi.apache.org";
+
+    private final static String UNKNOWN_TOKEN = "eyJhbGciOiJIUzI1NiJ9" +
+            ".eyJzdWIiOiJ1bmtub3duX3Rva2VuIiwiaXNzIjoiS2VyYmVyb3NQcm9" +
+            "2aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWR" +
+            "fdXNlcm5hbWUiOiJ1bmtub3duX3Rva2VuIiwia2lkIjoxLCJleHAiOjE" +
+            "2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.PpOGx3Ul5ydokOOuzKd" +
+            "aRKv1kxy6Q4jGy7rBPU8PqxY";
+
+    private NiFiProperties properties;
+
+
+    private JwtService jwtService;
+    private JwtAuthenticationProvider jwtAuthenticationProvider;
+
+    @Before
+    public void setUp() throws Exception {
+        TestKeyService keyService = new TestKeyService();
+        jwtService = new JwtService(keyService);
+
+        // Set up Kerberos identity mappings
+        Properties props = new Properties();
+        props.put(properties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX, "^(.*?)@(.*?)$");
+        props.put(properties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX, "$1");
+        properties = new StandardNiFiProperties(props);
+
+        jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, mock(Authorizer.class));
+    }
+
+    @Test
+    public void testAdminIdentityAndTokenIsValid() throws Exception {
+        // Arrange
+        LoginAuthenticationToken loginAuthenticationToken =
+                new LoginAuthenticationToken(ADMIN_IDENTITY,
+                                             EXPIRATION_MILLIS,
+                                      "MockIdentityProvider");
+        String token = jwtService.generateSignedToken(loginAuthenticationToken);
+        final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS);
+
+        // Act
+        final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request);
+        final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal();
+
+        // Assert
+        assertEquals(ADMIN_IDENTITY, details.getUsername());
+    }
+
+    @Test
+    public void testKerberosRealmedIdentityAndTokenIsValid() throws Exception {
+        // Arrange
+        LoginAuthenticationToken loginAuthenticationToken =
+                new LoginAuthenticationToken(REALMED_ADMIN_KERBEROS_IDENTITY,
+                        EXPIRATION_MILLIS,
+                        "MockIdentityProvider");
+        String token = jwtService.generateSignedToken(loginAuthenticationToken);
+        final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS);
+
+        // Act
+        final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request);
+        final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal();
+
+        // Assert
+        // Check we now have the mapped identity
+        assertEquals(ADMIN_IDENTITY, details.getUsername());
+    }
+
+    @Test
+    public void testFailToAuthenticateWithUnknownToken() throws Exception {
+        // Arrange
+        expectedException.expect(InvalidAuthenticationException.class);
+        expectedException.expectMessage("Unable to validate the access token.");
+
+        // Generate a token with a known token
+        LoginAuthenticationToken loginAuthenticationToken =
+                new LoginAuthenticationToken(ADMIN_IDENTITY,
+                        EXPIRATION_MILLIS,
+                        "MockIdentityProvider");
+        jwtService.generateSignedToken(loginAuthenticationToken);
+
+        // Act
+        // Try to  authenticate with an unknown token
+        final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(UNKNOWN_TOKEN, CLIENT_ADDRESS);
+        final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request);
+
+        // Assert
+        // Expect exception
+    }
+
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
index 0727ccf..70f9a6d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.web.security.jwt;
 
+import static org.apache.nifi.util.NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX;
+import static org.apache.nifi.util.NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -28,8 +30,11 @@ import io.jsonwebtoken.JwtException;
 import java.nio.charset.StandardCharsets;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 import org.apache.commons.codec.binary.Base64;
@@ -37,7 +42,10 @@ import org.apache.nifi.admin.service.AdministrationException;
 import org.apache.nifi.admin.service.KeyService;
 import org.apache.nifi.authorization.user.NiFiUserDetails;
 import org.apache.nifi.authorization.user.StandardNiFiUser;
+import org.apache.nifi.authorization.util.IdentityMapping;
+import org.apache.nifi.authorization.util.IdentityMappingUtil;
 import org.apache.nifi.key.Key;
+import org.apache.nifi.properties.StandardNiFiProperties;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.codehaus.jettison.json.JSONObject;
 import org.junit.After;
@@ -58,6 +66,9 @@ public class JwtServiceTest {
 
     private static final Logger logger = LoggerFactory.getLogger(JwtServiceTest.class);
 
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
     /**
      * These constant strings were generated using the tool at http://jwt.io
      */
@@ -132,17 +143,30 @@ public class JwtServiceTest {
             + "6MSwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.6kDjDanA"
             + "g0NQDb3C8FmgbBAYDoIfMAEkF4WMVALsbJA";
 
+    private static final String KERBEROS_PROVIDER_TOKEN = "eyJhbGciOiJIUzI1NiJ9" +
+            ".eyJzdWIiOiJuaWZpYWRtaW5AbmlmaS5hcGFjaGUub3JnIiwiaXNzIjoiS2VyYmVyb" +
+            "3NQcm92aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWRfdXN" +
+            "lcm5hbWUiOiJuaWZpYWRtaW5AbmlmaS5hcGFjaGUub3JnIiwia2lkIjo2LCJleHAiO" +
+            "jE2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.Mmnx6ssdjQ5_5VVRiyPWU60Oegc" +
+            "NdhWezaKKNK48Mew";
+
     private static final String DEFAULT_HEADER = "{\"alg\":\"HS256\"}";
     private static final String DEFAULT_IDENTITY = "alopresto";
+    private static final String REALMED_KERBEROS_IDENTITY = "nifiadmin@nifi.apache.org";
+    private static final String KERBEROS_IDENTITY = "nifiadmin";
 
     private static final String TOKEN_DELIMITER = ".";
 
     private static final String HMAC_SECRET = "test_hmac_shared_secret";
 
+    private static List<IdentityMapping> identityMappings;
+
     private KeyService mockKeyService;
+    private KeyService testKeyService;
 
     // Class under test
     private JwtService jwtService;
+    private JwtService jwtServiceUsingTestKeyService;
 
     public static String generateHS256Token(String rawHeader, String rawPayload, boolean isValid, boolean isSigned) {
         return generateHS256Token(rawHeader, rawPayload, HMAC_SECRET, isValid, isSigned);
@@ -189,7 +213,7 @@ public class JwtServiceTest {
             Key answerKey = key;
             @Override
             public Key answer(InvocationOnMock invocation) throws Throwable {
-                if(invocation.getMethod().equals(KeyService.class.getMethod("deleteKey", String.class))) {
+                if(invocation.getMethod().equals(KeyService.class.getMethod("deleteKey", Integer.class))) {
                     answerKey = null;
                 }
                 return answerKey;
@@ -210,8 +234,15 @@ public class JwtServiceTest {
         mockKeyService = mock(KeyService.class);
         when(mockKeyService.getKey(anyInt())).thenAnswer(keyAnswer);
         when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key);
-        doAnswer(keyAnswer).when(mockKeyService).deleteKey(anyString());
+        doAnswer(keyAnswer).when(mockKeyService).deleteKey(anyInt());
+
         jwtService = new JwtService(mockKeyService);
+        jwtServiceUsingTestKeyService = new JwtService(new TestKeyService());
+
+        Properties props = new Properties();
+        props.setProperty(SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX+"kerb",  "^(.*?)@(.*?)$");
+        props.setProperty(SECURITY_IDENTITY_MAPPING_VALUE_PREFIX+"kerb", "$1");
+        identityMappings = IdentityMappingUtil.getIdentityMappings(new StandardNiFiProperties(props));
     }
 
     @After
@@ -226,12 +257,25 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         assertEquals("Identity", DEFAULT_IDENTITY, identity);
     }
 
+    @Test
+    public void testShouldGetAuthenticationForValidKerberosToken() throws Exception {
+        // Arrange
+        String token = KERBEROS_PROVIDER_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.info("Extracted identity: " + identity);
+
+        // Assert
+        assertEquals("Identity", REALMED_KERBEROS_IDENTITY, identity);
+    }
+
     @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForInvalidToken() throws Exception {
         // Arrange
@@ -239,7 +283,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -252,7 +296,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -265,7 +309,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -278,7 +322,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -291,7 +335,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -304,7 +348,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -318,7 +362,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -331,7 +375,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -344,7 +388,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -357,7 +401,7 @@ public class JwtServiceTest {
 
         // Act
         String identity = jwtService.getAuthenticationFromToken(token);
-        logger.debug("Extracted identity: " + identity);
+        logger.info("Extracted identity: " + identity);
 
         // Assert
         // Should fail
@@ -372,7 +416,7 @@ public class JwtServiceTest {
         LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto",
                 EXPIRATION_MILLIS,
                 "MockIdentityProvider");
-        logger.debug("Generating token for " + loginAuthenticationToken);
+        logger.info("Generating token for " + loginAuthenticationToken);
 
         final String EXPECTED_HEADER = DEFAULT_HEADER;
 
@@ -381,7 +425,7 @@ public class JwtServiceTest {
 
         // Act
         String token = jwtService.generateSignedToken(loginAuthenticationToken);
-        logger.debug("Generated JWT: " + token);
+        logger.info("Generated JWT: " + token);
 
         // Run after the SUT generates the token to ensure the same issued at time
         // Split the token, decode the middle section, and form a new String
@@ -390,7 +434,7 @@ public class JwtServiceTest {
                 DECODED_PAYLOAD.length() - 1));
         logger.trace("Actual token was issued at " + ISSUED_AT_SEC);
 
-        // Always use LinkedHashMap to enforce order of the keys because the signature depends on order
+        // Always use LinkedHashMap to enforce order of the signingKeys because the signature depends on order
         Map<String, Object> claims = new LinkedHashMap<>();
         claims.put("sub", "alopresto");
         claims.put("iss", "MockIdentityProvider");
@@ -403,7 +447,7 @@ public class JwtServiceTest {
 
         final String EXPECTED_PAYLOAD = new JSONObject(claims).toString();
         final String EXPECTED_TOKEN_STRING = generateHS256Token(EXPECTED_HEADER, EXPECTED_PAYLOAD, true, true);
-        logger.debug("Expected JWT: " + EXPECTED_TOKEN_STRING);
+        logger.info("Expected JWT: " + EXPECTED_TOKEN_STRING);
 
         // Assert
         assertEquals("JWT token", EXPECTED_TOKEN_STRING, token);
@@ -413,7 +457,7 @@ public class JwtServiceTest {
     public void testShouldNotGenerateTokenWithNullAuthenticationToken() throws Exception {
         // Arrange
         LoginAuthenticationToken nullLoginAuthenticationToken = null;
-        logger.debug("Generating token for " + nullLoginAuthenticationToken);
+        logger.info("Generating token for " + nullLoginAuthenticationToken);
 
         // Act
         jwtService.generateSignedToken(nullLoginAuthenticationToken);
@@ -428,7 +472,7 @@ public class JwtServiceTest {
         final int EXPIRATION_MILLIS = 60000;
         LoginAuthenticationToken emptyIdentityLoginAuthenticationToken = new LoginAuthenticationToken("",
                 EXPIRATION_MILLIS, "MockIdentityProvider");
-        logger.debug("Generating token for " + emptyIdentityLoginAuthenticationToken);
+        logger.info("Generating token for " + emptyIdentityLoginAuthenticationToken);
 
         // Act
         jwtService.generateSignedToken(emptyIdentityLoginAuthenticationToken);
@@ -443,7 +487,7 @@ public class JwtServiceTest {
         final int EXPIRATION_MILLIS = 60000;
         LoginAuthenticationToken nullIdentityLoginAuthenticationToken = new LoginAuthenticationToken(null,
                 EXPIRATION_MILLIS, "MockIdentityProvider");
-        logger.debug("Generating token for " + nullIdentityLoginAuthenticationToken);
+        logger.info("Generating token for " + nullIdentityLoginAuthenticationToken);
 
         // Act
         jwtService.generateSignedToken(nullIdentityLoginAuthenticationToken);
@@ -459,7 +503,7 @@ public class JwtServiceTest {
         LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY,
                 EXPIRATION_MILLIS,
                 "MockIdentityProvider");
-        logger.debug("Generating token for " + loginAuthenticationToken);
+        logger.info("Generating token for " + loginAuthenticationToken);
 
         // Set up the bad key service
         KeyService missingKeyService = mock(KeyService.class);
@@ -474,9 +518,6 @@ public class JwtServiceTest {
         // Should throw exception
     }
 
-    @Rule
-    public ExpectedException expectedException = ExpectedException.none();
-
     @Test
     public void testShouldLogOutUser() throws Exception {
         // Arrange
@@ -488,16 +529,51 @@ public class JwtServiceTest {
         LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY,
                 EXPIRATION_MILLIS,
                 "MockIdentityProvider");
-        logger.debug("Generating token for " + loginAuthenticationToken);
+        logger.info("Generating token for " + loginAuthenticationToken);
 
         // Act
         String token = jwtService.generateSignedToken(loginAuthenticationToken);
-        logger.debug("Generated JWT: " + token);
+        logger.info("Generated JWT: " + token);
+        logger.info("Validating token...");
         String authID = jwtService.getAuthenticationFromToken(token);
         assertEquals(DEFAULT_IDENTITY, authID);
-        logger.debug("Logging out user: " + DEFAULT_IDENTITY);
+        logger.info("Token was valid");
+        logger.info("Logging out user: " + authID);
         jwtService.logOut(token);
-        logger.debug("Logged out user: " + DEFAULT_IDENTITY);
+        logger.info("Logged out user: " + authID);
+        logger.info("Checking that token is now invalid...");
+        jwtService.getAuthenticationFromToken(token);
+
+        // Assert
+        // Should throw exception when user is not found
+    }
+
+    @Test
+    public void testShouldLogOutUserUsingAuthHeader() throws Exception {
+        // Arrange
+        expectedException.expect(JwtException.class);
+        expectedException.expectMessage("Unable to validate the access token.");
+
+        // Token expires in 60 seconds
+        final int EXPIRATION_MILLIS = 60000;
+        LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY,
+                EXPIRATION_MILLIS,
+                "MockIdentityProvider");
+        logger.info("Generating token for " + loginAuthenticationToken);
+
+        // Act
+        String token = jwtService.generateSignedToken(loginAuthenticationToken);
+
+        logger.info("Generated JWT: " + token);
+        logger.info("Validating token...");
+        String authID = jwtService.getAuthenticationFromToken(token);
+        assertEquals(DEFAULT_IDENTITY, authID);
+        logger.info("Token was valid");
+        logger.info("Logging out user: " + authID);
+        String header = "Bearer " + token;
+        jwtService.logOutUsingAuthHeader(header);
+        logger.info("Logged out user: " + authID);
+        logger.info("Checking that token is now invalid...");
         jwtService.getAuthenticationFromToken(token);
 
         // Assert
@@ -508,7 +584,7 @@ public class JwtServiceTest {
     public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws Exception {
         // Arrange
         expectedException.expect(JwtException.class);
-        expectedException.expectMessage("Log out failed: The user identity was not present in the request token to log out user.");
+        expectedException.expectMessage("Unable to validate the access token.");
 
         // Act
         jwtService.logOut(null);
@@ -517,4 +593,72 @@ public class JwtServiceTest {
         // Should throw exception when authorization header is null
     }
 
+    @Test
+    public void testShouldLogOutKerberosUser() throws Exception {
+        // Arrange
+
+        expectedException.expect(JwtException.class);
+        expectedException.expectMessage("Unable to validate the access token.");
+
+        // Token expires in 60 seconds
+        final int EXPIRATION_MILLIS = 60000;
+        LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(KERBEROS_IDENTITY,
+                EXPIRATION_MILLIS,
+                "MockIdentityProvider");
+        logger.info("Generating token for " + loginAuthenticationToken);
+
+        // Act
+        String token = jwtServiceUsingTestKeyService.generateSignedToken(loginAuthenticationToken);
+        logger.info("Generated JWT: " + token);
+        logger.info("Validating token...");
+        String authID = jwtServiceUsingTestKeyService.getAuthenticationFromToken(token);
+        logger.info("Token was valid, unmapped user identity was: " + authID);
+        assertEquals(KERBEROS_IDENTITY, authID);
+        logger.info("Using identity mappings " + Arrays.toString(identityMappings.toArray()) + " to map identity: " + authID);
+        String mappedIdentity = IdentityMappingUtil.mapIdentity(authID, identityMappings);
+        logger.info("Logging out user with mapped identity: " + mappedIdentity);
+        jwtServiceUsingTestKeyService.logOut(mappedIdentity);
+        logger.info("Logged out user with mapped identity: " + mappedIdentity);
+        logger.info("Checking that token for " + mappedIdentity + " is now invalid...");
+        jwtServiceUsingTestKeyService.getAuthenticationFromToken(token);
+
+        // Assert
+        // Should throw exception when user is not found
+    }
+
+    @Test
+    public void testShouldLogOutRealmedKerberosUser() throws Exception {
+        // Arrange
+
+        expectedException.expect(JwtException.class);
+        expectedException.expectMessage("Unable to validate the access token.");
+
+        // Token expires in 60 seconds
+        final int EXPIRATION_MILLIS = 60000;
+        // map the kerberos identity before we create our token, just as is done in AccessResource
+        final String mappedIdentity = IdentityMappingUtil.mapIdentity(REALMED_KERBEROS_IDENTITY, identityMappings);
+
+        LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity,
+                EXPIRATION_MILLIS,
+                "MockIdentityProvider");
+        logger.info("Generating token for " + loginAuthenticationToken);
+
+        // Act
+        String token = jwtServiceUsingTestKeyService.generateSignedToken(loginAuthenticationToken);
+        logger.info("Generated JWT: " + token);
+        logger.info("Validating token...");
+        String authID = jwtServiceUsingTestKeyService.getAuthenticationFromToken(token);
+        logger.info("Token was valid, unmapped user identity was: " + authID);
+        assertEquals(KERBEROS_IDENTITY, authID);
+        logger.info("Using identity mappings " + Arrays.toString(identityMappings.toArray()) + " to map identity: " + authID);
+        logger.info("Logging out user with mapped identity: " + authID);
+        jwtServiceUsingTestKeyService.logOut(authID);
+        logger.info("Logged out user with mapped identity: " + authID);
+        logger.info("Checking that token for " + authID + " is now invalid...");
+        jwtServiceUsingTestKeyService.getAuthenticationFromToken(token);
+
+        // Assert
+        // Should throw exception when user is not found
+    }
+
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java
new file mode 100644
index 0000000..4d2f415
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java
@@ -0,0 +1,73 @@
+/*
+ * 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.nifi.web.security.jwt;
+
+import org.apache.nifi.admin.service.KeyService;
+import org.apache.nifi.key.Key;
+import org.h2.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.UUID;
+
+public class TestKeyService implements KeyService {
+
+    ArrayList<Key> signingKeys = new ArrayList<Key>();
+
+    public TestKeyService() {
+
+    }
+
+    @Override
+    public Key getKey(int id) {
+        Key key = null;
+        for(Key k : signingKeys) {
+            if(k.getId() == id) {
+                key = k;
+            }
+        }
+        return key;
+    }
+
+    @Override
+    public Key getOrCreateKey(String identity) {
+        for(Key k : signingKeys) {
+            if(StringUtils.equals(k.getIdentity(), identity)) {
+                return k;
+            }
+        }
+
+        Key key = generateKey(identity);
+        signingKeys.add(key);
+        return key;
+    }
+
+    @Override
+    public void deleteKey(Integer keyId) {
+        Key keyToRemove = null;
+        for(Key k : signingKeys) {
+            if(k.getId() == keyId) {
+                keyToRemove = k;
+            }
+        }
+        signingKeys.remove(keyToRemove);
+    }
+
+    private Key generateKey(String identity) {
+        Integer keyId = signingKeys.size();
+        return new Key(keyId, identity, UUID.randomUUID().toString());
+    }
+}