From cf6f5172503ce438c6c22c334c9367f774db7b24 Mon Sep 17 00:00:00 2001 From: thenatog Date: Fri, 8 Mar 2019 16:53:11 -0500 Subject: [PATCH] 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 --- .../nifi/admin/dao/impl/StandardKeyDAO.java | 1 + .../nifi-web/nifi-web-api/pom.xml | 7 + .../apache/nifi/web/api/AccessResource.java | 32 +++ .../accesscontrol/ITAccessTokenEndpoint.java | 207 +++++++++++++++++- .../nifi-web/nifi-web-security/pom.xml | 12 + .../nifi/web/security/jwt/JwtService.java | 35 ++- .../nifi/web/security/jwt/JwtServiceTest.java | 97 +++++++- .../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 9d19361264..44d9716682 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 2d8ffec5e4..35577849d8 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 @@ -175,6 +175,13 @@ nifi-web-security provided + + org.apache.nifi + nifi-web-security + 1.10.0-SNAPSHOT + test-jar + test + org.apache.nifi nifi-web-optimistic-locking 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 f2dd6970f9..8796dce07f 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 9b6caa8023..406619fd1a 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 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 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 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 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 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 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 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 91a88aa167..9608f418af 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 @@ -31,6 +31,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + 3.1.1 + + + + test-jar + + + + org.codehaus.mojo jaxb2-maven-plugin 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 bd581419fe..63392a8fa0 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 59c66eff9a..368851e74c 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 keyAnswer = new Answer() { + 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 c81ec9a933..2f2cea779c 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'; }