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

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


The following commit(s) were added to refs/heads/master by this push:
     new cf6f517  NIFI-6085 - Added /access/logout endpoint to allow JWT auth 
tokens to be removed correctly. Added some tests. Found an error in the KeyDAO 
which did not allow key deletion. NIFI-6085 - Updated logOut method to use 
NiFiUserUtils and updated tests. NIFI-6085 - Added some more integration tests. 
NIFI-6085 Suppressed stacktrace when token is used after being invalidated.
cf6f517 is described below

commit cf6f5172503ce438c6c22c334c9367f774db7b24
Author: thenatog <[email protected]>
AuthorDate: Fri Mar 8 16:53:11 2019 -0500

    NIFI-6085 - Added /access/logout endpoint to allow JWT auth tokens to be 
removed correctly. Added some tests. Found an error in the KeyDAO which did not 
allow key deletion.
    NIFI-6085 - Updated logOut method to use NiFiUserUtils and updated tests.
    NIFI-6085 - Added some more integration tests.
    NIFI-6085 Suppressed stacktrace when token is used after being invalidated.
    
    This closes #3362.
    
    Signed-off-by: Andy LoPresto <[email protected]>
---
 .../apache/nifi/admin/dao/impl/StandardKeyDAO.java |   1 +
 .../nifi-framework/nifi-web/nifi-web-api/pom.xml   |   7 +
 .../org/apache/nifi/web/api/AccessResource.java    |  32 ++++
 .../accesscontrol/ITAccessTokenEndpoint.java       | 207 ++++++++++++++++++++-
 .../nifi-web/nifi-web-security/pom.xml             |  12 ++
 .../apache/nifi/web/security/jwt/JwtService.java   |  35 +++-
 .../nifi/web/security/jwt/JwtServiceTest.java      |  97 +++++++++-
 .../controllers/nf-ng-canvas-header-controller.js  |   5 +
 8 files changed, 381 insertions(+), 15 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
index 9d19361..44d9716 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/impl/StandardKeyDAO.java
@@ -161,6 +161,7 @@ public class StandardKeyDAO implements KeyDAO {
         try {
             // add each authority for the specified user
             statement = connection.prepareStatement(DELETE_KEYS);
+            statement.setString(1, identity);
             statement.executeUpdate();
         } catch (SQLException sqle) {
             throw new DataAccessException(sqle);
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 2d8ffec..3557784 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
@@ -177,6 +177,13 @@
         </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-web-security</artifactId>
+            <version>1.10.0-SNAPSHOT</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-web-optimistic-locking</artifactId>
             <scope>provided</scope>
         </dependency>
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 f2dd697..8796dce 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
@@ -344,6 +344,9 @@ public class AccessResource extends ApplicationResource {
                     .build();
             httpServletResponse.sendRedirect(logoutUri.toString());
         }
+
+        String authorizationHeader = 
httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION);
+        jwtService.logOut(authorizationHeader);
     }
 
     @GET
@@ -744,6 +747,35 @@ public class AccessResource extends ApplicationResource {
         return generateCreatedResponse(uri, token).build();
     }
 
+    @GET
+    @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 = 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 authorizationHeader = 
httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION);
+        final String token = 
StringUtils.substringAfterLast(authorizationHeader, " ");
+        try {
+            jwtService.logOut(token);
+            return generateOkResponse().build();
+        } catch (final JwtException e) {
+            return Response.serverError().build();
+        }
+    }
+
     private 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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
index 9b6caa8..406619f 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/accesscontrol/ITAccessTokenEndpoint.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.integration.accesscontrol;
 
+import org.apache.nifi.web.security.jwt.JwtServiceTest;
+import net.minidev.json.JSONObject;
 import org.apache.commons.io.FileUtils;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.integration.util.NiFiTestServer;
