NIFIREG-361 - Improved handling of the /access/logout endpoint.
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
index 278f635..d8275cb 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java
@@ -48,7 +48,9 @@
 import org.springframework.stereotype.Component;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -258,6 +260,42 @@
         return generateCreatedResponse(uri, token).build();
     }
 
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.WILDCARD)
+    @Path("/logout")
+    @ApiOperation(
+            value = "Performs a logout for other providers that have been issued a JWT.",
+            notes = NON_GUARANTEED_ENDPOINT
+    )
+    @ApiResponses(
+            value = {
+                    @ApiResponse(code = 200, message = "User was logged out successfully."),
+                    @ApiResponse(code = 401, message = "Authentication token provided was empty or not in the correct JWT format."),
+                    @ApiResponse(code = 500, message = "Client failed to log out."),
+            }
+    )
+    public Response logOut(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
+        if (!httpServletRequest.isSecure()) {
+            throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS.");
+        }
+
+        String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
+
+        if(userIdentity != null && !userIdentity.isEmpty()) {
+            try {
+                logger.info("Logging out user " + userIdentity);
+                jwtService.logOut(userIdentity);
+                return generateOkResponse().build();
+            } catch (final JwtException e) {
+                logger.error("Logout of user " + userIdentity + " failed due to: " + e.getMessage());
+                return Response.serverError().build();
+            }
+        } else {
+            return Response.status(401, "Authentication token provided was empty or not in the correct JWT format.").build();
+        }
+    }
+
     @POST
     @Consumes(MediaType.WILDCARD)
     @Produces(MediaType.TEXT_PLAIN)
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
index d33fd8b..bce1e39 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
@@ -171,6 +171,15 @@
     }
 
     /**
+     * Generates an Ok response with no content.
+     *
+     * @return an Ok response with no content
+     */
+    protected Response.ResponseBuilder generateOkResponse() {
+        return noCache(Response.ok());
+    }
+
+    /**
      * Generates a 201 Created response with the specified content.
      *
      * @param uri    The URI
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
index d47b301..d24e665 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
@@ -170,6 +170,20 @@
 
     }
 
+    public void logOut(String userIdentity) {
+        if (userIdentity == null || userIdentity.isEmpty()) {
+            throw new JwtException("Log out failed: The user identity was not present in the request token to log out user.");
+        }
+
+        try {
+            keyService.deleteKey(userIdentity);
+            logger.info("Deleted token from database.");
+        } catch (Exception e) {
+            logger.error("Unable to log out user: " + userIdentity + ". Failed to remove their token from database.");
+            throw e;
+        }
+    }
+
     private static long validateTokenExpiration(long proposedTokenExpiration, String identity) {
         final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
         final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index 4147b3d..7490d3e 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -91,6 +91,12 @@
 
     private static final String tokenLoginPath = "access/token/login";
     private static final String tokenIdentityProviderPath = "access/token/identity-provider";
+    // A JWT signed by a key of 'secret'
+    private static final String SIGNED_BY_WRONG_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
+            ".eyJzdWIiOiJuaWZpYWRtaW4iLCJpc3MiOiJMZGFwSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6IkxkYXB" +
+            "JZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibmlmaWFkbWluIiwia2lkIjoiNDd" +
+            "lMjA1NzctY2I3Yi00M2MzLWFhOGYtZjI0ZDcyODQ3MDEwIiwiaWF0IjoxNTgxNTI5NTA1LCJleHAiOjE" +
+            "1ODE1NzI3MDV9.vvMpwLJt1w_6Id_tlS1knxTkJ2gv7_j5ySG6PmNjF0s";
 
     @TestConfiguration
     @Profile("ITSecureLdap")
@@ -297,6 +303,60 @@
     }
 
     @Test
+    public void testLogout() {
+
+        // Given: the client is connected to an unsecured NiFi Registry
+        // and the /access endpoint is queried using a JWT for the nifiadmin LDAP user
+        final Response response = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Response.class);
+
+        // and the server returns a 200 OK with the expected current user
+        assertEquals(200, response.getStatus());
+
+        // When: the /access/logout endpoint with the JWT for the nifiadmin logs out the user
+        final Response logout_response = client
+                .target(createURL("/access/logout"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .delete(Response.class);
+
+        assertEquals(200, logout_response.getStatus());
+
+        // Then: the /access endpoint is queried using the logged out JWT
+        final Response retryResponse = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Response.class);
+
+        // and the server returns a 401 Unauthorized as the user is now logged out
+        assertEquals(401, retryResponse.getStatus());
+        String retryJson = retryResponse.readEntity(String.class);
+        assertEquals("Unable to validate the access token. Contact the system administrator.\n", retryJson);
+
+        // Reset: We successfully logged out our user. Run setup to fix up the user, so the @After code can run to re-establish authorizations.
+        setup();
+    }
+
+    @Test
+    public void testLogoutWithJWTSignedByWrongKey() throws Exception {
+
+        // Given: use the /access/logout endpoint with the JWT for the nifiadmin LDAP user to log out
+        final Response logoutResponse = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + SIGNED_BY_WRONG_KEY)
+                .delete(Response.class);
+
+        assertEquals(401, logoutResponse.getStatus());
+        String responseMessage = logoutResponse.readEntity(String.class);
+        assertEquals("Unable to validate the access token. Contact the system administrator.\n", responseMessage);
+    }
+
+    @Test
     public void testUsers() throws Exception {
 
         // Given: the client and server have been configured correctly for LDAP authentication
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js
index 0ad0434..1bd5e28 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/nf-registry.js
@@ -71,9 +71,24 @@
      * Invalidate old tokens and route to login page
      */
     logout: function () {
+    /**
+        $.ajax({
+            type: 'DELETE',
+            url: '../nifi-registry-api/access/logout',
+        }).done(function () {
+            delete this.nfRegistryService.currentUser.identity;
+            delete this.nfRegistryService.currentUser.anonymous;
+            this.nfStorage.removeItem('jwt');
+            this.router.navigateByUrl('login');
+        }).fail(nfErrorHandler.handleAjaxError);
+        **/
+
+
+        this.nfRegistryApi.deleteToLogout().subscribe(function () {
+
+        });
         delete this.nfRegistryService.currentUser.identity;
         delete this.nfRegistryService.currentUser.anonymous;
-        this.nfStorage.removeItem('jwt');
         this.router.navigateByUrl('login');
     },
 
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
index 676824e..29d9b54 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.api.js
@@ -726,6 +726,37 @@
     },
 
     /**
+     * Logout a user.
+     *
+     * @returns {*}
+     */
+    deleteToLogout: function () {
+        var self = this;
+        var options = {
+            headers: headers,
+            withCredentials: true,
+            responseType: 'text'
+        };
+
+        return this.http.delete('../nifi-registry-api/access/logout', options).pipe(
+            map(function (response) {
+                // remove the token from local storage
+                self.nfStorage.removeItem('jwt');
+                return response;
+            }),
+            catchError(function (error) {
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: 'Please contact your System Administrator.',
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                });
+                return of('');
+            })
+        );
+    },
+
+    /**
      * Kerberos ticket exchange.
      *
      * @returns {*}