You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by mc...@apache.org on 2015/11/18 20:01:58 UTC

[1/6] nifi git commit: NIFI-655: - Fixed typo in error message for unrecognized authentication strategy.

Repository: nifi
Updated Branches:
  refs/heads/NIFI-655 16608aa85 -> c94d0271d


NIFI-655: - Fixed typo in error message for unrecognized authentication strategy.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/45b24a4b
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/45b24a4b
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/45b24a4b

Branch: refs/heads/NIFI-655
Commit: 45b24a4b6015e14525da0feb93d0a3dbc98180f1
Parents: 16608aa
Author: Andy LoPresto <an...@andylopresto.com>
Authored: Tue Nov 17 17:26:39 2015 -0800
Committer: Matt Gilman <ma...@gmail.com>
Committed: Wed Nov 18 08:30:44 2015 -0500

----------------------------------------------------------------------
 .../src/main/java/org/apache/nifi/ldap/LdapProvider.java       | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/45b24a4b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
index 3c26d9f..628fc02 100644
--- a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
@@ -58,6 +58,8 @@ public class LdapProvider extends AbstractLdapProvider {
 
         // connection time out
         final String rawConnectTimeout = configurationContext.getProperty("Connect Timeout");
+
+        // TODO: Refactor to utility method to remove duplicate code
         if (StringUtils.isNotBlank(rawConnectTimeout)) {
             try {
                 final Long connectTimeout = FormatUtils.getTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
@@ -89,7 +91,7 @@ public class LdapProvider extends AbstractLdapProvider {
         try {
             authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy);
         } catch (final IllegalArgumentException iae) {
-            throw new ProviderCreationException(String.format("Unrecgonized authentication strategy '%s'. Possible values are [%s]",
+            throw new ProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]",
                     rawAuthenticationStrategy, StringUtils.join(LdapAuthenticationStrategy.values(), ", ")));
         }
 
@@ -142,7 +144,7 @@ public class LdapProvider extends AbstractLdapProvider {
                                         sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
                                                 rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, TLS);
                                     } catch (final IllegalArgumentException iae) {
-                                        throw new ProviderCreationException(String.format("Unrecgonized client auth '%s'", rawClientAuth));
+                                        throw new ProviderCreationException(String.format("Unrecognized client auth '%s'", rawClientAuth));
                                     }
                                 }
                             }


[5/6] nifi git commit: NIFI-655: - Refactoring key service to expose the key id. - Handling client side expiration better. - Removing specialized active directory provider and abstract ldap provider.

Posted by mc...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
index 628fc02..cbd5ea4 100644
--- a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/LdapProvider.java
@@ -27,59 +27,72 @@ import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import javax.net.ssl.SSLContext;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.authentication.AuthenticationResponse;
+import org.apache.nifi.authentication.LoginCredentials;
+import org.apache.nifi.authentication.LoginIdentityProvider;
 import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
+import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext;
+import org.apache.nifi.authentication.exception.IdentityAccessException;
+import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
 import org.apache.nifi.authorization.exception.ProviderCreationException;
+import org.apache.nifi.authorization.exception.ProviderDestructionException;
 import org.apache.nifi.security.util.SslContextFactory;
-import org.apache.nifi.security.util.SslContextFactory.ClientAuth;
 import org.apache.nifi.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ldap.CommunicationException;
 import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy;
 import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
 import org.springframework.ldap.core.support.DigestMd5DirContextAuthenticationStrategy;
 import org.springframework.ldap.core.support.LdapContextSource;
 import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.BindAuthenticator;
 import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
 import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.ldap.search.LdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapUserDetails;
 
 /**
- * LDAP based implementation of a login identity provider.
+ * Abstract LDAP based implementation of a login identity provider.
  */
-public class LdapProvider extends AbstractLdapProvider {
+public class LdapProvider implements LoginIdentityProvider {
 
+    private static final Logger logger = LoggerFactory.getLogger(LdapProvider.class);
     private static final String TLS = "TLS";
 
-    @Override
-    protected AbstractLdapAuthenticationProvider getLdapAuthenticationProvider(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
-        final LdapContextSource context = new LdapContextSource();
+    private AbstractLdapAuthenticationProvider provider;
+    private long expiration;
 
-        final Map<String, Object> baseEnvironment = new HashMap<>();
-
-        // connection time out
-        final String rawConnectTimeout = configurationContext.getProperty("Connect Timeout");
+    @Override
+    public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
+    }
 
-        // TODO: Refactor to utility method to remove duplicate code
-        if (StringUtils.isNotBlank(rawConnectTimeout)) {
-            try {
-                final Long connectTimeout = FormatUtils.getTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
-                baseEnvironment.put("com.sun.jndi.ldap.connect.timeout", connectTimeout.toString());
-            } catch (final IllegalArgumentException iae) {
-                throw new ProviderCreationException(String.format("The Connect Timeout '%s' is not a valid time duration", rawConnectTimeout));
-            }
+    @Override
+    public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        final String rawExpiration = configurationContext.getProperty("Expiration Duration");
+        if (StringUtils.isBlank(rawExpiration)) {
+            throw new ProviderCreationException("The Expiration Duration must be specified.");
         }
 
-        // read time out
-        final String rawReadTimeout = configurationContext.getProperty("Read Timeout");
-        if (StringUtils.isNotBlank(rawReadTimeout)) {
-            try {
-                final Long readTimeout = FormatUtils.getTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
-                baseEnvironment.put("com.sun.jndi.ldap.read.timeout", readTimeout.toString());
-            } catch (final IllegalArgumentException iae) {
-                throw new ProviderCreationException(String.format("The Read Timeout '%s' is not a valid time duration", rawReadTimeout));
-            }
+        try {
+            expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS);
+        } catch (final IllegalArgumentException iae) {
+            throw new ProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration));
         }
 
+        final LdapContextSource context = new LdapContextSource();
+
+        final Map<String, Object> baseEnvironment = new HashMap<>();
+
+        // connect/read time out
+        setTimeout(configurationContext, baseEnvironment, "Connect Timeout", "com.sun.jndi.ldap.connect.timeout");
+        setTimeout(configurationContext, baseEnvironment, "Read Timeout", "com.sun.jndi.ldap.read.timeout");
+
         // set the base environment is necessary
         if (!baseEnvironment.isEmpty()) {
             context.setBaseEnvironmentProperties(baseEnvironment);
@@ -140,7 +153,7 @@ public class LdapProvider extends AbstractLdapProvider {
                                     sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, TLS);
                                 } else {
                                     try {
-                                        final ClientAuth clientAuth = ClientAuth.valueOf(rawClientAuth);
+                                        final SslContextFactory.ClientAuth clientAuth = SslContextFactory.ClientAuth.valueOf(rawClientAuth);
                                         sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
                                                 rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, TLS);
                                     } catch (final IllegalArgumentException iae) {
@@ -205,7 +218,56 @@ public class LdapProvider extends AbstractLdapProvider {
         }
 
         // create the underlying provider
-        final LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator);
-        return ldapAuthenticationProvider;
+        provider = new LdapAuthenticationProvider(authenticator);
     }