@@ -48,14 +50,19 @@ import javax.ws.rs.core.Response;
 import java.io.File;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.util.Calendar;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.StringJoiner;
 
 /**
  * Access token endpoint test.
  */
 public class ITAccessTokenEndpoint {
 
+    private final String user = "unregistered-user@nifi";
+    private final String password = "password";
     private static final String CLIENT_ID = "token-endpoint-id";
     private static final String CONTEXT_PATH = "/nifi-api";
 
@@ -114,7 +121,7 @@ public class ITAccessTokenEndpoint {
     }
 
     // -----------
-    // LOGIN CONIG
+    // LOGIN CONFIG
     // -----------
     /**
      * Test getting access configuration.
@@ -254,7 +261,7 @@ public class ITAccessTokenEndpoint {
         // verify unknown
         Assert.assertEquals("UNKNOWN", accessStatus.getStatus());
 
-        response = TOKEN_USER.testCreateToken(accessTokenUrl, 
"unregistered-user@nifi", "password");
+        response = TOKEN_USER.testCreateToken(accessTokenUrl, user, password);
 
         // ensure the request is successful
         Assert.assertEquals(201, response.getStatus());
@@ -279,6 +286,202 @@ public class ITAccessTokenEndpoint {
         Assert.assertEquals("ACTIVE", accessStatus.getStatus());
     }
 
+    @Test
+    public void testLogOutSuccess() throws Exception {
+        String accessStatusUrl = BASE_URL + "/access";
+        String accessTokenUrl = BASE_URL + "/access/token";
+        String logoutUrl = BASE_URL + "/access/logout";
+
+        Response response = TOKEN_USER.testGet(accessStatusUrl);
+
+        // ensure the request is successful
+        Assert.assertEquals(200, response.getStatus());
+
+        AccessStatusEntity accessStatusEntity = 
response.readEntity(AccessStatusEntity.class);
+        AccessStatusDTO accessStatus = accessStatusEntity.getAccessStatus();
+
+        // verify unknown
+        Assert.assertEquals("UNKNOWN", accessStatus.getStatus());
+
+        response = TOKEN_USER.testCreateToken(accessTokenUrl, user, password);
+
+        // ensure the request is successful
+        Assert.assertEquals(201, response.getStatus());
+
+        // get the token
+        String token = response.readEntity(String.class);
+
+        // authorization header
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + token);
+
+        // check the status with the token
+        response = TOKEN_USER.testGetWithHeaders(accessStatusUrl, null, 
headers);
+
+        // ensure the request is successful
+        Assert.assertEquals(200, response.getStatus());
+
+        accessStatusEntity = response.readEntity(AccessStatusEntity.class);
+        accessStatus = accessStatusEntity.getAccessStatus();
+
+        // verify unregistered
+        Assert.assertEquals("ACTIVE", accessStatus.getStatus());
+
+
+        // log out
+        response = TOKEN_USER.testGetWithHeaders(logoutUrl, null, headers);
+        Assert.assertEquals(200, response.getStatus());
+
+        // ensure we can no longer use our token
+        response = TOKEN_USER.testGetWithHeaders(accessStatusUrl, null, 
headers);
+        Assert.assertEquals(401, response.getStatus());
+    }
+
+    @Test
+    public void testLogOutNoTokenHeader() throws Exception {
+        String accessStatusUrl = BASE_URL + "/access";
+        String accessTokenUrl = BASE_URL + "/access/token";
+        String logoutUrl = BASE_URL + "/access/logout";
+
+        Response response = TOKEN_USER.testGet(accessStatusUrl);
+
+        // ensure the request is successful
+        Assert.assertEquals(200, response.getStatus());
+
+        AccessStatusEntity accessStatusEntity = 
response.readEntity(AccessStatusEntity.class);
+        AccessStatusDTO accessStatus = accessStatusEntity.getAccessStatus();
+
+        // verify unknown
+        Assert.assertEquals("UNKNOWN", accessStatus.getStatus());
+
+        response = TOKEN_USER.testCreateToken(accessTokenUrl, user, password);
+
+        // ensure the request is successful
+        Assert.assertEquals(201, response.getStatus());
+
+        // get the token
+        String token = response.readEntity(String.class);
+
+        // authorization header
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + token);
+
+        // check the status with the token
+        response = TOKEN_USER.testGetWithHeaders(accessStatusUrl, null, 
headers);
+
+        // ensure the request is successful
+        Assert.assertEquals(200, response.getStatus());
+
+        accessStatusEntity = response.readEntity(AccessStatusEntity.class);
+        accessStatus = accessStatusEntity.getAccessStatus();
+
+        // verify unregistered
+        Assert.assertEquals("ACTIVE", accessStatus.getStatus());
+
+
+        // log out should fail as we provided no token for logout to use
+        response = TOKEN_USER.testGetWithHeaders(logoutUrl, null, null);
+        Assert.assertEquals(500, response.getStatus());
+    }
+
+    @Test
+    public void testLogOutUnknownToken() throws Exception {
+        // Arrange
+        final String ALG_HEADER = "{\"alg\":\"HS256\"}";
+        final int EXPIRATION_SECONDS = 60;
+        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;
+
+        // Always use LinkedHashMap to enforce order of the keys because the 
signature depends on order
+        Map<String, Object> claims = new LinkedHashMap<>();
+        claims.put("sub", "unknownuser");
+        claims.put("iss", "MockIdentityProvider");
+        claims.put("aud", "MockIdentityProvider");
+        claims.put("preferred_username", "unknownuser");
+        claims.put("kid", 1);
+        claims.put("exp", TOKEN_EXPIRATION_SECONDS);
+        claims.put("iat", TOKEN_ISSUED_AT);
+        final String EXPECTED_PAYLOAD = new JSONObject(claims).toString();
+
+        String accessStatusUrl = BASE_URL + "/access";
+        String accessTokenUrl = BASE_URL + "/access/token";
+        String logoutUrl = BASE_URL + "/access/logout";
+
+        Response response = TOKEN_USER.testCreateToken(accessTokenUrl, user, 
password);
+        Response responseA = TOKEN_USER.testCreateToken(accessTokenUrl, 
"jack", password);
+
+        // ensure the request is successful
+        Assert.assertEquals(201, response.getStatus());
+        // get the token
+        String token = response.readEntity(String.class);
+        // authorization header
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Authorization", "Bearer " + token);
+        // check the status with the token
+        response = TOKEN_USER.testGetWithHeaders(accessStatusUrl, null, 
headers);
+        Assert.assertEquals(200, response.getStatus());
+
+        // Generate a token that will not match signatures with the generated 
token.
+        final String UNKNOWN_USER_TOKEN = 
JwtServiceTest.generateHS256Token(ALG_HEADER, EXPECTED_PAYLOAD, true, true);
+        Map<String, String> badHeaders = new HashMap<>();
+        badHeaders.put("Authorization", "Bearer " + UNKNOWN_USER_TOKEN);
+
+        // Log out should fail as we provide a bad token to use, signatures 
will mismatch
+        response = TOKEN_USER.testGetWithHeaders(logoutUrl, null, badHeaders);
+        Assert.assertEquals(401, response.getStatus());
+    }
+
+    @Test
+    public void testLogOutSplicedTokenSignature() throws Exception {
+        // Arrange
+        final String ALG_HEADER = "{\"alg\":\"HS256\"}";
+        final int EXPIRATION_SECONDS = 60;
+        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;
+
+        String accessTokenUrl = BASE_URL + "/access/token";
+        String logoutUrl = BASE_URL + "/access/logout";
+
+        Response response = TOKEN_USER.testCreateToken(accessTokenUrl, user, 
password);
+        // ensure the request is successful
+        Assert.assertEquals(201, response.getStatus());
+        // replace the user in the token with an unknown user
+        String realToken = response.readEntity(String.class);
+        String realSignature = realToken.split("\\.")[2];
+
+        // 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
+        Map<String, Object> claims = new LinkedHashMap<>();
+        claims.put("sub", "unknownuser");
+        claims.put("iss", "MockIdentityProvider");
+        claims.put("aud", "MockIdentityProvider");
+        claims.put("preferred_username", "unknownuser");
+        claims.put("kid", 1);
+        claims.put("exp", TOKEN_EXPIRATION_SECONDS);
+        claims.put("iat", TOKEN_ISSUED_AT);
+        final String EXPECTED_PAYLOAD = new JSONObject(claims).toString();
+        final String tempToken = JwtServiceTest.generateHS256Token(ALG_HEADER, 
EXPECTED_PAYLOAD, true, true);
+
+        // Splice this token with the real token from above
+        String[] splitToken = tempToken.split("\\.");
+        StringJoiner joiner = new StringJoiner(".");
+        joiner.add(splitToken[0]);
+        joiner.add(splitToken[1]);
+        joiner.add(realSignature);
+        String splicedUserToken = joiner.toString();
+
+        Map<String, String> badHeaders = new HashMap<>();
+        badHeaders.put("Authorization", "Bearer " + splicedUserToken);
+
+        // Log out should fail as we provide a bad token to use, signatures 
will mismatch
+        response = TOKEN_USER.testGetWithHeaders(logoutUrl, null, badHeaders);
+        Assert.assertEquals(401, response.getStatus());
+    }
+
     @AfterClass
     public static void cleanup() throws Exception {
         // shutdown the server
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
index 91a88aa..9608f41 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
@@ -32,6 +32,18 @@
         </resources>
         <plugins>
             <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.1.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
                 <groupId>org.codehaus.mojo</groupId>
                 <artifactId>jaxb2-maven-plugin</artifactId>
                 <executions>
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 bd58141..63392a8 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
@@ -27,16 +27,16 @@ import io.jsonwebtoken.SignatureAlgorithm;
 import io.jsonwebtoken.SignatureException;
 import io.jsonwebtoken.SigningKeyResolverAdapter;
 import io.jsonwebtoken.UnsupportedJwtException;
+import java.nio.charset.StandardCharsets;
+import java.util.Calendar;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.admin.service.AdministrationException;
 import org.apache.nifi.admin.service.KeyService;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
 import org.apache.nifi.key.Key;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.slf4j.LoggerFactory;
 
-import java.nio.charset.StandardCharsets;
-import java.util.Calendar;
-
 /**
  *
  */
@@ -76,7 +76,19 @@ public class JwtService {
         } catch (JwtException e) {
             logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
             final String errorMessage = "There was an error validating the 
JWT";
-            logger.error(errorMessage, e);
+
+            // A common attack is someone trying to use a token after the user 
is logged out
+            // No need to show a stacktrace for an expected and handled 
scenario
+            String causeMessage = e.getLocalizedMessage();
+            if (e.getCause() != null) {
+                causeMessage += "\n\tCaused by: " + 
e.getCause().getLocalizedMessage();
+            }
+            if (logger.isDebugEnabled()) {
+                logger.error(errorMessage, e);
+            } else {
+                logger.error(errorMessage);
+                logger.error(causeMessage);
+            }
             throw e;
         }
     }
