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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1090a97  NIFI-7870 Resolved access to extension resources when using 
JWT
1090a97 is described below

commit 1090a9748a8ce5fe21d5a79d85a46858f1288f85
Author: Nathan Gough <[email protected]>
AuthorDate: Mon Feb 22 11:28:01 2021 -0500

    NIFI-7870 Resolved access to extension resources when using JWT
    
    - Added SameSite Session Cookie __Host-Authorization-Bearer for sending JWT
    - Configured Spring Security CSRF Filter comparing Authorization header and 
Cookie JWT
    - Implemented BearerTokenResolver for retrieving JWT
    
    This closes #4988
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../replication/ThreadPoolRequestReplicator.java   |  36 +++--
 .../nifi-framework/nifi-web/nifi-web-api/pom.xml   |   6 +
 .../apache/nifi/web/NiFiCsrfTokenRepository.java   |  91 +++++++++++
 .../nifi/web/NiFiWebApiSecurityConfiguration.java  |  19 ++-
 .../org/apache/nifi/web/api/AccessResource.java    |  95 +++++------
 .../apache/nifi/web/security/LogoutException.java  |  35 ++++
 .../nifi/web/security/jwt/BearerTokenResolver.java |  30 ++++
 .../web/security/jwt/JwtAuthenticationFilter.java  |  41 +----
 .../apache/nifi/web/security/jwt/JwtService.java   |  13 +-
 ...ionFilter.java => NiFiBearerTokenResolver.java} |  57 +++----
 .../jwt/JwtAuthenticationFilterTest.groovy         | 180 ---------------------
 .../nifi/web/security/jwt/JwtServiceTest.java      |  32 ----
 .../security/jwt/NiFiBearerTokenResolverTest.java  | 124 ++++++++++++++
 .../webapp/js/nf/canvas/nf-canvas-error-handler.js |   2 +-
 .../src/main/webapp/js/nf/login/nf-login.js        |  13 +-
 .../nifi-web-ui/src/main/webapp/js/nf/nf-common.js |   8 +-
 16 files changed, 413 insertions(+), 369 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
index 49383fd..6665095 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
@@ -40,7 +40,7 @@ import org.apache.nifi.reporting.Severity;
 import org.apache.nifi.util.ComponentIdGenerator;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
