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';
}