You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by bb...@apache.org on 2016/02/19 19:22:18 UTC

[2/2] nifi git commit: NIFI-1497: - Introducing a one time use password service for use in query parameters when accessing UI extensions and downloading resources. - Using one time use tokens when accessing ui extensions and downloading resources. - Ensu

NIFI-1497: - Introducing a one time use password service for use in query parameters when accessing UI extensions and downloading resources. - Using one time use tokens when accessing ui extensions and downloading resources. - Ensuring appropriate roles when accessing component details through the web context for custom UIs. - Addressing typo in class name. - Ensuring appropriate roles when accessing content through the content access. - Code clean up. - Refactoring some basic scripts for accessing JWT tokens so UI extensions can reuse common functionality.

Signed-off-by: Bryan Bende <bb...@apache.org>


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

Branch: refs/heads/master
Commit: a8edab2e7955b175c5cbd2a3266fb51fa9712aee
Parents: 86ab442
Author: Matt Gilman <ma...@gmail.com>
Authored: Fri Feb 19 10:50:31 2016 -0500
Committer: Bryan Bende <bb...@apache.org>
Committed: Fri Feb 19 10:54:53 2016 -0500

----------------------------------------------------------------------
 .../web/NiFiWebApiSecurityConfiguration.java    |  35 ++-
 .../nifi/web/StandardNiFiContentAccess.java     |  70 ++++--
 .../StandardNiFiWebConfigurationContext.java    |   4 +
 .../apache/nifi/web/StandardNiFiWebContext.java |   4 +
 .../org/apache/nifi/web/api/AccessResource.java | 122 ++++++++++-
 .../src/main/resources/nifi-web-api-context.xml |   1 +
 .../accesscontrol/AccessTokenEndpointTest.java  |   2 +-
 .../nifi/web/ContentViewerController.java       |  40 +++-
 .../src/main/webapp/WEB-INF/jsp/header.jsp      |  64 +++++-
 .../src/main/webapp/css/main.css                |   8 +
 .../web/security/NiFiAuthenticationFilter.java  |   6 +-
 .../security/NiFiAuthenticationProvider.java    |  10 +-
 .../anonymous/NiFiAnonymousUserFilter.java      |   7 +-
 .../authorization/NiFiAuthorizationService.java |   6 +-
 .../security/jwt/JwtAuthenticationFilter.java   |   6 +-
 .../otp/OtpAuthenticationException.java         |  34 +++
 .../security/otp/OtpAuthenticationFilter.java   |  96 ++++++++
 .../nifi/web/security/otp/OtpService.java       | 219 +++++++++++++++++++
 .../NewAccountAuthorizationRequestToken.java    |   2 +-
 .../token/NiFiAuthorizationRequestToken.java    |  54 +++++
 .../token/NiFiAuthortizationRequestToken.java   |  54 -----
 .../security/token/OtpAuthenticationToken.java  |  56 +++++
 .../security/x509/X509AuthenticationFilter.java |   6 +-
 .../resources/nifi-web-security-context.xml     |   5 +-
 .../NiFiAuthorizationServiceTest.java           |   6 +-
 .../otp/OtpAuthenticationFilterTest.java        | 210 ++++++++++++++++++
 .../nifi/web/security/otp/OtpServiceTest.java   | 149 +++++++++++++
 .../nifi-framework/nifi-web/nifi-web-ui/pom.xml |  14 ++
 .../resources/filters/bulletin-board.properties |   1 +
 .../main/resources/filters/canvas.properties    |   1 +
 .../main/resources/filters/cluster.properties   |   1 +
 .../main/resources/filters/counters.properties  |   1 +
 .../main/resources/filters/history.properties   |   1 +
 .../src/main/resources/filters/login.properties |   1 +
 .../resources/filters/provenance.properties     |   1 +
 .../main/resources/filters/summary.properties   |   1 +
 .../main/resources/filters/templates.properties |   1 +
 .../src/main/resources/filters/users.properties |   1 +
 .../main/webapp/js/nf/canvas/nf-canvas-utils.js |   2 +-
 .../main/webapp/js/nf/canvas/nf-custom-ui.js    |  42 ++--
 .../webapp/js/nf/canvas/nf-queue-listing.js     | 136 +++++++++---
 .../src/main/webapp/js/nf/nf-ajax-setup.js      |  39 ++++
 .../src/main/webapp/js/nf/nf-common.js          |  41 ++--
 .../src/main/webapp/js/nf/nf-storage.js         |  25 ++-
 .../js/nf/provenance/nf-provenance-table.js     | 136 +++++++++---
 .../js/nf/templates/nf-templates-table.js       |  33 ++-
 .../update/attributes/api/RuleResource.java     |  22 +-
 .../src/main/webapp/WEB-INF/jsp/worksheet.jsp   |   3 +
 48 files changed, 1550 insertions(+), 229 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
index 1488aba..64ecc5f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
@@ -24,7 +24,9 @@ import org.apache.nifi.web.security.anonymous.NiFiAnonymousUserFilter;
 import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
 import org.apache.nifi.web.security.jwt.JwtService;
 import org.apache.nifi.web.security.node.NodeAuthorizedUserFilter;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.otp.OtpAuthenticationFilter;
+import org.apache.nifi.web.security.otp.OtpService;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
 import org.apache.nifi.web.security.x509.X509CertificateExtractor;
 import org.apache.nifi.web.security.x509.X509IdentityProvider;
@@ -54,12 +56,14 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
     private UserService userService;
     private AuthenticationUserDetailsService userDetailsService;
     private JwtService jwtService;
+    private OtpService otpService;
     private X509CertificateExtractor certificateExtractor;
     private X509IdentityProvider certificateIdentityProvider;
     private LoginIdentityProvider loginIdentityProvider;
 
     private NodeAuthorizedUserFilter nodeAuthorizedUserFilter;
     private JwtAuthenticationFilter jwtAuthenticationFilter;
+    private OtpAuthenticationFilter otpAuthenticationFilter;
     private X509AuthenticationFilter x509AuthenticationFilter;
     private NiFiAnonymousUserFilter anonymousAuthenticationFilter;
 
@@ -69,9 +73,12 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
 
     @Override
     public void configure(WebSecurity webSecurity) throws Exception {
+        // ignore the access endpoints for obtaining the access config, the access token
+        // granting, and access status for a given user (note: we are not ignoring the
+        // the /access/download-token and /access/ui-extension-token endpoints
         webSecurity
                 .ignoring()
-                    .antMatchers("/access/**");
+                    .antMatchers("/access", "/access/config", "/access/token");
     }
 
     @Override