+
+    private void setTimeout(final LoginIdentityProviderConfigurationContext configurationContext,
+            final Map<String, Object> baseEnvironment,
+            final String configurationProperty,
+            final String environmentKey) {
+        
+        final String rawTimeout = configurationContext.getProperty(configurationProperty);
+        if (StringUtils.isNotBlank(rawTimeout)) {
+            try {
+                final Long timeout = FormatUtils.getTimeDuration(rawTimeout, TimeUnit.MILLISECONDS);
+                baseEnvironment.put(environmentKey, timeout.toString());
+            } catch (final IllegalArgumentException iae) {
+                throw new ProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout));
+            }
+        }
+    }
+    
+    @Override
+    public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException {
+        if (provider == null) {
+            throw new IdentityAccessException("The LDAP authentication provider is not initialized.");
+        }
+
+        try {
+            // perform the authentication
+            final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword());
+            final Authentication authentication = provider.authenticate(token);
+
+            // attempt to get the ldap user details to get the DN
+            if (authentication.getPrincipal() instanceof LdapUserDetails) {
+                final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
+                return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
+            } else {
+                return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
+            }
+        } catch (final CommunicationException | AuthenticationServiceException e) {
+            logger.error(e.getMessage());
+            if (logger.isDebugEnabled()) {
+                logger.debug(StringUtils.EMPTY, e);
+            }
+            throw new IdentityAccessException("Unable to query the configured directory server. See the logs for additional details.", e);
+        } catch (final BadCredentialsException bce) {
+            throw new InvalidLoginCredentialsException(bce.getMessage(), bce);
+        }
+    }
+
+    @Override
+    public final void preDestruction() throws ProviderDestructionException {
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider
index 2158cf7..b5ca1fe 100644
--- a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider
+++ b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider
@@ -12,5 +12,4 @@
 # 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.
-org.apache.nifi.ldap.LdapProvider
-org.apache.nifi.ldap.ActiveDirectoryProvider
\ No newline at end of file
+org.apache.nifi.ldap.LdapProvider
\ No newline at end of file


[6/6] nifi git commit: NIFI-655: - Refactoring key service to expose the key id. - Handling client side expiration better. - Removing specialized active directory provider and abstract ldap provider.

Posted by mc...@apache.org.
NIFI-655:
- Refactoring key service to expose the key id.
- Handling client side expiration better.
- Removing specialized active directory provider and abstract ldap provider.

Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/c94d0271
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/c94d0271
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/c94d0271

Branch: refs/heads/NIFI-655
Commit: c94d0271d990df6d6cd9a1dfee7b1706f451483c
Parents: 7d04dfe
Author: Matt Gilman <ma...@gmail.com>
Authored: Wed Nov 18 14:01:45 2015 -0500
Committer: Matt Gilman <ma...@gmail.com>
Committed: Wed Nov 18 14:01:45 2015 -0500

----------------------------------------------------------------------
 .../nifi/security/util/CertificateUtils.java    |  31 ++---
 .../java/org/apache/nifi/admin/dao/KeyDAO.java  |  18 ++-
 .../nifi/admin/dao/impl/StandardKeyDAO.java     |  67 ++++++++--
 .../apache/nifi/admin/service/KeyService.java   |   8 +-
 .../nifi/admin/service/action/GetKeyAction.java |  41 -------
 .../admin/service/action/GetKeyByIdAction.java  |  42 +++++++
 .../service/action/GetKeyByIdentityAction.java  |  42 +++++++
 .../service/action/GetOrCreateKeyAction.java    |   7 +-
 .../admin/service/impl/StandardKeyService.java  |  19 ++-
 .../src/main/java/org/apache/nifi/key/Key.java  |  69 +++++++++++
 .../org/apache/nifi/web/api/AccessResource.java |  82 ++++++-------
 .../InvalidAuthenticationExceptionMapper.java   |  44 +++++++
 .../src/main/resources/nifi-web-api-context.xml |   1 +
 .../InvalidAuthenticationException.java         |  35 ++++++
 .../web/security/NiFiAuthenticationFilter.java  |   5 +-
 .../security/jwt/JwtAuthenticationFilter.java   |   9 +-
 .../nifi/web/security/jwt/JwtService.java       |  20 ++-
 .../security/x509/X509AuthenticationFilter.java |   4 +-
 .../nifi/web/security/jwt/JwtServiceTest.java   |  11 +-
 .../webapp/js/nf/canvas/nf-canvas-header.js     |   2 +-
 .../src/main/webapp/js/nf/canvas/nf-canvas.js   |  14 +--
 .../src/main/webapp/js/nf/login/nf-login.js     |  12 +-
 .../src/main/webapp/js/nf/nf-common.js          | 112 ++++++++++-------
 .../src/main/webapp/js/nf/nf-dialog.js          |  23 ++--
 .../src/main/webapp/js/nf/nf-storage.js         |  75 ++++++++----
 .../apache/nifi/ldap/AbstractLdapProvider.java  | 106 ----------------
 .../nifi/ldap/ActiveDirectoryProvider.java      |  51 --------
 .../java/org/apache/nifi/ldap/LdapProvider.java | 122 ++++++++++++++-----
 ...he.nifi.authentication.LoginIdentityProvider |   3 +-
 29 files changed, 627 insertions(+), 448 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
index 5126933..ea3a6c6 100644
--- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/CertificateUtils.java
@@ -87,30 +87,21 @@ public final class CertificateUtils {
      */
     public static String extractUsername(String dn) {
         String username = dn;
-        String cn = "";
 
         // ensure the dn is specified
         if (StringUtils.isNotBlank(dn)) {
-
-            // attempt to locate the cn
-            if (dn.startsWith("CN=")) {
-                cn = StringUtils.substringBetween(dn, "CN=", ",");
-            } else if (dn.startsWith("/CN=")) {
-                cn = StringUtils.substringBetween(dn, "CN=", "/");
-            } else if (dn.startsWith("C=") || dn.startsWith("/C=")) {
-                cn = StringUtils.substringAfter(dn, "CN=");
-            } else if (dn.startsWith("/") && StringUtils.contains(dn, "CN=")) {
-                cn = StringUtils.substringAfter(dn, "CN=");
-            }
-
-            // attempt to get the username from the cn
-            if (StringUtils.isNotBlank(cn)) {
-                if (cn.endsWith(")")) {
-                    username = StringUtils.substringBetween(cn, "(", ")");
-                } else if (cn.contains(" ")) {
-                    username = StringUtils.substringAfterLast(cn, " ");
+            // determine the separate
+            final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") > 0 ? "/" : ",";
+            
+            // attempt to locate the cd
+            final String cnPattern = "cn=";
+            final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern);
+            if (cnIndex >= 0) {
+                int separatorIndex = StringUtils.indexOf(dn, separator, cnIndex);
+                if (separatorIndex > 0) {
+                    username = StringUtils.substring(dn, cnIndex + cnPattern.length(), separatorIndex);
                 } else {
-                    username = cn;
+                    username = StringUtils.substring(dn, cnIndex + cnPattern.length());
                 }
             }
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java
----------------------------------------------------------------------
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 0bc6e99..2a24e0b 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
@@ -16,18 +16,28 @@
  */
 package org.apache.nifi.admin.dao;
 
+import org.apache.nifi.key.Key;
+
 /**
  * Key data access.
  */
 public interface KeyDAO {
 
     /**
-     * Gets the key for the specified user identity. Returns null if no key exists for the user identity.
+     * Gets the key for the specified user identity. Returns null if no key exists for the key id.
      *
-     * @param identity The user identity
+     * @param id The key id
+     * @return The key or null
+     */
+    Key findKeyById(int id);
+
+    /**
+     * Gets the latest key for the specified identity. Returns null if no key exists for the user identity.
+     *
+     * @param identity The identity
      * @return The key or null
      */
-    String getKey(String identity);
+    Key findLatestKeyByIdentity(String identity);
 
     /**
      * Creates a key for the specified user identity.
@@ -35,5 +45,5 @@ public interface KeyDAO {
      * @param identity The user identity
      * @return The key
      */
-    String createKey(String identity);
+    Key createKey(String identity);
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
----------------------------------------------------------------------
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 994e95b..f4bdc1d 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
@@ -20,17 +20,23 @@ import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Statement;
 import java.util.UUID;
 import org.apache.nifi.admin.RepositoryUtils;
 import org.apache.nifi.admin.dao.DataAccessException;
 import org.apache.nifi.admin.dao.KeyDAO;
+import org.apache.nifi.key.Key;
 
 /**
  *
  */
 public class StandardKeyDAO implements KeyDAO {
 
-    private static final String SELECT_KEY_FOR_USER = "SELECT KEY "
+    private static final String SELECT_KEY_FOR_USER_BY_ID = "SELECT ID, IDENTITY, KEY "
+            + "FROM KEY "
+            + "WHERE ID = ?";
+
+    private static final String SELECT_KEY_FOR_USER_BY_IDENTITY = "SELECT ID, IDENTITY, KEY "
             + "FROM KEY "
             + "WHERE IDENTITY = ?";
 
@@ -47,18 +53,49 @@ public class StandardKeyDAO implements KeyDAO {
     }
 
     @Override
-    public String getKey(String identity) {
+    public Key findKeyById(int id) {
+        Key key = null;
+
+        PreparedStatement statement = null;
+        ResultSet rs = null;
+        try {
+            // add each authority for the specified user
+            statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_ID);
+            statement.setInt(1, id);
+
+            // execute the query
+            rs = statement.executeQuery();
+
+            // if the key was found, add it
+            if (rs.next()) {
+                key = new Key();
+                key.setId(rs.getInt("ID"));
+                key.setIdentity(rs.getString("IDENTITY"));
+                key.setKey(rs.getString("KEY"));
+            }
+        } catch (SQLException sqle) {
+            throw new DataAccessException(sqle);
+        } finally {
+            RepositoryUtils.closeQuietly(rs);
+            RepositoryUtils.closeQuietly(statement);
+        }
+
+        return key;
+    }
+
+    @Override
+    public Key findLatestKeyByIdentity(String identity) {
         if (identity == null) {
             throw new IllegalArgumentException("Specified identity cannot be null.");
         }
 
-        String key = null;
+        Key key = null;
 
         PreparedStatement statement = null;
         ResultSet rs = null;
         try {
             // add each authority for the specified user
-            statement = connection.prepareStatement(SELECT_KEY_FOR_USER);
+            statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_IDENTITY);
             statement.setString(1, identity);
 
             // execute the query
@@ -66,7 +103,10 @@ public class StandardKeyDAO implements KeyDAO {
 
             // if the key was found, add it
             if (rs.next()) {
-                key = rs.getString("KEY");
+                key = new Key();
+                key.setId(rs.getInt("ID"));
+                key.setIdentity(rs.getString("IDENTITY"));
+                key.setKey(rs.getString("KEY"));
             }
         } catch (SQLException sqle) {
             throw new DataAccessException(sqle);
@@ -79,20 +119,27 @@ public class StandardKeyDAO implements KeyDAO {
     }
 
     @Override
-    public String createKey(final String identity) {
+    public Key createKey(final String identity) {
         PreparedStatement statement = null;
         ResultSet rs = null;
         try {
-            final String key = UUID.randomUUID().toString();
+            final String keyValue = UUID.randomUUID().toString();
 
             // add each authority for the specified user
-            statement = connection.prepareStatement(INSERT_KEY);
+            statement = connection.prepareStatement(INSERT_KEY, Statement.RETURN_GENERATED_KEYS);
             statement.setString(1, identity);
-            statement.setString(2, key);
+            statement.setString(2, keyValue);
 
             // insert the key
             int updateCount = statement.executeUpdate();
-            if (updateCount == 1) {
+            rs = statement.getGeneratedKeys();
+
+            // verify the results
+            if (updateCount == 1 && rs.next()) {
+                final Key key = new Key();
+                key.setId(rs.getInt(1));
+                key.setIdentity(identity);
+                key.setKey(keyValue);
                 return key;
             } else {
                 throw new DataAccessException("Unable to add key for user.");

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java
----------------------------------------------------------------------
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 9346625..ae64c41 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
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.admin.service;
 
+import org.apache.nifi.key.Key;
+
 /**
  * Supports retrieving and issues keys for signing user tokens.
  */
@@ -24,10 +26,10 @@ public interface KeyService {
     /**
      * Gets a key for the specified user identity. Returns null if the user has not had a key issued
      *
-     * @param identity The user identity
+     * @param id The key id
      * @return The key or null
      */
-    String getKey(String identity);
+    Key getKey(int id);
 
     /**
      * Gets a key for the specified user identity. If a key does not exist, one will be created.
@@ -36,5 +38,5 @@ public interface KeyService {
      * @return The key
      * @throws AdministrationException if it failed to get/create the key
      */
-    String getOrCreateKey(String identity);
+    Key getOrCreateKey(String identity);
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyAction.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyAction.java
deleted file mode 100644
index f12b1ef..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyAction.java
+++ /dev/null
@@ -1,41 +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.nifi.admin.service.action;
-
-import org.apache.nifi.admin.dao.DAOFactory;
-import org.apache.nifi.authorization.AuthorityProvider;
-
-import org.apache.nifi.admin.dao.KeyDAO;
-
-/**
- * Gets a key for the specified user identity.
- */
-public class GetKeyAction implements AdministrationAction<String> {
-
-    private final String identity;
-
-    public GetKeyAction(String identity) {
-        this.identity = identity;
-    }
-
-    @Override
-    public String execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
-        final KeyDAO keyDao = daoFactory.getKeyDAO();
-        return keyDao.getKey(identity);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java
new file mode 100644
index 0000000..8763b9d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdAction.java
@@ -0,0 +1,42 @@
+/*
+ * 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.admin.service.action;
+
+import org.apache.nifi.admin.dao.DAOFactory;
+import org.apache.nifi.authorization.AuthorityProvider;
+
+import org.apache.nifi.admin.dao.KeyDAO;
+import org.apache.nifi.key.Key;
+
+/**
+ * Gets a key for the specified key id.
+ */
+public class GetKeyByIdAction implements AdministrationAction<Key> {
+
+    private final int id;
+
+    public GetKeyByIdAction(int id) {
+        this.id = id;
+    }
+
+    @Override
+    public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
+        final KeyDAO keyDao = daoFactory.getKeyDAO();
+        return keyDao.findKeyById(id);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java
new file mode 100644
index 0000000..9bcb0b3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetKeyByIdentityAction.java
@@ -0,0 +1,42 @@
+/*
+ * 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.admin.service.action;
+
+import org.apache.nifi.admin.dao.DAOFactory;
+import org.apache.nifi.authorization.AuthorityProvider;
+
+import org.apache.nifi.admin.dao.KeyDAO;
+import org.apache.nifi.key.Key;
+
+/**
+ * Gets a key for the specified key id.
+ */
+public class GetKeyByIdentityAction implements AdministrationAction<Key> {
+
+    private final String identity;
+
+    public GetKeyByIdentityAction(String identity) {
+        this.identity = identity;
+    }
+
+    @Override
+    public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
+        final KeyDAO keyDao = daoFactory.getKeyDAO();
+        return keyDao.findLatestKeyByIdentity(identity);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java
index 209cbcd..bb85b6f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/GetOrCreateKeyAction.java
@@ -20,11 +20,12 @@ import org.apache.nifi.admin.dao.DAOFactory;
 import org.apache.nifi.authorization.AuthorityProvider;
 
 import org.apache.nifi.admin.dao.KeyDAO;
+import org.apache.nifi.key.Key;
 
 /**
  * Gets a key for the specified user identity.
  */
-public class GetOrCreateKeyAction implements AdministrationAction<String> {
+public class GetOrCreateKeyAction implements AdministrationAction<Key> {
 
     private final String identity;
 
@@ -33,10 +34,10 @@ public class GetOrCreateKeyAction implements AdministrationAction<String> {
     }
 
     @Override
-    public String execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
+    public Key execute(DAOFactory daoFactory, AuthorityProvider authorityProvider) {
         final KeyDAO keyDao = daoFactory.getKeyDAO();
 
-        String key = keyDao.getKey(identity);
+        Key key = keyDao.findLatestKeyByIdentity(identity);
         if (key == null) {
             key = keyDao.createKey(identity);
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
----------------------------------------------------------------------
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 7dff9d8..ca0a124 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.GetKeyAction;
+import org.apache.nifi.admin.service.action.GetKeyByIdAction;
 import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
 import org.apache.nifi.admin.service.transaction.Transaction;
 import org.apache.nifi.admin.service.transaction.TransactionBuilder;
@@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import org.apache.nifi.key.Key;
 
 /**
  *
@@ -44,19 +45,17 @@ public class StandardKeyService implements KeyService {
     private TransactionBuilder transactionBuilder;
 
     @Override
-    public String getKey(String identity) {
-        // TODO: Change this service to look up by "key ID" instead of identity
-        // TODO: Change the return type to a Key POJO to support key rotation
+    public Key getKey(int id) {
         Transaction transaction = null;
-        String key = null;
+        Key key = null;
 
         readLock.lock();
         try {
             // start the transaction
             transaction = transactionBuilder.start();
 
-            // seed the accounts
-            GetKeyAction addActions = new GetKeyAction(identity);
+            // get the key
+            GetKeyByIdAction addActions = new GetKeyByIdAction(id);
             key = transaction.execute(addActions);
 
             // commit the transaction
@@ -76,11 +75,9 @@ public class StandardKeyService implements KeyService {
     }
 
     @Override
-    public String getOrCreateKey(String identity) {
-        // TODO: Change this service to look up by "key ID" instead of identity
-        // TODO: Change the return type to a Key POJO to support key rotation
+    public Key getOrCreateKey(String identity) {
         Transaction transaction = null;
-        String key = null;
+        Key key = null;
 
         writeLock.lock();
         try {

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..b7158c2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java
@@ -0,0 +1,69 @@
+/*
+ * 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.key;
+
+import java.io.Serializable;
+
+/**
+ * An signing key for a NiFi user.
+ */
+public class Key implements Serializable {
+
+    private int id;
+    private String identity;
+    private String key;
+
+    /**
+     * The key id.
+     * 
+     * @return the id
+     */
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    /**
+     * The identity of the user this key is associated with.
+     * 
+     * @return the identity
+     */
+    public String getIdentity() {
+        return identity;
+    }
+
+    public void setIdentity(String identity) {
+        this.identity = identity;
+    }
+
+    /**
+     * The signing key.
+     * 
+     * @return the signing key
+     */
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
----------------------------------------------------------------------
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 2e1c44e..27d7d29 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
@@ -55,6 +55,7 @@ import org.apache.nifi.web.api.dto.RevisionDTO;
 import org.apache.nifi.web.api.entity.AccessStatusEntity;
 import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
 import org.apache.nifi.web.api.request.ClientIdParameter;
+import org.apache.nifi.web.security.InvalidAuthenticationException;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.UntrustedProxyException;
 import org.apache.nifi.web.security.jwt.JwtService;
@@ -185,55 +186,52 @@ public class AccessResource extends ApplicationResource {
                     accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
                     accessStatus.setMessage("No credentials supplied, unknown user.");
                 } else {
-                    // Extract the Base64 encoded token from the Authorization header
-                    final String token = StringUtils.substringAfterLast(authorization, " ");
-
                     try {
+                        // Extract the Base64 encoded token from the Authorization header
+                        final String token = StringUtils.substringAfterLast(authorization, " ");
                         final String principal = jwtService.getAuthenticationFromToken(token);
 
-                        // ensure we have something we can work with (certificate or credentials)
-                        if (principal == null) {
-                            throw new IllegalArgumentException("The specific token is not valid.");
-                        } else {
-                            // set the user identity
-                            accessStatus.setIdentity(principal);
-                            accessStatus.setUsername(CertificateUtils.extractUsername(principal));
+                        // set the user identity
+                        accessStatus.setIdentity(principal);
+                        accessStatus.setUsername(CertificateUtils.extractUsername(principal));
 
-                            // without a certificate, this is not a proxied request
-                            final List<String> chain = Arrays.asList(principal);
+                        // without a certificate, this is not a proxied request
+                        final List<String> chain = Arrays.asList(principal);
 
-                            // check authorization for this user
-                            checkAuthorization(chain);
+                        // check authorization for this user
+                        checkAuthorization(chain);
 
-                            // no issues with authorization
-                            accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                            accessStatus.setMessage("Account is active and authorized");
-                        }
+                        // no issues with authorization
+                        accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
+                        accessStatus.setMessage("Account is active and authorized");
                     } catch (JwtException e) {
-                        // TODO: Handle the exception from a failed JWT verification
-                        throw new AccessDeniedException("The JWT could not be verified", e);
+                        throw new InvalidAuthenticationException(e.getMessage(), e);
                     }
                 }
             } else {
-                final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
-
-                // get the proxy chain and ensure its populated
-                final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
-                if (proxyChain.isEmpty()) {
-                    logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
-                    throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
-                }
+                try {
+                    final AuthenticationResponse authenticationResponse = certificateIdentityProvider.authenticate(certificates);
+
+                    // get the proxy chain and ensure its populated
+                    final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(httpServletRequest, authenticationResponse.getIdentity());
+                    if (proxyChain.isEmpty()) {
+                        logger.error(String.format("Unable to parse the proxy chain %s from the incoming request.", authenticationResponse.getIdentity()));
+                        throw new IllegalArgumentException("Unable to determine the user from the incoming request.");
+                    }
 
-                // ensure the proxy chain is authorized
-                checkAuthorization(proxyChain);
+                    // set the user identity
+                    accessStatus.setIdentity(proxyChain.get(0));
+                    accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
 
-                // set the user identity
-                accessStatus.setIdentity(proxyChain.get(0));
-                accessStatus.setUsername(CertificateUtils.extractUsername(proxyChain.get(0)));
+                    // ensure the proxy chain is authorized
+                    checkAuthorization(proxyChain);
 
-                // no issues with authorization
-                accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                accessStatus.setMessage("Account is active and authorized");
+                    // no issues with authorization
+                    accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
+                    accessStatus.setMessage("Account is active and authorized");
+                } catch (final IllegalArgumentException iae) {
+                    throw new InvalidAuthenticationException(iae.getMessage(), iae);
+                }
             }
         } catch (final UsernameNotFoundException unfe) {
             accessStatus.setStatus(AccessStatusDTO.Status.UNREGISTERED.name());
@@ -323,20 +321,20 @@ public class AccessResource extends ApplicationResource {
                 final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password));
                 final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
                 final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
-                
+
                 long expiration = authenticationResponse.getExpiration();
                 if (expiration > maxExpiration) {
                     expiration = maxExpiration;
-                    
-                    logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", expiration, 
+
+                    logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", expiration,
                             authenticationResponse.getExpiration(), authenticationResponse.getIdentity()));
                 } else if (expiration < minExpiration) {
                     expiration = minExpiration;
-                    
-                    logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", expiration, 
+
+                    logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", expiration,
                             authenticationResponse.getExpiration(), authenticationResponse.getIdentity()));
                 }
-                
+
                 // create the authentication token
                 // TODO: Some Spring beans return "" for getClass().getSimpleName(). Using getName() temporarily
                 loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, loginIdentityProvider.getClass().getName());

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/InvalidAuthenticationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/InvalidAuthenticationExceptionMapper.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/InvalidAuthenticationExceptionMapper.java
new file mode 100644
index 0000000..14d5139
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/config/InvalidAuthenticationExceptionMapper.java
@@ -0,0 +1,44 @@
+/*
+ * 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.api.config;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Maps access denied exceptions into a client response.
+ */
+@Provider
+public class InvalidAuthenticationExceptionMapper implements ExceptionMapper<InvalidAuthenticationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(InvalidAuthenticationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(InvalidAuthenticationException exception) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.UNAUTHORIZED).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 73929d8..9f3d2f5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -255,6 +255,7 @@
 
     <!-- exception mapping -->
     <bean class="org.apache.nifi.web.api.config.AccessDeniedExceptionMapper" scope="singleton"/>
+    <bean class="org.apache.nifi.web.api.config.InvalidAuthenticationExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.AuthenticationCredentialsNotFoundExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.AccountNotFoundExceptionMapper" scope="singleton"/>
     <bean class="org.apache.nifi.web.api.config.AdministrationExceptionMapper" scope="singleton"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/InvalidAuthenticationException.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/InvalidAuthenticationException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/InvalidAuthenticationException.java
new file mode 100644
index 0000000..1065152
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/InvalidAuthenticationException.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * Thrown if the authentication of a given request is invalid. For instance,
+ * an expired certificate or token.
+ */
+public class InvalidAuthenticationException extends AuthenticationException {
+
+    public InvalidAuthenticationException(String msg) {
+        super(msg);
+    }
+
+    public InvalidAuthenticationException(String msg, Throwable t) {
+        super(msg, t);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
index ec34ace..f09d610 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationFilter.java
@@ -36,7 +36,6 @@ import org.slf4j.LoggerFactory;
 import org.springframework.security.authentication.AccountStatusException;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationServiceException;
-import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -134,8 +133,8 @@ public abstract class NiFiAuthenticationFilter implements Filter {
                 response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                 out.println("Access is denied.");
             }
-        } else if (ae instanceof BadCredentialsException) {
-            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        } else if (ae instanceof InvalidAuthenticationException) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
             out.println(ae.getMessage());
         } else if (ae instanceof AccountStatusException) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
----------------------------------------------------------------------
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 dea5bba..20675fb 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
@@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.Arrays;
+import org.apache.nifi.web.security.InvalidAuthenticationException;
 
 /**
  */
@@ -49,7 +50,6 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
         // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource
 
         // get the principal out of the user token
-        // look for an authorization token
         final String authorization = request.getHeader(AUTHORIZATION);
 
         // if there is no authorization header, we don't know the user
@@ -61,9 +61,6 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
 
             try {
                 final String jwtPrincipal = jwtService.getAuthenticationFromToken(token);
-                if (jwtPrincipal == null) {
-                    return null;
-                }
 
                 if (isNewAccountRequest(request)) {
                     return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
@@ -71,9 +68,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
                     return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal));
                 }
             } catch (JwtException e) {
-                // TODO: Is this the correct way to handle an unverified token?
-                logger.error("Could not verify JWT", e);
-                return null;
+                throw new InvalidAuthenticationException(e.getMessage(), e);
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
----------------------------------------------------------------------
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 f006e5b..9635354 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
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
 
 import java.nio.charset.StandardCharsets;
 import java.util.Calendar;
+import org.apache.nifi.key.Key;
 
 /**
  *
@@ -88,17 +89,16 @@ public class JwtService {
                 public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                     final String identity = claims.getSubject();
 
-                    // TODO: Currently the kid field is identical to identity, but will be a unique key ID when key rotation is implemented
-                    final String keyId = claims.get(KEY_ID_CLAIM, String.class);
-                    // The key is unique per identity and should be retrieved from the key service
-                    final String key = keyService.getKey(keyId);
+                    // Get the key based on the key id in the claims
+                    final Integer keyId = claims.get(KEY_ID_CLAIM, Integer.class);
+                    final Key key = keyService.getKey(keyId);
 
                     // Ensure we were able to find a key that was previously issued by this key service for this user
-                    if (key == null) {
+                    if (key == null || key.getKey() == null) {
                         throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
                     }
 
-                    return key.getBytes(StandardCharsets.UTF_8);
+                    return key.getKey().getBytes(StandardCharsets.UTF_8);
                 }
             }).parseClaimsJws(base64EncodedToken);
         } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
@@ -137,21 +137,19 @@ public class JwtService {
 
         try {
             // Get/create the key for this user
-            final String key = keyService.getOrCreateKey(identity);
-            final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+            final Key key = keyService.getOrCreateKey(identity);
+            final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8);
 
             logger.trace("Generating JWT for " + authenticationToken);
 
             // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
 
-            // TODO: Change kid field to key ID when KeyService is refactored
-
             // Build the token
             return Jwts.builder().setSubject(identity)
                     .setIssuer(authenticationToken.getIssuer())
                     .setAudience(authenticationToken.getIssuer())
                     .claim(USERNAME_CLAIM, username)
-                    .claim(KEY_ID_CLAIM, identity)
+                    .claim(KEY_ID_CLAIM, key.getId())
                     .setExpiration(expiration.getTime())
                     .setIssuedAt(Calendar.getInstance().getTime())
                     .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
index e626f74..dd7d47e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
@@ -21,6 +21,7 @@ import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.nifi.authentication.AuthenticationResponse;
+import org.apache.nifi.web.security.InvalidAuthenticationException;
 import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
@@ -28,7 +29,6 @@ import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
 import org.apache.nifi.web.security.user.NewAccountRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.security.authentication.BadCredentialsException;
 
 /**
  * Custom X509 filter that will inspect the HTTP headers for a proxied user before extracting the user details from the client certificate.
@@ -58,7 +58,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
         try {
             authenticationResponse = certificateIdentityProvider.authenticate(certificates);
         } catch (final IllegalArgumentException iae) {
-            throw new BadCredentialsException(iae.getMessage(), iae);
+            throw new InvalidAuthenticationException(iae.getMessage(), iae);
         }
 
         final List<String> proxyChain = ProxiedEntitiesUtils.buildProxiedEntitiesChain(request, authenticationResponse.getIdentity());

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
----------------------------------------------------------------------
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 c9107b9..ea3e122 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
@@ -22,10 +22,12 @@ import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import org.apache.nifi.key.Key;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.when;
 
 /**
@@ -117,9 +119,14 @@ public class JwtServiceTest {
 
     @Before
     public void setUp() throws Exception {
+        final Key key = new Key();
+        key.setId(0);
+        key.setIdentity(HMAC_SECRET);
+        key.setKey(HMAC_SECRET);
+        
         mockKeyService = Mockito.mock(KeyService.class);
-        when(mockKeyService.getKey(anyString())).thenReturn(HMAC_SECRET);
-        when(mockKeyService.getOrCreateKey(anyString())).thenReturn(HMAC_SECRET);
+        when(mockKeyService.getKey(anyInt())).thenReturn(key);
+        when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key);
         jwtService = new JwtService(mockKeyService);
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js
index eab5297..7d63534 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-header.js
@@ -142,7 +142,7 @@ nf.CanvasHeader = (function () {
             });
 
             // show the login link if supported and user is currently anonymous
-            var isAnonymous = $('#current-user').text() === nf.Canvas.ANONYMOUS_USER_TEXT;
+            var isAnonymous = $('#current-user').text() === nf.Common.ANONYMOUS_USER_TEXT;
             if (supportsLogin === true && isAnonymous) {
                 // login link
                 $('#login-link').click(function () {

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
index 09a8212..c9498fe 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
@@ -936,7 +936,6 @@ nf.Canvas = (function () {
     };
 
     return {
-        ANONYMOUS_USER_TEXT: 'Anonymous user',
         CANVAS_OFFSET: 0,
         /**
          * Determines if the current broswer supports SVG.
@@ -1056,17 +1055,8 @@ nf.Canvas = (function () {
                             $('#logout-link-container').show();
                         }
                     } else {
-                        // alert user's of anonymous access
-                        $('#anonymous-user-alert').show().qtip($.extend({}, nf.Common.config.tooltipConfig, {
-                            content: 'You are accessing with limited authority. Log in or request an account to access with additional authority granted to you by an administrator.',
-                            position: {
-                                my: 'top right',
-                                at: 'bottom left'
-                            }
-                        }));
-
-                        // render the anonymous user text
-                        $('#current-user').text(nf.Canvas.ANONYMOUS_USER_TEXT).show();
+                        // set the anonymous user label
+                        nf.Common.setAnonymousUserLabel();
                     }
                     deferred.resolve();
                 }).fail(function (xhr, status, error) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
index 89db6ce..bd39a44 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
@@ -96,8 +96,10 @@ nf.Login = (function () {
                 'password': $('#password').val()
             }
         }).done(function (jwt) {
-            // store the jwt and reload the page
-            nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));
+            // get the payload and store the token with the appropirate expiration
+            var token = nf.Common.getJwtPayload(jwt);
+            var expiration = parseInt(token['exp'], 10) * nf.Common.MILLIS_PER_SECOND;
+            nf.Storage.setItem('jwt', jwt, expiration);
 
             // check to see if they actually have access now
             $.ajax({
@@ -112,8 +114,7 @@ nf.Login = (function () {
                     nf.Common.scheduleTokenRefresh();
             
                     // show the user
-                    var user = nf.Common.getJwtSubject(jwt);
-                    $('#nifi-user-submit-justification').text(user);
+                    $('#nifi-user-submit-justification').text(token['preferred_username']);
 
                     // show the registration form
                     initializeNiFiRegistration();
@@ -133,8 +134,7 @@ nf.Login = (function () {
                 nf.Common.scheduleTokenRefresh();
 
                 // show the user
-                var user = nf.Common.getJwtSubject(jwt);
-                $('#nifi-user-submit-justification').text(user);
+                $('#nifi-user-submit-justification').text(token['preferred_username']);
 
                 if (xhr.status === 401) {
                     initializeNiFiRegistration();

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index 3be6b91..9202819 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -54,9 +54,17 @@ $(document).ready(function () {
     // include jwt when possible
     $.ajaxSetup({
         'beforeSend': function(xhr) {
+            var hadToken = nf.Storage.hasItem('jwt');
+            
+            // get the token to include in all requests
             var token = nf.Storage.getItem('jwt');
-            if (token) {
+            if (token !== null) {
                 xhr.setRequestHeader('Authorization', 'Bearer ' + token);
+            } else {
+                // if the current user was logged in with a token and the token just expired, reload
+                if (hadToken === true) {
+                    return false;
+                }
             }
         }
     });
@@ -83,6 +91,8 @@ nf.Common = (function () {
     var tokenRefreshInterval = null;
     
     return {
+        ANONYMOUS_USER_TEXT: 'Anonymous user',
+        
         config: {
             sensitiveText: 'Sensitive value set',
             tooltipConfig: {
@@ -100,9 +110,6 @@ nf.Common = (function () {
                     at: 'top right',
                     my: 'bottom left'
                 }
-            },
-            urls: {
-                token: '../nifi-api/access/token'
             }
         },
 
@@ -148,7 +155,7 @@ nf.Common = (function () {
             }
             
             // set the interval to one hour
-            var interval = nf.Common.MILLIS_PER_HOUR;
+            var interval = 10 * nf.Common.MILLIS_PER_MINUTE;
             
             var checkExpiration = function () {
                 var expiration = nf.Storage.getItemExpiration('jwt');
@@ -161,13 +168,16 @@ nf.Common = (function () {
                     // get the time remainging plus a little bonus time to reload the token
                     var timeRemaining = expirationDate.valueOf() - now.valueOf() - nf.Common.MILLIS_PER_MINUTE;
                     if (timeRemaining < interval) {
-                        // if the token will expire before the next interval minus some bonus time, refresh now
-                        $.ajax({
-                            type: 'POST',
-                            url: nf.Common.config.urls.token
-                        }).done(function (jwt) {
-                            nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt));
-                        });
+                        if ($('#current-user').text() !== nf.Common.ANONYMOUS_USER_TEXT && !$('#anonymous-user-alert').is(':visible')) {
+                            // if the token will expire before the next interval minus some bonus time, notify the user to re-login
+                            $('#anonymous-user-alert').show().qtip($.extend({}, nf.Common.config.tooltipConfig, {
+                                content: 'Your session will expire soon. Please log in again to avoid being automatically logged out.',
+                                position: {
+                                    my: 'top right',
+                                    at: 'bottom left'
+                                }
+                            }));
+                        }
                     }
                 }
             };
@@ -180,56 +190,46 @@ nf.Common = (function () {
         },
 
         /**
-         * Extracts the subject from the specified jwt. If the jwt is not as expected
-         * an empty string is returned.
-         * 
-         * @param {string} jwt
-         * @returns {string}
+         * Sets the anonymous user label.
          */
-        getJwtSubject: function (jwt) {
-            if (nf.Common.isDefinedAndNotNull(jwt)) {
-                var segments = jwt.split(/\./);
-                if (segments.length !== 3) {
-                    return '';
-                }
-
-                var rawPayload = $.base64.atob(segments[1]);
-                var payload = JSON.parse(rawPayload);
-
-                if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) {
-                    return payload['preferred_username'];
-                } else {
-                    '';
-                }
+        setAnonymousUserLabel: function () {
+            var anonymousUserAlert = $('#anonymous-user-alert');
+            if (anonymousUserAlert.data('qtip')) {
+                anonymousUserAlert.qtip('api').destroy(true);
             }
+                        
+            // alert user's of anonymous access
+            anonymousUserAlert.show().qtip($.extend({}, nf.Common.config.tooltipConfig, {
+                content: 'You are accessing with limited authority. Log in or request an account to access with additional authority granted to you by an administrator.',
+                position: {
+                    my: 'top right',
+                    at: 'bottom left'
+                }
+            }));
 
-            return '';
+            // render the anonymous user text
+            $('#current-user').text(nf.Common.ANONYMOUS_USER_TEXT).show();  
         },
 
         /**
-         * Extracts the expiration from the specified jwt. If the jwt is not as expected
-         * a null value is returned.
+         * Extracts the subject from the specified jwt. If the jwt is not as expected
+         * an empty string is returned.
          * 
          * @param {string} jwt
-         * @returns {integer}
+         * @returns {string}
          */
-        getJwtExpiration: function (jwt) {
+        getJwtPayload: function (jwt) {
             if (nf.Common.isDefinedAndNotNull(jwt)) {
                 var segments = jwt.split(/\./);
                 if (segments.length !== 3) {
-                    return null;
+                    return '';
                 }
 
                 var rawPayload = $.base64.atob(segments[1]);
                 var payload = JSON.parse(rawPayload);
 
-                if (nf.Common.isDefinedAndNotNull(payload['exp'])) {
-                    try {
-                        // jwt exp is in seconds
-                        return parseInt(payload['exp'], 10) * nf.Common.MILLIS_PER_SECOND;
-                    } catch (e) {
-                        return null;
-                    }
+                if (nf.Common.isDefinedAndNotNull(payload)) {
+                    return payload;
                 } else {
                     return null;
                 }
@@ -313,6 +313,28 @@ nf.Common = (function () {
          * @argument {string} error     The error
          */
         handleAjaxError: function (xhr, status, error) {
+            if (status === 'canceled') {
+                if ($('#splash').is(':visible')) {
+                    $('#message-title').text('Session Expired');
+                    $('#message-content').text('Your session has expired. Please reload to log in again.');
+
+                    // show the error pane
+                    $('#message-pane').show();
+
+                    // close the canvas
+                    nf.Common.closeCanvas();
+                } else {
+                    nf.Dialog.showOkDialog({
+                        dialogContent: 'Your session has expired. Please press Ok to log in again.',
+                        overlayBackground: false,
+                        okHandler: function () {
+                            window.location = '/nifi';
+                        }
+                    });
+                }
+                return;
+            }
+            
             // if an error occurs while the splash screen is visible close the canvas show the error message
             if ($('#splash').is(':visible')) {
                 if (xhr.status === 401) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-dialog.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-dialog.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-dialog.js
index 2d1183f..d305e4d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-dialog.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-dialog.js
@@ -23,15 +23,6 @@ $(document).ready(function () {
 
     // configure the ok dialog
     $('#nf-ok-dialog').modal({
-        buttons: [{
-                buttonText: 'Ok',
-                handler: {
-                    click: function () {
-                        // close the dialog
-                        $('#nf-ok-dialog').modal('hide');
-                    }
-                }
-            }],
         handler: {
             close: function () {
                 // clear the content
@@ -77,6 +68,20 @@ nf.Dialog = (function () {
             var content = $('<p></p>').append(options.dialogContent);
             $('#nf-ok-dialog-content').append(content);
 
+            // update the button model
+            $('#nf-ok-dialog').modal('setButtonModel', [{
+                buttonText: 'Ok',
+                handler: {
+                    click: function () {
+                        // close the dialog
+                        $('#nf-ok-dialog').modal('hide');
+                        if (typeof options.okHandler === 'function') {
+                            options.okHandler.call(this);
+                        }
+                    }
+                }
+            }]);
+
             // show the dialog
             $('#nf-ok-dialog').modal('setHeaderText', options.headerText).modal('setOverlayBackground', options.overlayBackground).modal('show');
         },

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js
index cdb2e9e..7b0c3f6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js
@@ -42,32 +42,18 @@ nf.Storage = (function () {
     };
     
     /**
-     * If the item at key is not expired, the value of field is returned. Otherwise, null.
+     * Gets an enty for the key. The entry expiration is not checked.
      * 
      * @param {string} key
-     * @param {string} field
-     * @return {object} the value
      */
-    var getEntryField = function (key, field) {
+    var getEntry = function (key) {
         try {
             // parse the entry
             var entry = JSON.parse(localStorage.getItem(key));
 
             // ensure the entry and item are present
             if (nf.Common.isDefinedAndNotNull(entry)) {
-                
-                // if the entry is expired, drop it and return null
-                if (checkExpiration(entry)) {
-                    nf.Storage.removeItem(key);
-                    return null;
-                }
-
-                // if the entry has the specified field return its value
-                if (nf.Common.isDefinedAndNotNull(entry[field])) {
-                    return entry[field];
-                } else {
-                    return null;
-                }
+                return entry;
             } else {
                 return null;
             }
@@ -75,7 +61,7 @@ nf.Storage = (function () {
             return null;
         }
     };
-
+    
     return {
         /**
          * Initializes the storage. Items will be persisted for two days. Once the scripts runs
@@ -86,11 +72,9 @@ nf.Storage = (function () {
                 try {
                     // get the next item
                     var key = localStorage.key(i);
-                    var entry = JSON.parse(localStorage.getItem(key));
-
-                    if (checkExpiration(entry)) {
-                        nf.Storage.removeItem(key);
-                    }
+                    
+                    // attempt to get the item which will expire if necessary
+                    nf.Storage.getItem(key);
                 } catch (e) {
                 }
             }
@@ -118,6 +102,17 @@ nf.Storage = (function () {
         },
         
         /**
+         * Returns whether there is an entry for this key. This will not check the expiration. If
+         * the entry is expired, it will return null on a subsequent getItem invocation.
+         * 
+         * @param {string} key
+         * @returns {boolean}
+         */
+        hasItem: function (key) {
+            return getEntry(key) !== null;
+        },
+        
+        /**
          * Gets the item with the specified key. If an item with this key does
          * not exist, null is returned. If an item exists but cannot be parsed
          * or is malformed/unrecognized, null is returned.
@@ -125,18 +120,44 @@ nf.Storage = (function () {
          * @param {type} key
          */
         getItem: function (key) {
-            return getEntryField(key, 'item');
+            var entry = getEntry(key);
+            if (entry === null) {
+                return null;
+            }
+
+            // if the entry is expired, drop it and return null
+            if (checkExpiration(entry)) {
+                nf.Storage.removeItem(key);
+                return null;
+            }
+
+            // if the entry has the specified field return its value
+            if (nf.Common.isDefinedAndNotNull(entry['item'])) {
+                return entry['item'];
+            } else {
+                return null;
+            }
         },
         
         /**
-         * Gets the expiration for the specified item. If the item does not exists our could 
-         * not be parsed, returns null.
+         * Gets the expiration for the specified item. This will not check the expiration. If
+         * the entry is expired, it will return null on a subsequent getItem invocation.
          * 
          * @param {string} key
          * @returns {integer}
          */
         getItemExpiration: function (key) {
-            return getEntryField(key, 'expires');
+            var entry = getEntry(key);
+            if (entry === null) {
+                return null;
+            }
+
+            // if the entry has the specified field return its value
+            if (nf.Common.isDefinedAndNotNull(entry['expires'])) {
+                return entry['expires'];
+            } else {
+                return null;
+            }
         },
         
         /**

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/AbstractLdapProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/AbstractLdapProvider.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/AbstractLdapProvider.java
deleted file mode 100644
index 64c48bf..0000000
--- a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/AbstractLdapProvider.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.nifi.ldap;
-
-import java.util.concurrent.TimeUnit;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.authentication.AuthenticationResponse;
-import org.apache.nifi.authentication.LoginCredentials;
-import org.apache.nifi.authentication.LoginIdentityProvider;
-import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
-import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext;
-import org.apache.nifi.authentication.exception.IdentityAccessException;
-import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException;
-import org.apache.nifi.authorization.exception.ProviderCreationException;
-import org.apache.nifi.authorization.exception.ProviderDestructionException;
-import org.apache.nifi.util.FormatUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ldap.CommunicationException;
-import org.springframework.security.authentication.AuthenticationServiceException;
-import org.springframework.security.authentication.BadCredentialsException;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
-import org.springframework.security.ldap.userdetails.LdapUserDetails;
-
-/**
- * Abstract LDAP based implementation of a login identity provider.
- */
-public abstract class AbstractLdapProvider implements LoginIdentityProvider {
-
-    private static final Logger logger = LoggerFactory.getLogger(AbstractLdapProvider.class);
-
-    private AbstractLdapAuthenticationProvider provider;
-    private long expiration;
-
-    @Override
-    public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException {
-    }
-
-    @Override
-    public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
-        final String rawExpiration = configurationContext.getProperty("Expiration Duration");
-        if (StringUtils.isBlank(rawExpiration)) {
-            throw new ProviderCreationException("The Expiration Duration must be specified.");
-        }
-
-        try {
-            expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS);
-        } catch (final IllegalArgumentException iae) {
-            throw new ProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration));
-        }
-
-        provider = getLdapAuthenticationProvider(configurationContext);
-    }
-
-    protected abstract AbstractLdapAuthenticationProvider getLdapAuthenticationProvider(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException;
-
-    @Override
-    public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException {
-        if (provider == null) {
-            throw new IdentityAccessException("The LDAP authentication provider is not initialized.");
-        }
-
-        try {
-            // perform the authentication
-            final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword());
-            final Authentication authentication = provider.authenticate(token);
-
-            // attempt to get the ldap user details to get the DN
-            if (authentication.getPrincipal() instanceof LdapUserDetails) {
-                final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
-                return new AuthenticationResponse(userDetails.getDn(), credentials.getUsername(), expiration);
-            } else {
-                return new AuthenticationResponse(authentication.getName(), credentials.getUsername(), expiration);
-            }
-        } catch (final CommunicationException | AuthenticationServiceException e) {
-            logger.error(e.getMessage());
-            if (logger.isDebugEnabled()) {
-                logger.debug(StringUtils.EMPTY, e);
-            }
-            throw new IdentityAccessException("Unable to query the configured directory server. See the logs for additional details.", e);
-        } catch (final BadCredentialsException bce) {
-            throw new InvalidLoginCredentialsException(bce.getMessage(), bce);
-        }
-    }
-
-    @Override
-    public final void preDestruction() throws ProviderDestructionException {
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c94d0271/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/ActiveDirectoryProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/ActiveDirectoryProvider.java b/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/ActiveDirectoryProvider.java
deleted file mode 100644
index e43c1ee..0000000
--- a/nifi-nar-bundles/nifi-ldap-iaa-providers-bundle/nifi-ldap-iaa-providers/src/main/java/org/apache/nifi/ldap/ActiveDirectoryProvider.java
+++ /dev/null
@@ -1,51 +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.nifi.ldap;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext;
-import org.apache.nifi.authorization.exception.ProviderCreationException;
-import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
-import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
-
-/**
- * Active Directory based implementation of a login identity provider.
- */
-public class ActiveDirectoryProvider extends AbstractLdapProvider {
-
-    @Override
-    protected AbstractLdapAuthenticationProvider getLdapAuthenticationProvider(LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException {
-
-        final String url = configurationContext.getProperty("Url");
-        if (StringUtils.isBlank(url)) {
-            throw new ProviderCreationException("The Active Directory 'Url' must be specified.");
-        }
-
-        final String domain = configurationContext.getProperty("Domain");
-        final String userSearchBase = configurationContext.getProperty("User Search Base");
-
-        final ActiveDirectoryLdapAuthenticationProvider activeDirectoryAuthenticationProvider
-                = new ActiveDirectoryLdapAuthenticationProvider(StringUtils.isBlank(domain) ? null : domain, url, StringUtils.isBlank(userSearchBase) ? null : userSearchBase);
-
-        final String userSearchFilter = configurationContext.getProperty("User Search Filter");
-        if (StringUtils.isNotBlank(userSearchFilter)) {
-            activeDirectoryAuthenticationProvider.setSearchFilter(userSearchFilter);
-        }
-
-        return activeDirectoryAuthenticationProvider;
-    }
-}


[3/6] nifi git commit: NIFI-655. - Added issuer field to LoginAuthenticationToken. - Updated AccessResource to pass identity provider class name when creating LoginAuthenticationTokens. - Began refactoring JWT logic from request parsing logic in JwtServi

Posted by mc...@apache.org.
NIFI-655. - Added issuer field to LoginAuthenticationToken. - Updated AccessResource to pass identity provider class name when creating LoginAuthenticationTokens. - Began refactoring JWT logic from request parsing logic in JwtService. - Added unit tests for JWT logic.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/3bc11e13
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/3bc11e13
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/3bc11e13

Branch: refs/heads/NIFI-655
Commit: 3bc11e13d7f7dd4242881cde03c9b2ff6fc7da02
Parents: caeede5
Author: Andy LoPresto <an...@andylopresto.com>
Authored: Tue Nov 17 18:22:39 2015 -0800
Committer: Matt Gilman <ma...@gmail.com>
Committed: Wed Nov 18 08:31:23 2015 -0500

----------------------------------------------------------------------
 .../org/apache/nifi/web/api/AccessResource.java |   6 +-
 .../nifi/web/security/jwt/JwtService.java       | 100 ++++++--
 .../token/LoginAuthenticationToken.java         |  64 ++++-
 .../nifi/web/security/jwt/JwtServiceTest.java   | 254 +++++++++++++++++++
 4 files changed, 395 insertions(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/3bc11e13/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
----------------------------------------------------------------------
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 8f4cd5a..b4778ad 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
@@ -191,7 +191,7 @@ public class AccessResource extends ApplicationResource {
                     final String principal = jwtService.getAuthentication(httpServletRequest);
 
                     // TODO - catch jwt exception?
-                    // ensure we have something we can work with (certificate or crendentials)
+                    // ensure we have something we can work with (certificate or credentials)
                     if (principal == null) {
                         throw new IllegalArgumentException("The specific token is not valid.");
                     } else {
@@ -334,7 +334,7 @@ public class AccessResource extends ApplicationResource {
                 }
                 
                 // create the authentication token
-                loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration);
+                loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, loginIdentityProvider.getClass().getSimpleName());
             } catch (final InvalidLoginCredentialsException ilce) {
                 throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
             } catch (final IdentityAccessException iae) {
@@ -355,7 +355,7 @@ public class AccessResource extends ApplicationResource {
             authorizeProxyIfNecessary(proxyChain);
 
             // create the authentication token
-            loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration());
+            loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration(), certificateIdentityProvider.getClass().getSimpleName());
         }
 
         // generate JWT for response

http://git-wip-us.apache.org/repos/asf/nifi/blob/3bc11e13/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
----------------------------------------------------------------------
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 25e52d6..acbbcfe 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
@@ -20,25 +20,30 @@ import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.Jws;
 import io.jsonwebtoken.JwsHeader;
+import io.jsonwebtoken.JwtException;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.MalformedJwtException;
 import io.jsonwebtoken.SignatureAlgorithm;
 import io.jsonwebtoken.SignatureException;
 import io.jsonwebtoken.SigningKeyResolverAdapter;
 import io.jsonwebtoken.UnsupportedJwtException;
-import java.nio.charset.StandardCharsets;
-import java.util.Calendar;
-import javax.servlet.http.HttpServletRequest;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.admin.service.AdministrationException;
 import org.apache.nifi.admin.service.KeyService;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
 
 /**
  *
  */
 public class JwtService {
+    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class);
 
+    private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
     private final static String AUTHORIZATION = "Authorization";
 
     private final KeyService keyService;
@@ -54,51 +59,104 @@ public class JwtService {
      * @return The user identifier from the token
      */
     public String getAuthentication(final HttpServletRequest request) {
+        // TODO: Refactor request token extraction out of this service
         // extract/verify token from incoming request
         final String authorization = request.getHeader(AUTHORIZATION);
-        final String token = StringUtils.substringAfterLast(authorization, " ");
+        final String base64EncodedToken = StringUtils.substringAfterLast(authorization, " ");
+
+        return getAuthenticationFromToken(base64EncodedToken);
+    }
+
+    public String getAuthenticationFromToken(final String base64EncodedToken) {
+        // The library representations of the JWT should be kept internal to this service.
+        try {
+            final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
+            return jws.getBody().getSubject();
+        } catch (JwtException e) {
+            logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
+            final String errorMessage = "There was an error parsing the Base64-encoded JWT";
+            logger.error(errorMessage, e);
+            throw new JwtException(errorMessage, e);
+        }
+    }
 
+    private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
         try {
-            final Jws<Claims> jwt = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
+            // TODO: Check algorithm for validity
+            // TODO: Ensure signature verification occurs
+            return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
                 @Override
                 public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                     final String identity = claims.getSubject();
+
+                    // The key is unique per identity and should be retrieved from the key service
                     final String key = keyService.getKey(identity);
 
-                    // ensure we were able to find a key that was previously issued by this key service for this user
+                    // Ensure we were able to find a key that was previously issued by this key service for this user
                     if (key == null) {
                         throw new UnsupportedJwtException("Unable to determine signing key for " + identity);
                     }
 
                     return key.getBytes(StandardCharsets.UTF_8);
                 }
-            }).parseClaimsJws(token);
-            return jwt.getBody().getSubject();
+            }).parseClaimsJws(base64EncodedToken);
         } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
-            return null;
+            // TODO: Exercise all exceptions to ensure none leak key material to logs
+            final String errorMessage = "There was an error parsing the Base64-encoded JWT";
+            logger.error(errorMessage, e);
+            throw new JwtException(errorMessage, e);
         }
     }
 
     /**
      * Generates a signed JWT token from the provided (Spring Security) login authentication token.
      *
-     * @param authenticationToken the authentication token
+     * @param authenticationToken
      * @return a signed JWT containing the user identity and the identity provider, Base64-encoded
+     * @throws JwtException
      */
-    public String generateSignedToken(final LoginAuthenticationToken authenticationToken) {
-        // set expiration to one day from now
-        final Calendar calendar = Calendar.getInstance();
-        calendar.setTimeInMillis(calendar.getTimeInMillis() + authenticationToken.getExpiration());
+    public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException {
+        if (authenticationToken == null) {
+            throw new IllegalArgumentException("Cannot generate a JWT for a null authentication token");
+        }
+
+        // Set expiration from the token
+        final Calendar expiration = Calendar.getInstance();
+        expiration.setTimeInMillis(authenticationToken.getExpiration());
 
-        // create a token the specified authentication
-        final String identity = authenticationToken.getPrincipal().toString();
+        final Object principal = authenticationToken.getPrincipal();
+        if (principal == null || StringUtils.isEmpty(principal.toString())) {
+            final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationToken.getIssuer();
+            logger.error(errorMessage);
+            throw new JwtException(errorMessage);
+        }
+
+        // Create a JWT with the specified authentication
+        final String identity = principal.toString();
         final String username = authenticationToken.getName();
 
-        // get/create the key for this user
-        final String key = keyService.getOrCreateKey(identity);
-        final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+        try {
+            // Get/create the key for this user
+            final String key = keyService.getOrCreateKey(identity);
+            final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+
+            logger.trace("Generating JWT for " + authenticationToken);
 
-        // build the token
-        return Jwts.builder().setSubject(identity).claim("preferred_username", username).setExpiration(calendar.getTime()).signWith(SignatureAlgorithm.HS512, keyBytes).compact();
+            // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
+
+            // Build the token
+            return Jwts.builder().setSubject(identity)
+                    .setIssuer(authenticationToken.getIssuer())
+                    .setAudience(authenticationToken.getIssuer())
+                    .claim("preferred_username", username)
+                    .setExpiration(expiration.getTime())
+                    .setIssuedAt(Calendar.getInstance().getTime())
+                    .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
+        } catch (NullPointerException | AdministrationException e) {
+            // TODO: Remove exception handling and pass through
+            final String errorMessage = "Could not retrieve the signing key for JWT";
+            logger.error(errorMessage, e);
+            throw new JwtException(errorMessage, e);
+        }
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/3bc11e13/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java
index 0bb0932..a06ee89 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java
@@ -19,25 +19,46 @@ package org.apache.nifi.web.security.token;
 import org.apache.nifi.security.util.CertificateUtils;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
 /**
- * This is an Authentication Token for logging in. Once a user is authenticated, they can be issues an ID token.
+ * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token.
  */
 public class LoginAuthenticationToken extends AbstractAuthenticationToken {
 
     private final String identity;
     private final String username;
     private final long expiration;
+    private final String issuer;
 
-    public LoginAuthenticationToken(final String identity, final long expiration) {
-        this(identity, null, expiration);
+    /**
+     * Creates a representation of the authentication token for a user.
+     *
+     * @param identity   The unique identifier for this user
+     * @param expiration The relative time to expiration in milliseconds
+     * @param issuer     The IdentityProvider implementation that generated this token
+     */
+    public LoginAuthenticationToken(final String identity, final long expiration, final String issuer) {
+        this(identity, null, expiration, issuer);
     }
 
-    public LoginAuthenticationToken(final String identity, final String username, final long expiration) {
+    /**
+     * Creates a representation of the authentication token for a user.
+     *
+     * @param identity   The unique identifier for this user (cannot be null or empty)
+     * @param username   The preferred username for this user
+     * @param expiration The relative time to expiration in milliseconds
+     * @param issuer     The IdentityProvider implementation that generated this token
+     */
+    public LoginAuthenticationToken(final String identity, final String username, final long expiration, final String issuer) {
         super(null);
         setAuthenticated(true);
         this.identity = identity;
         this.username = username;
-        this.expiration = expiration;
+        this.issuer = issuer;
+        Calendar now = Calendar.getInstance();
+        this.expiration = now.getTimeInMillis() + expiration;
     }
 
     @Override
@@ -50,10 +71,19 @@ public class LoginAuthenticationToken extends AbstractAuthenticationToken {
         return identity;
     }
 
+    /**
+     * Returns the expiration instant in milliseconds. This value is an absolute point in time (i.e. Nov 16, 2015 11:30:00.000 GMT), not a relative time (i.e. 60 minutes). It is calculated by adding the relative expiration from the constructor to the timestamp at object creation.
+     *
+     * @return the expiration in millis
+     */
     public long getExpiration() {
         return expiration;
     }
 
+    public String getIssuer() {
+        return issuer;
+    }
+
     @Override
     public String getName() {
         if (username == null) {
@@ -64,4 +94,28 @@ public class LoginAuthenticationToken extends AbstractAuthenticationToken {
         }
     }
 
+    @Override
+    public String toString() {
+        Calendar expirationTime = Calendar.getInstance();
+        expirationTime.setTimeInMillis(getExpiration());
+        long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
+
+        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS");
+        dateFormat.setTimeZone(expirationTime.getTimeZone());
+        String expirationTimeString = dateFormat.format(expirationTime.getTime());
+
+        return new StringBuilder("LoginAuthenticationToken for ")
+                .append(getName())
+                .append(" issued by ")
+                .append(getIssuer())
+                .append(" expiring at ")
+                .append(expirationTimeString)
+                .append(" [")
+                .append(getExpiration())
+                .append(" ms, ")
+                .append(remainingTime)
+                .append(" ms remaining]")
+                .toString();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/3bc11e13/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..a7e763e
--- /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/JwtServiceTest.java
@@ -0,0 +1,254 @@
+package org.apache.nifi.web.security.jwt;
+
+import io.jsonwebtoken.JwtException;
+import org.apache.commons.codec.CharEncoding;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.nifi.admin.service.AdministrationException;
+import org.apache.nifi.admin.service.KeyService;
+import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.codehaus.jettison.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+/**
+ * Created by alopresto on 11/11/15.
+ */
+public class JwtServiceTest {
+
+    private static final Logger logger = LoggerFactory.getLogger(JwtServiceTest.class);
+
+    /**
+     * These constant strings were generated using the tool at http://jwt.io
+     */
+
+    private static final String VALID_SIGNED_TOKEN = "";
+    private static final String INVALID_SIGNED_TOKEN = "";
+    private static final String VALID_UNSIGNED_TOKEN = "";
+    private static final String INVALID_UNSIGNED_TOKEN = "";
+    private static final String VALID_MALSIGNED_TOKEN = "";
+    private static final String INVALID_MALSIGNED_TOKEN = "";
+
+    private static final String DEFAULT_HEADER = "{\"alg\":\"HS256\"}";
+
+    private static final String TOKEN_DELIMITER = ".";
+
+    private static final String HMAC_SECRET = "test_hmac_shared_secret";
+
+    private KeyService mockKeyService;
+
+    // Class under test
+    private JwtService jwtService;
+
+
+    private String generateHS256Token(String rawHeader, String rawPayload, boolean isValid, boolean isSigned) {
+        return generateHS256Token(rawHeader, rawPayload, HMAC_SECRET, isValid, isSigned);
+    }
+
+    private String generateHS256Token(String rawHeader, String rawPayload, String hmacSecret, boolean isValid, boolean isSigned) {
+        try {
+            logger.info("Generating token for " + rawHeader + " + " + rawPayload);
+
+            String base64Header = Base64.encodeBase64URLSafeString(rawHeader.getBytes(CharEncoding.UTF_8));
+            String base64Payload = Base64.encodeBase64URLSafeString(rawPayload.getBytes(CharEncoding.UTF_8));
+            // TODO: Support valid/invalid manipulation
+
+            final String body = base64Header + TOKEN_DELIMITER + base64Payload;
+
+            String signature = generateHMAC(hmacSecret, body);
+
+            return body + TOKEN_DELIMITER + signature;
+        } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException e) {
+            final String errorMessage = "Could not generate the token";
+            logger.error(errorMessage, e);
+            fail(errorMessage);
+            return null;
+        }
+    }
+
+    private String generateHMAC(String hmacSecret, String body) throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
+        Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
+        SecretKeySpec secret_key = new SecretKeySpec(hmacSecret.getBytes("UTF-8"), "HmacSHA256");
+        hmacSHA256.init(secret_key);
+        return Base64.encodeBase64URLSafeString(hmacSHA256.doFinal(body.getBytes("UTF-8")));
+    }
+
+
+    @Before
+    public void setUp() throws Exception {
+        mockKeyService = Mockito.mock(KeyService.class);
+        when(mockKeyService.getOrCreateKey(anyString())).thenReturn(HMAC_SECRET);
+        jwtService = new JwtService(mockKeyService);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+
+    }
+
+    @Test
+    public void testShouldGetAuthenticationForValidToken() throws Exception {
+
+    }
+
+    @Test
+    public void testShouldNotGetAuthenticationForInvalidToken() throws Exception {
+        // Arrange
+        String token = INVALID_SIGNED_TOKEN;
+
+        String header = "{" +
+                "  \"alg\":\"HS256\"" +
+                "}";
+        String payload = "{" +
+                "  \"sub\":\"alopresto\"," +
+                "  \"preferred_username\":\"alopresto\"," +
+                "  \"exp\":2895419760" +
+                "}";
+
+        // Act
+        logger.info("Test token: " + generateHS256Token(header, payload, true, true));
+
+
+        // Assert
+
+
+    }
+
+    @Test
+    public void testShouldNotGetAuthenticationForEmptyToken() throws Exception {
+
+    }
+
+    @Test
+    public void testShouldNotGetAuthenticationForUnsignedToken() throws Exception {
+
+    }
+
+    @Test
+    public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithm() throws Exception {
+
+    }
+
+    @Test
+    public void testShouldGenerateSignedToken() throws Exception {
+        // Arrange
+
+        // Token expires in 60 seconds
+        final int EXPIRATION_MILLIS = 60000;
+        LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto", EXPIRATION_MILLIS, "MockIdentityProvider");
+        logger.debug("Generating token for " + loginAuthenticationToken);
+
+        final String EXPECTED_HEADER = DEFAULT_HEADER;
+
+        // Convert the expiration time from ms to s
+        final long TOKEN_EXPIRATION_SEC = (long) (loginAuthenticationToken.getExpiration() / 1000.0);
+
+        // Act
+        String token = jwtService.generateSignedToken(loginAuthenticationToken);
+        logger.debug("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
+        final String DECODED_PAYLOAD = new String(Base64.decodeBase64(token.split("\\.")[1].getBytes()));
+        final long ISSUED_AT_SEC = Long.valueOf(DECODED_PAYLOAD.substring(DECODED_PAYLOAD.lastIndexOf(":") + 1, 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
+        Map<String, Object> claims = new LinkedHashMap<>();
+        claims.put("sub", "alopresto");
+        claims.put("iss", "MockIdentityProvider");
+        claims.put("aud", "MockIdentityProvider");
+        claims.put("preferred_username", "alopresto");
+        claims.put("exp", TOKEN_EXPIRATION_SEC);
+        claims.put("iat", ISSUED_AT_SEC);
+        logger.trace("JSON Object to String: " + new JSONObject(claims).toString());
+
+        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);
+
+        // Assert
+        assertEquals("JWT token", EXPECTED_TOKEN_STRING, token);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testShouldNotGenerateTokenWithNullAuthenticationToken() throws Exception {
+        // Arrange
+        LoginAuthenticationToken nullLoginAuthenticationToken = null;
+        logger.debug("Generating token for " + nullLoginAuthenticationToken);
+
+        // Act
+        jwtService.generateSignedToken(nullLoginAuthenticationToken);
+
+        // Assert
+
+        // Should throw exception
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGenerateTokenWithEmptyIdentity() throws Exception {
+        // Arrange
+        final int EXPIRATION_MILLIS = 60000;
+        LoginAuthenticationToken emptyIdentityLoginAuthenticationToken = new LoginAuthenticationToken("", EXPIRATION_MILLIS, "MockIdentityProvider");
+        logger.debug("Generating token for " + emptyIdentityLoginAuthenticationToken);
+
+        // Act
+        jwtService.generateSignedToken(emptyIdentityLoginAuthenticationToken);
+
+        // Assert
+
+        // Should throw exception
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGenerateTokenWithNullIdentity() throws Exception {
+        // Arrange
+        final int EXPIRATION_MILLIS = 60000;
+        LoginAuthenticationToken nullIdentityLoginAuthenticationToken = new LoginAuthenticationToken(null, EXPIRATION_MILLIS, "MockIdentityProvider");
+        logger.debug("Generating token for " + nullIdentityLoginAuthenticationToken);
+
+        // Act
+        jwtService.generateSignedToken(nullIdentityLoginAuthenticationToken);
+
+        // Assert
+
+        // Should throw exception
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGenerateTokenWithMissingKey() throws Exception {
+        // Arrange
+        final int EXPIRATION_MILLIS = 60000;
+        LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto", EXPIRATION_MILLIS, "MockIdentityProvider");
+        logger.debug("Generating token for " + loginAuthenticationToken);
+
+        // Set up the bad key service
+        KeyService missingKeyService = Mockito.mock(KeyService.class);
+        when(missingKeyService.getOrCreateKey(anyString())).thenThrow(new AdministrationException("Could not find a key for that user"));
+        jwtService = new JwtService(missingKeyService);
+
+        // Act
+        jwtService.generateSignedToken(loginAuthenticationToken);
+
+        // Assert
+
+        // Should throw exception
+    }
+}
\ No newline at end of file


[4/6] nifi git commit: NIFI-655. - Changed issuer field to use FQ class name because some classes return an empty string for getSimpleName(). - Finished refactoring JWT logic from request parsing logic in JwtService. - Updated AccessResource and JwtAuthe

Posted by mc...@apache.org.
NIFI-655. - Changed issuer field to use FQ class name because some classes return an empty string for getSimpleName(). - Finished refactoring JWT logic from request parsing logic in JwtService. - Updated AccessResource and JwtAuthenticationFilter to call new JwtService methods decoupled from request header parsing. - Added extensive unit tests for JWT logic.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/7d04dfea
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/7d04dfea
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/7d04dfea

Branch: refs/heads/NIFI-655
Commit: 7d04dfeac012307201e84dd00883dd6a79e6e2cc
Parents: 3bc11e1
Author: Andy LoPresto <an...@andylopresto.com>
Authored: Wed Nov 18 00:45:38 2015 -0800
Committer: Matt Gilman <ma...@gmail.com>
Committed: Wed Nov 18 08:31:39 2015 -0500

----------------------------------------------------------------------
 .../admin/service/impl/StandardKeyService.java  |  10 +-
 .../org/apache/nifi/web/api/AccessResource.java |  54 +++---
 .../security/jwt/JwtAuthenticationFilter.java   |  44 ++++-
 .../nifi/web/security/jwt/JwtService.java       |  64 +++----
 .../nifi/web/security/jwt/JwtServiceTest.java   | 182 ++++++++++++++++---
 5 files changed, 264 insertions(+), 90 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java
----------------------------------------------------------------------
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 1b2f8c9..7dff9d8 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
@@ -18,6 +18,9 @@ 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.GetKeyAction;
+import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
 import org.apache.nifi.admin.service.transaction.Transaction;
 import org.apache.nifi.admin.service.transaction.TransactionBuilder;
 import org.apache.nifi.admin.service.transaction.TransactionException;
@@ -26,9 +29,6 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
-import org.apache.nifi.admin.service.KeyService;
-import org.apache.nifi.admin.service.action.GetKeyAction;
-import org.apache.nifi.admin.service.action.GetOrCreateKeyAction;
 
 /**
  *
@@ -45,6 +45,8 @@ public class StandardKeyService implements KeyService {
 
     @Override
     public String getKey(String identity) {
+        // TODO: Change this service to look up by "key ID" instead of identity
+        // TODO: Change the return type to a Key POJO to support key rotation
         Transaction transaction = null;
         String key = null;
 
@@ -75,6 +77,8 @@ public class StandardKeyService implements KeyService {
 
     @Override
     public String getOrCreateKey(String identity) {
+        // TODO: Change this service to look up by "key ID" instead of identity
+        // TODO: Change the return type to a Key POJO to support key rotation
         Transaction transaction = null;
         String key = null;
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
----------------------------------------------------------------------
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 b4778ad..2e1c44e 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
@@ -23,6 +23,7 @@ import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
+import io.jsonwebtoken.JwtException;
 import org.apache.nifi.util.NiFiProperties;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
@@ -184,30 +185,33 @@ public class AccessResource extends ApplicationResource {
                     accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
                     accessStatus.setMessage("No credentials supplied, unknown user.");
                 } else {
-                    // TODO - use this token with the JWT service
+                    // Extract the Base64 encoded token from the Authorization header
                     final String token = StringUtils.substringAfterLast(authorization, " ");
 
-                    // TODO - do not call this method of the jwt service
-                    final String principal = jwtService.getAuthentication(httpServletRequest);
-
-                    // TODO - catch jwt exception?
-                    // ensure we have something we can work with (certificate or credentials)
-                    if (principal == null) {
-                        throw new IllegalArgumentException("The specific token is not valid.");
-                    } else {
-                        // set the user identity
-                        accessStatus.setIdentity(principal);
-                        accessStatus.setUsername(CertificateUtils.extractUsername(principal));
-
-                        // without a certificate, this is not a proxied request
-                        final List<String> chain = Arrays.asList(principal);
-
-                        // check authorization for this user
-                        checkAuthorization(chain);
-
-                        // no issues with authorization
-                        accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                        accessStatus.setMessage("Account is active and authorized");
+                    try {
+                        final String principal = jwtService.getAuthenticationFromToken(token);
+
+                        // ensure we have something we can work with (certificate or credentials)
+                        if (principal == null) {
+                            throw new IllegalArgumentException("The specific token is not valid.");
+                        } else {
+                            // set the user identity
+                            accessStatus.setIdentity(principal);
+                            accessStatus.setUsername(CertificateUtils.extractUsername(principal));
+
+                            // without a certificate, this is not a proxied request
+                            final List<String> chain = Arrays.asList(principal);
+
+                            // check authorization for this user
+                            checkAuthorization(chain);
+
+                            // no issues with authorization
+                            accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
+                            accessStatus.setMessage("Account is active and authorized");
+                        }
+                    } catch (JwtException e) {
+                        // TODO: Handle the exception from a failed JWT verification
+                        throw new AccessDeniedException("The JWT could not be verified", e);
                     }
                 }
             } else {
@@ -334,7 +338,8 @@ public class AccessResource extends ApplicationResource {
                 }
                 
                 // create the authentication token
-                loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, loginIdentityProvider.getClass().getSimpleName());
+                // TODO: Some Spring beans return "" for getClass().getSimpleName(). Using getName() temporarily
+                loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, loginIdentityProvider.getClass().getName());
             } catch (final InvalidLoginCredentialsException ilce) {
                 throw new IllegalArgumentException("The supplied username and password are not valid.", ilce);
             } catch (final IdentityAccessException iae) {
@@ -355,7 +360,8 @@ public class AccessResource extends ApplicationResource {
             authorizeProxyIfNecessary(proxyChain);
 
             // create the authentication token
-            loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration(), certificateIdentityProvider.getClass().getSimpleName());
+            // TODO: Some Spring beans return "" for getClass().getSimpleName(). Using getName() temporarily
+            loginAuthenticationToken = new LoginAuthenticationToken(proxyChain.get(0), authenticationResponse.getExpiration(), certificateIdentityProvider.getClass().getName());
         }
 
         // generate JWT for response

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
----------------------------------------------------------------------
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 5a84e93..dea5bba 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
@@ -16,9 +16,8 @@
  */
 package org.apache.nifi.web.security.jwt;
 
-import java.util.Arrays;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import io.jsonwebtoken.JwtException;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.apache.nifi.web.security.token.NewAccountAuthenticationRequestToken;
 import org.apache.nifi.web.security.token.NiFiAuthenticationRequestToken;
@@ -26,12 +25,18 @@ import org.apache.nifi.web.security.user.NewAccountRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Arrays;
+
 /**
  */
 public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
 
     private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
 
+    private static final String AUTHORIZATION = "Authorization";
+
     private JwtService jwtService;
 
     @Override
@@ -41,16 +46,35 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
             return null;
         }
 
+        // TODO: Refactor request header extraction logic to shared utility as it is duplicated in AccessResource
+
         // get the principal out of the user token
-        final String jwtPrincipal = jwtService.getAuthentication(request);
-        if (jwtPrincipal == null) {
-            return null;
-        }
+        // look for an authorization token
+        final String authorization = request.getHeader(AUTHORIZATION);
 
-        if (isNewAccountRequest(request)) {
-            return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
+        // if there is no authorization header, we don't know the user
+        if (authorization == null) {
+            return null;
         } else {
-            return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal));
+            // Extract the Base64 encoded token from the Authorization header
+            final String token = StringUtils.substringAfterLast(authorization, " ");
+
+            try {
+                final String jwtPrincipal = jwtService.getAuthenticationFromToken(token);
+                if (jwtPrincipal == null) {
+                    return null;
+                }
+
+                if (isNewAccountRequest(request)) {
+                    return new NewAccountAuthenticationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
+                } else {
+                    return new NiFiAuthenticationRequestToken(Arrays.asList(jwtPrincipal));
+                }
+            } catch (JwtException e) {
+                // TODO: Is this the correct way to handle an unverified token?
+                logger.error("Could not verify JWT", e);
+                return null;
+            }
         }
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtService.java
----------------------------------------------------------------------
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 acbbcfe..f006e5b 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
@@ -33,7 +33,6 @@ import org.apache.nifi.admin.service.KeyService;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.slf4j.LoggerFactory;
 
-import javax.servlet.http.HttpServletRequest;
 import java.nio.charset.StandardCharsets;
 import java.util.Calendar;
 
@@ -44,7 +43,8 @@ public class JwtService {
     private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class);
 
     private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
-    private final static String AUTHORIZATION = "Authorization";
+    private static final String KEY_ID_CLAIM = "kid";
+    private static final String USERNAME_CLAIM = "preferred_username";
 
     private final KeyService keyService;
 
@@ -52,49 +52,50 @@ public class JwtService {
         this.keyService = keyService;
     }
 
-    /**
-     * Gets the Authentication by extracting a JWT token from the specified request.
-     *
-     * @param request Request to extract the token from
-     * @return The user identifier from the token
-     */
-    public String getAuthentication(final HttpServletRequest request) {
-        // TODO: Refactor request token extraction out of this service
-        // extract/verify token from incoming request
-        final String authorization = request.getHeader(AUTHORIZATION);
-        final String base64EncodedToken = StringUtils.substringAfterLast(authorization, " ");
-
-        return getAuthenticationFromToken(base64EncodedToken);
-    }
-
-    public String getAuthenticationFromToken(final String base64EncodedToken) {
+    public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException {
         // The library representations of the JWT should be kept internal to this service.
         try {
             final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
+
+            if (jws == null) {
+                throw new JwtException("Unable to parse token");
+            }
+
+            // Additional validation that subject is present
+            if (StringUtils.isEmpty(jws.getBody().getSubject())) {
+                throw new JwtException("No subject available in token");
+            }
+
+            // TODO: Validate issuer against active registry?
+            if (StringUtils.isEmpty(jws.getBody().getIssuer())) {
+                // TODO: Remove after testing
+//                logger.info("Decoded JWT payload: " + jws.toString());
+                throw new JwtException("No issuer available in token");
+            }
             return jws.getBody().getSubject();
         } catch (JwtException e) {
             logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
-            final String errorMessage = "There was an error parsing the Base64-encoded JWT";
+            final String errorMessage = "There was an error validating the JWT";
             logger.error(errorMessage, e);
-            throw new JwtException(errorMessage, e);
+            throw e;
         }
     }
 
     private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
         try {
-            // TODO: Check algorithm for validity
-            // TODO: Ensure signature verification occurs
             return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
                 @Override
                 public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
                     final String identity = claims.getSubject();
 
+                    // TODO: Currently the kid field is identical to identity, but will be a unique key ID when key rotation is implemented
+                    final String keyId = claims.get(KEY_ID_CLAIM, String.class);
                     // The key is unique per identity and should be retrieved from the key service
-                    final String key = keyService.getKey(identity);
+                    final String key = keyService.getKey(keyId);
 
                     // Ensure we were able to find a key that was previously issued by this key service for this user
                     if (key == null) {
-                        throw new UnsupportedJwtException("Unable to determine signing key for " + identity);
+                        throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
                     }
 
                     return key.getBytes(StandardCharsets.UTF_8);
@@ -102,8 +103,7 @@ public class JwtService {
             }).parseClaimsJws(base64EncodedToken);
         } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
             // TODO: Exercise all exceptions to ensure none leak key material to logs
-            final String errorMessage = "There was an error parsing the Base64-encoded JWT";
-            logger.error(errorMessage, e);
+            final String errorMessage = "There was an error validating the JWT";
             throw new JwtException(errorMessage, e);
         }
     }
@@ -111,9 +111,9 @@ public class JwtService {
     /**
      * Generates a signed JWT token from the provided (Spring Security) login authentication token.
      *
-     * @param authenticationToken
+     * @param authenticationToken an instance of the Spring Security token after login credentials have been verified against the respective information source
      * @return a signed JWT containing the user identity and the identity provider, Base64-encoded
-     * @throws JwtException
+     * @throws JwtException if there is a problem generating the signed token
      */
     public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException {
         if (authenticationToken == null) {
@@ -144,17 +144,19 @@ public class JwtService {
 
             // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
 
+            // TODO: Change kid field to key ID when KeyService is refactored
+
             // Build the token
             return Jwts.builder().setSubject(identity)
                     .setIssuer(authenticationToken.getIssuer())
                     .setAudience(authenticationToken.getIssuer())
-                    .claim("preferred_username", username)
+                    .claim(USERNAME_CLAIM, username)
+                    .claim(KEY_ID_CLAIM, identity)
                     .setExpiration(expiration.getTime())
                     .setIssuedAt(Calendar.getInstance().getTime())
                     .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
         } catch (NullPointerException | AdministrationException e) {
-            // TODO: Remove exception handling and pass through
-            final String errorMessage = "Could not retrieve the signing key for JWT";
+            final String errorMessage = "Could not retrieve the signing key for JWT for " + identity;
             logger.error(errorMessage, e);
             throw new JwtException(errorMessage, e);
         }

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d04dfea/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
----------------------------------------------------------------------
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 a7e763e..c9107b9 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
@@ -9,6 +9,7 @@ import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.codehaus.jettison.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.Mockito;
 import org.slf4j.Logger;
@@ -38,14 +39,38 @@ public class JwtServiceTest {
      * These constant strings were generated using the tool at http://jwt.io
      */
 
-    private static final String VALID_SIGNED_TOKEN = "";
-    private static final String INVALID_SIGNED_TOKEN = "";
-    private static final String VALID_UNSIGNED_TOKEN = "";
-    private static final String INVALID_UNSIGNED_TOKEN = "";
-    private static final String VALID_MALSIGNED_TOKEN = "";
-    private static final String INVALID_MALSIGNED_TOKEN = "";
+    private static final String VALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.8fUgH9jLvE1essgrcoV8OCyDhXvSXUH_1xqeqDqWycU";
+
+    // This token has an empty subject field
+    private static final String INVALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.jcjRBLtDzREmdjkJf3xry-ucyCmSRygBaP-HCWBkwlI";
+
+    private static final String VALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9";
+
+    // This token has an empty subject field
+    private static final String INVALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9";
+
+    // Algorithm field is "none"
+    private static final String VALID_MALSIGNED_TOKEN = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.mPO_wMNMl_zjMNevhNvUoXbSJ9Kx6jAe5OxDIAzKQbI";
+
+    // Algorithm field is "none" and no signature is present
+    private static final String VALID_MALSIGNED_NO_SIG_TOKEN = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.";
+
+    // This token has an empty subject field
+    private static final String INVALID_MALSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.WAwmUY4KHKV2oARNodkqDkbZsfRXGZfD2Ccy64GX9QF";
+
+    private static final String EXPIRED_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.y3M1TlzXZ80cVTkfcNxaHpq6aAlM1y2HGCZWEOcvmSU";
+
+    // Subject is "mgilman" but signed with "alopresto" key
+    private static final String IMPOSTER_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtZ2lsbWFuIiwiaXNzIjoiTW9ja0lkZW50aXR5UHJvdmlkZXIiLCJhdWQiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsb3ByZXN0byIsImtpZCI6ImFsb3ByZXN0byIsImV4cCI6MjQ0NzgwODc2MSwiaWF0IjoxNDQ3ODA4NzAxfQ.l-9nHmYTEMgLshX8qCEqbc2O4BH_GYBVQIFkUKsJvLA";
+
+    // Issuer field is set to unknown provider
+    private static final String UNKNOWN_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJVbmtub3duSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.SAd9tyNwSaijWet9wvAWSNmpxmPSK4XQuLx7h3ARqBo";
+
+    // Issuer field is absent
+    private static final String NO_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbG9wcmVzdG8iLCJhdWQiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsb3ByZXN0byIsImtpZCI6ImFsb3ByZXN0byIsImV4cCI6MjQ0NzgwODc2MSwiaWF0IjoxNDQ3ODA4NzAxfQ.Hdha7K69sz6224vidvuZ6A6UdGLdZ_U1egS0txuVXAk";
 
     private static final String DEFAULT_HEADER = "{\"alg\":\"HS256\"}";
+    private static final String DEFAULT_IDENTITY = "alopresto";
 
     private static final String TOKEN_DELIMITER = ".";
 
@@ -93,6 +118,7 @@ public class JwtServiceTest {
     @Before
     public void setUp() throws Exception {
         mockKeyService = Mockito.mock(KeyService.class);
+        when(mockKeyService.getKey(anyString())).thenReturn(HMAC_SECRET);
         when(mockKeyService.getOrCreateKey(anyString())).thenReturn(HMAC_SECRET);
         jwtService = new JwtService(mockKeyService);
     }
@@ -104,45 +130,156 @@ public class JwtServiceTest {
 
     @Test
     public void testShouldGetAuthenticationForValidToken() throws Exception {
+        // Arrange
+        String token = VALID_SIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
 
+        // Assert
+        assertEquals("Identity", DEFAULT_IDENTITY, identity);
     }
 
-    @Test
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForInvalidToken() throws Exception {
         // Arrange
         String token = INVALID_SIGNED_TOKEN;
 
-        String header = "{" +
-                "  \"alg\":\"HS256\"" +
-                "}";
-        String payload = "{" +
-                "  \"sub\":\"alopresto\"," +
-                "  \"preferred_username\":\"alopresto\"," +
-                "  \"exp\":2895419760" +
-                "}";
-
         // Act
-        logger.info("Test token: " + generateHS256Token(header, payload, true, true));
-
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
 
         // Assert
 
-
+        // Should fail
     }
 
-    @Test
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForEmptyToken() throws Exception {
+        // Arrange
+        String token = "";
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
 
+        // Assert
+
+        // Should fail
     }
 
-    @Test
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForUnsignedToken() throws Exception {
+        // Arrange
+        String token = VALID_UNSIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
 
+        // Should fail
     }
 
-    @Test
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForMalsignedToken() throws Exception {
+        // Arrange
+        String token = VALID_MALSIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
     public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithm() throws Exception {
+        // Arrange
+        String token = VALID_MALSIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithmAndNoSignature() throws Exception {
+        // Arrange
+        String token = VALID_MALSIGNED_NO_SIG_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Ignore("Not yet implemented")
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForTokenFromUnknownIdentityProvider() throws Exception {
+        // Arrange
+        String token = UNKNOWN_ISSUER_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForTokenFromEmptyIdentityProvider() throws Exception {
+        // Arrange
+        String token = NO_ISSUER_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForExpiredToken() throws Exception {
+        // Arrange
+        String token = EXPIRED_SIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
+
+        // Should fail
+    }
+
+    @Test(expected = JwtException.class)
+    public void testShouldNotGetAuthenticationForImposterToken() throws Exception {
+        // Arrange
+        String token = IMPOSTER_SIGNED_TOKEN;
+
+        // Act
+        String identity = jwtService.getAuthenticationFromToken(token);
+        logger.debug("Extracted identity: " + identity);
+
+        // Assert
 
+        // Should fail
     }
 
     @Test
@@ -176,6 +313,7 @@ public class JwtServiceTest {
         claims.put("iss", "MockIdentityProvider");
         claims.put("aud", "MockIdentityProvider");
         claims.put("preferred_username", "alopresto");
+        claims.put("kid", "alopresto");
         claims.put("exp", TOKEN_EXPIRATION_SEC);
         claims.put("iat", ISSUED_AT_SEC);
         logger.trace("JSON Object to String: " + new JSONObject(claims).toString());


[2/6] nifi git commit: NIFI-655. - Added logback-test.xml configuration resource for nifi-web-security.

Posted by mc...@apache.org.
NIFI-655. - Added logback-test.xml configuration resource for nifi-web-security.

Signed-off-by: Matt Gilman <ma...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/caeede57
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/caeede57
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/caeede57

Branch: refs/heads/NIFI-655
Commit: caeede5773ac75061049a1c792a7b15188cae098
Parents: 45b24a4
Author: Andy LoPresto <an...@andylopresto.com>
Authored: Tue Nov 17 17:27:31 2015 -0800
Committer: Matt Gilman <ma...@gmail.com>
Committed: Wed Nov 18 08:31:08 2015 -0500

----------------------------------------------------------------------
 .../src/test/resources/logback-test.xml         | 36 ++++++++++++++++++++
 1 file changed, 36 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/caeede57/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/resources/logback-test.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/resources/logback-test.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..fe5d27e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/resources/logback-test.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<configuration>
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%-4r [%t] %-5p %c - %m%n</pattern>
+        </encoder>
+    </appender>
+    
+    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
+        <file>./target/log</file>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%date %level [%thread] %logger{40} %msg%n</pattern>
+        </encoder>
+    </appender>
+    
+    
+    <logger name="org.apache.nifi" level="TRACE"/>
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+    </root>
+</configuration>