@@ -157,4 +169,19 @@ public class JwtService {
             throw new JwtException(errorMessage, e);
         }
     }
+
+    public void logOut(String authorizationHeader) {
+        if (authorizationHeader == null || authorizationHeader.isEmpty()) {
+            throw new JwtException("Log out failed: The required Authorization 
header was not present in the request to log out user.");
+        }
+
+        String identity = NiFiUserUtils.getNiFiUserIdentity();
+
+        try {
+            keyService.deleteKey(identity);
+        } catch (Exception e) {
+            logger.error("Unable to log out user: " + identity + ". Failed to 
remove their token from database.");
+            throw e;
+        }
+    }
 }
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 59c66ef..368851e 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
@@ -21,16 +21,24 @@ import org.apache.commons.codec.CharEncoding;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.nifi.admin.service.AdministrationException;
 import org.apache.nifi.admin.service.KeyService;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.StandardNiFiUser;
 import org.apache.nifi.key.Key;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.codehaus.jettison.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.Mockito;
+import org.junit.rules.ExpectedException;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
@@ -44,6 +52,8 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class JwtServiceTest {
@@ -136,11 +146,11 @@ public class JwtServiceTest {
     // Class under test
     private JwtService jwtService;
 
-    private String generateHS256Token(String rawHeader, String rawPayload, 
boolean isValid, boolean isSigned) {
+    public static String generateHS256Token(String rawHeader, String 
rawPayload, boolean isValid, boolean isSigned) {
         return generateHS256Token(rawHeader, rawPayload, HMAC_SECRET, isValid, 
isSigned);
     }
 
-    private String generateHS256Token(String rawHeader, String rawPayload, 
String hmacSecret, boolean isValid,
+    private static String generateHS256Token(String rawHeader, String 
rawPayload, String hmacSecret, boolean isValid,
             boolean isSigned) {
         try {
             logger.info("Generating token for " + rawHeader + " + " + 
rawPayload);
@@ -162,7 +172,7 @@ public class JwtServiceTest {
         }
     }
 
-    private String generateHMAC(String hmacSecret, String body) throws 
NoSuchAlgorithmException,
+    private static String generateHMAC(String hmacSecret, String body) throws 
NoSuchAlgorithmException,
             UnsupportedEncodingException, InvalidKeyException {
         Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
         SecretKeySpec secret_key = new 
SecretKeySpec(hmacSecret.getBytes("UTF-8"), "HmacSHA256");
@@ -177,15 +187,38 @@ public class JwtServiceTest {
         key.setIdentity(DEFAULT_IDENTITY);
         key.setKey(HMAC_SECRET);
 
-        mockKeyService = Mockito.mock(KeyService.class);
-        when(mockKeyService.getKey(anyInt())).thenReturn(key);
+        Answer<Key> keyAnswer = new Answer<Key>() {
+            Key answerKey = key;
+            @Override
+            public Key answer(InvocationOnMock invocation) throws Throwable {
+                
if(invocation.getMethod().equals(KeyService.class.getMethod("deleteKey", 
String.class))) {
+                    answerKey = null;
+                }
+                return answerKey;
+            }
+        };
+
+        StandardNiFiUser nifiUser = mock(StandardNiFiUser.class);
+        when(nifiUser.getIdentity()).thenReturn(DEFAULT_IDENTITY);
+        NiFiUserDetails nifiUserDetails = mock(NiFiUserDetails.class);
+        when(nifiUserDetails.getNiFiUser()).thenReturn(nifiUser);
+
+        Authentication authentication = mock(Authentication.class);
+        SecurityContext securityContext = mock(SecurityContext.class);
+        when(securityContext.getAuthentication()).thenReturn(authentication);
+        SecurityContextHolder.setContext(securityContext);
+        
when(SecurityContextHolder.getContext().getAuthentication().getPrincipal()).thenReturn(nifiUserDetails);
+
+        mockKeyService = mock(KeyService.class);
+        when(mockKeyService.getKey(anyInt())).thenAnswer(keyAnswer);
         when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key);
+        doAnswer(keyAnswer).when(mockKeyService).deleteKey(anyString());
         jwtService = new JwtService(mockKeyService);
     }
 
     @After
     public void tearDown() throws Exception {
-
+        jwtService = null;
     }
 
     @Test
@@ -425,13 +458,13 @@ public class JwtServiceTest {
     public void testShouldNotGenerateTokenWithMissingKey() throws Exception {
         // Arrange
         final int EXPIRATION_MILLIS = 60000;
-        LoginAuthenticationToken loginAuthenticationToken = new 
LoginAuthenticationToken("alopresto",
+        LoginAuthenticationToken loginAuthenticationToken = new 
LoginAuthenticationToken(DEFAULT_IDENTITY,
                 EXPIRATION_MILLIS,
                 "MockIdentityProvider");
         logger.debug("Generating token for " + loginAuthenticationToken);
 
         // Set up the bad key service
-        KeyService missingKeyService = Mockito.mock(KeyService.class);
+        KeyService missingKeyService = mock(KeyService.class);
         when(missingKeyService.getOrCreateKey(anyString())).thenThrow(new 
AdministrationException("Could not find a "
                 + "key for that user"));
         jwtService = new JwtService(missingKeyService);
@@ -442,4 +475,50 @@ public class JwtServiceTest {
         // Assert
         // Should throw exception
     }
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void testShouldLogOutUser() 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.debug("Generating token for " + loginAuthenticationToken);
+
+        // Act
+        String token = 
jwtService.generateSignedToken(loginAuthenticationToken);
+        logger.debug("Generated JWT: " + token);
+        String authID = jwtService.getAuthenticationFromToken(token);
+        assertEquals(DEFAULT_IDENTITY, authID);
+        logger.debug("Logging out user: " + DEFAULT_IDENTITY);
+        jwtService.logOut(token);
+        logger.debug("Logged out user: " + DEFAULT_IDENTITY);
+        jwtService.getAuthenticationFromToken(token);
+
+        // Assert
+        // Should throw exception when user is not found
+    }
+
+    @Test
+    public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws 
Exception {
+        // Arrange
+        expectedException.expect(JwtException.class);
+        expectedException.expectMessage("Log out failed: The required 
Authorization header was not present in the request to log out user.");
+
+        // Act
+        jwtService.logOut(null);
+
+        // Assert
+        // Should throw exception when authorization header is null
+    }
+
+
 }
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
index c81ec9a..2f2cea7 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-header-controller.js
@@ -117,6 +117,11 @@
              */
             this.logoutCtrl = {
                 logout: function () {
+                    $.ajax({
+                        type: 'GET',
+                        url: '../nifi-api/access/logout',
+                        dataType: 'json'
+                    })
                     nfStorage.removeItem("jwt");
                     window.location = '../nifi/logout';
                 }

Reply via email to