@@ -95,6 +102,9 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
 
         // jwt
         http.addFilterAfter(jwtFilterBean(), AnonymousAuthenticationFilter.class);
+
+        // otp
+        http.addFilterAfter(otpFilterBean(), AnonymousAuthenticationFilter.class);
     }
 
     @Bean
@@ -135,6 +145,20 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
         return jwtAuthenticationFilter;
     }
 
+    public OtpAuthenticationFilter otpFilterBean() throws Exception {
+        if (otpAuthenticationFilter == null) {
+            otpAuthenticationFilter = new OtpAuthenticationFilter();
+            otpAuthenticationFilter.setProperties(properties);
+            otpAuthenticationFilter.setAuthenticationManager(authenticationManager());
+
+            // only consider the tokens when configured for login
+            if (loginIdentityProvider != null) {
+                otpAuthenticationFilter.setOtpService(otpService);
+            }
+        }
+        return otpAuthenticationFilter;
+    }
+
     @Bean
     public X509AuthenticationFilter x509FilterBean() throws Exception {
         if (x509AuthenticationFilter == null) {
@@ -157,7 +181,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
     }
 
     @Autowired
-    public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService) {
+    public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
         this.userDetailsService = userDetailsService;
     }
 
@@ -177,6 +201,11 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
     }
 
     @Autowired
+    public void setOtpService(OtpService otpService) {
+        this.otpService = otpService;
+    }
+
+    @Autowired
     public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
         this.loginIdentityProvider = loginIdentityProvider;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java
index f994c52..afaf3ed 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java
@@ -17,19 +17,10 @@
 package org.apache.nifi.web;
 
 import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.ClientResponse.Status;
 import com.sun.jersey.core.util.MultivaluedMapImpl;
-import java.io.Serializable;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.ws.rs.HttpMethod;
-import javax.ws.rs.core.MultivaluedMap;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.authorization.Authority;
 import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.cluster.manager.exception.UnknownNodeException;
 import org.apache.nifi.cluster.manager.impl.WebClusterManager;
@@ -38,13 +29,26 @@ import org.apache.nifi.cluster.protocol.NodeIdentifier;
 import org.apache.nifi.controller.repository.claim.ContentDirection;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.security.user.NiFiUserDetails;
+import org.apache.nifi.web.security.user.NiFiUserUtils;
 import org.apache.nifi.web.util.WebUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MultivaluedMap;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /**
  *
  */