-import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -244,25 +244,13 @@ public class ThreadPoolRequestReplicator implements 
RequestReplicator {
 
         // remove the access token if present, since the user is already 
authenticated... authorization
         // will happen when the request is replicated using the proxy chain 
above
-        headers.remove(JwtAuthenticationFilter.AUTHORIZATION);
+        headers.remove(NiFiBearerTokenResolver.AUTHORIZATION);
 
         // if knox sso cookie name is set, remove any authentication cookie 
since this user is already authenticated
         // and will be included in the proxied entities chain above... 
authorization will happen when the
         // request is replicated
-        final String knoxCookieName = nifiProperties.getKnoxCookieName();
-        if (headers.containsKey("Cookie") && 
StringUtils.isNotBlank(knoxCookieName)) {
-            final String rawCookies = headers.get("Cookie");
-            final String[] rawCookieParts = rawCookies.split(";");
-            final Set<String> filteredCookieParts = 
Stream.of(rawCookieParts).map(String::trim).filter(cookie -> 
!cookie.startsWith(knoxCookieName + "=")).collect(Collectors.toSet());
-
-            // if that was the only cookie, remove it
-            if (filteredCookieParts.isEmpty()) {
-                headers.remove("Cookie");
-            } else {
-                // otherwise rebuild the cookies without the knox token
-                headers.put("Cookie", StringUtils.join(filteredCookieParts, "; 
"));
-            }
-        }
+        removeCookie(headers, nifiProperties.getKnoxCookieName());
+        removeCookie(headers, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
 
         // remove the host header
         headers.remove("Host");
@@ -869,4 +857,20 @@ public class ThreadPoolRequestReplicator implements 
RequestReplicator {
         expiredRequestIds.forEach(id -> onResponseConsumed(id));
         return responseMap.size();
     }
+
+    private void removeCookie(Map<String, String> headers, final String 
cookieName) {
+        if (headers.containsKey("Cookie") && 
StringUtils.isNotBlank(cookieName)) {
+            final String rawCookies = headers.get("Cookie");
+            final String[] rawCookieParts = rawCookies.split(";");
+            final Set<String> filteredCookieParts = 
Stream.of(rawCookieParts).map(String::trim).filter(cookie -> 
!cookie.startsWith(cookieName + "=")).collect(Collectors.toSet());
+
+            // if that was the only cookie, remove it
+            if (filteredCookieParts.isEmpty()) {
+                headers.remove("Cookie");
+            } else {
+                // otherwise rebuild the cookies without the knox token
+                headers.put("Cookie", StringUtils.join(filteredCookieParts, "; 
"));
+            }
+        }
+    }
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
index f2e01dc..865681e 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
@@ -436,5 +436,11 @@
             <groupId>org.slf4j</groupId>
             <artifactId>jcl-over-slf4j</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-http</artifactId>
+            <version>${jetty.version}</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
new file mode 100644
index 0000000..1c4fc4c
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+
+import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A {@link CsrfTokenRepository} implementation for NiFi that matches the NiFi 
Cookie JWT against the
+ * Authorization header JWT to protect against CSRF. If the request is an 
idempotent method type, then only the Cookie
+ * is required to be present - this allows authenticating access to static 
resources using a Cookie. If the request is a non-idempotent
+ * method, NiFi requires the Authorization header (eg. for POST requests).
+ */
+public final class NiFiCsrfTokenRepository implements CsrfTokenRepository {
+
+    private static String EMPTY = "empty";
+    private CookieCsrfTokenRepository cookieRepository;
+
+    public NiFiCsrfTokenRepository() {
+        cookieRepository = new CookieCsrfTokenRepository();
+    }
+
+    @Override
+    public CsrfToken generateToken(HttpServletRequest request) {
+        // Return an empty value CsrfToken - it will not be saved to the 
response as our CSRF token is added elsewhere
+        return new DefaultCsrfToken(EMPTY, EMPTY, EMPTY);
+    }
+
+    @Override
+    public void saveToken(CsrfToken token, HttpServletRequest request,
+                          HttpServletResponse response) {
+        // Do nothing - we don't need to add new CSRF tokens to the response
+    }
+
+    @Override
+    public CsrfToken loadToken(HttpServletRequest request) {
+        CsrfToken cookie = cookieRepository.loadToken(request);
+        // We add the Bearer string here in order to match the Authorization 
header on comparison in CsrfFilter
+        return cookie != null ? new DefaultCsrfToken(cookie.getHeaderName(), 
cookie.getParameterName(), String.format("Bearer %s", cookie.getToken())) : 
null;
+    }
+
+    /**
+     * Sets the name of the HTTP request parameter that should be used to 
provide a token.
+     *
+     * @param parameterName the name of the HTTP request parameter that should 
be used to
+     * provide a token
+     */
+    public void setParameterName(String parameterName) {
+        cookieRepository.setParameterName(parameterName);
+    }
+
+    /**
+     * Sets the name of the HTTP header that should be used to provide the 
token.
+     *
+     * @param headerName the name of the HTTP header that should be used to 
provide the
+     * token
+     */
+    public void setHeaderName(String headerName) {
+        cookieRepository.setHeaderName(headerName);
+    }
+
+    /**
+     * Sets the name of the cookie that the expected CSRF token is saved to 
and read from.
+     *
+     * @param cookieName the name of the cookie that the expected CSRF token 
is saved to
+     * and read from
+     */
+    public void setCookieName(String cookieName) {
+        cookieRepository.setCookieName(cookieName);
+    }
+}
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 7403185..cf1e525 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
@@ -16,12 +16,12 @@
  */
 package org.apache.nifi.web;
 
-import java.util.Arrays;
 import org.apache.nifi.util.NiFiProperties;
 import 
org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
 import 
org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
 import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
 import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
 import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter;
 import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider;
 import org.apache.nifi.web.security.otp.OtpAuthenticationFilter;
@@ -46,10 +46,15 @@ import 
org.springframework.security.config.annotation.web.configuration.WebSecur
 import org.springframework.security.config.http.SessionCreationPolicy;
 import 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
 import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import 
org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
 import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.cors.CorsConfigurationSource;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 
+import java.util.Arrays;
+
 /**
  * NiFi Web Api Spring security. Applies the various NiFiAuthenticationFilter 
servlet filters which will extract authentication
  * credentials from API requests.
@@ -116,14 +121,16 @@ public class NiFiWebApiSecurityConfiguration extends 
WebSecurityConfigurerAdapte
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {
+        NiFiCsrfTokenRepository csrfRepository = new NiFiCsrfTokenRepository();
+        csrfRepository.setHeaderName(NiFiBearerTokenResolver.AUTHORIZATION);
+        csrfRepository.setCookieName(NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+
         http
                 .cors().and()
                 .rememberMe().disable()
-                .authorizeRequests()
-                    .anyRequest().fullyAuthenticated()
-                    .and()
-                .sessionManagement()
-                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+                .authorizeRequests().anyRequest().fullyAuthenticated().and()
+                
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+                .csrf().requireCsrfProtectionMatcher(new 
AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, new 
RequestHeaderRequestMatcher("Cookie"))).csrfTokenRepository(csrfRepository);
 
         // x509
         http.addFilterBefore(x509FilterBean(), 
AnonymousAuthenticationFilter.class);
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 bf4a792..0221485 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -60,12 +60,13 @@ import org.apache.nifi.web.api.dto.AccessStatusDTO;
 import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
 import org.apache.nifi.web.api.entity.AccessStatusEntity;
 import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.web.security.LogoutException;
 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.JwtAuthenticationProvider;
 import org.apache.nifi.web.security.jwt.JwtAuthenticationRequestToken;
 import org.apache.nifi.web.security.jwt.JwtService;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
 import org.apache.nifi.web.security.kerberos.KerberosService;
 import org.apache.nifi.web.security.knox.KnoxService;
 import org.apache.nifi.web.security.logout.LogoutRequest;
@@ -82,6 +83,8 @@ import 
org.apache.nifi.web.security.token.OtpAuthenticationToken;
 import org.apache.nifi.web.security.x509.X509AuthenticationProvider;
 import org.apache.nifi.web.security.x509.X509AuthenticationRequestToken;
 import org.apache.nifi.web.security.x509.X509CertificateExtractor;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpHeader;
 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -90,6 +93,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.saml.SAMLCredential;
 import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.web.util.WebUtils;
 
 import javax.servlet.ServletContext;
 import javax.servlet.http.Cookie;
@@ -106,6 +110,7 @@ import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import java.io.IOException;
@@ -143,6 +148,7 @@ public class AccessResource extends ApplicationResource {
     private static final Pattern REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT = 
Pattern.compile("(\\.google\\.com)");
     private static final Pattern ID_TOKEN_LOGOUT_FORMAT = 
Pattern.compile("(\\.okta)");
     private static final int msTimeout = 30_000;
+    private static final int VALID_FOR_SESSION_ONLY = -1;
 
     private static final String SAML_REQUEST_IDENTIFIER = 
"saml-request-identifier";
     private static final String SAML_METADATA_MEDIA_TYPE = 
"application/samlmetadata+xml";
@@ -342,7 +348,7 @@ public class AccessResource extends ApplicationResource {
         initializeSamlServiceProvider();
 
         // ensure the request has the cookie with the request id
-        final String samlRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), SAML_REQUEST_IDENTIFIER);
+        final String samlRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
         if (samlRequestIdentifier == null) {
             forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
"The login request identifier was not found in the request. Unable to 
continue.");
             return;
@@ -435,7 +441,7 @@ public class AccessResource extends ApplicationResource {
         initializeSamlServiceProvider();
 
         // ensure the request has the cookie with the request identifier
-        final String samlRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), SAML_REQUEST_IDENTIFIER);
+        final String samlRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
         if (samlRequestIdentifier == null) {
             final String message = "The login request identifier was not found 
in the request. Unable to continue.";
             logger.warn(message);
@@ -479,7 +485,7 @@ public class AccessResource extends ApplicationResource {
         }
 
         // ensure the logout request identifier is present
-        final String logoutRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), LOGOUT_REQUEST_IDENTIFIER);
+        final String logoutRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
         if (StringUtils.isBlank(logoutRequestIdentifier)) {
             forwardToLogoutMessagePage(httpServletRequest, 
httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
             return;
@@ -585,7 +591,7 @@ public class AccessResource extends ApplicationResource {
         initializeSamlServiceProvider();
 
         // ensure the logout request identifier is present
-        final String logoutRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), LOGOUT_REQUEST_IDENTIFIER);
+        final String logoutRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
         if (StringUtils.isBlank(logoutRequestIdentifier)) {
             forwardToLogoutMessagePage(httpServletRequest, 
httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
             return;
@@ -732,7 +738,7 @@ public class AccessResource extends ApplicationResource {
             return;
         }
 
-        final String oidcRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+        final String oidcRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
         if (oidcRequestIdentifier == null) {
             forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
"The login request identifier was " +
                     "not found in the request. Unable to continue.");
@@ -785,7 +791,6 @@ public class AccessResource extends ApplicationResource {
 
                 // store the NiFi token
                 oidcService.storeJwt(oidcRequestIdentifier, nifiJwt);
-
             } catch (final Exception e) {
                 logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
 
@@ -831,7 +836,7 @@ public class AccessResource extends ApplicationResource {
             return 
Response.status(Response.Status.CONFLICT).entity(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG).build();
         }
 
-        final String oidcRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+        final String oidcRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
         if (oidcRequestIdentifier == null) {
             final String message = "The login request identifier was not found 
in the request. Unable to continue.";
             logger.warn(message);
@@ -847,8 +852,7 @@ public class AccessResource extends ApplicationResource {
             throw new IllegalArgumentException("A JWT for this login request 
identifier could not be found. Unable to continue.");
         }
 
-        // generate the response
-        return generateOkResponse(jwt).build();
+        return generateTokenResponse(generateOkResponse(jwt), jwt);
     }
 
     @GET
@@ -868,6 +872,10 @@ public class AccessResource extends ApplicationResource {
             throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
         }
 
+        final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
+        removeCookie(httpServletResponse, 
NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+        logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
+
         // Get the oidc discovery url
         String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
 
@@ -920,7 +928,7 @@ public class AccessResource extends ApplicationResource {
             return;
         }
 
-        final String oidcRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+        final String oidcRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
         if (oidcRequestIdentifier == null) {
             forwardToLogoutMessagePage(httpServletRequest, 
httpServletResponse, "The login request identifier was " +
                     "not found in the request. Unable to continue.");
@@ -1164,8 +1172,8 @@ public class AccessResource extends ApplicationResource {
 
             // if there is not certificate, consider a token
             if (certificates == null) {
-                // look for an authorization token
-                final String authorization = 
httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION);
+                // look for an authorization token in header or cookie
+                final String authorization = new 
NiFiBearerTokenResolver().resolve(httpServletRequest);
 
                 // if there is no authorization header, we don't know the user
                 if (authorization == null) {
@@ -1173,10 +1181,8 @@ public class AccessResource extends ApplicationResource {
                     accessStatus.setMessage("No credentials supplied, unknown 
user.");
                 } else {
                     try {
-                        // Extract the Base64 encoded token from the 
Authorization header
-                        final String token = 
StringUtils.substringAfterLast(authorization, " ");
-
-                        final JwtAuthenticationRequestToken jwtRequest = new 
JwtAuthenticationRequestToken(token, httpServletRequest.getRemoteAddr());
+                        // authenticate the token
+                        final JwtAuthenticationRequestToken jwtRequest = new 
JwtAuthenticationRequestToken(authorization, 
httpServletRequest.getRemoteAddr());
                         final NiFiAuthenticationToken authenticationResponse = 
(NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(jwtRequest);
                         final NiFiUser nifiUser = ((NiFiUserDetails) 
authenticationResponse.getDetails()).getNiFiUser();
 
@@ -1328,7 +1334,7 @@ public class AccessResource extends ApplicationResource {
             value = "Creates a token for accessing the REST API via Kerberos 
ticket exchange / SPNEGO negotiation",
             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>'.",
+                    "in the format 'Authorization: Bearer <token>'. It is also 
stored in the browser as a cookie.",
             response = String.class
     )
     @ApiResponses(
@@ -1383,7 +1389,7 @@ public class AccessResource extends ApplicationResource {
 
                 // build the response
                 final URI uri = URI.create(generateResourceUri("access", 
"kerberos"));
-                return generateCreatedResponse(uri, token).build();
+                return generateTokenResponse(generateCreatedResponse(uri, 
token), token);
             } catch (final AuthenticationException e) {
                 throw new AccessDeniedException(e.getMessage(), e);
             }
@@ -1391,12 +1397,12 @@ public class AccessResource extends ApplicationResource 
{
     }
 
     /**
-     * Creates a token for accessing the REST API via username/password.
+     * Creates a token for accessing the REST API via username/password stored 
as a cookie in the browser.
      *
      * @param httpServletRequest the servlet request
      * @param username           the username
      * @param password           the password
-     * @return A JWT (string)
+     * @return A JWT (string) in a cookie and as the body
      */
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -1405,8 +1411,8 @@ public class AccessResource extends ApplicationResource {
     @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>'.",
+                    "the body, and the signature. The expiration of the token 
is a contained within the body. It is stored in the browser as a cookie, but 
also returned in" +
+                    "the response body to be stored/used by third party client 
scripts.",
             response = String.class
     )
     @ApiResponses(
@@ -1459,7 +1465,7 @@ public class AccessResource extends ApplicationResource {
 
         // build the response
         final URI uri = URI.create(generateResourceUri("access", "token"));
-        return generateCreatedResponse(uri, token).build();
+        return generateTokenResponse(generateCreatedResponse(uri, token), 
token);
     }
 
     @DELETE
@@ -1490,8 +1496,9 @@ public class AccessResource extends ApplicationResource {
 
         try {
             logger.info("Logging out " + mappedUserIdentity);
-            
jwtService.logOutUsingAuthHeader(httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION));
-            logger.info("Successfully invalidated JWT for " + 
mappedUserIdentity);
+            logOutUser(httpServletRequest);
+            removeCookie(httpServletResponse, 
NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+            logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
 
             // create a LogoutRequest and tell the LogoutRequestManager about 
it for later retrieval
             final LogoutRequest logoutRequest = new 
LogoutRequest(UUID.randomUUID().toString(), mappedUserIdentity);
@@ -1507,7 +1514,10 @@ public class AccessResource extends ApplicationResource {
 
             return generateOkResponse().build();
         } catch (final JwtException e) {
-            logger.error("Logout of user " + mappedUserIdentity + " failed due 
to: " + e.getMessage(), e);
+            logger.error("JWT processing failed for [{}], due to: ", 
mappedUserIdentity, e.getMessage(), e);
+            return Response.serverError().build();
+        } catch (final LogoutException e) {
+            logger.error("Logout failed for user [{}] due to: ", 
mappedUserIdentity, e.getMessage(), e);
             return Response.serverError().build();
         }
     }
@@ -1543,7 +1553,7 @@ public class AccessResource extends ApplicationResource {
         LogoutRequest logoutRequest = null;
 
         // check if a logout request identifier is present and if so complete 
the request
-        final String logoutRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), LOGOUT_REQUEST_IDENTIFIER);
+        final String logoutRequestIdentifier = 
WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
         if (logoutRequestIdentifier != null) {
             logoutRequest = 
logoutRequestManager.complete(logoutRequestIdentifier);
         }
@@ -1577,25 +1587,6 @@ public class AccessResource extends ApplicationResource {
         return proposedTokenExpiration;
     }
 
-    /**
-     * Gets the value of a cookie matching the specified name. If no cookie 
with that name exists, null is returned.
-     *
-     * @param cookies the cookies
-     * @param name    the name of the cookie
-     * @return the value of the corresponding cookie, or null if the cookie 
does not exist
-     */
-    private String getCookieValue(final Cookie[] cookies, final String name) {
-        if (cookies != null) {
-            for (final Cookie cookie : cookies) {
-                if (name.equals(cookie.getName())) {
-                    return cookie.getValue();
-                }
-            }
-        }
-
-        return null;
-    }
-
     private String getOidcCallback() {
         return generateResourceUri("access", "oidc", "callback");
     }
@@ -1812,4 +1803,14 @@ public class AccessResource extends ApplicationResource {
         this.logoutRequestManager = logoutRequestManager;
     }
 
+    private void logOutUser(HttpServletRequest httpServletRequest) {
+        final String jwt = new 
NiFiBearerTokenResolver().resolve(httpServletRequest);
+        jwtService.logOut(jwt);
+    }
+
+    private Response generateTokenResponse(ResponseBuilder builder, String 
token) {
+        // currently there is no way to use javax.servlet-api to set 
SameSite=Strict, so we do this using Jetty
+        HttpCookie jwtCookie = new 
HttpCookie(NiFiBearerTokenResolver.JWT_COOKIE_NAME, token, null, "/", 
VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
+        return builder.header(HttpHeader.SET_COOKIE.asString(), 
jwtCookie.getRFC6265SetCookie()).build();
+    }
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/LogoutException.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/LogoutException.java
new file mode 100644
index 0000000..4af1191
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/LogoutException.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 LogoutException extends AuthenticationException {
+
+    public LogoutException(String msg) {
+        super(msg);
+    }
+
+    public LogoutException(String msg, Throwable t) {
+        super(msg, t);
+    }
+
+}
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/BearerTokenResolver.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java
new file mode 100644
index 0000000..caf8789
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/BearerTokenResolver.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.security.jwt;
+
+import javax.servlet.http.HttpServletRequest;
+
+public interface BearerTokenResolver {
+    /**
+     * Resolve any
+     * <a href="https://tools.ietf.org/html/rfc6750#section-1.2"; 
target="_blank">Bearer
+     * Token</a> value from the request.
+     * @param request the request
+     * @return the Bearer Token value or {@code null} if none found
+     */
+    String resolve(HttpServletRequest request);
+}
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 9a25ff3..6ac2401 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,60 +16,35 @@
  */
 package org.apache.nifi.web.security.jwt;
 
-import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.util.StringUtils;
 import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.security.core.Authentication;
 
 import javax.servlet.http.HttpServletRequest;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
-/**
- */
 public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
 
     private static final Logger logger = 
LoggerFactory.getLogger(JwtAuthenticationFilter.class);
 
     // The Authorization header contains authentication credentials
-    public static final String AUTHORIZATION = "Authorization";
-    private static final Pattern tokenPattern = Pattern.compile("^Bearer 
(\\S*\\.\\S*\\.\\S*)$");
+    private static NiFiBearerTokenResolver bearerTokenResolver = new 
NiFiBearerTokenResolver();
 
     @Override
     public Authentication attemptAuthentication(final HttpServletRequest 
request) {
-        // only support jwt login when running securely
+        // Only support JWT login when running securely
         if (!request.isSecure()) {
             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 authorizationHeader = request.getHeader(AUTHORIZATION);
+        // Get JWT from Authorization header or cookie value
+        final String headerToken = bearerTokenResolver.resolve(request);
 
-        // if there is no authorization header, we don't know the user
-        if (authorizationHeader == null || 
!validJwtFormat(authorizationHeader)) {
-            return null;
+        if (StringUtils.isNotBlank(headerToken)) {
+            return new JwtAuthenticationRequestToken(headerToken, 
request.getRemoteAddr());
         } else {
-            // Extract the Base64 encoded token from the Authorization header
-            final String token = getTokenFromHeader(authorizationHeader);
-            return new JwtAuthenticationRequestToken(token, 
request.getRemoteAddr());
-        }
-    }
-
-    private boolean validJwtFormat(String authenticationHeader) {
-        Matcher matcher = tokenPattern.matcher(authenticationHeader);
-        return matcher.matches();
-    }
-
-    public static String getTokenFromHeader(String authenticationHeader) {
-        Matcher matcher = tokenPattern.matcher(authenticationHeader);
-        if(matcher.matches()) {
-            return matcher.group(1);
-        } else {
-            throw new InvalidAuthenticationException("JWT did not match 
expected pattern.");
+            return null;
         }
     }
-
 }
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 886ae46..d235d49 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
@@ -31,6 +31,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.admin.service.AdministrationException;
 import org.apache.nifi.admin.service.KeyService;
 import org.apache.nifi.key.Key;
+import org.apache.nifi.web.security.LogoutException;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.slf4j.LoggerFactory;
 
@@ -186,7 +187,7 @@ public class JwtService {
      * @throws JwtException if there is a problem with the token input
      * @throws Exception if there is an issue logging the user out
      */
-    public void logOut(String token) {
+    public void logOut(String token) throws LogoutException {
         Jws<Claims> claims = parseTokenFromBase64EncodedString(token);
 
         // Get the key ID from the claims
@@ -199,13 +200,9 @@ public class JwtService {
         try {
             keyService.deleteKey(keyId);
         } catch (Exception e) {
-            logger.error("The key with key ID: " + keyId + " failed to be 
removed from the user database.");
-            throw e;
+            final String errorMessage = String.format("The key with key ID: %s 
failed to be removed from the user database.", keyId);
+            logger.error(errorMessage);
+            throw new LogoutException(errorMessage);
         }
     }
-
-    public void logOutUsingAuthHeader(String authorizationHeader) {
-        String base64EncodedToken = 
JwtAuthenticationFilter.getTokenFromHeader(authorizationHeader);
-        logOut(base64EncodedToken);
-    }
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/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/NiFiBearerTokenResolver.java
similarity index 50%
copy from 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java
copy to 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolver.java
index 9a25ff3..c936c4d 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/NiFiBearerTokenResolver.java
@@ -16,60 +16,55 @@
  */
 package org.apache.nifi.web.security.jwt;
 
+import org.apache.nifi.util.StringUtils;
 import org.apache.nifi.web.security.InvalidAuthenticationException;
-import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.security.core.Authentication;
+import org.springframework.web.util.WebUtils;
 
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-/**
- */
-public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
-
-    private static final Logger logger = 
LoggerFactory.getLogger(JwtAuthenticationFilter.class);
-
-    // The Authorization header contains authentication credentials
+public class NiFiBearerTokenResolver implements BearerTokenResolver {
+    private static final Logger logger = 
LoggerFactory.getLogger(NiFiBearerTokenResolver.class);
+    private static final Pattern BEARER_HEADER_PATTERN = 
Pattern.compile("^Bearer (\\S*\\.\\S*\\.\\S*){1}$");
+    private static final Pattern JWT_PATTERN = 
Pattern.compile("^(\\S*\\.\\S*\\.\\S*)$");
     public static final String AUTHORIZATION = "Authorization";
-    private static final Pattern tokenPattern = Pattern.compile("^Bearer 
(\\S*\\.\\S*\\.\\S*)$");
+    public static final String JWT_COOKIE_NAME = "__Host-Authorization-Bearer";
 
     @Override
-    public Authentication attemptAuthentication(final HttpServletRequest 
request) {
-        // only support jwt login when running securely
-        if (!request.isSecure()) {
-            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
+    public String resolve(HttpServletRequest request) {
         final String authorizationHeader = request.getHeader(AUTHORIZATION);
+        final Cookie cookieHeader = WebUtils.getCookie(request, 
JWT_COOKIE_NAME);
 
-        // if there is no authorization header, we don't know the user
-        if (authorizationHeader == null || 
!validJwtFormat(authorizationHeader)) {
-            return null;
+        if (StringUtils.isNotBlank(authorizationHeader) && 
validAuthorizationHeaderFormat(authorizationHeader)) {
+            return getTokenFromHeader(authorizationHeader);
+        } else if(cookieHeader != null && 
validJwtFormat(cookieHeader.getValue())) {
+            return cookieHeader.getValue();
         } else {
-            // Extract the Base64 encoded token from the Authorization header
-            final String token = getTokenFromHeader(authorizationHeader);
-            return new JwtAuthenticationRequestToken(token, 
request.getRemoteAddr());
+            logger.debug("Authorization header was not present or not in a 
valid format.");
+            return null;
         }
     }
 
-    private boolean validJwtFormat(String authenticationHeader) {
-        Matcher matcher = tokenPattern.matcher(authenticationHeader);
+    private boolean validAuthorizationHeaderFormat(String authorizationHeader) 
{
+        Matcher matcher = BEARER_HEADER_PATTERN.matcher(authorizationHeader);
+        return matcher.matches();
+    }
+
+    private boolean validJwtFormat(String jwt) {
+        Matcher matcher = JWT_PATTERN.matcher(jwt);
         return matcher.matches();
     }
 
-    public static String getTokenFromHeader(String authenticationHeader) {
-        Matcher matcher = tokenPattern.matcher(authenticationHeader);
-        if(matcher.matches()) {
+    private String getTokenFromHeader(String authenticationHeader) {
+        Matcher matcher = BEARER_HEADER_PATTERN.matcher(authenticationHeader);
+        if (matcher.matches()) {
             return matcher.group(1);
         } else {
             throw new InvalidAuthenticationException("JWT did not match 
expected pattern.");
         }
     }
-
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/jwt/JwtAuthenticationFilterTest.groovy
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/jwt/JwtAuthenticationFilterTest.groovy
deleted file mode 100644
index dcabb0f..0000000
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/jwt/JwtAuthenticationFilterTest.groovy
+++ /dev/null
@@ -1,180 +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.jwt
-
-import groovy.json.JsonOutput
-import org.apache.nifi.web.security.InvalidAuthenticationException
-import org.junit.After
-import org.junit.AfterClass
-import org.junit.Before
-import org.junit.BeforeClass
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.ExpectedException
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4.class)
-class JwtAuthenticationFilterTest extends GroovyTestCase {
-
-    public static String jwtString
-
-    @Rule
-    public ExpectedException expectedException = ExpectedException.none()
-
-    @BeforeClass
-    static void setUpOnce() throws Exception {
-        final String ALG_HEADER = "{\"alg\":\"HS256\"}"
-        final int EXPIRATION_SECONDS = 500
-        Calendar now = Calendar.getInstance()
-        final long currentTime = (long) (now.getTimeInMillis() / 1000.0)
-        final long TOKEN_ISSUED_AT = currentTime
-        final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS
-
-        // Generate a token that we will add a valid signature from a 
different token
-        // Always use LinkedHashMap to enforce order of the keys because the 
signature depends on order
-        final String EXPECTED_PAYLOAD =
-                JsonOutput.toJson(
-                    sub:'unknownuser',
-                    iss:'MockIdentityProvider',
-                    aud:'MockIdentityProvider',
-                    preferred_username:'unknownuser',
-                    kid:1,
-                    exp:TOKEN_EXPIRATION_SECONDS,
-                    iat:TOKEN_ISSUED_AT)
-
-        // Set up our JWT string with a test token
-        jwtString = JwtServiceTest.generateHS256Token(ALG_HEADER, 
EXPECTED_PAYLOAD, true, true)
-
-    }
-
-    @AfterClass
-    static void tearDownOnce() throws Exception {
-
-    }
-
-    @Before
-    void setUp() throws Exception {
-
-    }
-
-    @After
-    void tearDown() throws Exception {
-
-    }
-
-    @Test
-    void testValidAuthenticationHeaderString() {
-        // Arrange
-        String authenticationHeader = "Bearer " + jwtString
-
-        // Act
-        boolean isValidHeader = new 
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
-        // Assert
-        assertTrue(isValidHeader)
-    }
-
-    @Test
-    void testMissingBearer() {
-        // Arrange
-
-        // Act
-        boolean isValidHeader = new 
JwtAuthenticationFilter().validJwtFormat(jwtString)
-
-        // Assert
-        assertFalse(isValidHeader)
-    }
-
-    @Test
-    void testExtraCharactersAtBeginningOfToken() {
-        // Arrange
-        String authenticationHeader = "xBearer " + jwtString
-
-        // Act
-        boolean isValidToken = new 
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
-        // Assert
-        assertFalse(isValidToken)
-    }
-
-    @Test
-    void testBadTokenFormat() {
-        // Arrange
-        String[] tokenStrings = jwtString.split("\\.")
-        String badToken = "Bearer " + tokenStrings[1] + tokenStrings[2]
-
-        // Act
-        boolean isValidToken = new 
JwtAuthenticationFilter().validJwtFormat(badToken)
-
-        // Assert
-        assertFalse(isValidToken)
-    }
-
-    @Test
-    void testMultipleTokenInvalid() {
-        // Arrange
-        String authenticationHeader = "Bearer " + jwtString
-        authenticationHeader = authenticationHeader + " " + 
authenticationHeader
-
-        // Act
-        boolean isValidToken = new 
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
-        // Assert
-        assertFalse(isValidToken)
-    }
-
-    @Test
-    void testExtractToken() {
-        // Arrange
-        String authenticationHeader = "Bearer " + jwtString
-
-        // Act
-        String extractedToken = new 
JwtAuthenticationFilter().getTokenFromHeader(authenticationHeader)
-
-        // Assert
-        assertEquals(jwtString, extractedToken)
-    }
-
-    @Test
-    void testMultipleTokenDottedInvalid() {
-        // Arrange
-        String authenticationHeader = "Bearer " + jwtString
-        authenticationHeader = authenticationHeader + "." + 
authenticationHeader
-
-        // Act
-        boolean isValidToken = new 
JwtAuthenticationFilter().validJwtFormat(authenticationHeader)
-
-        // Assert
-        assertFalse(isValidToken)
-    }
-
-    @Test
-    void testMultipleTokenNotExtracted() {
-        // Arrange
-        expectedException.expect(InvalidAuthenticationException.class)
-        expectedException.expectMessage("JWT did not match expected pattern.")
-        String authenticationHeader = "Bearer " + jwtString
-        authenticationHeader = authenticationHeader + " " + 
authenticationHeader
-
-        // Act
-        String token = new 
JwtAuthenticationFilter().getTokenFromHeader(authenticationHeader)
-
-        // Assert
-        // Expect InvalidAuthenticationException
-    }
-}
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 eb1f5ee..513eb0c 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
@@ -599,38 +599,6 @@ public class JwtServiceTest {
     }
 
     @Test
-    public void testShouldLogOutUserUsingAuthHeader() throws Exception {
-        // Arrange
-        expectedException.expect(JwtException.class);
-        expectedException.expectMessage("Unable to validate the access 
token.");
-
-        // Token expires in 60 seconds
-        final int EXPIRATION_MILLIS = 60000;
-        LoginAuthenticationToken loginAuthenticationToken = new 
LoginAuthenticationToken(DEFAULT_IDENTITY,
-                EXPIRATION_MILLIS,
-                "MockIdentityProvider");
-        logger.info("Generating token for " + loginAuthenticationToken);
-
-        // Act
-        String token = 
jwtService.generateSignedToken(loginAuthenticationToken);
-
-        logger.info("Generated JWT: " + token);
-        logger.info("Validating token...");
-        String authID = jwtService.getAuthenticationFromToken(token);
-        assertEquals(DEFAULT_IDENTITY, authID);
-        logger.info("Token was valid");
-        logger.info("Logging out user: " + authID);
-        String header = "Bearer " + token;
-        jwtService.logOutUsingAuthHeader(header);
-        logger.info("Logged out user: " + authID);
-        logger.info("Checking that token is now invalid...");
-        jwtService.getAuthenticationFromToken(token);
-
-        // Assert
-        // Should throw exception when user is not found
-    }
-
-    @Test
     public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws 
Exception {
         // Arrange
         expectedException.expect(JwtException.class);
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/NiFiBearerTokenResolverTest.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/NiFiBearerTokenResolverTest.java
new file mode 100644
index 0000000..e14c7ed
--- /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/NiFiBearerTokenResolverTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.security.jwt;
+
+import groovy.json.JsonOutput;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class NiFiBearerTokenResolverTest {
+
+    public static String jwtString;
+
+    @Mock
+    private static HttpServletRequest request;
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        final String ALG_HEADER = "{\"alg\":\"HS256\"}";
+        final int EXPIRATION_SECONDS = 500;
+        Calendar now = Calendar.getInstance();
+        final long currentTime = (long) (now.getTimeInMillis() / 1000.0);
+        final long TOKEN_ISSUED_AT = currentTime;
+        final long TOKEN_EXPIRATION_SECONDS = currentTime + EXPIRATION_SECONDS;
+
+        Map<String, String> hashMap = new HashMap<String, String>() {{
+            put("sub", "unknownuser");
+            put("iss", "MockIdentityProvider");
+            put("aud", "MockIdentityProvider");
+            put("preferred_username", "unknownuser");
+            put("kid", String.valueOf(1));
+            put("exp", String.valueOf(TOKEN_EXPIRATION_SECONDS));
+            put("iat", String.valueOf(TOKEN_ISSUED_AT));
+        }};
+
+        // Generate a token that we will add a valid signature from a 
different token
+        // Always use LinkedHashMap to enforce order of the keys because the 
signature depends on order
+        final String EXPECTED_PAYLOAD = JsonOutput.toJson(hashMap);
+
+        // Set up our JWT string with a test token
+        jwtString = JwtServiceTest.generateHS256Token(ALG_HEADER, 
EXPECTED_PAYLOAD, true, true);
+
+        request = mock(HttpServletRequest.class);
+    }
+
+    @Test
+    public void testValidAuthenticationHeaderString() {
+        String authenticationHeader = "Bearer " + jwtString;
+        
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+        String isValidHeader = new NiFiBearerTokenResolver().resolve(request);
+
+        assertEquals(jwtString, isValidHeader);
+    }
+
+    @Test
+    public void testMissingBearer() {
+        String authenticationHeader = jwtString;
+        
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+        String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+        assertNull(resolvedToken);
+    }
+
+    @Test
+    public void testExtraCharactersAtBeginningOfToken() {
+        String authenticationHeader = "xBearer " + jwtString;
+        
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+        String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+        assertNull(resolvedToken);
+    }
+
+    @Test
+    public void testBadTokenFormat() {
+        String[] tokenStrings = jwtString.split("\\.");
+        
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.valueOf("Bearer
 " + tokenStrings[1] + tokenStrings[2]));
+        String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+        assertNull(resolvedToken);
+    }
+
+    @Test
+    public void testMultipleTokenInvalid() {
+        String authenticationHeader = "Bearer " + jwtString;
+        
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.format("%s
 %s", authenticationHeader, authenticationHeader));
+        String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
+
+        assertNull(resolvedToken);
+    }
+
+    @Test
+    public void testExtractToken() {
+        String authenticationHeader = "Bearer " + jwtString;
+        
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
+        String extractedToken = new NiFiBearerTokenResolver().resolve(request);
+
+        assertEquals(jwtString, extractedToken);
+    }
+}
\ No newline at end of file
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
index ad4305f..17464c8 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-error-handler.js
@@ -45,7 +45,7 @@
         // In case no further requests will be successful based on the status,
         // the canvas is disabled, and the message pane is shown.
         if ($('#message-pane').is(':visible')) {
-            nfCommon.showLogoutLink();
+            nfCommon.updateLogoutLink();
 
             // hide the splash screen if required
             if ($('#splash').is(':visible')) {
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 3c1b95f..72cd6c2 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
@@ -99,7 +99,7 @@
                 'password': $('#password').val()
             }
         }).done(function (jwt) {
-            // get the payload and store the token with the appropirate 
expiration
+            // Get the payload and store the token with the appropriate 
expiration. JWT is also stored automatically in a cookie.
             var token = nfCommon.getJwtPayload(jwt);
             var expiration = parseInt(token['exp'], 10) * 
nfCommon.MILLIS_PER_SECOND;
             nfStorage.setItem('jwt', jwt, expiration);
@@ -112,9 +112,6 @@
             }).done(function (response) {
                 var accessStatus = response.accessStatus;
 
-                // update the logout link appropriately
-                showLogoutLink();
-
                 // update according to the access status
                 if (accessStatus.status === 'ACTIVE') {
                     // reload as appropriate - no need to schedule token 
refresh as the page is reloading
@@ -155,10 +152,6 @@
         });
     };
 
-    var showLogoutLink = function () {
-        nfCommon.showLogoutLink();
-    };
-
     var nfLogin = {
         /**
          * Initializes the login page.
@@ -166,9 +159,7 @@
         init: function () {
             nfStorage.init();
 
-            if (nfStorage.getItem('jwt') !== null) {
-                showLogoutLink();
-            }
+            nfCommon.updateLogoutLink();
 
             // supporting logging in via enter press
             $('#username, #password').on('keyup', function (e) {
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 be6b370..2b79896 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
@@ -852,11 +852,11 @@
         /**
          * Shows the logout link if appropriate.
          */
-        showLogoutLink: function () {
-            if (nfStorage.getItem('jwt') === null) {
-                $('#user-logout-container').css('display', 'none');
-            } else {
+        updateLogoutLink: function () {
+            if (nfStorage.getItem('jwt') !== null) {
                 $('#user-logout-container').css('display', 'block');
+            } else {
+                $('#user-logout-container').css('display', 'none');
             }
         },
 

Reply via email to