This is an automated email from the ASF dual-hosted git repository. alopresto pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push: new d846a74 NIFI-7568 - Applied Kerberos mappings to authentication requests. Kerberos mappings should now be applied correctly in H2 database for username/password based login. Added tests. Logout now deletes signing key by key ID rather than identity. Validate token expiration now uses mapped identity instead, which allows logging of the mapped identity. Updated delete key to expect only 0 or 1 keys deleted. d846a74 is described below commit d846a74730edb4d142e0a3051c60e9fec33f1f09 Author: Nathan Gough <thena...@gmail.com> AuthorDate: Mon Jun 29 19:18:22 2020 -0400 NIFI-7568 - Applied Kerberos mappings to authentication requests. Kerberos mappings should now be applied correctly in H2 database for username/password based login. Added tests. Logout now deletes signing key by key ID rather than identity. Validate token expiration now uses mapped identity instead, which allows logging of the mapped identity. Updated delete key to expect only 0 or 1 keys deleted. This closes #4416. Signed-off-by: Andy LoPresto <alopre...@apache.org> --- .../java/org/apache/nifi/admin/dao/KeyDAO.java | 6 +- .../apache/nifi/admin/dao/impl/StandardKeyDAO.java | 8 +- .../org/apache/nifi/admin/service/KeyService.java | 4 +- ...{DeleteKeysAction.java => DeleteKeyAction.java} | 17 +- .../admin/service/impl/StandardKeyService.java | 26 +-- .../src/main/java/org/apache/nifi/key/Key.java | 9 + .../org/apache/nifi/web/api/AccessResource.java | 19 +- .../accesscontrol/ITAccessTokenEndpoint.java | 9 +- .../nifi/integration/util/NiFiTestAuthorizer.java | 4 +- .../util/NiFiTestLoginIdentityProvider.java | 1 + .../apache/nifi/integration/util/NiFiTestUser.java | 8 + .../nifi-mapped-identities.properties | 144 +++++++++++++++ .../web/security/jwt/JwtAuthenticationFilter.java | 2 +- .../apache/nifi/web/security/jwt/JwtService.java | 27 ++- .../jwt/JwtAuthenticationProviderTest.java | 132 ++++++++++++++ .../nifi/web/security/jwt/JwtServiceTest.java | 202 ++++++++++++++++++--- .../nifi/web/security/jwt/TestKeyService.java | 73 ++++++++ 17 files changed, 613 insertions(+), 78 deletions(-) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java index 9626445..3cfaf2f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/dao/KeyDAO.java @@ -48,9 +48,9 @@ public interface KeyDAO { Key createKey(String identity); /** - * Deletes all keys for the specified user identity. + * Deletes a key using the key ID. * - * @param identity The user identity + * @param keyId The key ID */ - void deleteKeys(String identity); + Integer deleteKey(Integer keyId); } 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 44d9716..28d090d 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 @@ -47,7 +47,7 @@ public class StandardKeyDAO implements KeyDAO { + ")"; private static final String DELETE_KEYS = "DELETE FROM KEY " - + "WHERE IDENTITY = ?"; + + "WHERE ID = ?"; private final Connection connection; @@ -156,13 +156,13 @@ public class StandardKeyDAO implements KeyDAO { } @Override - public void deleteKeys(String identity) { + public Integer deleteKey(Integer keyId) { PreparedStatement statement = null; try { // add each authority for the specified user statement = connection.prepareStatement(DELETE_KEYS); - statement.setString(1, identity); - statement.executeUpdate(); + statement.setInt(1, keyId); + return statement.executeUpdate(); } catch (SQLException sqle) { throw new DataAccessException(sqle); } catch (DataAccessException dae) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java index 4543475..5ac10cb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/KeyService.java @@ -43,7 +43,7 @@ public interface KeyService { /** * Deletes keys for the specified identity. * - * @param identity The user identity + * @param keyId The user's key ID */ - void deleteKey(String identity); + void deleteKey(Integer keyId); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeysAction.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java similarity index 72% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeysAction.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java index 6b8a2d5..c72d2c3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeysAction.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/action/DeleteKeyAction.java @@ -23,23 +23,22 @@ import org.apache.nifi.admin.dao.KeyDAO; /** * */ -public class DeleteKeysAction implements AdministrationAction<Void> { +public class DeleteKeyAction implements AdministrationAction<Integer> { - private final String identity; + private final Integer keyId; /** - * Creates a new transactions for deleting keys for specified user. + * Creates a new transactions for deleting keys for a specified user based on their keyId. * - * @param identity user identity + * @param keyId user identity */ - public DeleteKeysAction(String identity) { - this.identity = identity; + public DeleteKeyAction(Integer keyId) { + this.keyId = keyId; } @Override - public Void execute(DAOFactory daoFactory) throws DataAccessException { + public Integer execute(DAOFactory daoFactory) throws DataAccessException { final KeyDAO keyDao = daoFactory.getKeyDAO(); - keyDao.deleteKeys(identity); - return null; + return keyDao.deleteKey(keyId); } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java index 7a7f62d..8f4198a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/admin/service/impl/StandardKeyService.java @@ -19,7 +19,7 @@ package org.apache.nifi.admin.service.impl; import org.apache.nifi.admin.dao.DataAccessException; import org.apache.nifi.admin.service.AdministrationException; import org.apache.nifi.admin.service.KeyService; -import org.apache.nifi.admin.service.action.DeleteKeysAction; +import org.apache.nifi.admin.service.action.DeleteKeyAction; import org.apache.nifi.admin.service.action.GetKeyByIdAction; import org.apache.nifi.admin.service.action.GetOrCreateKeyAction; import org.apache.nifi.admin.service.transaction.Transaction; @@ -35,7 +35,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * + * This key service manages user JWT signing keys in the H2 user database. */ public class StandardKeyService implements KeyService { @@ -51,7 +51,7 @@ public class StandardKeyService implements KeyService { @Override public Key getKey(int id) { Transaction transaction = null; - Key key = null; + Key key; readLock.lock(); try { @@ -81,7 +81,7 @@ public class StandardKeyService implements KeyService { @Override public Key getOrCreateKey(String identity) { Transaction transaction = null; - Key key = null; + Key key; writeLock.lock(); try { @@ -109,7 +109,7 @@ public class StandardKeyService implements KeyService { } @Override - public void deleteKey(String identity) { + public void deleteKey(Integer keyId) { Transaction transaction = null; writeLock.lock(); @@ -118,11 +118,16 @@ public class StandardKeyService implements KeyService { transaction = transactionBuilder.start(); // delete the keys - DeleteKeysAction deleteKeys = new DeleteKeysAction(identity); - transaction.execute(deleteKeys); - - // commit the transaction - transaction.commit(); + DeleteKeyAction deleteKey = new DeleteKeyAction(keyId); + Integer rowsDeleted = transaction.execute(deleteKey); + + // commit the transaction if we found one and only one matching keyId/user identity + if (rowsDeleted == 1) { + transaction.commit(); + } else { + rollback(transaction); + throw new AdministrationException("Unable to find user key for key ID " + keyId + " to remove token."); + } } catch (TransactionException | DataAccessException te) { rollback(transaction); throw new AdministrationException(te); @@ -157,5 +162,4 @@ public class StandardKeyService implements KeyService { public void setProperties(NiFiProperties properties) { this.properties = properties; } - } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java index 9ce7a9a..ce1c6d5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/key/Key.java @@ -66,4 +66,13 @@ public class Key implements Serializable { this.key = key; } + public Key(int id, String identity, String key) { + this.id = id; + this.identity = identity; + this.key = key; + } + + public Key() { + } + } 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 0e3e70f..5ee98cd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java @@ -60,6 +60,7 @@ import org.apache.nifi.authorization.AccessDeniedException; import org.apache.nifi.authorization.user.NiFiUser; import org.apache.nifi.authorization.user.NiFiUserDetails; import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.authorization.util.IdentityMappingUtil; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.web.api.dto.AccessConfigurationDTO; import org.apache.nifi.web.api.dto.AccessStatusDTO; @@ -659,11 +660,12 @@ public class AccessResource extends ApplicationResource { final String expirationFromProperties = properties.getKerberosAuthenticationExpiration(); long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS); - final String identity = authentication.getName(); - expiration = validateTokenExpiration(expiration, identity); + final String rawIdentity = authentication.getName(); + String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); + expiration = validateTokenExpiration(expiration, mappedIdentity); // create the authentication token - final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(identity, expiration, "KerberosService"); + final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, "KerberosService"); // generate JWT for response final String token = jwtService.generateSignedToken(loginAuthenticationToken); @@ -729,10 +731,12 @@ public class AccessResource extends ApplicationResource { try { // attempt to authenticate final AuthenticationResponse authenticationResponse = loginIdentityProvider.authenticate(new LoginCredentials(username, password)); - long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), authenticationResponse.getIdentity()); + final String rawIdentity = authenticationResponse.getIdentity(); + String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties)); + long expiration = validateTokenExpiration(authenticationResponse.getExpiration(), mappedIdentity); // create the authentication token - loginAuthenticationToken = new LoginAuthenticationToken(authenticationResponse.getIdentity(), expiration, authenticationResponse.getIssuer()); + loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, expiration, authenticationResponse.getIssuer()); } catch (final InvalidLoginCredentialsException ilce) { throw new IllegalArgumentException("The supplied username and password are not valid.", ilce); } catch (final IdentityAccessException iae) { @@ -769,10 +773,11 @@ public class AccessResource extends ApplicationResource { String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); - if(userIdentity != null && !userIdentity.isEmpty()) { + if (userIdentity != null && !userIdentity.isEmpty()) { try { logger.info("Logging out user " + userIdentity); - jwtService.logOut(userIdentity); + jwtService.logOutUsingAuthHeader(httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION)); + logger.info("Successfully logged out user" + userIdentity); return generateOkResponse().build(); } catch (final JwtException e) { logger.error("Logout of user " + userIdentity + " failed due to: " + e.getMessage()); 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 9f1ae29..a97a744 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 @@ -44,13 +44,13 @@ public class ITAccessTokenEndpoint { private static OneWaySslAccessControlHelper helper; - private final String user = "unregistered-user@nifi"; + private final String user = "nifiad...@nifi.apache.org"; private final String password = "password"; private static final String CLIENT_ID = "token-endpoint-id"; @BeforeClass public static void setup() throws Exception { - helper = new OneWaySslAccessControlHelper(); + helper = new OneWaySslAccessControlHelper("src/test/resources/access-control/nifi-mapped-identities.properties"); } // ----------- @@ -92,7 +92,7 @@ public class ITAccessTokenEndpoint { public void testCreateProcessorUsingToken() throws Exception { String url = helper.getBaseUrl() + "/access/token"; - Response response = helper.getUser().testCreateToken(url, "user@nifi", "whatever"); + Response response = helper.getUser().testCreateToken(url, user, password); // ensure the request is successful Assert.assertEquals(201, response.getStatus()); @@ -154,7 +154,7 @@ public class ITAccessTokenEndpoint { Response response = helper.getUser().testCreateToken(url, "user@nifi", "not a real password"); - // ensure the request is successful + // ensure the request is not successful Assert.assertEquals(400, response.getStatus()); } @@ -262,7 +262,6 @@ public class ITAccessTokenEndpoint { // verify unregistered Assert.assertEquals("ACTIVE", accessStatus.getStatus()); - // log out response = helper.getUser().testDeleteWithHeaders(logoutUrl, headers); Assert.assertEquals(200, response.getStatus()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java index 3d56591..956eeec 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestAuthorizer.java @@ -44,7 +44,7 @@ public class NiFiTestAuthorizer implements Authorizer { public static final String PRIVILEGED_USER_DN = "privileged@nifi"; public static final String EXECUTED_CODE_USER_DN = "executecode@nifi"; - public static final String TOKEN_USER = "user@nifi"; + public static final String MAPPED_TOKEN_USER = "nifiadmin"; /** * Creates a new FileAuthorizationProvider. @@ -83,7 +83,7 @@ public class NiFiTestAuthorizer implements Authorizer { } // allow the token user - if (TOKEN_USER.equals(request.getIdentity())) { + if (MAPPED_TOKEN_USER.equals(request.getIdentity())) { return AuthorizationResult.approved(); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java index 508a0d1..a244c19 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestLoginIdentityProvider.java @@ -43,6 +43,7 @@ public class NiFiTestLoginIdentityProvider implements LoginIdentityProvider { users = new HashMap<>(); users.put("user@nifi", "whatever"); users.put("unregistered-user@nifi", "password"); + users.put("nifiad...@nifi.apache.org", "password"); } private void checkUser(final String user, final String password) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java index 72d59ba..c99895a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestUser.java @@ -17,6 +17,8 @@ package org.apache.nifi.integration.util; import org.apache.nifi.web.security.ProxiedEntitiesUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.ws.rs.client.Client; import javax.ws.rs.client.Entity; @@ -25,6 +27,7 @@ import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.Response; +import java.util.Arrays; import java.util.Map; /** @@ -34,8 +37,11 @@ public class NiFiTestUser { private final Client client; private final String proxyDn; + private final Logger logger; public NiFiTestUser(Client client, String proxyDn) { + logger = LoggerFactory.getLogger(NiFiTestUser.class); + this.client = client; if (proxyDn != null) { this.proxyDn = ProxiedEntitiesUtils.formatProxyDn(proxyDn); @@ -156,6 +162,8 @@ public class NiFiTestUser { } } + logger.info("POST Request to URL: " + url + " with headers: " + Arrays.toString(headers.entrySet().toArray())); + // perform the request return resourceBuilder.post(Entity.json(entity)); } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/access-control/nifi-mapped-identities.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/access-control/nifi-mapped-identities.properties new file mode 100644 index 0000000..858ea8e --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/resources/access-control/nifi-mapped-identities.properties @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + +# Core Properties # +nifi.flow.configuration.file=target/test-classes/access-control/flow.xml.gz +nifi.flow.configuration.archive.dir=target/archive +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=2 sec + +# Mapped Identities # +nifi.security.identity.mapping.pattern.kerb=^(.*?)@(.*?)$ +nifi.security.identity.mapping.value.kerb=$1 + +nifi.authorizer.configuration.file=target/test-classes/access-control/authorizers.xml +nifi.login.identity.provider.configuration.file=target/test-classes/access-control/login-identity-providers.xml +nifi.templates.directory=target/test-classes/access-control/templates +nifi.ui.banner.text=TEST BANNER +nifi.ui.autorefresh.interval=30 sec +nifi.nar.library.directory=target/test-classes/access-control/lib +nifi.nar.working.directory=target/test-classes/access-control/nar + +nifi.state.management.configuration.file=target/test-classes/access-control/state-management.xml +nifi.state.management.embedded.zookeeper.start=false +nifi.state.management.embedded.zookeeper.properties= +nifi.state.management.embedded.zookeeper.max.instances=3 +nifi.state.management.provider.local=local-provider +nifi.state.management.provider.cluster= + +# H2 Settings +nifi.database.directory=target/test-classes/database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.provenance.repository.implementation=org.apache.nifi.provenance.VolatileProvenanceRepository +nifi.flowfile.repository.directory=target/test-classes/flowfile_repository +nifi.flowfile.repository.partitions=256 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.queue.swap.threshold=20000 +nifi.swap.storage.directory=target/test-classes/flowfile_repository/swap +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=target/test-classes/content_repository +nifi.content.repository.archive.enabled=false + +# Provenance Repository Properties +nifi.provenance.repository.directory.default=./target/provenance_repository +nifi.provenance.repository.query.threads=2 +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=30 secs +nifi.provenance.repository.rollover.size=100 MB + +# Component Status Repository +nifi.components.status.repository.implementation=org.apache.nifi.controller.status.history.VolatileComponentStatusRepository +nifi.components.status.repository.buffer.size=288 +nifi.components.status.snapshot.frequency=10 secs + +# Site to Site properties +#For testing purposes. Default value should actually be empty! +nifi.remote.input.host= +nifi.remote.input.socket.port= +nifi.remote.input.secure=false + +# web properties # +nifi.web.war.directory=target/test-classes/lib +nifi.web.http.host= +nifi.web.http.port= +nifi.web.https.host= +nifi.web.https.port=8443 +nifi.web.jetty.working.directory=target/test-classes/access-control/jetty + +# security properties # +nifi.sensitive.props.key=REPLACE_ME +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore=target/test-classes/access-control/keystore.jks +nifi.security.keystoreType=JKS +nifi.security.keystorePasswd=passwordpassword +nifi.security.keyPasswd= +nifi.security.truststore=target/test-classes/access-control/truststore.jks +nifi.security.truststoreType=JKS +nifi.security.truststorePasswd=passwordpassword +nifi.security.user.login.identity.provider=test-provider +nifi.security.user.authorizer=test-provider + +# cluster common properties (cluster manager and nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false +nifi.cluster.protocol.socket.timeout=30 sec +nifi.cluster.protocol.connection.handshake.timeout=45 sec +# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured # +nifi.cluster.protocol.use.multicast=false +nifi.cluster.protocol.multicast.address= +nifi.cluster.protocol.multicast.port= +nifi.cluster.protocol.multicast.service.broadcast.delay=500 ms +nifi.cluster.protocol.multicast.service.locator.attempts=3 +nifi.cluster.protocol.multicast.service.locator.attempts.delay=1 sec + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=2 +# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx # +nifi.cluster.node.unicast.manager.address= +nifi.cluster.node.unicast.manager.protocol.port= +nifi.cluster.node.unicast.manager.authority.provider.port= + +# cluster manager properties (only configure for cluster manager) # +nifi.cluster.is.manager=false +nifi.cluster.manager.address= +nifi.cluster.manager.protocol.port= +nifi.cluster.manager.authority.provider.port= +nifi.cluster.manager.authority.provider.threads=10 +nifi.cluster.manager.node.firewall.file= +nifi.cluster.manager.node.event.history.size=10 +nifi.cluster.manager.node.api.connection.timeout=30 sec +nifi.cluster.manager.node.api.read.timeout=30 sec +nifi.cluster.manager.node.api.request.threads=10 +nifi.cluster.manager.flow.retrieval.delay=5 sec +nifi.cluster.manager.protocol.threads=10 +nifi.cluster.manager.safemode.duration=0 sec diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java index 0d21a8a..9a25ff3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationFilter.java @@ -63,7 +63,7 @@ public class JwtAuthenticationFilter extends NiFiAuthenticationFilter { return matcher.matches(); } - private String getTokenFromHeader(String authenticationHeader) { + public static String getTokenFromHeader(String authenticationHeader) { Matcher matcher = tokenPattern.matcher(authenticationHeader); if(matcher.matches()) { return matcher.group(1); 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 ecbfc67..9569631 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 @@ -169,16 +169,33 @@ public class JwtService { } } - 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."); + /** + * Log out the authenticated user using the 'kid' (Key ID) claim from the base64 encoded JWT + * + * @param token a signed, base64 encoded, JSON Web Token in form HEADER.PAYLOAD.SIGNATURE + * @throws JwtException if there is a problem with the token input + * @throws Exception if there is an issue logging the user out + */ + public void logOut(String token) { + Jws<Claims> claims = parseTokenFromBase64EncodedString(token); + + // Get the key ID from the claims + final Integer keyId = claims.getBody().get(KEY_ID_CLAIM, Integer.class); + + if (keyId == null) { + throw new JwtException("The key claim (kid) was not present in the request token to log out user."); } try { - keyService.deleteKey(userIdentity); + keyService.deleteKey(keyId); } catch (Exception e) { - logger.error("Unable to log out user: " + userIdentity + ". Failed to remove their token from database."); + logger.error("The key with key ID: " + keyId + " failed to be removed from the user database."); throw e; } } + + public void logOutUsingAuthHeader(String authorizationHeader) { + String base64EncodedToken = JwtAuthenticationFilter.getTokenFromHeader(authorizationHeader); + logOut(base64EncodedToken); + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java new file mode 100644 index 0000000..8a1dbd4 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt; + +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.user.NiFiUserDetails; +import org.apache.nifi.properties.StandardNiFiProperties; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.security.InvalidAuthenticationException; +import org.apache.nifi.web.security.token.LoginAuthenticationToken; +import org.apache.nifi.web.security.token.NiFiAuthenticationToken; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class JwtAuthenticationProviderTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final static int EXPIRATION_MILLIS = 60000; + private final static String CLIENT_ADDRESS = "127.0.0.1"; + private final static String ADMIN_IDENTITY = "nifiadmin"; + private final static String REALMED_ADMIN_KERBEROS_IDENTITY = "nifiad...@nifi.apache.org"; + + private final static String UNKNOWN_TOKEN = "eyJhbGciOiJIUzI1NiJ9" + + ".eyJzdWIiOiJ1bmtub3duX3Rva2VuIiwiaXNzIjoiS2VyYmVyb3NQcm9" + + "2aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWR" + + "fdXNlcm5hbWUiOiJ1bmtub3duX3Rva2VuIiwia2lkIjoxLCJleHAiOjE" + + "2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.PpOGx3Ul5ydokOOuzKd" + + "aRKv1kxy6Q4jGy7rBPU8PqxY"; + + private NiFiProperties properties; + + + private JwtService jwtService; + private JwtAuthenticationProvider jwtAuthenticationProvider; + + @Before + public void setUp() throws Exception { + TestKeyService keyService = new TestKeyService(); + jwtService = new JwtService(keyService); + + // Set up Kerberos identity mappings + Properties props = new Properties(); + props.put(properties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX, "^(.*?)@(.*?)$"); + props.put(properties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX, "$1"); + properties = new StandardNiFiProperties(props); + + jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, mock(Authorizer.class)); + } + + @Test + public void testAdminIdentityAndTokenIsValid() throws Exception { + // Arrange + LoginAuthenticationToken loginAuthenticationToken = + new LoginAuthenticationToken(ADMIN_IDENTITY, + EXPIRATION_MILLIS, + "MockIdentityProvider"); + String token = jwtService.generateSignedToken(loginAuthenticationToken); + final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS); + + // Act + final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); + final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal(); + + // Assert + assertEquals(ADMIN_IDENTITY, details.getUsername()); + } + + @Test + public void testKerberosRealmedIdentityAndTokenIsValid() throws Exception { + // Arrange + LoginAuthenticationToken loginAuthenticationToken = + new LoginAuthenticationToken(REALMED_ADMIN_KERBEROS_IDENTITY, + EXPIRATION_MILLIS, + "MockIdentityProvider"); + String token = jwtService.generateSignedToken(loginAuthenticationToken); + final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS); + + // Act + final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); + final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal(); + + // Assert + // Check we now have the mapped identity + assertEquals(ADMIN_IDENTITY, details.getUsername()); + } + + @Test + public void testFailToAuthenticateWithUnknownToken() throws Exception { + // Arrange + expectedException.expect(InvalidAuthenticationException.class); + expectedException.expectMessage("Unable to validate the access token."); + + // Generate a token with a known token + LoginAuthenticationToken loginAuthenticationToken = + new LoginAuthenticationToken(ADMIN_IDENTITY, + EXPIRATION_MILLIS, + "MockIdentityProvider"); + jwtService.generateSignedToken(loginAuthenticationToken); + + // Act + // Try to authenticate with an unknown token + final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(UNKNOWN_TOKEN, CLIENT_ADDRESS); + final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request); + + // Assert + // Expect exception + } + +} \ No newline at end of file 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 0727ccf..70f9a6d 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 @@ -16,6 +16,8 @@ */ package org.apache.nifi.web.security.jwt; +import static org.apache.nifi.util.NiFiProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX; +import static org.apache.nifi.util.NiFiProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyInt; @@ -28,8 +30,11 @@ import io.jsonwebtoken.JwtException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Properties; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; @@ -37,7 +42,10 @@ 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.authorization.util.IdentityMapping; +import org.apache.nifi.authorization.util.IdentityMappingUtil; import org.apache.nifi.key.Key; +import org.apache.nifi.properties.StandardNiFiProperties; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.codehaus.jettison.json.JSONObject; import org.junit.After; @@ -58,6 +66,9 @@ public class JwtServiceTest { private static final Logger logger = LoggerFactory.getLogger(JwtServiceTest.class); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + /** * These constant strings were generated using the tool at http://jwt.io */ @@ -132,17 +143,30 @@ public class JwtServiceTest { + "6MSwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.6kDjDanA" + "g0NQDb3C8FmgbBAYDoIfMAEkF4WMVALsbJA"; + private static final String KERBEROS_PROVIDER_TOKEN = "eyJhbGciOiJIUzI1NiJ9" + + ".eyJzdWIiOiJuaWZpYWRtaW5AbmlmaS5hcGFjaGUub3JnIiwiaXNzIjoiS2VyYmVyb" + + "3NQcm92aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWRfdXN" + + "lcm5hbWUiOiJuaWZpYWRtaW5AbmlmaS5hcGFjaGUub3JnIiwia2lkIjo2LCJleHAiO" + + "jE2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.Mmnx6ssdjQ5_5VVRiyPWU60Oegc" + + "NdhWezaKKNK48Mew"; + private static final String DEFAULT_HEADER = "{\"alg\":\"HS256\"}"; private static final String DEFAULT_IDENTITY = "alopresto"; + private static final String REALMED_KERBEROS_IDENTITY = "nifiad...@nifi.apache.org"; + private static final String KERBEROS_IDENTITY = "nifiadmin"; private static final String TOKEN_DELIMITER = "."; private static final String HMAC_SECRET = "test_hmac_shared_secret"; + private static List<IdentityMapping> identityMappings; + private KeyService mockKeyService; + private KeyService testKeyService; // Class under test private JwtService jwtService; + private JwtService jwtServiceUsingTestKeyService; public static String generateHS256Token(String rawHeader, String rawPayload, boolean isValid, boolean isSigned) { return generateHS256Token(rawHeader, rawPayload, HMAC_SECRET, isValid, isSigned); @@ -189,7 +213,7 @@ public class JwtServiceTest { Key answerKey = key; @Override public Key answer(InvocationOnMock invocation) throws Throwable { - if(invocation.getMethod().equals(KeyService.class.getMethod("deleteKey", String.class))) { + if(invocation.getMethod().equals(KeyService.class.getMethod("deleteKey", Integer.class))) { answerKey = null; } return answerKey; @@ -210,8 +234,15 @@ public class JwtServiceTest { mockKeyService = mock(KeyService.class); when(mockKeyService.getKey(anyInt())).thenAnswer(keyAnswer); when(mockKeyService.getOrCreateKey(anyString())).thenReturn(key); - doAnswer(keyAnswer).when(mockKeyService).deleteKey(anyString()); + doAnswer(keyAnswer).when(mockKeyService).deleteKey(anyInt()); + jwtService = new JwtService(mockKeyService); + jwtServiceUsingTestKeyService = new JwtService(new TestKeyService()); + + Properties props = new Properties(); + props.setProperty(SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX+"kerb", "^(.*?)@(.*?)$"); + props.setProperty(SECURITY_IDENTITY_MAPPING_VALUE_PREFIX+"kerb", "$1"); + identityMappings = IdentityMappingUtil.getIdentityMappings(new StandardNiFiProperties(props)); } @After @@ -226,12 +257,25 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert assertEquals("Identity", DEFAULT_IDENTITY, identity); } + @Test + public void testShouldGetAuthenticationForValidKerberosToken() throws Exception { + // Arrange + String token = KERBEROS_PROVIDER_TOKEN; + + // Act + String identity = jwtService.getAuthenticationFromToken(token); + logger.info("Extracted identity: " + identity); + + // Assert + assertEquals("Identity", REALMED_KERBEROS_IDENTITY, identity); + } + @Test(expected = JwtException.class) public void testShouldNotGetAuthenticationForInvalidToken() throws Exception { // Arrange @@ -239,7 +283,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -252,7 +296,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -265,7 +309,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -278,7 +322,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -291,7 +335,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -304,7 +348,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -318,7 +362,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -331,7 +375,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -344,7 +388,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -357,7 +401,7 @@ public class JwtServiceTest { // Act String identity = jwtService.getAuthenticationFromToken(token); - logger.debug("Extracted identity: " + identity); + logger.info("Extracted identity: " + identity); // Assert // Should fail @@ -372,7 +416,7 @@ public class JwtServiceTest { LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto", EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.debug("Generating token for " + loginAuthenticationToken); + logger.info("Generating token for " + loginAuthenticationToken); final String EXPECTED_HEADER = DEFAULT_HEADER; @@ -381,7 +425,7 @@ public class JwtServiceTest { // Act String token = jwtService.generateSignedToken(loginAuthenticationToken); - logger.debug("Generated JWT: " + token); + logger.info("Generated JWT: " + token); // Run after the SUT generates the token to ensure the same issued at time // Split the token, decode the middle section, and form a new String @@ -390,7 +434,7 @@ public class JwtServiceTest { DECODED_PAYLOAD.length() - 1)); logger.trace("Actual token was issued at " + ISSUED_AT_SEC); - // Always use LinkedHashMap to enforce order of the keys because the signature depends on order + // Always use LinkedHashMap to enforce order of the signingKeys because the signature depends on order Map<String, Object> claims = new LinkedHashMap<>(); claims.put("sub", "alopresto"); claims.put("iss", "MockIdentityProvider"); @@ -403,7 +447,7 @@ public class JwtServiceTest { final String EXPECTED_PAYLOAD = new JSONObject(claims).toString(); final String EXPECTED_TOKEN_STRING = generateHS256Token(EXPECTED_HEADER, EXPECTED_PAYLOAD, true, true); - logger.debug("Expected JWT: " + EXPECTED_TOKEN_STRING); + logger.info("Expected JWT: " + EXPECTED_TOKEN_STRING); // Assert assertEquals("JWT token", EXPECTED_TOKEN_STRING, token); @@ -413,7 +457,7 @@ public class JwtServiceTest { public void testShouldNotGenerateTokenWithNullAuthenticationToken() throws Exception { // Arrange LoginAuthenticationToken nullLoginAuthenticationToken = null; - logger.debug("Generating token for " + nullLoginAuthenticationToken); + logger.info("Generating token for " + nullLoginAuthenticationToken); // Act jwtService.generateSignedToken(nullLoginAuthenticationToken); @@ -428,7 +472,7 @@ public class JwtServiceTest { final int EXPIRATION_MILLIS = 60000; LoginAuthenticationToken emptyIdentityLoginAuthenticationToken = new LoginAuthenticationToken("", EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.debug("Generating token for " + emptyIdentityLoginAuthenticationToken); + logger.info("Generating token for " + emptyIdentityLoginAuthenticationToken); // Act jwtService.generateSignedToken(emptyIdentityLoginAuthenticationToken); @@ -443,7 +487,7 @@ public class JwtServiceTest { final int EXPIRATION_MILLIS = 60000; LoginAuthenticationToken nullIdentityLoginAuthenticationToken = new LoginAuthenticationToken(null, EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.debug("Generating token for " + nullIdentityLoginAuthenticationToken); + logger.info("Generating token for " + nullIdentityLoginAuthenticationToken); // Act jwtService.generateSignedToken(nullIdentityLoginAuthenticationToken); @@ -459,7 +503,7 @@ public class JwtServiceTest { LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY, EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.debug("Generating token for " + loginAuthenticationToken); + logger.info("Generating token for " + loginAuthenticationToken); // Set up the bad key service KeyService missingKeyService = mock(KeyService.class); @@ -474,9 +518,6 @@ public class JwtServiceTest { // Should throw exception } - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Test public void testShouldLogOutUser() throws Exception { // Arrange @@ -488,16 +529,51 @@ public class JwtServiceTest { LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY, EXPIRATION_MILLIS, "MockIdentityProvider"); - logger.debug("Generating token for " + loginAuthenticationToken); + logger.info("Generating token for " + loginAuthenticationToken); // Act String token = jwtService.generateSignedToken(loginAuthenticationToken); - logger.debug("Generated JWT: " + token); + logger.info("Generated JWT: " + token); + logger.info("Validating token..."); String authID = jwtService.getAuthenticationFromToken(token); assertEquals(DEFAULT_IDENTITY, authID); - logger.debug("Logging out user: " + DEFAULT_IDENTITY); + logger.info("Token was valid"); + logger.info("Logging out user: " + authID); jwtService.logOut(token); - logger.debug("Logged out user: " + DEFAULT_IDENTITY); + logger.info("Logged out user: " + authID); + logger.info("Checking that token is now invalid..."); + jwtService.getAuthenticationFromToken(token); + + // Assert + // Should throw exception when user is not found + } + + @Test + public void testShouldLogOutUserUsingAuthHeader() throws Exception { + // Arrange + expectedException.expect(JwtException.class); + expectedException.expectMessage("Unable to validate the access token."); + + // Token expires in 60 seconds + final int EXPIRATION_MILLIS = 60000; + LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY, + EXPIRATION_MILLIS, + "MockIdentityProvider"); + logger.info("Generating token for " + loginAuthenticationToken); + + // Act + String token = jwtService.generateSignedToken(loginAuthenticationToken); + + logger.info("Generated JWT: " + token); + logger.info("Validating token..."); + String authID = jwtService.getAuthenticationFromToken(token); + assertEquals(DEFAULT_IDENTITY, authID); + logger.info("Token was valid"); + logger.info("Logging out user: " + authID); + String header = "Bearer " + token; + jwtService.logOutUsingAuthHeader(header); + logger.info("Logged out user: " + authID); + logger.info("Checking that token is now invalid..."); jwtService.getAuthenticationFromToken(token); // Assert @@ -508,7 +584,7 @@ public class JwtServiceTest { public void testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws Exception { // Arrange expectedException.expect(JwtException.class); - expectedException.expectMessage("Log out failed: The user identity was not present in the request token to log out user."); + expectedException.expectMessage("Unable to validate the access token."); // Act jwtService.logOut(null); @@ -517,4 +593,72 @@ public class JwtServiceTest { // Should throw exception when authorization header is null } + @Test + public void testShouldLogOutKerberosUser() 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(KERBEROS_IDENTITY, + EXPIRATION_MILLIS, + "MockIdentityProvider"); + logger.info("Generating token for " + loginAuthenticationToken); + + // Act + String token = jwtServiceUsingTestKeyService.generateSignedToken(loginAuthenticationToken); + logger.info("Generated JWT: " + token); + logger.info("Validating token..."); + String authID = jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); + logger.info("Token was valid, unmapped user identity was: " + authID); + assertEquals(KERBEROS_IDENTITY, authID); + logger.info("Using identity mappings " + Arrays.toString(identityMappings.toArray()) + " to map identity: " + authID); + String mappedIdentity = IdentityMappingUtil.mapIdentity(authID, identityMappings); + logger.info("Logging out user with mapped identity: " + mappedIdentity); + jwtServiceUsingTestKeyService.logOut(mappedIdentity); + logger.info("Logged out user with mapped identity: " + mappedIdentity); + logger.info("Checking that token for " + mappedIdentity + " is now invalid..."); + jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); + + // Assert + // Should throw exception when user is not found + } + + @Test + public void testShouldLogOutRealmedKerberosUser() 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; + // map the kerberos identity before we create our token, just as is done in AccessResource + final String mappedIdentity = IdentityMappingUtil.mapIdentity(REALMED_KERBEROS_IDENTITY, identityMappings); + + LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(mappedIdentity, + EXPIRATION_MILLIS, + "MockIdentityProvider"); + logger.info("Generating token for " + loginAuthenticationToken); + + // Act + String token = jwtServiceUsingTestKeyService.generateSignedToken(loginAuthenticationToken); + logger.info("Generated JWT: " + token); + logger.info("Validating token..."); + String authID = jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); + logger.info("Token was valid, unmapped user identity was: " + authID); + assertEquals(KERBEROS_IDENTITY, authID); + logger.info("Using identity mappings " + Arrays.toString(identityMappings.toArray()) + " to map identity: " + authID); + logger.info("Logging out user with mapped identity: " + authID); + jwtServiceUsingTestKeyService.logOut(authID); + logger.info("Logged out user with mapped identity: " + authID); + logger.info("Checking that token for " + authID + " is now invalid..."); + jwtServiceUsingTestKeyService.getAuthenticationFromToken(token); + + // Assert + // Should throw exception when user is not found + } + } 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/TestKeyService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java new file mode 100644 index 0000000..4d2f415 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/TestKeyService.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.web.security.jwt; + +import org.apache.nifi.admin.service.KeyService; +import org.apache.nifi.key.Key; +import org.h2.util.StringUtils; + +import java.util.ArrayList; +import java.util.UUID; + +public class TestKeyService implements KeyService { + + ArrayList<Key> signingKeys = new ArrayList<Key>(); + + public TestKeyService() { + + } + + @Override + public Key getKey(int id) { + Key key = null; + for(Key k : signingKeys) { + if(k.getId() == id) { + key = k; + } + } + return key; + } + + @Override + public Key getOrCreateKey(String identity) { + for(Key k : signingKeys) { + if(StringUtils.equals(k.getIdentity(), identity)) { + return k; + } + } + + Key key = generateKey(identity); + signingKeys.add(key); + return key; + } + + @Override + public void deleteKey(Integer keyId) { + Key keyToRemove = null; + for(Key k : signingKeys) { + if(k.getId() == keyId) { + keyToRemove = k; + } + } + signingKeys.remove(keyToRemove); + } + + private Key generateKey(String identity) { + Integer keyId = signingKeys.size(); + return new Key(keyId, identity, UUID.randomUUID().toString()); + } +}