@@ -54,17 +58,16 @@ public class StandardNiFiContentAccess implements ContentAccess {
     public static final String CLIENT_ID_PARAM = "clientId";
 
     private static final Pattern FLOWFILE_CONTENT_URI_PATTERN = Pattern
-        .compile("/controller/process-groups/((?:root)|(?:[a-f0-9\\-]{36}))/connections/([a-f0-9\\-]{36})/flowfiles/([a-f0-9\\-]{36})/content");
+        .compile("/controller/process-groups/((?:root)|(?:[a-f0-9\\-]{36}))/connections/([a-f0-9\\-]{36})/flowfiles/([a-f0-9\\-]{36})/content.*");
 
     private static final Pattern PROVENANCE_CONTENT_URI_PATTERN = Pattern
-        .compile("/controller/provenance/events/([0-9]+)/content/((?:input)|(?:output))");
+        .compile("/controller/provenance/events/([0-9]+)/content/((?:input)|(?:output)).*");
 
     private NiFiProperties properties;
     private NiFiServiceFacade serviceFacade;
     private WebClusterManager clusterManager;
 
     @Override
-    @PreAuthorize("hasRole('ROLE_PROVENANCE')")
     public DownloadableContent getContent(final ContentRequestContext request) {
         // if clustered, send request to cluster manager
         if (properties.isClusterManager()) {
@@ -99,6 +102,11 @@ public class StandardNiFiContentAccess implements ContentAccess {
                 }
             }
 
+            // ensure we were able to detect the cluster node id
+            if (request.getClusterNodeId() == null) {
+                throw new IllegalArgumentException("Unable to determine the which node has the content.");
+            }
+
             // get the target node and ensure it exists
             final Node targetNode = clusterManager.getNode(request.getClusterNodeId());
             if (targetNode == null) {
@@ -113,6 +121,16 @@ public class StandardNiFiContentAccess implements ContentAccess {
             final ClientResponse clientResponse = nodeResponse.getClientResponse();
             final MultivaluedMap<String, String> responseHeaders = clientResponse.getHeaders();
 
+            // ensure an appropriate response
+            if (Status.NOT_FOUND.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()) {
+                throw new ResourceNotFoundException(clientResponse.getEntity(String.class));
+            } else if (Status.FORBIDDEN.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()
+                        || Status.UNAUTHORIZED.getStatusCode() == clientResponse.getStatusInfo().getStatusCode()) {
+                throw new AccessDeniedException(clientResponse.getEntity(String.class));
+            } else if (Status.OK.getStatusCode() != clientResponse.getStatusInfo().getStatusCode()) {
+                throw new IllegalStateException(clientResponse.getEntity(String.class));
+            }
+
             // get the file name
             final String contentDisposition = responseHeaders.getFirst("Content-Disposition");
             final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\"");
@@ -140,7 +158,7 @@ public class StandardNiFiContentAccess implements ContentAccess {
                 final String connectionId = flowFileMatcher.group(2);
                 final String flowfileId = flowFileMatcher.group(3);
 
-                return serviceFacade.getContent(groupId, connectionId, flowfileId, dataUri);
+                return getFlowFileContent(groupId, connectionId, flowfileId, dataUri);
             }
 
             // provenance event content
@@ -150,7 +168,7 @@ public class StandardNiFiContentAccess implements ContentAccess {
                     final Long eventId = Long.parseLong(provenanceMatcher.group(1));
                     final ContentDirection direction = ContentDirection.valueOf(provenanceMatcher.group(2).toUpperCase());
 
-                    return serviceFacade.getContent(eventId, dataUri, direction);
+                    return getProvenanceEventContent(eventId, dataUri, direction);
                 } catch (final IllegalArgumentException iae) {
                     throw new IllegalArgumentException("The specified data reference URI is not valid.");
                 }
@@ -161,6 +179,24 @@ public class StandardNiFiContentAccess implements ContentAccess {
         }
     }
 
+    private DownloadableContent getFlowFileContent(final String groupId, final String connectionId, final String flowfileId, final String dataUri) {
+        // ensure the user is authorized as DFM - not checking with @PreAuthorized annotation as aspect not trigger on call within a class
+        if (!NiFiUserUtils.getAuthorities().contains(Authority.ROLE_DFM.toString())) {
+            throw new AccessDeniedException("Access is denied.");
+        }
+
+        return serviceFacade.getContent(groupId, connectionId, flowfileId, dataUri);
+    }
+
+    private DownloadableContent getProvenanceEventContent(final Long eventId, final String dataUri, final ContentDirection direction) {
+        // ensure the user is authorized as Provenance - not checking with @PreAuthorized annotation as aspect not trigger on call within a class
+        if (!NiFiUserUtils.getAuthorities().contains(Authority.ROLE_PROVENANCE.toString())) {
+            throw new AccessDeniedException("Access is denied.");
+        }
+
+        return serviceFacade.getContent(eventId, dataUri, direction);
+    }
+
     public void setProperties(NiFiProperties properties) {
         this.properties = properties;
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java
index 9502bf2..cae1175 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebConfigurationContext.java
@@ -86,6 +86,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
     private AuditService auditService;
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public ControllerService getControllerService(String serviceIdentifier) {
         return controllerServiceLookup.getControllerService(serviceIdentifier);
     }
@@ -157,6 +158,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
     }
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public String getCurrentUserDn() {
         String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY;
 
@@ -169,6 +171,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
     }
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public String getCurrentUserName() {
         String userName = NiFiUser.ANONYMOUS_USER_IDENTITY;
 
@@ -181,6 +184,7 @@ public class StandardNiFiWebConfigurationContext implements NiFiWebConfiguration
     }
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public ComponentDetails getComponentDetails(final NiFiWebRequestContext requestContext) throws ResourceNotFoundException, ClusterRequestException {
         final String id = requestContext.getId();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java
index 7e9e77e..9667ad6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiWebContext.java
@@ -81,6 +81,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
     private AuditService auditService;
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public ControllerService getControllerService(String serviceIdentifier) {
         return controllerServiceLookup.getControllerService(serviceIdentifier);
     }
@@ -128,6 +129,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
     }
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public String getCurrentUserDn() {
         String userIdentity = NiFiUser.ANONYMOUS_USER_IDENTITY;
 
@@ -140,6 +142,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
     }
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public String getCurrentUserName() {
         String userName = NiFiUser.ANONYMOUS_USER_IDENTITY;
 
@@ -152,6 +155,7 @@ public class StandardNiFiWebContext implements NiFiWebContext {
     }
 
     @Override
+    @PreAuthorize("hasAnyRole('ROLE_MONITOR', 'ROLE_DFM', 'ROLE_ADMIN')")
     public ProcessorInfo getProcessor(final NiFiWebContextConfig config) throws ResourceNotFoundException, ClusterRequestException {
 
         final Revision revision = config.getRevision();

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/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 7bf9690..24536fc 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
@@ -24,6 +24,7 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
 import io.jsonwebtoken.JwtException;
+import org.apache.nifi.user.NiFiUser;
 import org.apache.nifi.util.NiFiProperties;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
@@ -60,8 +61,11 @@ import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.UntrustedProxyException;
 import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
 import org.apache.nifi.web.security.jwt.JwtService;
+import org.apache.nifi.web.security.otp.OtpService;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
+import org.apache.nifi.web.security.token.OtpAuthenticationToken;
+import org.apache.nifi.web.security.user.NiFiUserUtils;
 import org.apache.nifi.web.security.x509.X509CertificateExtractor;
 import org.apache.nifi.web.security.x509.X509IdentityProvider;
 import org.slf4j.Logger;
@@ -92,8 +96,9 @@ public class AccessResource extends ApplicationResource {
     private X509CertificateExtractor certificateExtractor;
     private X509IdentityProvider certificateIdentityProvider;
     private JwtService jwtService;
+    private OtpService otpService;
 
-    private AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService;
+    private AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService;
 
     /**
      * Retrieves the access configuration for this NiFi.
@@ -285,7 +290,107 @@ public class AccessResource extends ApplicationResource {
      * @throws AuthenticationException if the proxy chain is not authorized
      */
     private UserDetails checkAuthorization(final List<String> proxyChain) throws AuthenticationException {
-        return userDetailsService.loadUserDetails(new NiFiAuthortizationRequestToken(proxyChain));
+        return userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain));
+    }
+
+    /**
+     * Creates a single use access token for downloading FlowFile content.
+     *
+     * @param httpServletRequest    the servlet request
+     * @return  A token (string)
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.TEXT_PLAIN)
+    @Path("/download-token")
+    @ApiOperation(
+        value = "Creates a single use access token for downloading FlowFile content.",
+        notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " +
+            "It is used as a query parameter name 'access_token'.",
+        response = String.class
+    )
+    @ApiResponses(
+        value = {
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " +
+                "(i.e. may not have any tokens to grant or be configured to support username/password login)"),
+            @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.")
+        }
+    )
+    public Response createDownloadToken(@Context HttpServletRequest httpServletRequest) {
+        // only support access tokens when communicating over HTTPS
+        if (!httpServletRequest.isSecure()) {
+            throw new IllegalStateException("Download tokens are only issued over HTTPS.");
+        }
+
+        // if not configuration for login, don't consider credentials
+        if (loginIdentityProvider == null) {
+            throw new IllegalStateException("Download tokens not supported by this NiFi.");
+        }
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new AccessDeniedException("Unable to determine user details.");
+        }
+
+        final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(user.getIdentity());
+
+        // generate otp for response
+        final String token = otpService.generateDownloadToken(authenticationToken);
+
+        // build the response
+        final URI uri = URI.create(generateResourceUri("access", "download-token"));
+        return generateCreatedResponse(uri, token).build();
+    }
+
+    /**
+     * Creates a single use access token for accessing a NiFi UI extension.
+     *
+     * @param httpServletRequest    the servlet request
+     * @return  A token (string)
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.TEXT_PLAIN)
+    @Path("/ui-extension-token")
+    @ApiOperation(
+        value = "Creates a single use access token for accessing a NiFi UI extension.",
+        notes = "The token returned is a base64 encoded string. It is valid for a single request up to five minutes from being issued. " +
+            "It is used as a query parameter name 'access_token'.",
+        response = String.class
+    )
+    @ApiResponses(
+        value = {
+            @ApiResponse(code = 403, message = "Client is not authorized to make this request."),
+            @ApiResponse(code = 409, message = "Unable to create the download token because NiFi is not in the appropriate state. " +
+                "(i.e. may not have any tokens to grant or be configured to support username/password login)"),
+            @ApiResponse(code = 500, message = "Unable to create download token because an unexpected error occurred.")
+        }
+    )
+    public Response createUiExtensionToken(@Context HttpServletRequest httpServletRequest) {
+        // only support access tokens when communicating over HTTPS
+        if (!httpServletRequest.isSecure()) {
+            throw new IllegalStateException("UI extension access tokens are only issued over HTTPS.");
+        }
+
+        // if not configuration for login, don't consider credentials
+        if (loginIdentityProvider == null) {
+            throw new IllegalStateException("UI extension access tokens not supported by this NiFi.");
+        }
+
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            throw new AccessDeniedException("Unable to determine user details.");
+        }
+
+        final OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(user.getIdentity());
+
+        // generate otp for response
+        final String token = otpService.generateUiExtensionToken(authenticationToken);
+
+        // build the response
+        final URI uri = URI.create(generateResourceUri("access", "ui-extension-token"));
+        return generateCreatedResponse(uri, token).build();
     }
 
     /**
@@ -302,6 +407,9 @@ public class AccessResource extends ApplicationResource {
     @Path("/token")
     @ApiOperation(
             value = "Creates a token for accessing the REST API via username/password",
+            notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " +
+                "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " +
+                "in the format 'Authorization: Bearer <token>'.",
             response = String.class
     )
     @ApiResponses(
@@ -399,7 +507,7 @@ public class AccessResource extends ApplicationResource {
     private void authorizeProxyIfNecessary(final List<String> proxyChain) throws AuthenticationException {
         if (proxyChain.size() > 1) {
             try {
-                userDetailsService.loadUserDetails(new NiFiAuthortizationRequestToken(proxyChain));
+                userDetailsService.loadUserDetails(new NiFiAuthorizationRequestToken(proxyChain));
             } catch (final UsernameNotFoundException unfe) {
                 // if a username not found exception was thrown, the proxies were authorized and now
                 // we can issue a new token to the end user which they will use to identify themselves
@@ -427,6 +535,10 @@ public class AccessResource extends ApplicationResource {
         this.jwtService = jwtService;
     }
 
+    public void setOtpService(OtpService otpService) {
+        this.otpService = otpService;
+    }
+
     public void setCertificateExtractor(X509CertificateExtractor certificateExtractor) {
         this.certificateExtractor = certificateExtractor;
     }
@@ -435,7 +547,7 @@ public class AccessResource extends ApplicationResource {
         this.certificateIdentityProvider = certificateIdentityProvider;
     }
 
-    public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService) {
+    public void setUserDetailsService(AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
         this.userDetailsService = userDetailsService;
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/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 b38bca5..c9326b2 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
@@ -259,6 +259,7 @@
         <property name="certificateIdentityProvider" ref="certificateIdentityProvider"/>
         <property name="loginIdentityProvider" ref="loginIdentityProvider"/>
         <property name="jwtService" ref="jwtService"/>
+        <property name="otpService" ref="otpService"/>
         <property name="userDetailsService" ref="userDetailsService"/>
     </bean>
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
index 82fe73a..fe48490 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/AccessTokenEndpointTest.java
@@ -204,7 +204,7 @@ public class AccessTokenEndpointTest {
      * @throws Exception ex
      */
     @Test
-    public void testUnkownUser() throws Exception {
+    public void testUnknownUser() throws Exception {
         String url = BASE_URL + "/access/token";
 
         ClientResponse response = TOKEN_USER.testCreateToken(url, "not a real user", "not a real password");

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
index a6f6aec..9b053f0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
@@ -21,6 +21,7 @@ import com.ibm.icu.text.CharsetMatch;
 import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
 
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
@@ -71,7 +72,19 @@ public class ContentViewerController extends HttpServlet {
         final ServletContext servletContext = request.getServletContext();
         final ContentAccess contentAccess = (ContentAccess) servletContext.getAttribute("nifi-content-access");
 
-        final ContentRequestContext contentRequest = getContentRequest(request);
+        final ContentRequestContext contentRequest;
+        try {
+            contentRequest = getContentRequest(request);
+        } catch (final Exception e) {
+            request.setAttribute("title", "Error");
+            request.setAttribute("messages", "Unable to interpret content request.");
+
+            // forward to the error page
+            final ServletContext viewerContext = servletContext.getContext("/nifi");
+            viewerContext.getRequestDispatcher("/message").forward(request, response);
+            return;
+        }
+
         if (contentRequest.getDataUri() == null) {
             request.setAttribute("title", "Error");
             request.setAttribute("messages", "The data reference must be specified.");
@@ -274,26 +287,41 @@ public class ContentViewerController extends HttpServlet {
      * @return Get the content request context based on the specified request
      */
     private ContentRequestContext getContentRequest(final HttpServletRequest request) {
+        final String ref = request.getParameter("ref");
+        final String clientId = request.getParameter("clientId");
+        final String proxiedEntitiesChain = request.getHeader("X-ProxiedEntitiesChain");
+
+        final URI refUri = URI.create(ref);
+        final String query = refUri.getQuery();
+        final String[] queryParameters = query.split("&");
+
+        String rawClusterNodeId = null;
+        for (int i = 0; i < queryParameters.length; i++) {
+            if (queryParameters[0].startsWith("clusterNodeId=")) {
+                rawClusterNodeId = StringUtils.substringAfterLast(queryParameters[0], "clusterNodeId=");
+            }
+        }
+        final String clusterNodeId = rawClusterNodeId;
+
         return new ContentRequestContext() {
             @Override
             public String getDataUri() {
-                return request.getParameter("ref");
+                return ref;
             }
 
             @Override
             public String getClusterNodeId() {
-                final String ref = request.getParameter("ref");
-                return StringUtils.substringAfterLast(ref, "clusterNodeId=");
+                return clusterNodeId;
             }
 
             @Override
             public String getClientId() {
-                return request.getParameter("clientId");
+                return clientId;
             }
 
             @Override
             public String getProxiedEntitiesChain() {
-                return request.getHeader("X-ProxiedEntitiesChain");
+                return proxiedEntitiesChain;
             }
         };
     }

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp
index 0f6fae3..c000284 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/WEB-INF/jsp/header.jsp
@@ -25,14 +25,37 @@
         <link href="../nifi/css/message-pane.css" rel="stylesheet" type="text/css" />
         <link href="../nifi/css/message-page.css" rel="stylesheet" type="text/css" />
         <link rel="stylesheet" href="../nifi/js/jquery/combo/jquery.combo.css" type="text/css" />
+        <link rel="stylesheet" href="../nifi/js/jquery/modal/jquery.modal.css" type="text/css" />
         <link rel="stylesheet" href="../nifi/css/reset.css" type="text/css" />
         <script type="text/javascript" src="../nifi/js/jquery/jquery-2.1.1.min.js"></script>
+        <script type="text/javascript" src="../nifi/js/jquery/jquery.center.js"></script>
         <script type="text/javascript" src="../nifi/js/jquery/combo/jquery.combo.js"></script>
+        <script type="text/javascript" src="../nifi/js/jquery/modal/jquery.modal.js"></script>
         <script type="text/javascript" src="../nifi/js/nf/nf-namespace.js"></script>
+        <script type="text/javascript" src="../nifi/js/nf/nf-storage.js"></script>
+        <script type="text/javascript" src="../nifi/js/nf/nf-ajax-setup.js"></script>
         <script type="text/javascript" src="../nifi/js/nf/nf-universal-capture.js"></script>
         <script type="text/javascript">
             var $$ = $.noConflict(true);
             $$(document).ready(function () {
+                // initialize the dialog
+                $$('#content-viewer-message-dialog').modal({
+                    overlayBackground: false,
+                    buttons: [{
+                        buttonText: 'Ok',
+                        handler: {
+                            click: function () {
+                                $$('#content-viewer-message-dialog').modal('hide');
+                            }
+                        }
+                    }],
+                    handler: {
+                        close: function () {
+                            $$('#content-viewer-message').text('');
+                        }
+                    }
+                });
+
                 var url = $$('#requestUrl').text();
                 var ref = $$('#ref').text();
                 
@@ -74,12 +97,40 @@
                             currentLocation = option.value;
                             return;
                         }
-                        
+
                         // if the selection has changesd, reload the page
                         if (currentLocation !== option.value) {
-                            window.location.href = url + '?' + $$.param($$.extend({
-                                mode: option.value
-                            }, params));
+                            // get an access token if necessary
+                            var getAccessToken = $.Deferred(function (deferred) {
+                                if (nf.Storage.hasItem('jwt')) {
+                                    $.ajax({
+                                        type: 'POST',
+                                        url: '../nifi-api/access/ui-extension-token'
+                                    }).done(function (token) {
+                                        deferred.resolve(token);
+                                    }).fail(function () {
+                                        $$('#content-viewer-message').text('Unable to generate a token to view the content.');
+                                        $$('#content-viewer-message-dialog').modal('show');
+                                        deferred.reject();
+                                    })
+                                } else {
+                                    deferred.resolve('');
+                                }
+                            }).promise();
+
+                            // reload as appropriate
+                            getAccessToken.done(function (uiExtensionToken) {
+                                var contentParameter = {
+                                    mode: option.value
+                                };
+
+                                // include the download token if applicable
+                                if (typeof uiExtensionToken !== 'undefined' && uiExtensionToken !== null && $$.trim(uiExtensionToken) !== '') {
+                                    contentParameter['access_token'] = uiExtensionToken;
+                                }
+
+                                window.location.href = url + '?' + $$.param($$.extend(contentParameter, params));
+                            });
                         }
                     }
                 });
@@ -91,6 +142,11 @@
         <span id="clusterNodeId" class="hidden"><%= request.getParameter("clusterNodeId") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getParameter("clusterNodeId")) %></span>
         <span id="mode" class="hidden"><%= request.getParameter("mode") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getParameter("mode")) %></span>
         <span id="requestUrl" class="hidden"><%= org.apache.nifi.util.EscapeUtils.escapeHtml(request.getRequestURL().toString()) %></span>
+        <div id="content-viewer-message-dialog">
+            <div class="dialog-content" style="margin-top: -20px;">
+                <div id="content-viewer-message"></div>
+            </div>
+        </div>
         <div id="view-as-label">View as</div>
         <div id="view-as" class="pointer button-normal"></div>
         <div id="content-filename"><span class="content-label">filename</span><%= request.getAttribute("filename") == null ? "" : org.apache.nifi.util.EscapeUtils.escapeHtml(request.getAttribute("filename").toString()) %></div>

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/css/main.css
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/css/main.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/css/main.css
index fe68b62..6e83c76 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/css/main.css
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/webapp/css/main.css
@@ -19,6 +19,14 @@
     padding: 0;
 }
 
+#content-viewer-message-dialog {
+    z-index: 1302;
+    display: none;
+    height: 225px;
+    width: 375px;
+    font-size: 62.5%
+}
+
 #view-as-label {
     position: absolute;
     top: 72px;

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/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 d63f01e..0520ac8 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
@@ -27,7 +27,7 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.user.NiFiUser;
 import org.apache.nifi.util.NiFiProperties;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.apache.nifi.web.security.user.NiFiUserUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -82,7 +82,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
     private void authenticate(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
         String dnChain = null;
         try {
-            final NiFiAuthortizationRequestToken authenticated = attemptAuthentication(request);
+            final NiFiAuthorizationRequestToken authenticated = attemptAuthentication(request);
             if (authenticated != null) {
                 dnChain = ProxiedEntitiesUtils.formatProxyDn(StringUtils.join(authenticated.getChain(), "><"));
 
@@ -125,7 +125,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
      * @return The NiFiAutorizationRequestToken used to later authorized the client
      * @throws InvalidAuthenticationException If the request contained an authentication attempt, but could not authenticate
      */
-    public abstract NiFiAuthortizationRequestToken attemptAuthentication(HttpServletRequest request);
+    public abstract NiFiAuthorizationRequestToken attemptAuthentication(HttpServletRequest request);
 
     protected void successfulAuthorization(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
         if (log.isDebugEnabled()) {

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.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/NiFiAuthenticationProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java
index 0887901..e51a26e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/NiFiAuthenticationProvider.java
@@ -18,7 +18,7 @@ package org.apache.nifi.web.security;
 
 import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken;
 import org.apache.nifi.web.security.token.NewAccountAuthorizationToken;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.apache.nifi.web.security.token.NiFiAuthorizationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
@@ -32,15 +32,15 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
  */
 public class NiFiAuthenticationProvider implements AuthenticationProvider {
 
-    private final AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService;
+    private final AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService;
 
-    public NiFiAuthenticationProvider(final AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> userDetailsService) {
+    public NiFiAuthenticationProvider(final AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> userDetailsService) {
         this.userDetailsService = userDetailsService;
     }
 
     @Override
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
-        final NiFiAuthortizationRequestToken request = (NiFiAuthortizationRequestToken) authentication;
+        final NiFiAuthorizationRequestToken request = (NiFiAuthorizationRequestToken) authentication;
 
         try {
             // defer to the nifi user details service to authorize the user
@@ -67,7 +67,7 @@ public class NiFiAuthenticationProvider implements AuthenticationProvider {
 
     @Override
     public boolean supports(Class<?> authentication) {
-        return NiFiAuthortizationRequestToken.class.isAssignableFrom(authentication);
+        return NiFiAuthorizationRequestToken.class.isAssignableFrom(authentication);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/anonymous/NiFiAnonymousUserFilter.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/anonymous/NiFiAnonymousUserFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/anonymous/NiFiAnonymousUserFilter.java
index e67ed2c..05c5fb8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/anonymous/NiFiAnonymousUserFilter.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/anonymous/NiFiAnonymousUserFilter.java
@@ -58,8 +58,11 @@ public class NiFiAnonymousUserFilter extends AnonymousAuthenticationFilter {
                 user.getAuthorities().addAll(EnumSet.allOf(Authority.class));
             }
 
-            // only create an authentication token if the anonymous user has some authorities
-            if (!user.getAuthorities().isEmpty()) {
+            // only create an authentication token if the anonymous user has some authorities or they are accessing a ui
+            // extension. ui extensions have run this security filter but we shouldn't require authentication/authorization
+            // when accessing static resources like images, js, and css. authentication/authorization is required when
+            // interacting with nifi however and that will be verified in the NiFiWebContext or NiFiWebConfigurationContext
+            if (!user.getAuthorities().isEmpty() || !request.getContextPath().startsWith("/nifi-api")) {
                 NiFiUserDetails userDetails = new NiFiUserDetails(user);
 
                 // get the granted authorities

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/authorization/NiFiAuthorizationService.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/authorization/NiFiAuthorizationService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/authorization/NiFiAuthorizationService.java
index 75c01bf..dd87cfa 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/authorization/NiFiAuthorizationService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/authorization/NiFiAuthorizationService.java
@@ -30,7 +30,7 @@ import org.apache.nifi.user.NiFiUser;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.security.UntrustedProxyException;
 import org.apache.nifi.web.security.user.NiFiUserDetails;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.dao.DataAccessException;
@@ -44,7 +44,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
 /**
  * UserDetailsService that will verify user identity and grant user authorities.
  */
-public class NiFiAuthorizationService implements AuthenticationUserDetailsService<NiFiAuthortizationRequestToken> {
+public class NiFiAuthorizationService implements AuthenticationUserDetailsService<NiFiAuthorizationRequestToken> {
 
     private static final Logger logger = LoggerFactory.getLogger(NiFiAuthorizationService.class);
 
@@ -63,7 +63,7 @@ public class NiFiAuthorizationService implements AuthenticationUserDetailsServic
      * @throws org.springframework.dao.DataAccessException ex
      */
     @Override
-    public synchronized UserDetails loadUserDetails(NiFiAuthortizationRequestToken request) throws UsernameNotFoundException, DataAccessException {
+    public synchronized UserDetails loadUserDetails(NiFiAuthorizationRequestToken request) throws UsernameNotFoundException, DataAccessException {
         NiFiUserDetails userDetails = null;
         final List<String> chain = new ArrayList<>(request.getChain());
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/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 faf3cde..e13fcea 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
@@ -20,7 +20,7 @@ import io.jsonwebtoken.JwtException;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.apache.nifi.web.security.token.NewAccountAuthorizationRequestToken;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.apache.nifi.web.security.user.NewAccountRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -40,7 +40,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
     private JwtService jwtService;
 
     @Override
-    public NiFiAuthortizationRequestToken attemptAuthentication(final HttpServletRequest request) {
+    public NiFiAuthorizationRequestToken attemptAuthentication(final HttpServletRequest request) {
         // only suppport jwt login when running securely
         if (!request.isSecure()) {
             return null;
@@ -68,7 +68,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
                 if (isNewAccountRequest(request)) {
                     return new NewAccountAuthorizationRequestToken(new NewAccountRequest(Arrays.asList(jwtPrincipal), getJustification(request)));
                 } else {
-                    return new NiFiAuthortizationRequestToken(Arrays.asList(jwtPrincipal));
+                    return new NiFiAuthorizationRequestToken(Arrays.asList(jwtPrincipal));
                 }
             } catch (JwtException e) {
                 throw new InvalidAuthenticationException(e.getMessage(), e);

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationException.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/otp/OtpAuthenticationException.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationException.java
new file mode 100644
index 0000000..ee2d573
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.otp;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * Thrown if a one time use token fails authentication.
+ */
+public class OtpAuthenticationException extends AuthenticationException {
+
+    public OtpAuthenticationException(String msg) {
+        super(msg);
+    }
+
+    public OtpAuthenticationException(String msg, Throwable t) {
+        super(msg, t);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.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/otp/OtpAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.java
new file mode 100644
index 0000000..9c44298
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationFilter.java
@@ -0,0 +1,96 @@
+/*
+ * 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.otp;
+
+import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.web.security.NiFiAuthenticationFilter;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+/**
+ */
+public class OtpAuthenticationFilter extends NiFiAuthenticationFilter {
+
+    private static final Logger logger = LoggerFactory.getLogger(OtpAuthenticationFilter.class);
+
+    private static final Pattern PROVENANCE_DOWNLOAD_PATTERN =
+        Pattern.compile("/controller/provenance/events/[0-9]+/content/(?:(?:output)|(?:input))");
+    private static final Pattern QUEUE_DOWNLOAD_PATTERN =
+        Pattern.compile("/controller/process-groups/(?:(?:root)|(?:[a-f0-9\\-]{36}))/connections/[a-f0-9\\-]{36}/flowfiles/[a-f0-9\\-]{36}/content");
+    private static final Pattern TEMPLATE_DOWNLOAD_PATTERN =
+        Pattern.compile("/controller/templates/[a-f0-9\\-]{36}");
+
+    protected static final String ACCESS_TOKEN = "access_token";
+
+    private OtpService otpService;
+
+    @Override
+    public NiFiAuthorizationRequestToken attemptAuthentication(final HttpServletRequest request) {
+        // only support otp login when running securely
+        if (!request.isSecure()) {
+            return null;
+        }
+
+        // get the accessToken out of the query string
+        final String accessToken = request.getParameter(ACCESS_TOKEN);
+
+        // if there is no authorization header, we don't know the user
+        if (accessToken == null) {
+            return null;
+        } else {
+            if (otpService == null) {
+                throw new InvalidAuthenticationException("NiFi is not configured to support username/password logins.");
+            }
+
+            try {
+                String identity = null;
+                if (request.getContextPath().equals("/nifi-api")) {
+                    if (isDownloadRequest(request.getPathInfo())) {
+                        // handle download requests
+                        identity = otpService.getAuthenticationFromDownloadToken(accessToken);
+                    }
+                } else {
+                    // handle requests to other context paths (other UI extensions)
+                    identity = otpService.getAuthenticationFromUiExtensionToken(accessToken);
+                }
+
+                // the path is a support path for otp tokens
+                if (identity == null) {
+                    return null;
+                }
+
+                return new NiFiAuthorizationRequestToken(Arrays.asList(identity));
+            } catch (final OtpAuthenticationException oae) {
+                throw new InvalidAuthenticationException(oae.getMessage(), oae);
+            }
+        }
+    }
+
+    private boolean isDownloadRequest(final String pathInfo) {
+        return PROVENANCE_DOWNLOAD_PATTERN.matcher(pathInfo).matches() || QUEUE_DOWNLOAD_PATTERN.matcher(pathInfo).matches() || TEMPLATE_DOWNLOAD_PATTERN.matcher(pathInfo).matches();
+    }
+
+    public void setOtpService(OtpService otpService) {
+        this.otpService = otpService;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpService.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/otp/OtpService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpService.java
new file mode 100644
index 0000000..cd5a90c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpService.java
@@ -0,0 +1,219 @@
+/*
+ * 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.otp;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.nifi.web.security.token.OtpAuthenticationToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * OtpService is a service for generating and verifying one time password tokens.
+ */
+public class OtpService {
+
+    private static final Logger logger = LoggerFactory.getLogger(OtpService.class);
+
+    private static final String HMAC_SHA256 = "HmacSHA256";
+
+    // protected for testing purposes
+    protected static final int MAX_CACHE_SOFT_LIMIT = 100;
+
+    private final Cache<CacheKey, String> downloadTokenCache;
+    private final Cache<CacheKey, String> uiExtensionCache;
+
+    /**
+     * Creates a new OtpService with an expiration of 5 minutes.
+     */
+    public OtpService() {
+        this(5, TimeUnit.MINUTES);
+    }
+
+    /**
+     * Creates a new OtpService.
+     *
+     * @param duration                  The expiration duration
+     * @param units                     The expiration units
+     * @throws NullPointerException     If units is null
+     * @throws IllegalArgumentException If duration is negative
+     */
+    public OtpService(final int duration, final TimeUnit units) {
+        downloadTokenCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
+        uiExtensionCache = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
+    }
+
+    /**
+     * Generates a download token for the specified authentication.
+     *
+     * @param authenticationToken       The authentication
+     * @return                          The one time use download token
+     */
+    public String generateDownloadToken(final OtpAuthenticationToken authenticationToken) {
+        return generateToken(downloadTokenCache.asMap(), authenticationToken);
+    }
+
+    /**
+     * Gets the authenticated identity from the specified one time use download token. This method will not return null.
+     *
+     * @param token                     The one time use download token
+     * @return                          The authenticated identity
+     * @throws OtpAuthenticationException   When the specified token does not correspond to an authenticated identity
+     */
+    public String getAuthenticationFromDownloadToken(final String token) throws OtpAuthenticationException {
+        return getAuthenticationFromToken(downloadTokenCache.asMap(), token);
+    }
+
+    /**
+     * Generates a UI extension token for the specified authentication.
+     *
+     * @param authenticationToken       The authentication
+     * @return                          The one time use UI extension token
+     */
+    public String generateUiExtensionToken(final OtpAuthenticationToken authenticationToken) {
+        return generateToken(uiExtensionCache.asMap(), authenticationToken);
+    }
+
+    /**
+     * Gets the authenticated identity from the specified one time use UI extension token. This method will not return null.
+     *
+     * @param token                     The one time use UI extension token
+     * @return                          The authenticated identity
+     * @throws OtpAuthenticationException   When the specified token does not correspond to an authenticated identity
+     */
+    public String getAuthenticationFromUiExtensionToken(final String token) throws OtpAuthenticationException {
+        return getAuthenticationFromToken(uiExtensionCache.asMap(), token);
+    }
+
+    /**
+     * Generates a token and stores it in the specified cache.
+     *
+     * @param cache                     The cache
+     * @param authenticationToken       The authentication
+     * @return                          The one time use token
+     */
+    private String generateToken(final ConcurrentMap<CacheKey, String> cache, final OtpAuthenticationToken authenticationToken) {
+        if (cache.size() >= MAX_CACHE_SOFT_LIMIT) {
+            throw new IllegalStateException("The maximum number of single use tokens have been issued.");
+        }
+
+        // hash the authentication and build a cache key
+        final CacheKey cacheKey = new CacheKey(hash(authenticationToken));
+
+        // store the token unless the token is already stored which should not update it's original timestamp
+        cache.putIfAbsent(cacheKey, authenticationToken.getName());
+
+        // return the token
+        return cacheKey.getToken();
+    }
+
+    /**
+     * Gets the corresponding authentication for the specified one time use token. The specified token will be removed.
+     *
+     * @param cache                     The cache
+     * @param token                     The one time use token
+     * @return                          The authenticated identity
+     */
+    private String getAuthenticationFromToken(final ConcurrentMap<CacheKey, String> cache, final String token) throws OtpAuthenticationException {
+        final String authenticatedUser = cache.remove(new CacheKey(token));
+        if (authenticatedUser == null) {
+            throw new OtpAuthenticationException("Unable to validate the access token.");
+        }
+
+        return authenticatedUser;
+    }
+
+    /**
+     * Hashes the specified authentication token. The resulting value will be used as the one time use token.
+     *
+     * @param authenticationToken   the authentication token
+     * @return                      the one time use token
+     */
+    private String hash(final OtpAuthenticationToken authenticationToken) {
+        try {
+            // input is the user identity and timestamp
+            final String input = authenticationToken.getName() + "-" + System.nanoTime();
+
+            // create the secret using secure random
+            final SecureRandom secureRandom = new SecureRandom();
+            final byte[] randomBytes = new byte[32];
+            secureRandom.nextBytes(randomBytes);
+            final SecretKeySpec secret = new SecretKeySpec(randomBytes, HMAC_SHA256); // 256 bit
+
+            // hash the input
+            final Mac hmacSha256 = Mac.getInstance(HMAC_SHA256);
+            hmacSha256.init(secret);
+            final byte[] output = hmacSha256.doFinal(input.getBytes(StandardCharsets.UTF_8));
+
+            // return the result as a base 64 string
+            return Base64.encodeBase64URLSafeString(output);
+        } catch (final NoSuchAlgorithmException | InvalidKeyException e) {
+            final String errorMessage = "There was an error generating the OTP";
+            logger.error(errorMessage, e);
+            throw new IllegalStateException("Unable to generate single use token.");
+        }
+    }
+
+    /**
+     * Key for the cache. Necessary to override the default String.equals() to utilize MessageDigest.isEquals() to prevent timing attacks.
+     */
+    private static class CacheKey {
+        final String token;
+
+        public CacheKey(String token) {
+            this.token = token;
+        }
+
+        public String getToken() {
+            return token;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final CacheKey otherCacheKey = (CacheKey) o;
+            return MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), otherCacheKey.token.getBytes(StandardCharsets.UTF_8));
+        }
+
+        @Override
+        public int hashCode() {
+            return token.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return "CacheKey{token ending in '..." + token.substring(token.length() - 6) + "'}";
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthorizationRequestToken.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/NewAccountAuthorizationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthorizationRequestToken.java
index 35c371d..693d420 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthorizationRequestToken.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NewAccountAuthorizationRequestToken.java
@@ -21,7 +21,7 @@ import org.apache.nifi.web.security.user.NewAccountRequest;
 /**
  * An authentication token that is used as an authorization request when submitting a new account.
  */
-public class NewAccountAuthorizationRequestToken extends NiFiAuthortizationRequestToken {
+public class NewAccountAuthorizationRequestToken extends NiFiAuthorizationRequestToken {
 
     final NewAccountRequest newAccountRequest;
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationRequestToken.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/NiFiAuthorizationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationRequestToken.java
new file mode 100644
index 0000000..c20aaf3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthorizationRequestToken.java
@@ -0,0 +1,54 @@
+/*
+ * 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.token;
+
+import java.util.Collections;
+import java.util.List;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+
+/**
+ * An authentication token that is used as an authorization request. The request has already been authenticated and is now going to be authorized.
+ * The request chain is specified during creation and is used authorize the user(s).
+ */
+public class NiFiAuthorizationRequestToken extends AbstractAuthenticationToken {
+
+    private final List<String> chain;
+
+    public NiFiAuthorizationRequestToken(final List<String> chain) {
+        super(null);
+        this.chain = chain;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return null;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return chain;
+    }
+
+    public List<String> getChain() {
+        return Collections.unmodifiableList(chain);
+    }
+
+    @Override
+    public final void setAuthenticated(boolean authenticated) {
+        throw new IllegalArgumentException("Cannot change the authenticated state.");
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthortizationRequestToken.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/NiFiAuthortizationRequestToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthortizationRequestToken.java
deleted file mode 100644
index a1459a4..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/NiFiAuthortizationRequestToken.java
+++ /dev/null
@@ -1,54 +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.web.security.token;
-
-import java.util.Collections;
-import java.util.List;
-import org.springframework.security.authentication.AbstractAuthenticationToken;
-
-/**
- * An authentication token that is used as an authorization request. The request has already been authenticated and is now going to be authorized.
- * The request chain is specified during creation and is used authorize the user(s).
- */
-public class NiFiAuthortizationRequestToken extends AbstractAuthenticationToken {
-
-    private final List<String> chain;
-
-    public NiFiAuthortizationRequestToken(final List<String> chain) {
-        super(null);
-        this.chain = chain;
-    }
-
-    @Override
-    public Object getCredentials() {
-        return null;
-    }
-
-    @Override
-    public Object getPrincipal() {
-        return chain;
-    }
-
-    public List<String> getChain() {
-        return Collections.unmodifiableList(chain);
-    }
-
-    @Override
-    public final void setAuthenticated(boolean authenticated) {
-        throw new IllegalArgumentException("Cannot change the authenticated state.");
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/OtpAuthenticationToken.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/OtpAuthenticationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/OtpAuthenticationToken.java
new file mode 100644
index 0000000..fc51912
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/OtpAuthenticationToken.java
@@ -0,0 +1,56 @@
+/*
+ * 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.token;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+
+/**
+ * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token.
+ */
+public class OtpAuthenticationToken extends AbstractAuthenticationToken {
+
+    private final String identity;
+
+    /**
+     * Creates a representation of the otp authentication token for a user.
+     *
+     * @param identity   The unique identifier for this user
+     */
+    public OtpAuthenticationToken(final String identity) {
+        super(null);
+        setAuthenticated(true);
+        this.identity = identity;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return null;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return identity;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("OtpAuthenticationToken for ")
+                .append(getName())
+                .toString();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/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 2c792f6..019a53c 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
@@ -24,7 +24,7 @@ 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.NewAccountAuthorizationRequestToken;
-import org.apache.nifi.web.security.token.NiFiAuthortizationRequestToken;
+import org.apache.nifi.web.security.token.NiFiAuthorizationRequestToken;
 import org.apache.nifi.web.security.user.NewAccountRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -40,7 +40,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
     private X509IdentityProvider certificateIdentityProvider;
 
     @Override
-    public NiFiAuthortizationRequestToken attemptAuthentication(final HttpServletRequest request) {
+    public NiFiAuthorizationRequestToken attemptAuthentication(final HttpServletRequest request) {
         // only suppport x509 login when running securely
         if (!request.isSecure()) {
             return null;
@@ -64,7 +64,7 @@ public class X509AuthenticationFilter extends NiFiAuthenticationFilter {
         if (isNewAccountRequest(request)) {
             return new NewAccountAuthorizationRequestToken(new NewAccountRequest(proxyChain, getJustification(request)));
         } else {
-            return new NiFiAuthortizationRequestToken(proxyChain);
+            return new NiFiAuthorizationRequestToken(proxyChain);
         }
     }
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/a8edab2e/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
index d079293..44a93af 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
@@ -49,7 +49,10 @@
     <bean id="jwtService" class="org.apache.nifi.web.security.jwt.JwtService">
         <constructor-arg ref="userService"/>
     </bean>
-    
+
+    <!-- otp service -->
+    <bean id="otpService" class="org.apache.nifi.web.security.otp.OtpService"/>
+
     <!-- login identity provider -->
     <bean id="loginIdentityProvider" class="org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean">
         <property name="properties" ref="nifiProperties"/>