mirror of https://github.com/apache/nifi.git
NIFI-8766 Implemented RS512 Algorithm for JWT Signing
- Replaced per-user symmetric-key HS256 with shared and rotated RSA asymmetric-key RS512 implementation - Added nifi.security.user.jws.key.rotation.period property for RSA Key Pair rotation - Added JSON Web Tokens section to Administration Guide - Implemented persistent storage of RSA Public Keys for verification using Local State Manager - Implemented JWT revocation on logout with persistence using Local State Manager - Refactored JWT implementation using Spring Security OAuth2 and Nimbus JWT - Refactored Spring Security Provider configuration using Java instead of XML - Removed H2 storage of per-user keys - Upgraded nimbus-jose-jwt from 7.9 to 9.11.2 NIFI-8766 Corrected AuthenticationException handling in AccessResource.getAccessStatus - Added nifi.user.security.jws.key.rotation.period to default nifi.properties - Updated logging statements and clarified configuration and method documentation NIFI-8766 Changed Algorithm to PS512 and updated documentation Signed-off-by: Nathan Gough <thenatog@gmail.com> This closes #5262.
This commit is contained in:
parent
9bcbf83e5a
commit
a652280fbb
|
@ -1904,10 +1904,10 @@ The following binary components are provided under the Apache Software License v
|
|||
Converter: Jackson 2.5.0
|
||||
Copyright 2018, Jake Wharton
|
||||
|
||||
(ASLv2) Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:jar:6.0.1 - https://bitbucket.org/connect2id/nimbus-jose-jwt)
|
||||
(ASLv2) Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt - https://connect2id.com/products/nimbus-jose-jwt)
|
||||
The following NOTICE information applies:
|
||||
Nimbus JOSE+JWT 6.0.1
|
||||
Copyright 2018, Connect2id Ltd.
|
||||
Nimbus JOSE+JWT
|
||||
Copyright 2021, Connect2id Ltd.
|
||||
|
||||
(ASLv2) OkHttp Logging Interceptor (com.squareup.okhttp3:logging-interceptor:jar:3.12.2)
|
||||
The following NOTICE information applies:
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.net.InetSocketAddress;
|
|||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -170,6 +171,7 @@ public class NiFiProperties extends ApplicationProperties {
|
|||
public static final String SECURITY_GROUP_MAPPING_PATTERN_PREFIX = "nifi.security.group.mapping.pattern.";
|
||||
public static final String SECURITY_GROUP_MAPPING_VALUE_PREFIX = "nifi.security.group.mapping.value.";
|
||||
public static final String SECURITY_GROUP_MAPPING_TRANSFORM_PREFIX = "nifi.security.group.mapping.transform.";
|
||||
public static final String SECURITY_USER_JWS_KEY_ROTATION_PERIOD = "nifi.security.user.jws.key.rotation.period";
|
||||
|
||||
// oidc
|
||||
public static final String SECURITY_USER_OIDC_DISCOVERY_URL = "nifi.security.user.oidc.discovery.url";
|
||||
|
@ -366,6 +368,7 @@ public class NiFiProperties extends ApplicationProperties {
|
|||
public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY = "JDK";
|
||||
public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT = "30 secs";
|
||||
public static final String DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT = "30 secs";
|
||||
private static final String DEFAULT_SECURITY_USER_JWS_KEY_ROTATION_PERIOD = "PT1H";
|
||||
public static final String DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION = "true";
|
||||
|
||||
// cluster common defaults
|
||||
|
@ -798,6 +801,10 @@ public class NiFiProperties extends ApplicationProperties {
|
|||
return getProperty(SECURITY_AUTO_RELOAD_INTERVAL, DEFAULT_SECURITY_AUTO_RELOAD_INTERVAL);
|
||||
}
|
||||
|
||||
public Duration getSecurityUserJwsKeyRotationPeriod() {
|
||||
return Duration.parse(getProperty(SECURITY_USER_JWS_KEY_ROTATION_PERIOD, DEFAULT_SECURITY_USER_JWS_KEY_ROTATION_PERIOD));
|
||||
}
|
||||
|
||||
// getters for cluster protocol properties //
|
||||
public String getClusterProtocolHeartbeatInterval() {
|
||||
return getProperty(CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL,
|
||||
|
|
|
@ -489,6 +489,28 @@ To enable authentication via Apache Knox the following properties must be config
|
|||
this listing. The audience that is populated in the token can be configured in Knox.
|
||||
|==================================================================================================================================================
|
||||
|
||||
[[json_web_token]]
|
||||
=== JSON Web Tokens
|
||||
|
||||
NiFi uses JSON Web Tokens to provide authenticated access after the initial login process. Generated JSON Web Tokens include the authenticated user identity
|
||||
as well as the issuer and expiration from the configured Login Identity Provider.
|
||||
|
||||
NiFi uses generated RSA Key Pairs with a key size of 4096 bits to support the `PS512` algorithm for JSON Web Signatures. The system stores RSA
|
||||
Public Keys using the configured local State Provider and retains the RSA Private Key in memory. This approach supports signature verification
|
||||
for the expiration configured in the Login Identity Provider without persisting the private key.
|
||||
|
||||
JSON Web Token support includes revocation on logout using JSON Web Token Identifiers. The system denies access for expired tokens based on the
|
||||
Login Identity Provider configuration, but revocation invalidates the token prior to expiration. The system stores revoked identifiers using the
|
||||
configured local State Provider and runs a scheduled command to delete revoked identifiers after the associated expiration.
|
||||
|
||||
The following settings can be configured in _nifi.properties_ to control JSON Web Token signing.
|
||||
|
||||
[options="header"]
|
||||
|==================================================================================================================================================
|
||||
| Property Name | Description
|
||||
|`nifi.security.user.jws.key.rotation.period` | JSON Web Signature Key Rotation Period defines how often the system generates a new RSA Key Pair, expressed as an ISO 8601 duration. The default is one hour: `PT1H`
|
||||
|==================================================================================================================================================
|
||||
|
||||
[[multi-tenant-authorization]]
|
||||
== Multi-Tenant Authorization
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ import java.sql.Statement;
|
|||
|
||||
public class IdpDataSourceFactoryBean implements FactoryBean<JdbcConnectionPool> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeyDataSourceFactoryBean.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(IdpDataSourceFactoryBean.class);
|
||||
private static final String NF_USERNAME_PASSWORD = "nf";
|
||||
private static final int MAX_CONNECTIONS = 5;
|
||||
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
* 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.admin;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.h2.jdbcx.JdbcConnectionPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
|
||||
public class KeyDataSourceFactoryBean implements FactoryBean {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeyDataSourceFactoryBean.class);
|
||||
private static final String NF_USERNAME_PASSWORD = "nf";
|
||||
private static final int MAX_CONNECTIONS = 5;
|
||||
|
||||
// database file name
|
||||
private static final String USER_KEYS_DATABASE_FILE_NAME = "nifi-user-keys";
|
||||
|
||||
// ----------
|
||||
// keys table
|
||||
// ----------
|
||||
|
||||
private static final String CREATE_KEY_TABLE = "CREATE TABLE KEY ("
|
||||
+ "ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT, "
|
||||
+ "IDENTITY VARCHAR2(4096) NOT NULL UNIQUE, "
|
||||
+ "KEY VARCHAR2(100) NOT NULL"
|
||||
+ ")";
|
||||
|
||||
private JdbcConnectionPool connectionPool;
|
||||
|
||||
private NiFiProperties properties;
|
||||
|
||||
@Override
|
||||
public Object getObject() throws Exception {
|
||||
if (connectionPool == null) {
|
||||
|
||||
// locate the repository directory
|
||||
String repositoryDirectoryPath = properties.getProperty(NiFiProperties.REPOSITORY_DATABASE_DIRECTORY);
|
||||
|
||||
// ensure the repository directory is specified
|
||||
if (repositoryDirectoryPath == null) {
|
||||
throw new NullPointerException("Database directory must be specified.");
|
||||
}
|
||||
|
||||
// create a handle to the repository directory
|
||||
File repositoryDirectory = new File(repositoryDirectoryPath);
|
||||
|
||||
// create a handle to the database directory and file
|
||||
File databaseFile = new File(repositoryDirectory, USER_KEYS_DATABASE_FILE_NAME);
|
||||
String databaseUrl = getDatabaseUrl(databaseFile);
|
||||
|
||||
// create the pool
|
||||
connectionPool = JdbcConnectionPool.create(databaseUrl, NF_USERNAME_PASSWORD, NF_USERNAME_PASSWORD);
|
||||
connectionPool.setMaxConnections(MAX_CONNECTIONS);
|
||||
|
||||
Connection connection = null;
|
||||
ResultSet rs = null;
|
||||
Statement statement = null;
|
||||
try {
|
||||
// get a connection
|
||||
connection = connectionPool.getConnection();
|
||||
connection.setAutoCommit(false);
|
||||
|
||||
// create a statement for creating/updating the database
|
||||
statement = connection.createStatement();
|
||||
|
||||
// determine if the key table need to be created
|
||||
rs = connection.getMetaData().getTables(null, null, "KEY", null);
|
||||
if (!rs.next()) {
|
||||
statement.execute(CREATE_KEY_TABLE);
|
||||
}
|
||||
|
||||
// commit any changes
|
||||
connection.commit();
|
||||
} catch (SQLException sqle) {
|
||||
RepositoryUtils.rollback(connection, logger);
|
||||
throw sqle;
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
RepositoryUtils.closeQuietly(connection);
|
||||
}
|
||||
}
|
||||
|
||||
return connectionPool;
|
||||
}
|
||||
|
||||
private String getDatabaseUrl(File databaseFile) {
|
||||
String databaseUrl = "jdbc:h2:" + databaseFile + ";AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3";
|
||||
String databaseUrlAppend = properties.getProperty(NiFiProperties.H2_URL_APPEND);
|
||||
if (StringUtils.isNotBlank(databaseUrlAppend)) {
|
||||
databaseUrl += databaseUrlAppend;
|
||||
}
|
||||
return databaseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class getObjectType() {
|
||||
return JdbcConnectionPool.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setProperties(NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
// shutdown the connection pool
|
||||
if (connectionPool != null) {
|
||||
try {
|
||||
connectionPool.dispose();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Unable to dispose of connection pool: " + e.getMessage());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn(StringUtils.EMPTY, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -23,8 +23,6 @@ public interface DAOFactory {
|
|||
|
||||
ActionDAO getActionDAO();
|
||||
|
||||
KeyDAO getKeyDAO();
|
||||
|
||||
IdpCredentialDAO getIdpCredentialDAO();
|
||||
|
||||
IdpUserGroupDAO getIdpUserGroupDAO();
|
||||
|
|
|
@ -20,7 +20,6 @@ import org.apache.nifi.admin.dao.ActionDAO;
|
|||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.IdpCredentialDAO;
|
||||
import org.apache.nifi.admin.dao.IdpUserGroupDAO;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
|
||||
import java.sql.Connection;
|
||||
|
||||
|
@ -40,12 +39,6 @@ public class DAOFactoryImpl implements DAOFactory {
|
|||
return new StandardActionDAO(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyDAO getKeyDAO() {
|
||||
return new StandardKeyDAO(connection);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public IdpCredentialDAO getIdpCredentialDAO() {
|
||||
return new StandardIdpCredentialDAO(connection);
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
/*
|
||||
* 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.admin.dao.impl;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.UUID;
|
||||
import org.apache.nifi.admin.RepositoryUtils;
|
||||
import org.apache.nifi.admin.dao.DataAccessException;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class StandardKeyDAO implements KeyDAO {
|
||||
|
||||
private static final String SELECT_KEY_FOR_USER_BY_ID = "SELECT ID, IDENTITY, KEY "
|
||||
+ "FROM KEY "
|
||||
+ "WHERE ID = ?";
|
||||
|
||||
private static final String SELECT_KEY_FOR_USER_BY_IDENTITY = "SELECT ID, IDENTITY, KEY "
|
||||
+ "FROM KEY "
|
||||
+ "WHERE IDENTITY = ?";
|
||||
|
||||
private static final String INSERT_KEY = "INSERT INTO KEY ("
|
||||
+ "IDENTITY, KEY"
|
||||
+ ") VALUES ("
|
||||
+ "?, ?"
|
||||
+ ")";
|
||||
|
||||
private static final String DELETE_KEYS = "DELETE FROM KEY "
|
||||
+ "WHERE ID = ?";
|
||||
|
||||
private final Connection connection;
|
||||
|
||||
public StandardKeyDAO(Connection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key findKeyById(int id) {
|
||||
Key key = null;
|
||||
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// add each authority for the specified user
|
||||
statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_ID);
|
||||
statement.setInt(1, id);
|
||||
|
||||
// execute the query
|
||||
rs = statement.executeQuery();
|
||||
|
||||
// if the key was found, add it
|
||||
if (rs.next()) {
|
||||
key = new Key();
|
||||
key.setId(rs.getInt("ID"));
|
||||
key.setIdentity(rs.getString("IDENTITY"));
|
||||
key.setKey(rs.getString("KEY"));
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key findLatestKeyByIdentity(String identity) {
|
||||
if (identity == null) {
|
||||
throw new IllegalArgumentException("Specified identity cannot be null.");
|
||||
}
|
||||
|
||||
Key key = null;
|
||||
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// add each authority for the specified user
|
||||
statement = connection.prepareStatement(SELECT_KEY_FOR_USER_BY_IDENTITY);
|
||||
statement.setString(1, identity);
|
||||
|
||||
// execute the query
|
||||
rs = statement.executeQuery();
|
||||
|
||||
// if the key was found, add it
|
||||
if (rs.next()) {
|
||||
key = new Key();
|
||||
key.setId(rs.getInt("ID"));
|
||||
key.setIdentity(rs.getString("IDENTITY"));
|
||||
key.setKey(rs.getString("KEY"));
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key createKey(final String identity) {
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
final String keyValue = UUID.randomUUID().toString();
|
||||
|
||||
// add each authority for the specified user
|
||||
statement = connection.prepareStatement(INSERT_KEY, Statement.RETURN_GENERATED_KEYS);
|
||||
statement.setString(1, identity);
|
||||
statement.setString(2, keyValue);
|
||||
|
||||
// insert the key
|
||||
int updateCount = statement.executeUpdate();
|
||||
rs = statement.getGeneratedKeys();
|
||||
|
||||
// verify the results
|
||||
if (updateCount == 1 && rs.next()) {
|
||||
final Key key = new Key();
|
||||
key.setId(rs.getInt(1));
|
||||
key.setIdentity(identity);
|
||||
key.setKey(keyValue);
|
||||
return key;
|
||||
} else {
|
||||
throw new DataAccessException("Unable to add key for user.");
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deleteKey(Integer keyId) {
|
||||
PreparedStatement statement = null;
|
||||
try {
|
||||
// add each authority for the specified user
|
||||
statement = connection.prepareStatement(DELETE_KEYS);
|
||||
statement.setInt(1, keyId);
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} catch (DataAccessException dae) {
|
||||
throw dae;
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
* 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.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.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;
|
||||
import org.apache.nifi.admin.service.transaction.TransactionBuilder;
|
||||
import org.apache.nifi.admin.service.transaction.TransactionException;
|
||||
import org.apache.nifi.key.Key;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
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 {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardKeyService.class);
|
||||
|
||||
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private final Lock readLock = lock.readLock();
|
||||
private final Lock writeLock = lock.writeLock();
|
||||
|
||||
private TransactionBuilder transactionBuilder;
|
||||
private NiFiProperties properties;
|
||||
|
||||
@Override
|
||||
public Key getKey(int id) {
|
||||
Transaction transaction = null;
|
||||
Key key;
|
||||
|
||||
readLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// get the key
|
||||
GetKeyByIdAction addActions = new GetKeyByIdAction(id);
|
||||
key = transaction.execute(addActions);
|
||||
|
||||
// commit the transaction
|
||||
transaction.commit();
|
||||
} catch (TransactionException | DataAccessException te) {
|
||||
rollback(transaction);
|
||||
throw new AdministrationException(te);
|
||||
} catch (Throwable t) {
|
||||
rollback(transaction);
|
||||
throw t;
|
||||
} finally {
|
||||
closeQuietly(transaction);
|
||||
readLock.unlock();
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key getOrCreateKey(String identity) {
|
||||
Transaction transaction = null;
|
||||
Key key;
|
||||
|
||||
writeLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// get or create a key
|
||||
GetOrCreateKeyAction addActions = new GetOrCreateKeyAction(identity);
|
||||
key = transaction.execute(addActions);
|
||||
|
||||
// commit the transaction
|
||||
transaction.commit();
|
||||
} catch (TransactionException | DataAccessException te) {
|
||||
rollback(transaction);
|
||||
throw new AdministrationException(te);
|
||||
} catch (Throwable t) {
|
||||
rollback(transaction);
|
||||
throw t;
|
||||
} finally {
|
||||
closeQuietly(transaction);
|
||||
writeLock.unlock();
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteKey(Integer keyId) {
|
||||
Transaction transaction = null;
|
||||
|
||||
writeLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// delete the keys
|
||||
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);
|
||||
} catch (Throwable t) {
|
||||
rollback(transaction);
|
||||
throw t;
|
||||
} finally {
|
||||
closeQuietly(transaction);
|
||||
writeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void rollback(final Transaction transaction) {
|
||||
if (transaction != null) {
|
||||
transaction.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeQuietly(final Transaction transaction) {
|
||||
if (transaction != null) {
|
||||
try {
|
||||
transaction.close();
|
||||
} catch (final IOException ioe) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setTransactionBuilder(TransactionBuilder transactionBuilder) {
|
||||
this.transactionBuilder = transactionBuilder;
|
||||
}
|
||||
|
||||
public void setProperties(NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* An signing key for a NiFi user.
|
||||
*/
|
||||
public class Key implements Serializable {
|
||||
|
||||
private int id;
|
||||
private String identity;
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* The key id.
|
||||
*
|
||||
* @return the id
|
||||
*/
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The identity of the user this key is associated with.
|
||||
*
|
||||
* @return the identity
|
||||
*/
|
||||
public String getIdentity() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public void setIdentity(String identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* The signing key.
|
||||
*
|
||||
* @return the signing key
|
||||
*/
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Key(int id, String identity, String key) {
|
||||
this.id = id;
|
||||
this.identity = identity;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Key() {
|
||||
}
|
||||
|
||||
}
|
|
@ -18,11 +18,6 @@
|
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
|
||||
|
||||
<!-- initialize the user key data source -->
|
||||
<bean id="keyDataSource" class="org.apache.nifi.admin.KeyDataSourceFactoryBean" destroy-method="shutdown">
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
|
||||
<!-- initialize the audit data source -->
|
||||
<bean id="auditDataSource" class="org.apache.nifi.admin.AuditDataSourceFactoryBean" destroy-method="shutdown">
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
|
@ -33,11 +28,6 @@
|
|||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
|
||||
<!-- initialize the user key transaction builder -->
|
||||
<bean id="keyTransactionBuilder" class="org.apache.nifi.admin.service.transaction.impl.StandardTransactionBuilder">
|
||||
<property name="dataSource" ref="keyDataSource"/>
|
||||
</bean>
|
||||
|
||||
<!-- initialize the audit transaction builder -->
|
||||
<bean id="auditTransactionBuilder" class="org.apache.nifi.admin.service.transaction.impl.StandardTransactionBuilder">
|
||||
<property name="dataSource" ref="auditDataSource"/>
|
||||
|
@ -48,12 +38,6 @@
|
|||
<property name="dataSource" ref="idpDataSource"/>
|
||||
</bean>
|
||||
|
||||
<!-- administration service -->
|
||||
<bean id="keyService" class="org.apache.nifi.admin.service.impl.StandardKeyService">
|
||||
<property name="transactionBuilder" ref="keyTransactionBuilder"/>
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
|
||||
<!-- audit service -->
|
||||
<bean id="auditService" class="org.apache.nifi.admin.service.impl.StandardAuditService">
|
||||
<property name="transactionBuilder" ref="auditTransactionBuilder"/>
|
||||
|
|
|
@ -40,7 +40,8 @@ import org.apache.nifi.reporting.Severity;
|
|||
import org.apache.nifi.util.ComponentIdGenerator;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
|
||||
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.http.SecurityHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -244,13 +245,13 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
|
|||
|
||||
// remove the access token if present, since the user is already authenticated... authorization
|
||||
// will happen when the request is replicated using the proxy chain above
|
||||
headers.remove(NiFiBearerTokenResolver.AUTHORIZATION);
|
||||
headers.remove(SecurityHeader.AUTHORIZATION.getHeader());
|
||||
|
||||
// if knox sso cookie name is set, remove any authentication cookie since this user is already authenticated
|
||||
// and will be included in the proxied entities chain above... authorization will happen when the
|
||||
// request is replicated
|
||||
removeCookie(headers, nifiProperties.getKnoxCookieName());
|
||||
removeCookie(headers, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
|
||||
removeCookie(headers, SecurityCookieName.AUTHORIZATION_BEARER.getName());
|
||||
|
||||
// remove the host header
|
||||
headers.remove("Host");
|
||||
|
|
|
@ -162,6 +162,7 @@
|
|||
<nifi.security.user.authorizer>single-user-authorizer</nifi.security.user.authorizer>
|
||||
<nifi.security.allow.anonymous.authentication>false</nifi.security.allow.anonymous.authentication>
|
||||
<nifi.security.user.login.identity.provider>single-user-provider</nifi.security.user.login.identity.provider>
|
||||
<nifi.security.user.jws.key.rotation.period>PT1H</nifi.security.user.jws.key.rotation.period>
|
||||
<nifi.security.x509.principal.extractor />
|
||||
<nifi.security.ocsp.responder.url />
|
||||
<nifi.security.ocsp.responder.certificate />
|
||||
|
|
|
@ -194,6 +194,7 @@ nifi.security.truststorePasswd=${nifi.security.truststorePasswd}
|
|||
nifi.security.user.authorizer=${nifi.security.user.authorizer}
|
||||
nifi.security.allow.anonymous.authentication=${nifi.security.allow.anonymous.authentication}
|
||||
nifi.security.user.login.identity.provider=${nifi.security.user.login.identity.provider}
|
||||
nifi.security.user.jws.key.rotation.period=${nifi.security.user.jws.key.rotation.period}
|
||||
nifi.security.ocsp.responder.url=${nifi.security.ocsp.responder.url}
|
||||
nifi.security.ocsp.responder.certificate=${nifi.security.ocsp.responder.certificate}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
package org.apache.nifi.web;
|
||||
|
||||
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
@ -25,8 +25,6 @@ import javax.servlet.http.HttpServletRequest;
|
|||
* Request Matcher checks for the existence of a cookie with the configured name
|
||||
*/
|
||||
public class CsrfCookieRequestMatcher implements RequestMatcher {
|
||||
private static final String DEFAULT_CSRF_COOKIE_NAME = NiFiBearerTokenResolver.JWT_COOKIE_NAME;
|
||||
|
||||
/**
|
||||
* Matches request based on the presence of a cookie found using the configured name
|
||||
*
|
||||
|
@ -35,6 +33,6 @@ public class CsrfCookieRequestMatcher implements RequestMatcher {
|
|||
*/
|
||||
@Override
|
||||
public boolean matches(final HttpServletRequest httpServletRequest) {
|
||||
return WebUtils.getCookie(httpServletRequest, DEFAULT_CSRF_COOKIE_NAME) != null;
|
||||
return WebUtils.getCookie(httpServletRequest, SecurityCookieName.AUTHORIZATION_BEARER.getName()) != null;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.apache.nifi.web;
|
||||
|
||||
import org.apache.nifi.web.security.configuration.AuthenticationSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportResource;
|
||||
|
@ -24,13 +25,15 @@ import org.springframework.context.annotation.ImportResource;
|
|||
*
|
||||
*/
|
||||
@Configuration
|
||||
@Import({NiFiWebApiSecurityConfiguration.class})
|
||||
@Import({
|
||||
NiFiWebApiSecurityConfiguration.class,
|
||||
AuthenticationSecurityConfiguration.class
|
||||
})
|
||||
@ImportResource({"classpath:nifi-context.xml",
|
||||
"classpath:nifi-administration-context.xml",
|
||||
"classpath:nifi-authorizer-context.xml",
|
||||
"classpath:nifi-cluster-manager-context.xml",
|
||||
"classpath:nifi-cluster-protocol-context.xml",
|
||||
"classpath:nifi-web-security-context.xml",
|
||||
"classpath:nifi-web-api-context.xml"})
|
||||
public class NiFiWebApiConfiguration {
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@ package org.apache.nifi.web;
|
|||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
|
||||
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.jwt.JwtAuthenticationFilter;
|
||||
import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.http.SecurityHeader;
|
||||
import org.apache.nifi.web.security.jwt.resolver.StandardBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter;
|
||||
import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.oidc.OIDCEndpoints;
|
||||
|
@ -29,7 +29,6 @@ import org.apache.nifi.web.security.saml.SAMLEndpoints;
|
|||
import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
|
||||
import org.apache.nifi.web.security.x509.X509AuthenticationProvider;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509IdentityProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -41,6 +40,9 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
|||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
|
@ -64,10 +66,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
private X509AuthenticationFilter x509AuthenticationFilter;
|
||||
private X509CertificateExtractor certificateExtractor;
|
||||
private X509PrincipalExtractor principalExtractor;
|
||||
private X509IdentityProvider certificateIdentityProvider;
|
||||
private X509AuthenticationProvider x509AuthenticationProvider;
|
||||
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private JwtAuthenticationProvider jwtAuthenticationProvider;
|
||||
|
||||
private KnoxAuthenticationFilter knoxAuthenticationFilter;
|
||||
|
@ -105,7 +104,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
SAMLEndpoints.LOGIN_CONSUMER,
|
||||
SAMLEndpoints.LOGIN_EXCHANGE,
|
||||
// the logout sequence will be protected by a request identifier set in a Cookie so these
|
||||
// paths need to be listed here in order to pass through our normal authn filters
|
||||
// paths need to be listed here in order to pass through our normal authentication filters
|
||||
SAMLEndpoints.SINGLE_LOGOUT_REQUEST,
|
||||
SAMLEndpoints.SINGLE_LOGOUT_CONSUMER,
|
||||
SAMLEndpoints.LOCAL_LOGOUT,
|
||||
|
@ -115,8 +114,8 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
NiFiCsrfTokenRepository csrfRepository = new NiFiCsrfTokenRepository();
|
||||
csrfRepository.setHeaderName(NiFiBearerTokenResolver.AUTHORIZATION);
|
||||
csrfRepository.setCookieName(NiFiBearerTokenResolver.JWT_COOKIE_NAME);
|
||||
csrfRepository.setHeaderName(SecurityHeader.AUTHORIZATION.getHeader());
|
||||
csrfRepository.setCookieName(SecurityCookieName.AUTHORIZATION_BEARER.getName());
|
||||
|
||||
http
|
||||
.cors().and()
|
||||
|
@ -129,7 +128,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
http.addFilterBefore(x509FilterBean(), AnonymousAuthenticationFilter.class);
|
||||
|
||||
// jwt
|
||||
http.addFilterBefore(jwtFilterBean(), AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(bearerTokenAuthenticationFilter(), AnonymousAuthenticationFilter.class);
|
||||
|
||||
// knox
|
||||
http.addFilterBefore(knoxFilterBean(), AnonymousAuthenticationFilter.class);
|
||||
|
@ -167,16 +166,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
.authenticationProvider(anonymousAuthenticationProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationFilter jwtFilterBean() throws Exception {
|
||||
if (jwtAuthenticationFilter == null) {
|
||||
jwtAuthenticationFilter = new JwtAuthenticationFilter();
|
||||
jwtAuthenticationFilter.setProperties(properties);
|
||||
jwtAuthenticationFilter.setAuthenticationManager(authenticationManager());
|
||||
}
|
||||
return jwtAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KnoxAuthenticationFilter knoxFilterBean() throws Exception {
|
||||
if (knoxAuthenticationFilter == null) {
|
||||
|
@ -199,6 +188,18 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
return x509AuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BearerTokenAuthenticationFilter bearerTokenAuthenticationFilter() throws Exception {
|
||||
final BearerTokenAuthenticationFilter filter = new BearerTokenAuthenticationFilter(authenticationManager());
|
||||
filter.setBearerTokenResolver(bearerTokenResolver());
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BearerTokenResolver bearerTokenResolver() {
|
||||
return new StandardBearerTokenResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NiFiAnonymousAuthenticationFilter anonymousFilterBean() throws Exception {
|
||||
if (anonymousAuthenticationFilter == null) {
|
||||
|
@ -243,9 +244,4 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
|||
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
|
||||
this.principalExtractor = principalExtractor;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setCertificateIdentityProvider(X509IdentityProvider certificateIdentityProvider) {
|
||||
this.certificateIdentityProvider = certificateIdentityProvider;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.apache.nifi.web.api;
|
||||
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
|
@ -43,16 +42,14 @@ import org.apache.nifi.web.security.InvalidAuthenticationException;
|
|||
import org.apache.nifi.web.security.LogoutException;
|
||||
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
|
||||
import org.apache.nifi.web.security.UntrustedProxyException;
|
||||
import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.jwt.JwtAuthenticationRequestToken;
|
||||
import org.apache.nifi.web.security.jwt.JwtService;
|
||||
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.jwt.revocation.JwtLogoutListener;
|
||||
import org.apache.nifi.web.security.kerberos.KerberosService;
|
||||
import org.apache.nifi.web.security.knox.KnoxService;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
|
||||
import org.apache.nifi.web.security.x509.X509AuthenticationProvider;
|
||||
import org.apache.nifi.web.security.x509.X509AuthenticationRequestToken;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
|
@ -61,6 +58,9 @@ import org.slf4j.LoggerFactory;
|
|||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
|
@ -103,7 +103,9 @@ public class AccessResource extends ApplicationResource {
|
|||
|
||||
private LoginIdentityProvider loginIdentityProvider;
|
||||
private JwtAuthenticationProvider jwtAuthenticationProvider;
|
||||
private JwtService jwtService;
|
||||
private JwtLogoutListener jwtLogoutListener;
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
private BearerTokenResolver bearerTokenResolver;
|
||||
private KnoxService knoxService;
|
||||
private KerberosService kerberosService;
|
||||
protected LogoutRequestManager logoutRequestManager;
|
||||
|
@ -246,28 +248,29 @@ public class AccessResource extends ApplicationResource {
|
|||
// if there is not certificate, consider a token
|
||||
if (certificates == null) {
|
||||
// look for an authorization token in header or cookie
|
||||
final String authorization = new NiFiBearerTokenResolver().resolve(httpServletRequest);
|
||||
final String bearerToken = bearerTokenResolver.resolve(httpServletRequest);
|
||||
|
||||
// if there is no authorization header, we don't know the user
|
||||
if (authorization == null) {
|
||||
if (bearerToken == null) {
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
|
||||
accessStatus.setMessage("No credentials supplied, unknown user.");
|
||||
} else {
|
||||
try {
|
||||
// authenticate the token
|
||||
final JwtAuthenticationRequestToken jwtRequest = new JwtAuthenticationRequestToken(authorization, httpServletRequest.getRemoteAddr());
|
||||
final NiFiAuthenticationToken authenticationResponse = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(jwtRequest);
|
||||
final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser();
|
||||
final BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(bearerToken);
|
||||
final Authentication authentication = jwtAuthenticationProvider.authenticate(authenticationToken);
|
||||
final NiFiUserDetails userDetails = (NiFiUserDetails) authentication.getPrincipal();
|
||||
final String identity = userDetails.getUsername();
|
||||
|
||||
// set the user identity
|
||||
accessStatus.setIdentity(nifiUser.getIdentity());
|
||||
accessStatus.setIdentity(identity);
|
||||
|
||||
// attempt authorize to /flow
|
||||
accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
|
||||
accessStatus.setMessage("You are already logged in.");
|
||||
} catch (final InvalidAuthenticationException iae) {
|
||||
if (WebUtils.getCookie(httpServletRequest, NiFiBearerTokenResolver.JWT_COOKIE_NAME) != null) {
|
||||
removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
|
||||
} catch (final AuthenticationException iae) {
|
||||
if (WebUtils.getCookie(httpServletRequest, SecurityCookieName.AUTHORIZATION_BEARER.getName()) != null) {
|
||||
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
|
||||
}
|
||||
|
||||
throw iae;
|
||||
|
@ -281,7 +284,7 @@ public class AccessResource extends ApplicationResource {
|
|||
final X509AuthenticationRequestToken x509Request = new X509AuthenticationRequestToken(
|
||||
proxiedEntitiesChain, proxiedEntityGroups, principalExtractor, certificates, httpServletRequest.getRemoteAddr());
|
||||
|
||||
final NiFiAuthenticationToken authenticationResponse = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(x509Request);
|
||||
final Authentication authenticationResponse = x509AuthenticationProvider.authenticate(x509Request);
|
||||
final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser();
|
||||
|
||||
// set the user identity
|
||||
|
@ -351,8 +354,7 @@ public class AccessResource extends ApplicationResource {
|
|||
String authorizationHeaderValue = httpServletRequest.getHeader(KerberosService.AUTHORIZATION_HEADER_NAME);
|
||||
|
||||
if (!kerberosService.isValidKerberosHeader(authorizationHeaderValue)) {
|
||||
final Response response = generateNotAuthorizedResponse().header(KerberosService.AUTHENTICATION_CHALLENGE_HEADER_NAME, KerberosService.AUTHORIZATION_NEGOTIATE).build();
|
||||
return response;
|
||||
return generateNotAuthorizedResponse().header(KerberosService.AUTHENTICATION_CHALLENGE_HEADER_NAME, KerberosService.AUTHORIZATION_NEGOTIATE).build();
|
||||
} else {
|
||||
try {
|
||||
// attempt to authenticate
|
||||
|
@ -363,18 +365,13 @@ public class AccessResource extends ApplicationResource {
|
|||
}
|
||||
|
||||
final String expirationFromProperties = properties.getKerberosAuthenticationExpiration();
|
||||
long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS);
|
||||
long expiration = Math.round(FormatUtils.getPreciseTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS));
|
||||
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(mappedIdentity, expiration, "KerberosService");
|
||||
|
||||
// generate JWT for response
|
||||
final String token = jwtService.generateSignedToken(loginAuthenticationToken);
|
||||
|
||||
// build the response
|
||||
final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken);
|
||||
final URI uri = URI.create(generateResourceUri("access", "kerberos"));
|
||||
return generateTokenResponse(generateCreatedResponse(uri, token), token);
|
||||
} catch (final AuthenticationException e) {
|
||||
|
@ -447,10 +444,7 @@ public class AccessResource extends ApplicationResource {
|
|||
throw new AdministrationException(iae.getMessage(), iae);
|
||||
}
|
||||
|
||||
// generate JWT for response
|
||||
final String token = jwtService.generateSignedToken(loginAuthenticationToken);
|
||||
|
||||
// build the response
|
||||
final String token = bearerTokenProvider.getBearerToken(loginAuthenticationToken);
|
||||
final URI uri = URI.create(generateResourceUri("access", "token"));
|
||||
return generateTokenResponse(generateCreatedResponse(uri, token), token);
|
||||
}
|
||||
|
@ -482,10 +476,12 @@ public class AccessResource extends ApplicationResource {
|
|||
}
|
||||
|
||||
try {
|
||||
logger.info("Logging out " + mappedUserIdentity);
|
||||
logOutUser(httpServletRequest);
|
||||
removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
|
||||
logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
|
||||
logger.info("Logout Started [{}]", mappedUserIdentity);
|
||||
logger.debug("Removing Authorization Cookie [{}]", mappedUserIdentity);
|
||||
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
|
||||
|
||||
final String bearerToken = bearerTokenResolver.resolve(httpServletRequest);
|
||||
jwtLogoutListener.logout(bearerToken);
|
||||
|
||||
// create a LogoutRequest and tell the LogoutRequestManager about it for later retrieval
|
||||
final LogoutRequest logoutRequest = new LogoutRequest(UUID.randomUUID().toString(), mappedUserIdentity);
|
||||
|
@ -500,11 +496,8 @@ public class AccessResource extends ApplicationResource {
|
|||
httpServletResponse.addCookie(cookie);
|
||||
|
||||
return generateOkResponse().build();
|
||||
} catch (final JwtException e) {
|
||||
logger.error("JWT processing failed for [{}], due to: ", mappedUserIdentity, e.getMessage(), e);
|
||||
return Response.serverError().build();
|
||||
} catch (final LogoutException e) {
|
||||
logger.error("Logout failed for user [{}] due to: ", mappedUserIdentity, e.getMessage(), e);
|
||||
logger.error("Logout Failed Identity [{}]", mappedUserIdentity, e);
|
||||
return Response.serverError().build();
|
||||
}
|
||||
}
|
||||
|
@ -547,9 +540,9 @@ public class AccessResource extends ApplicationResource {
|
|||
}
|
||||
|
||||
if (logoutRequest == null) {
|
||||
logger.warn("Logout request did not exist for identifier: " + logoutRequestIdentifier);
|
||||
logger.warn("Logout Request [{}] not found", logoutRequestIdentifier);
|
||||
} else {
|
||||
logger.info("Completed logout request for " + logoutRequest.getMappedUserIdentity());
|
||||
logger.info("Logout Request [{}] Completed [{}]", logoutRequestIdentifier, logoutRequest.getMappedUserIdentity());
|
||||
}
|
||||
|
||||
// remove the cookie if it existed
|
||||
|
@ -588,14 +581,22 @@ public class AccessResource extends ApplicationResource {
|
|||
this.loginIdentityProvider = loginIdentityProvider;
|
||||
}
|
||||
|
||||
public void setJwtService(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) {
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
}
|
||||
|
||||
public void setBearerTokenResolver(final BearerTokenResolver bearerTokenResolver) {
|
||||
this.bearerTokenResolver = bearerTokenResolver;
|
||||
}
|
||||
|
||||
public void setJwtAuthenticationProvider(JwtAuthenticationProvider jwtAuthenticationProvider) {
|
||||
this.jwtAuthenticationProvider = jwtAuthenticationProvider;
|
||||
}
|
||||
|
||||
public void setJwtLogoutListener(final JwtLogoutListener jwtLogoutListener) {
|
||||
this.jwtLogoutListener = jwtLogoutListener;
|
||||
}
|
||||
|
||||
public void setKerberosService(KerberosService kerberosService) {
|
||||
this.kerberosService = kerberosService;
|
||||
}
|
||||
|
@ -616,11 +617,6 @@ public class AccessResource extends ApplicationResource {
|
|||
this.knoxService = knoxService;
|
||||
}
|
||||
|
||||
private void logOutUser(HttpServletRequest httpServletRequest) {
|
||||
final String jwt = new NiFiBearerTokenResolver().resolve(httpServletRequest);
|
||||
jwtService.logOut(jwt);
|
||||
}
|
||||
|
||||
public void setLogoutRequestManager(LogoutRequestManager logoutRequestManager) {
|
||||
this.logoutRequestManager = logoutRequestManager;
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ import org.apache.nifi.web.api.entity.ComponentEntity;
|
|||
import org.apache.nifi.web.api.entity.Entity;
|
||||
import org.apache.nifi.web.api.entity.TransactionResultEntity;
|
||||
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
|
||||
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.util.CacheKey;
|
||||
import org.apache.nifi.web.util.WebUtils;
|
||||
import org.eclipse.jetty.http.HttpCookie;
|
||||
|
@ -1285,7 +1285,7 @@ public abstract class ApplicationResource {
|
|||
|
||||
protected Response generateTokenResponse(ResponseBuilder builder, String token) {
|
||||
// currently there is no way to use javax.servlet-api to set SameSite=Strict, so we do this using Jetty
|
||||
HttpCookie jwtCookie = new HttpCookie(NiFiBearerTokenResolver.JWT_COOKIE_NAME, token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
|
||||
HttpCookie jwtCookie = new HttpCookie(SecurityCookieName.AUTHORIZATION_BEARER.getName(), token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
|
||||
return builder.header(HttpHeader.SET_COOKIE.asString(), jwtCookie.getRFC6265SetCookie()).build();
|
||||
}
|
||||
|
||||
|
|
|
@ -39,8 +39,8 @@ import org.apache.http.message.BasicNameValuePair;
|
|||
import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException;
|
||||
import org.apache.nifi.authorization.user.NiFiUserUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.jwt.JwtService;
|
||||
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.oidc.OIDCEndpoints;
|
||||
import org.apache.nifi.web.security.oidc.OidcService;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
|
@ -90,8 +90,8 @@ public class OIDCAccessResource extends AccessResource {
|
|||
private static final boolean LOGGING_IN = true;
|
||||
|
||||
private OidcService oidcService;
|
||||
private JwtService jwtService;
|
||||
private CloseableHttpClient httpClient;
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
private final CloseableHttpClient httpClient;
|
||||
|
||||
public OIDCAccessResource() {
|
||||
RequestConfig config = RequestConfig.custom()
|
||||
|
@ -161,10 +161,10 @@ public class OIDCAccessResource extends AccessResource {
|
|||
LoginAuthenticationToken oidcToken = oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(authorizationGrant);
|
||||
|
||||
// exchange the oidc token for the NiFi token
|
||||
String nifiJwt = jwtService.generateSignedToken(oidcToken);
|
||||
final String bearerToken = bearerTokenProvider.getBearerToken(oidcToken);
|
||||
|
||||
// store the NiFi token
|
||||
oidcService.storeJwt(oidcRequestIdentifier, nifiJwt);
|
||||
oidcService.storeJwt(oidcRequestIdentifier, bearerToken);
|
||||
} catch (final Exception e) {
|
||||
logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
|
||||
|
||||
|
@ -247,7 +247,7 @@ public class OIDCAccessResource extends AccessResource {
|
|||
}
|
||||
|
||||
final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
|
||||
removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
|
||||
removeCookie(httpServletResponse, SecurityCookieName.AUTHORIZATION_BEARER.getName());
|
||||
logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
|
||||
|
||||
// Get the oidc discovery url
|
||||
|
@ -559,8 +559,8 @@ public class OIDCAccessResource extends AccessResource {
|
|||
this.oidcService = oidcService;
|
||||
}
|
||||
|
||||
public void setJwtService(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) {
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
}
|
||||
|
||||
public void setProperties(final NiFiProperties properties) {
|
||||
|
|
|
@ -582,7 +582,9 @@
|
|||
<property name="certificateExtractor" ref="certificateExtractor"/>
|
||||
<property name="principalExtractor" ref="principalExtractor"/>
|
||||
<property name="jwtAuthenticationProvider" ref="jwtAuthenticationProvider"/>
|
||||
<property name="jwtService" ref="jwtService"/>
|
||||
<property name="jwtLogoutListener" ref="jwtLogoutListener"/>
|
||||
<property name="bearerTokenProvider" ref="bearerTokenProvider"/>
|
||||
<property name="bearerTokenResolver" ref="bearerTokenResolver"/>
|
||||
<property name="kerberosService" ref="kerberosService"/>
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
<property name="clusterCoordinator" ref="clusterCoordinator"/>
|
||||
|
@ -598,9 +600,9 @@
|
|||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
<bean id="oidcResource" class="org.apache.nifi.web.api.OIDCAccessResource" scope="singleton">
|
||||
<property name="jwtService" ref="jwtService"/>
|
||||
<property name="oidcService" ref="oidcService"/>
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
<property name="bearerTokenProvider" ref="bearerTokenProvider"/>
|
||||
</bean>
|
||||
<bean id="accessPolicyResource" class="org.apache.nifi.web.api.AccessPolicyResource" scope="singleton">
|
||||
<constructor-arg ref="serviceFacade"/>
|
||||
|
|
|
@ -1,422 +0,0 @@
|
|||
/*
|
||||
* 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.integration.accesscontrol;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import javax.ws.rs.core.Response;
|
||||
import net.minidev.json.JSONObject;
|
||||
import org.apache.nifi.integration.util.SourceTestProcessor;
|
||||
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
|
||||
import org.apache.nifi.web.api.dto.AccessStatusDTO;
|
||||
import org.apache.nifi.web.api.dto.ProcessorDTO;
|
||||
import org.apache.nifi.web.api.dto.RevisionDTO;
|
||||
import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
|
||||
import org.apache.nifi.web.api.entity.AccessStatusEntity;
|
||||
import org.apache.nifi.web.api.entity.ProcessorEntity;
|
||||
import org.apache.nifi.web.security.jwt.JwtServiceTest;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Access token endpoint test.
|
||||
*/
|
||||
public class ITAccessTokenEndpoint {
|
||||
|
||||
private static OneWaySslAccessControlHelper helper;
|
||||
|
||||
private final String user = "nifiadmin@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("src/test/resources/access-control/nifi-mapped-identities.properties");
|
||||
}
|
||||
|
||||
// -----------
|
||||
// LOGIN CONFIG
|
||||
// -----------
|
||||
/**
|
||||
* Test getting access configuration.
|
||||
*
|
||||
* @throws Exception ex
|
||||
*/
|
||||
@Test
|
||||
public void testGetAccessConfig() throws Exception {
|
||||
String url = helper.getBaseUrl() + "/access/config";
|
||||
|
||||
Response response = helper.getUser().testGet(url);
|
||||
|
||||
// ensure the request is successful
|
||||
Assert.assertEquals(200, response.getStatus());
|
||||
|
||||
// extract the process group
|
||||
AccessConfigurationEntity accessConfigEntity = response.readEntity(AccessConfigurationEntity.class);
|
||||
|
||||
// ensure there is content
|
||||
Assert.assertNotNull(accessConfigEntity);
|
||||
|
||||
// extract the process group dto
|
||||
AccessConfigurationDTO accessConfig = accessConfigEntity.getConfig();
|
||||
|
||||
// verify config
|
||||
Assert.assertTrue(accessConfig.getSupportsLogin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a token and creates a processor using it.
|
||||
*
|
||||
* @throws Exception ex
|
||||
*/
|
||||
@Test
|
||||
public void testCreateProcessorUsingToken() throws Exception {
|
||||
String url = helper.getBaseUrl() + "/access/token";
|
||||
|
||||
Response response = helper.getUser().testCreateToken(url, user, password);
|
||||
|
||||
// ensure the request is successful
|
||||
Assert.assertEquals(201, response.getStatus());
|
||||
|
||||
// get the token
|
||||
String token = response.readEntity(String.class);
|
||||
|
||||
// attempt to create a processor with it
|
||||
createProcessor(token);
|
||||
}
|
||||
|
||||
private ProcessorDTO createProcessor(final String token) throws Exception {
|
||||
String url = helper.getBaseUrl() + "/process-groups/root/processors";
|
||||
|
||||
// authorization header
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Authorization", "Bearer " + token);
|
||||
|
||||
// create the processor
|
||||
ProcessorDTO processor = new ProcessorDTO();
|
||||
processor.setName("Copy");
|
||||
processor.setType(SourceTestProcessor.class.getName());
|
||||
|
||||
// create the revision
|
||||
final RevisionDTO revision = new RevisionDTO();
|
||||
revision.setClientId(CLIENT_ID);
|
||||
revision.setVersion(0l);
|
||||
|
||||
// create the entity body
|
||||
ProcessorEntity entity = new ProcessorEntity();
|
||||
entity.setRevision(revision);
|
||||
entity.setComponent(processor);
|
||||
|
||||
// perform the request
|
||||
Response response = helper.getUser().testPostWithHeaders(url, entity, headers);
|
||||
|
||||
// ensure the request is successful
|
||||
Assert.assertEquals(201, response.getStatus());
|
||||
|
||||
// get the entity body
|
||||
entity = response.readEntity(ProcessorEntity.class);
|
||||
|
||||
// verify creation
|
||||
processor = entity.getComponent();
|
||||
Assert.assertEquals("Copy", processor.getName());
|
||||
Assert.assertEquals("org.apache.nifi.integration.util.SourceTestProcessor", processor.getType());
|
||||
|
||||
return processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the response when bad credentials are specified.
|
||||
*
|
||||
* @throws Exception ex
|
||||
*/
|
||||
@Test
|
||||
public void testInvalidCredentials() throws Exception {
|
||||
String url = helper.getBaseUrl() + "/access/token";
|
||||
|
||||
Response response = helper.getUser().testCreateToken(url, "user@nifi", "not a real password");
|
||||
|
||||
// ensure the request is not successful
|
||||
Assert.assertEquals(400, response.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the response when the user is known.
|
||||
*
|
||||
* @throws Exception ex
|
||||
*/
|
||||
@Test
|
||||
public void testUnknownUser() throws Exception {
|
||||
String url = helper.getBaseUrl() + "/access/token";
|
||||
|
||||
Response response = helper.getUser().testCreateToken(url, "not a real user", "not a real password");
|
||||
|
||||
// ensure the request is successful
|
||||
Assert.assertEquals(400, response.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Request access using access token.
|
||||
*
|
||||
* @throws Exception ex
|
||||
*/
|
||||
@Test
|
||||
public void testRequestAccessUsingToken() throws Exception {
|
||||
String accessStatusUrl = helper.getBaseUrl() + "/access";
|
||||
String accessTokenUrl = helper.getBaseUrl() + "/access/token";
|
||||
|
||||
Response response = helper.getUser().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 = helper.getUser().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 = helper.getUser().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());
|
||||
}
|
||||
|
||||
// // TODO: Revisit the HTTP status codes in this test after logout functionality change
|
||||
// @Ignore("This test is failing before refactoring")
|
||||
@Test
|
||||
public void testLogOutSuccess() throws Exception {
|
||||
String accessStatusUrl = helper.getBaseUrl() + "/access";
|
||||
String accessTokenUrl = helper.getBaseUrl() + "/access/token";
|
||||
String logoutUrl = helper.getBaseUrl() + "/access/logout";
|
||||
|
||||
Response response = helper.getUser().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 = helper.getUser().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 = helper.getUser().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 = helper.getUser().testDeleteWithHeaders(logoutUrl, headers);
|
||||
Assert.assertEquals(200, response.getStatus());
|
||||
|
||||
// ensure we can no longer use our token
|
||||
response = helper.getUser().testGetWithHeaders(accessStatusUrl, null, headers);
|
||||
Assert.assertEquals(401, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogOutNoTokenHeader() throws Exception {
|
||||
String accessStatusUrl = helper.getBaseUrl() + "/access";
|
||||
String accessTokenUrl = helper.getBaseUrl() + "/access/token";
|
||||
String logoutUrl = helper.getBaseUrl() + "/access/logout";
|
||||
|
||||
Response response = helper.getUser().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 = helper.getUser().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 = helper.getUser().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 = helper.getUser().testDeleteWithHeaders(logoutUrl, null);
|
||||
Assert.assertEquals(401, 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 = helper.getBaseUrl() + "/access";
|
||||
String accessTokenUrl = helper.getBaseUrl() + "/access/token";
|
||||
String logoutUrl = helper.getBaseUrl() + "/access/logout";
|
||||
|
||||
Response response = helper.getUser().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 = helper.getUser().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 = helper.getUser().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 = helper.getBaseUrl() + "/access/token";
|
||||
String logoutUrl = helper.getBaseUrl() + "/access/logout";
|
||||
|
||||
Response response = helper.getUser().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 = helper.getUser().testGetWithHeaders(logoutUrl, null, badHeaders);
|
||||
Assert.assertEquals(401, response.getStatus());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void cleanup() throws Exception {
|
||||
helper.cleanup();
|
||||
}
|
||||
}
|
|
@ -165,10 +165,6 @@
|
|||
<groupId>org.apache.nifi</groupId>
|
||||
<artifactId>nifi-framework-authorization</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
|
@ -214,6 +210,18 @@
|
|||
<groupId>org.springframework.security.kerberos</groupId>
|
||||
<artifactId>spring-security-kerberos-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-resource-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
|
@ -234,6 +242,11 @@
|
|||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-json-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jettison</groupId>
|
||||
<artifactId>jettison</artifactId>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.apache.nifi.web.security;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.authorization.user.NiFiUserUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -51,19 +50,16 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
|
|||
@Override
|
||||
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
|
||||
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Checking secure context token: " + authentication);
|
||||
}
|
||||
log.debug("Authenticating [{}]", authentication);
|
||||
|
||||
if (requiresAuthentication((HttpServletRequest) request)) {
|
||||
if (requiresAuthentication()) {
|
||||
authenticate((HttpServletRequest) request, (HttpServletResponse) response, chain);
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean requiresAuthentication(final HttpServletRequest request) {
|
||||
private boolean requiresAuthentication() {
|
||||
return NiFiUserUtils.getNiFiUser() == null;
|
||||
}
|
||||
|
||||
|
@ -71,9 +67,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
|
|||
try {
|
||||
final Authentication authenticationRequest = attemptAuthentication(request);
|
||||
if (authenticationRequest != null) {
|
||||
// log the request attempt - response details will be logged later
|
||||
log.info(String.format("Attempting request for (%s) %s %s (source ip: %s)", authenticationRequest.toString(), request.getMethod(),
|
||||
request.getRequestURL().toString(), request.getRemoteAddr()));
|
||||
log.info("Authentication Started {} [{}] {} {}", request.getRemoteAddr(), authenticationRequest, request.getMethod(), request.getRequestURL());
|
||||
|
||||
// attempt to authenticate the user
|
||||
final Authentication authenticated = authenticationManager.authenticate(authenticationRequest);
|
||||
|
@ -84,7 +78,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
|
|||
unsuccessfulAuthentication(request, response, ae);
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
log.error(String.format("Unable to authenticate: %s", e.getMessage()), e);
|
||||
log.error("Authentication Failed [{}]", e.getMessage(), e);
|
||||
|
||||
// set the response status
|
||||
response.setContentType("text/plain");
|
||||
|
@ -92,7 +86,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
|
|||
|
||||
// other exception - always error out
|
||||
PrintWriter out = response.getWriter();
|
||||
out.println(String.format("Failed to authenticate request. Please contact the system administrator."));
|
||||
out.println("Failed to authenticate request. Please contact the system administrator.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -117,7 +111,7 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
|
|||
* @param authResult The Authentication 'token'/object created by one of the various NiFiAuthenticationFilter subclasses.
|
||||
*/
|
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
|
||||
log.info("Authentication success for " + authResult);
|
||||
log.info("Authentication Success [{}] {} {} {}", authResult, request.getRemoteAddr(), request.getMethod(), request.getRequestURL());
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authResult);
|
||||
ProxiedEntitiesUtils.successfulAuthentication(request, response);
|
||||
|
@ -149,26 +143,20 @@ public abstract class NiFiAuthenticationFilter extends GenericFilterBean {
|
|||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
out.println(ae.getMessage());
|
||||
} else if (ae instanceof AuthenticationServiceException) {
|
||||
log.error(String.format("Unable to authenticate: %s", ae.getMessage()), ae);
|
||||
log.error("Authentication Service Failed: {}", ae.getMessage(), ae);
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
out.println(String.format("Unable to authenticate: %s", ae.getMessage()));
|
||||
} else {
|
||||
log.error(String.format("Unable to authenticate: %s", ae.getMessage()), ae);
|
||||
log.error("Authentication Exception: {}", ae.getMessage(), ae);
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
out.println("Access is denied.");
|
||||
}
|
||||
|
||||
// log the failure
|
||||
log.warn(String.format("Rejecting access to web api: %s", ae.getMessage()));
|
||||
log.warn("Authentication Failed {} {} {} [{}]", request.getRemoteAddr(), request.getMethod(), request.getRequestURL(), ae.getMessage());
|
||||
|
||||
// optionally log the stack trace
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(StringUtils.EMPTY, ae);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
log.debug("Authentication Failed", ae);
|
||||
}
|
||||
|
||||
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.nar.ExtensionManager;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Spring Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
@Import({
|
||||
JwtAuthenticationSecurityConfiguration.class,
|
||||
KerberosAuthenticationSecurityConfiguration.class,
|
||||
KnoxAuthenticationSecurityConfiguration.class,
|
||||
OidcAuthenticationSecurityConfiguration.class,
|
||||
SamlAuthenticationSecurityConfiguration.class,
|
||||
X509AuthenticationSecurityConfiguration.class
|
||||
})
|
||||
public class AuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
private final ExtensionManager extensionManager;
|
||||
|
||||
private final Authorizer authorizer;
|
||||
|
||||
@Autowired
|
||||
public AuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties,
|
||||
final ExtensionManager extensionManager,
|
||||
final Authorizer authorizer
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
this.extensionManager = extensionManager;
|
||||
this.authorizer = authorizer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LoginIdentityProviderFactoryBean loginIdentityProviderFactoryBean() {
|
||||
final LoginIdentityProviderFactoryBean loginIdentityProviderFactoryBean = new LoginIdentityProviderFactoryBean();
|
||||
loginIdentityProviderFactoryBean.setProperties(niFiProperties);
|
||||
loginIdentityProviderFactoryBean.setExtensionManager(extensionManager);
|
||||
return loginIdentityProviderFactoryBean;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Object loginIdentityProvider() throws Exception {
|
||||
return loginIdentityProviderFactoryBean().getObject();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LogoutRequestManager logoutRequestManager() {
|
||||
return new LogoutRequestManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NiFiAnonymousAuthenticationProvider anonymousAuthenticationProvider() {
|
||||
return new NiFiAnonymousAuthenticationProvider(niFiProperties, authorizer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import com.nimbusds.jose.proc.JWSKeySelector;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
|
||||
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
|
||||
import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
|
||||
import com.nimbusds.jwt.proc.JWTProcessor;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateManagerProvider;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.jwt.converter.StandardJwtAuthenticationConverter;
|
||||
import org.apache.nifi.web.security.jwt.jws.StandardJWSKeySelector;
|
||||
import org.apache.nifi.web.security.jwt.jws.StandardJwsSignerProvider;
|
||||
import org.apache.nifi.web.security.jwt.key.command.KeyExpirationCommand;
|
||||
import org.apache.nifi.web.security.jwt.key.command.KeyGenerationCommand;
|
||||
import org.apache.nifi.web.security.jwt.key.StandardVerificationKeySelector;
|
||||
import org.apache.nifi.web.security.jwt.key.service.StandardVerificationKeyService;
|
||||
import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.jwt.provider.StandardBearerTokenProvider;
|
||||
import org.apache.nifi.web.security.jwt.provider.SupportedClaim;
|
||||
import org.apache.nifi.web.security.jwt.revocation.JwtLogoutListener;
|
||||
import org.apache.nifi.web.security.jwt.revocation.JwtRevocationService;
|
||||
import org.apache.nifi.web.security.jwt.revocation.JwtRevocationValidator;
|
||||
import org.apache.nifi.web.security.jwt.revocation.StandardJwtLogoutListener;
|
||||
import org.apache.nifi.web.security.jwt.revocation.StandardJwtRevocationService;
|
||||
import org.apache.nifi.web.security.jwt.revocation.command.RevocationExpirationCommand;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* JSON Web Token Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class JwtAuthenticationSecurityConfiguration {
|
||||
private static final Set<String> REQUIRED_CLAIMS = new HashSet<>(Arrays.asList(
|
||||
SupportedClaim.ISSUER.getClaim(),
|
||||
SupportedClaim.SUBJECT.getClaim(),
|
||||
SupportedClaim.AUDIENCE.getClaim(),
|
||||
SupportedClaim.EXPIRATION.getClaim(),
|
||||
SupportedClaim.NOT_BEFORE.getClaim(),
|
||||
SupportedClaim.ISSUED_AT.getClaim(),
|
||||
SupportedClaim.JWT_ID.getClaim()
|
||||
));
|
||||
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
private final Authorizer authorizer;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private final StateManagerProvider stateManagerProvider;
|
||||
|
||||
private final Duration keyRotationPeriod;
|
||||
|
||||
@Autowired
|
||||
public JwtAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties,
|
||||
final Authorizer authorizer,
|
||||
final IdpUserGroupService idpUserGroupService,
|
||||
final StateManagerProvider stateManagerProvider
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
this.authorizer = authorizer;
|
||||
this.idpUserGroupService = idpUserGroupService;
|
||||
this.stateManagerProvider = stateManagerProvider;
|
||||
this.keyRotationPeriod = niFiProperties.getSecurityUserJwsKeyRotationPeriod();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationProvider jwtAuthenticationProvider() {
|
||||
final JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder());
|
||||
jwtAuthenticationProvider.setJwtAuthenticationConverter(jwtAuthenticationConverter());
|
||||
return jwtAuthenticationProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
final NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(jwtProcessor());
|
||||
final OAuth2TokenValidator<Jwt> jwtValidator = new DelegatingOAuth2TokenValidator<>(
|
||||
JwtValidators.createDefault(),
|
||||
jwtRevocationValidator()
|
||||
);
|
||||
jwtDecoder.setJwtValidator(jwtValidator);
|
||||
return jwtDecoder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OAuth2TokenValidator<Jwt> jwtRevocationValidator() {
|
||||
return new JwtRevocationValidator(jwtRevocationService());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtRevocationService jwtRevocationService() {
|
||||
final StateManager stateManager = stateManagerProvider.getStateManager(StandardJwtRevocationService.class.getName());
|
||||
return new StandardJwtRevocationService(stateManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtLogoutListener jwtLogoutListener() {
|
||||
return new StandardJwtLogoutListener(jwtDecoder(), jwtRevocationService());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWTProcessor<SecurityContext> jwtProcessor() {
|
||||
final DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
|
||||
jwtProcessor.setJWSKeySelector(jwsKeySelector());
|
||||
jwtProcessor.setJWTClaimsSetVerifier(claimsSetVerifier());
|
||||
return jwtProcessor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWSKeySelector<SecurityContext> jwsKeySelector() {
|
||||
return new StandardJWSKeySelector<>(verificationKeySelector());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWTClaimsSetVerifier<SecurityContext> claimsSetVerifier() {
|
||||
return new DefaultJWTClaimsVerifier<>(null, REQUIRED_CLAIMS);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardJwtAuthenticationConverter jwtAuthenticationConverter() {
|
||||
return new StandardJwtAuthenticationConverter(authorizer, idpUserGroupService, niFiProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BearerTokenProvider bearerTokenProvider() {
|
||||
return new StandardBearerTokenProvider(jwsSignerProvider());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardJwsSignerProvider jwsSignerProvider() {
|
||||
return new StandardJwsSignerProvider(verificationKeySelector());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardVerificationKeySelector verificationKeySelector() {
|
||||
return new StandardVerificationKeySelector(verificationKeyService(), keyRotationPeriod);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public VerificationKeyService verificationKeyService() {
|
||||
final StateManager stateManager = stateManagerProvider.getStateManager(StandardVerificationKeyService.class.getName());
|
||||
return new StandardVerificationKeyService(stateManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KeyGenerationCommand keyGenerationCommand() {
|
||||
final KeyGenerationCommand command = new KeyGenerationCommand(jwsSignerProvider(), verificationKeySelector());
|
||||
commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod);
|
||||
return command;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KeyExpirationCommand keyExpirationCommand() {
|
||||
final KeyExpirationCommand command = new KeyExpirationCommand(verificationKeyService());
|
||||
commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod);
|
||||
return command;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RevocationExpirationCommand revocationExpirationCommand() {
|
||||
final RevocationExpirationCommand command = new RevocationExpirationCommand(jwtRevocationService());
|
||||
commandScheduler().scheduleAtFixedRate(command, keyRotationPeriod);
|
||||
return command;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler commandScheduler() {
|
||||
final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setThreadNamePrefix(getClass().getSimpleName());
|
||||
return scheduler;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.kerberos.KerberosService;
|
||||
import org.apache.nifi.web.security.spring.KerberosServiceFactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Kerberos Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class KerberosAuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
@Autowired
|
||||
public KerberosAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KerberosService kerberosService() throws Exception {
|
||||
final KerberosServiceFactoryBean kerberosServiceFactoryBean = new KerberosServiceFactoryBean();
|
||||
kerberosServiceFactoryBean.setProperties(niFiProperties);
|
||||
return kerberosServiceFactoryBean.getObject();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.knox.KnoxService;
|
||||
import org.apache.nifi.web.security.knox.KnoxServiceFactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knox Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class KnoxAuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
private final Authorizer authorizer;
|
||||
|
||||
@Autowired
|
||||
public KnoxAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties,
|
||||
final Authorizer authorizer
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
this.authorizer = authorizer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KnoxAuthenticationProvider knoxAuthenticationProvider() {
|
||||
return new KnoxAuthenticationProvider(knoxService(), niFiProperties, authorizer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KnoxService knoxService() {
|
||||
final KnoxServiceFactoryBean knoxServiceFactoryBean = new KnoxServiceFactoryBean();
|
||||
knoxServiceFactoryBean.setProperties(niFiProperties);
|
||||
return knoxServiceFactoryBean.getObject();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.oidc.OidcService;
|
||||
import org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* OIDC Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class OidcAuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
@Autowired
|
||||
public OidcAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardOidcIdentityProvider oidcProvider() {
|
||||
return new StandardOidcIdentityProvider(niFiProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OidcService oidcService() {
|
||||
return new OidcService(oidcProvider());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpCredentialService;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.saml.SAMLService;
|
||||
import org.apache.nifi.web.security.saml.impl.StandardSAMLConfigurationFactory;
|
||||
import org.apache.nifi.web.security.saml.impl.StandardSAMLCredentialStore;
|
||||
import org.apache.nifi.web.security.saml.impl.StandardSAMLService;
|
||||
import org.apache.nifi.web.security.saml.impl.StandardSAMLStateManager;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* SAML Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class SamlAuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
private final BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private final IdpCredentialService idpCredentialService;
|
||||
|
||||
@Autowired
|
||||
public SamlAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties,
|
||||
final BearerTokenProvider bearerTokenProvider,
|
||||
final IdpCredentialService idpCredentialService
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
this.idpCredentialService = idpCredentialService;
|
||||
}
|
||||
|
||||
@Bean(initMethod = "initialize", destroyMethod = "shutdown")
|
||||
public SAMLService samlService() {
|
||||
return new StandardSAMLService(samlConfigurationFactory(), niFiProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardSAMLStateManager samlStateManager() {
|
||||
return new StandardSAMLStateManager(bearerTokenProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardSAMLCredentialStore samlCredentialStore() {
|
||||
return new StandardSAMLCredentialStore(idpCredentialService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public StandardSAMLConfigurationFactory samlConfigurationFactory() {
|
||||
return new StandardSAMLConfigurationFactory();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509AuthenticationProvider;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
|
||||
import org.apache.nifi.web.security.x509.X509CertificateValidator;
|
||||
import org.apache.nifi.web.security.x509.X509IdentityProvider;
|
||||
import org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
|
||||
/**
|
||||
* X.509 Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class X509AuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
|
||||
private final Authorizer authorizer;
|
||||
|
||||
@Autowired
|
||||
public X509AuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties,
|
||||
final Authorizer authorizer
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
this.authorizer = authorizer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public X509AuthenticationProvider x509AuthenticationProvider() {
|
||||
return new X509AuthenticationProvider(certificateIdentityProvider(), authorizer, niFiProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public X509CertificateExtractor certificateExtractor() {
|
||||
return new X509CertificateExtractor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public X509PrincipalExtractor principalExtractor() {
|
||||
return new SubjectDnX509PrincipalExtractor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OcspCertificateValidator ocspValidator() {
|
||||
return new OcspCertificateValidator(niFiProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public X509CertificateValidator certificateValidator() {
|
||||
final X509CertificateValidator certificateValidator = new X509CertificateValidator();
|
||||
certificateValidator.setOcspValidator(ocspValidator());
|
||||
return certificateValidator;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public X509IdentityProvider certificateIdentityProvider() {
|
||||
final X509IdentityProvider identityProvider = new X509IdentityProvider();
|
||||
identityProvider.setCertificateValidator(certificateValidator());
|
||||
identityProvider.setPrincipalExtractor(principalExtractor());
|
||||
return identityProvider;
|
||||
}
|
||||
}
|
|
@ -14,27 +14,21 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.admin.service.action;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
import org.apache.nifi.key.Key;
|
||||
package org.apache.nifi.web.security.http;
|
||||
|
||||
/**
|
||||
* Gets a key for the specified key id.
|
||||
* Enumeration of HTTP Cookie Names for Security
|
||||
*/
|
||||
public class GetKeyByIdAction implements AdministrationAction<Key> {
|
||||
public enum SecurityCookieName {
|
||||
AUTHORIZATION_BEARER("__Host-Authorization-Bearer");
|
||||
|
||||
private final int id;
|
||||
private String name;
|
||||
|
||||
public GetKeyByIdAction(int id) {
|
||||
this.id = id;
|
||||
SecurityCookieName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key execute(DAOFactory daoFactory) {
|
||||
final KeyDAO keyDao = daoFactory.getKeyDAO();
|
||||
return keyDao.findKeyById(id);
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
|
@ -14,27 +14,21 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.admin.service.action;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
import org.apache.nifi.key.Key;
|
||||
package org.apache.nifi.web.security.http;
|
||||
|
||||
/**
|
||||
* Gets a key for the specified key id.
|
||||
* Enumeration of HTTP Headers for Security
|
||||
*/
|
||||
public class GetKeyByIdentityAction implements AdministrationAction<Key> {
|
||||
public enum SecurityHeader {
|
||||
AUTHORIZATION("Authorization");
|
||||
|
||||
private final String identity;
|
||||
private String header;
|
||||
|
||||
public GetKeyByIdentityAction(String identity) {
|
||||
this.identity = identity;
|
||||
SecurityHeader(final String header) {
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key execute(DAOFactory daoFactory) {
|
||||
final KeyDAO keyDao = daoFactory.getKeyDAO();
|
||||
return keyDao.findLatestKeyByIdentity(identity);
|
||||
public String getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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.util.StringUtils;
|
||||
import org.apache.nifi.web.security.NiFiAuthenticationFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class JwtAuthenticationFilter extends NiFiAuthenticationFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
// The Authorization header contains authentication credentials
|
||||
private static NiFiBearerTokenResolver bearerTokenResolver = new NiFiBearerTokenResolver();
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(final HttpServletRequest request) {
|
||||
// Only support JWT login when running securely
|
||||
if (!request.isSecure()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get JWT from Authorization header or cookie value
|
||||
final String headerToken = bearerTokenResolver.resolve(request);
|
||||
|
||||
if (StringUtils.isNotBlank(headerToken)) {
|
||||
return new JwtAuthenticationRequestToken(headerToken, request.getRemoteAddr());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* 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 io.jsonwebtoken.JwtException;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.authorization.user.NiFiUser;
|
||||
import org.apache.nifi.authorization.user.NiFiUserDetails;
|
||||
import org.apache.nifi.authorization.user.StandardNiFiUser.Builder;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.InvalidAuthenticationException;
|
||||
import org.apache.nifi.web.security.NiFiAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class JwtAuthenticationProvider extends NiFiAuthenticationProvider {
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
public JwtAuthenticationProvider(JwtService jwtService, NiFiProperties nifiProperties, Authorizer authorizer, IdpUserGroupService idpUserGroupService) {
|
||||
super(nifiProperties, authorizer);
|
||||
this.jwtService = jwtService;
|
||||
this.idpUserGroupService = idpUserGroupService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
final JwtAuthenticationRequestToken request = (JwtAuthenticationRequestToken) authentication;
|
||||
|
||||
try {
|
||||
final String jwtPrincipal = jwtService.getAuthenticationFromToken(request.getToken());
|
||||
final String mappedIdentity = mapIdentity(jwtPrincipal);
|
||||
final Set<String> userGroupProviderGroups = getUserGroups(mappedIdentity);
|
||||
final Set<String> idpUserGroups = getIdpUserGroups(mappedIdentity);
|
||||
|
||||
final NiFiUser user = new Builder()
|
||||
.identity(mappedIdentity)
|
||||
.groups(userGroupProviderGroups)
|
||||
.identityProviderGroups(idpUserGroups)
|
||||
.clientAddress(request.getClientAddress())
|
||||
.build();
|
||||
|
||||
return new NiFiAuthenticationToken(new NiFiUserDetails(user));
|
||||
} catch (JwtException e) {
|
||||
throw new InvalidAuthenticationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return JwtAuthenticationRequestToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
private Set<String> getIdpUserGroups(final String mappedIdentity) {
|
||||
return idpUserGroupService.getUserGroups(mappedIdentity).stream()
|
||||
.map(ug -> ug.getGroupName())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* 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.web.security.NiFiAuthenticationRequestToken;
|
||||
|
||||
/**
|
||||
* This is an authentication request with a given JWT token.
|
||||
*/
|
||||
public class JwtAuthenticationRequestToken extends NiFiAuthenticationRequestToken {
|
||||
|
||||
private final String token;
|
||||
|
||||
/**
|
||||
* Creates a representation of the jwt authentication request for a user.
|
||||
*
|
||||
* @param token The unique token for this user
|
||||
* @param clientAddress the address of the client making the request
|
||||
*/
|
||||
public JwtAuthenticationRequestToken(final String token, final String clientAddress) {
|
||||
super(clientAddress);
|
||||
setAuthenticated(false);
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<JWT token>";
|
||||
}
|
||||
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
/*
|
||||
* 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 io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.MalformedJwtException;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.SignatureException;
|
||||
import io.jsonwebtoken.SigningKeyResolverAdapter;
|
||||
import io.jsonwebtoken.UnsupportedJwtException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.admin.service.AdministrationException;
|
||||
import org.apache.nifi.admin.service.KeyService;
|
||||
import org.apache.nifi.key.Key;
|
||||
import org.apache.nifi.web.security.LogoutException;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class JwtService {
|
||||
|
||||
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class);
|
||||
|
||||
private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
|
||||
private static final String KEY_ID_CLAIM = "kid";
|
||||
private static final String USERNAME_CLAIM = "preferred_username";
|
||||
|
||||
private final KeyService keyService;
|
||||
|
||||
public JwtService(final KeyService keyService) {
|
||||
this.keyService = keyService;
|
||||
}
|
||||
|
||||
public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException {
|
||||
// The library representations of the JWT should be kept internal to this service.
|
||||
try {
|
||||
final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
|
||||
|
||||
if (jws == null) {
|
||||
throw new JwtException("Unable to parse token");
|
||||
}
|
||||
|
||||
// Additional validation that subject is present
|
||||
if (StringUtils.isEmpty(jws.getBody().getSubject())) {
|
||||
throw new JwtException("No subject available in token");
|
||||
}
|
||||
|
||||
// TODO: Validate issuer against active registry?
|
||||
if (StringUtils.isEmpty(jws.getBody().getIssuer())) {
|
||||
throw new JwtException("No issuer available in token");
|
||||
}
|
||||
return jws.getBody().getSubject();
|
||||
} catch (JwtException e) {
|
||||
logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
|
||||
final String errorMessage = "There was an error validating the JWT";
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
|
||||
try {
|
||||
return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
|
||||
@Override
|
||||
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
|
||||
final String identity = claims.getSubject();
|
||||
|
||||
// Get the key based on the key id in the claims
|
||||
final Integer keyId = claims.get(KEY_ID_CLAIM, Integer.class);
|
||||
final Key key = keyService.getKey(keyId);
|
||||
|
||||
// Ensure we were able to find a key that was previously issued by this key service for this user
|
||||
if (key == null || key.getKey() == null) {
|
||||
throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
|
||||
}
|
||||
|
||||
return key.getKey().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
}).parseClaimsJws(base64EncodedToken);
|
||||
} catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException | AdministrationException e) {
|
||||
// TODO: Exercise all exceptions to ensure none leak key material to logs
|
||||
final String errorMessage = "Unable to validate the access token.";
|
||||
throw new JwtException(errorMessage, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signed JWT token from the provided (Spring Security) login authentication token.
|
||||
*
|
||||
* @param authenticationToken an instance of the Spring Security token after login credentials have been verified against the respective information source
|
||||
* @return a signed JWT containing the user identity and the identity provider, Base64-encoded
|
||||
* @throws JwtException if there is a problem generating the signed token
|
||||
*/
|
||||
public String generateSignedToken(final LoginAuthenticationToken authenticationToken) throws JwtException {
|
||||
if (authenticationToken == null) {
|
||||
throw new IllegalArgumentException("Cannot generate a JWT for a null authentication token");
|
||||
}
|
||||
|
||||
// Set expiration from the token
|
||||
final Calendar expiration = Calendar.getInstance();
|
||||
expiration.setTimeInMillis(authenticationToken.getExpiration());
|
||||
|
||||
final Object principal = authenticationToken.getPrincipal();
|
||||
if (principal == null || StringUtils.isEmpty(principal.toString())) {
|
||||
final String errorMessage = "Cannot generate a JWT for a token with an empty identity issued by " + authenticationToken.getIssuer();
|
||||
logger.error(errorMessage);
|
||||
throw new JwtException(errorMessage);
|
||||
}
|
||||
|
||||
// Create a JWT with the specified authentication
|
||||
final String identity = principal.toString();
|
||||
final String username = authenticationToken.getName();
|
||||
final String rawIssuer = authenticationToken.getIssuer();
|
||||
|
||||
try {
|
||||
// Get/create the key for this user
|
||||
final Key key = keyService.getOrCreateKey(identity);
|
||||
final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
logger.trace("Generating JWT for " + authenticationToken);
|
||||
|
||||
final String encodedIssuer = URLEncoder.encode(rawIssuer, "UTF-8");
|
||||
|
||||
// TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
|
||||
// Build the token
|
||||
return Jwts.builder().setSubject(identity)
|
||||
.setIssuer(encodedIssuer)
|
||||
.setAudience(encodedIssuer)
|
||||
.claim(USERNAME_CLAIM, username)
|
||||
.claim(KEY_ID_CLAIM, key.getId())
|
||||
.setExpiration(expiration.getTime())
|
||||
.setIssuedAt(Calendar.getInstance().getTime())
|
||||
.signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
|
||||
} catch (NullPointerException | AdministrationException e) {
|
||||
final String errorMessage = "Could not retrieve the signing key for JWT for " + identity;
|
||||
logger.error(errorMessage, e);
|
||||
throw new JwtException(errorMessage, e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
final String errorMessage = "Could not URL encode issuer: " + rawIssuer;
|
||||
logger.error(errorMessage, e);
|
||||
throw new JwtException(errorMessage, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) throws LogoutException {
|
||||
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(keyId);
|
||||
} catch (Exception e) {
|
||||
final String errorMessage = String.format("The key with key ID: %s failed to be removed from the user database.", keyId);
|
||||
logger.error(errorMessage);
|
||||
throw new LogoutException(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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.util.StringUtils;
|
||||
import org.apache.nifi.web.security.InvalidAuthenticationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class NiFiBearerTokenResolver implements BearerTokenResolver {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NiFiBearerTokenResolver.class);
|
||||
private static final Pattern BEARER_HEADER_PATTERN = Pattern.compile("^Bearer (\\S*\\.\\S*\\.\\S*){1}$");
|
||||
private static final Pattern JWT_PATTERN = Pattern.compile("^(\\S*\\.\\S*\\.\\S*)$");
|
||||
public static final String AUTHORIZATION = "Authorization";
|
||||
public static final String JWT_COOKIE_NAME = "__Host-Authorization-Bearer";
|
||||
|
||||
@Override
|
||||
public String resolve(HttpServletRequest request) {
|
||||
final String authorizationHeader = request.getHeader(AUTHORIZATION);
|
||||
final Cookie cookieHeader = WebUtils.getCookie(request, JWT_COOKIE_NAME);
|
||||
|
||||
if (StringUtils.isNotBlank(authorizationHeader) && validAuthorizationHeaderFormat(authorizationHeader)) {
|
||||
return getTokenFromHeader(authorizationHeader);
|
||||
} else if(cookieHeader != null && validJwtFormat(cookieHeader.getValue())) {
|
||||
return cookieHeader.getValue();
|
||||
} else {
|
||||
logger.debug("Authorization header was not present or not in a valid format.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean validAuthorizationHeaderFormat(String authorizationHeader) {
|
||||
Matcher matcher = BEARER_HEADER_PATTERN.matcher(authorizationHeader);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
private boolean validJwtFormat(String jwt) {
|
||||
Matcher matcher = JWT_PATTERN.matcher(jwt);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
private String getTokenFromHeader(String authenticationHeader) {
|
||||
Matcher matcher = BEARER_HEADER_PATTERN.matcher(authenticationHeader);
|
||||
if (matcher.matches()) {
|
||||
return matcher.group(1);
|
||||
} else {
|
||||
throw new InvalidAuthenticationException("JWT did not match expected pattern.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.converter;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.authorization.user.NiFiUser;
|
||||
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.authorization.util.UserGroupUtil;
|
||||
import org.apache.nifi.idp.IdpUserGroup;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Standard Converter from JSON Web Token to NiFi Authentication Token
|
||||
*/
|
||||
public class StandardJwtAuthenticationConverter implements Converter<Jwt, NiFiAuthenticationToken> {
|
||||
private final Authorizer authorizer;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private final List<IdentityMapping> identityMappings;
|
||||
|
||||
public StandardJwtAuthenticationConverter(final Authorizer authorizer, final IdpUserGroupService idpUserGroupService, final NiFiProperties properties) {
|
||||
this.authorizer = authorizer;
|
||||
this.idpUserGroupService = idpUserGroupService;
|
||||
this.identityMappings = IdentityMappingUtil.getIdentityMappings(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON Web Token to NiFi Authentication Token
|
||||
*
|
||||
* @param jwt JSON Web Token
|
||||
* @return NiFi Authentication Token
|
||||
*/
|
||||
@Override
|
||||
public NiFiAuthenticationToken convert(final Jwt jwt) {
|
||||
final NiFiUser user = getUser(jwt);
|
||||
return new NiFiAuthenticationToken(new NiFiUserDetails(user));
|
||||
}
|
||||
|
||||
private NiFiUser getUser(final Jwt jwt) {
|
||||
final String identity = IdentityMappingUtil.mapIdentity(jwt.getSubject(), identityMappings);
|
||||
|
||||
return new StandardNiFiUser.Builder()
|
||||
.identity(identity)
|
||||
.groups(UserGroupUtil.getUserGroups(authorizer, identity))
|
||||
.identityProviderGroups(getIdentityProviderGroups(identity))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Set<String> getIdentityProviderGroups(final String identity) {
|
||||
return idpUserGroupService.getUserGroups(identity).stream()
|
||||
.map(IdpUserGroup::getGroupName)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.jws;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* JSON Web Signature Signer Container
|
||||
*/
|
||||
public class JwsSignerContainer {
|
||||
private final String keyIdentifier;
|
||||
|
||||
private final JWSAlgorithm jwsAlgorithm;
|
||||
|
||||
private final JWSSigner jwsSigner;
|
||||
|
||||
public JwsSignerContainer(final String keyIdentifier, final JWSAlgorithm jwsAlgorithm, final JWSSigner jwsSigner) {
|
||||
this.keyIdentifier = Objects.requireNonNull(keyIdentifier, "Key Identifier required");
|
||||
this.jwsAlgorithm = Objects.requireNonNull(jwsAlgorithm, "JWS Algorithm required");
|
||||
this.jwsSigner = Objects.requireNonNull(jwsSigner, "JWS Signer required");
|
||||
}
|
||||
|
||||
public String getKeyIdentifier() {
|
||||
return keyIdentifier;
|
||||
}
|
||||
|
||||
public JWSAlgorithm getJwsAlgorithm() {
|
||||
return jwsAlgorithm;
|
||||
}
|
||||
|
||||
public JWSSigner getJwsSigner() {
|
||||
return jwsSigner;
|
||||
}
|
||||
}
|
|
@ -14,31 +14,19 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.admin.service.action;
|
||||
package org.apache.nifi.web.security.jwt.jws;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.DataAccessException;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
*
|
||||
* JSON Web Signature Signer Provider supports accessing signer and identifier properties
|
||||
*/
|
||||
public class DeleteKeyAction implements AdministrationAction<Integer> {
|
||||
|
||||
private final Integer keyId;
|
||||
|
||||
public interface JwsSignerProvider {
|
||||
/**
|
||||
* Creates a new transactions for deleting keys for a specified user based on their keyId.
|
||||
* Get JSON Web Signature Signer Container
|
||||
*
|
||||
* @param keyId user identity
|
||||
* @param expiration New JSON Web Token Expiration to be set for the returned Signer
|
||||
* @return JSON Web Signature Signer Container
|
||||
*/
|
||||
public DeleteKeyAction(Integer keyId) {
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer execute(DAOFactory daoFactory) throws DataAccessException {
|
||||
final KeyDAO keyDao = daoFactory.getKeyDAO();
|
||||
return keyDao.deleteKey(keyId);
|
||||
}
|
||||
JwsSignerContainer getJwsSignerContainer(Instant expiration);
|
||||
}
|
|
@ -14,17 +14,16 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.security.jwt;
|
||||
package org.apache.nifi.web.security.jwt.jws;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface BearerTokenResolver {
|
||||
/**
|
||||
* Listener handling JWS Signer events
|
||||
*/
|
||||
public interface SignerListener {
|
||||
/**
|
||||
* Resolve any
|
||||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer
|
||||
* Token</a> value from the request.
|
||||
* @param request the request
|
||||
* @return the Bearer Token value or {@code null} if none found
|
||||
* On Signer Updated
|
||||
*
|
||||
* @param jwsSignerContainer JWS Signer Container
|
||||
*/
|
||||
String resolve(HttpServletRequest request);
|
||||
void onSignerUpdated(JwsSignerContainer jwsSignerContainer);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.jws;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Listener handling Signing Key events
|
||||
*/
|
||||
public interface SigningKeyListener {
|
||||
/**
|
||||
* On Signing Key Used
|
||||
*
|
||||
* @param keyIdentifier Key Identifier
|
||||
* @param expiration JSON Web Token Expiration
|
||||
*/
|
||||
void onSigningKeyUsed(String keyIdentifier, Instant expiration);
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.jws;
|
||||
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.proc.JWSKeySelector;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.apache.nifi.web.security.jwt.key.VerificationKeySelector;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Standard JSON Web Signature Key Selector for selecting keys using Key Identifier
|
||||
* @param <C> Security Context
|
||||
*/
|
||||
public class StandardJWSKeySelector<C extends SecurityContext> implements JWSKeySelector<C> {
|
||||
private final VerificationKeySelector verificationKeySelector;
|
||||
|
||||
/**
|
||||
* Standard JSON Web Signature Key Selector constructor requires a Verification Key Selector
|
||||
* @param verificationKeySelector Verification Key Selector
|
||||
*/
|
||||
public StandardJWSKeySelector(final VerificationKeySelector verificationKeySelector) {
|
||||
this.verificationKeySelector = verificationKeySelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select JSON Web Signature Key using Key Identifier from JWS Header
|
||||
*
|
||||
* @param jwsHeader JSON Web Signature Header
|
||||
* @param context Context not used
|
||||
* @return List of found java.security.Key objects
|
||||
*/
|
||||
@Override
|
||||
public List<? extends Key> selectJWSKeys(final JWSHeader jwsHeader, final C context) {
|
||||
final String keyId = jwsHeader.getKeyID();
|
||||
return verificationKeySelector.getVerificationKeys(keyId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.jws;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Standard JSON Web Signature Signer Provider
|
||||
*/
|
||||
public class StandardJwsSignerProvider implements JwsSignerProvider, SignerListener {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardJwsSignerProvider.class);
|
||||
|
||||
private final AtomicReference<JwsSignerContainer> currentSigner = new AtomicReference<>();
|
||||
|
||||
private final SigningKeyListener signingKeyListener;
|
||||
|
||||
public StandardJwsSignerProvider(final SigningKeyListener signingKeyListener) {
|
||||
this.signingKeyListener = signingKeyListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Current JWS Signer Container and update expiration
|
||||
*
|
||||
* @param expiration New JSON Web Token Expiration to be set for the returned Signing Key
|
||||
* @return JWS Signer Container
|
||||
*/
|
||||
@Override
|
||||
public JwsSignerContainer getJwsSignerContainer(final Instant expiration) {
|
||||
final JwsSignerContainer jwsSignerContainer = currentSigner.get();
|
||||
if (jwsSignerContainer == null) {
|
||||
throw new IllegalStateException("JSON Web Signature Signer not configured");
|
||||
}
|
||||
final String keyIdentifier = jwsSignerContainer.getKeyIdentifier();
|
||||
LOGGER.debug("Signer Used with Key Identifier [{}]", keyIdentifier);
|
||||
signingKeyListener.onSigningKeyUsed(keyIdentifier, expiration);
|
||||
return jwsSignerContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* On Signer Updated changes the current JWS Signer
|
||||
*
|
||||
* @param jwsSignerContainer JWS Signer Container
|
||||
*/
|
||||
@Override
|
||||
public void onSignerUpdated(final JwsSignerContainer jwsSignerContainer) {
|
||||
LOGGER.debug("Signer Updated with Key Identifier [{}]", jwsSignerContainer.getKeyIdentifier());
|
||||
currentSigner.set(jwsSignerContainer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import org.apache.nifi.web.security.jwt.jws.SigningKeyListener;
|
||||
import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.Key;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Standard Verification Key Selector implements listener interfaces for updating Key status
|
||||
*/
|
||||
public class StandardVerificationKeySelector implements SigningKeyListener, VerificationKeyListener, VerificationKeySelector {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardVerificationKeySelector.class);
|
||||
|
||||
private final VerificationKeyService verificationKeyService;
|
||||
|
||||
private final Duration keyRotationPeriod;
|
||||
|
||||
public StandardVerificationKeySelector(final VerificationKeyService verificationKeyService, final Duration keyRotationPeriod) {
|
||||
this.verificationKeyService = Objects.requireNonNull(verificationKeyService, "Verification Key Service required");
|
||||
this.keyRotationPeriod = Objects.requireNonNull(keyRotationPeriod, "Key Rotation Period required");
|
||||
}
|
||||
|
||||
/**
|
||||
* On Verification Key Generated persist encoded Key with expiration
|
||||
*
|
||||
* @param keyIdentifier Key Identifier
|
||||
* @param key Key
|
||||
*/
|
||||
@Override
|
||||
public void onVerificationKeyGenerated(final String keyIdentifier, final Key key) {
|
||||
final Instant expiration = Instant.now().plus(keyRotationPeriod);
|
||||
verificationKeyService.save(keyIdentifier, key, expiration);
|
||||
LOGGER.debug("Verification Key Saved [{}] Expiration [{}]", keyIdentifier, expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Verification Keys
|
||||
*
|
||||
* @param keyIdentifier Key Identifier
|
||||
* @return List of Keys
|
||||
*/
|
||||
@Override
|
||||
public List<? extends Key> getVerificationKeys(final String keyIdentifier) {
|
||||
final Optional<Key> key = verificationKeyService.findById(keyIdentifier);
|
||||
final List<? extends Key> keys = key.map(Collections::singletonList).orElse(Collections.emptyList());
|
||||
LOGGER.debug("Key Identifier [{}] Verification Keys Found [{}]", keyIdentifier, keys.size());
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* On Signing Key Used set new expiration
|
||||
*
|
||||
* @param keyIdentifier Key Identifier
|
||||
* @param expiration JSON Web Token Expiration
|
||||
*/
|
||||
@Override
|
||||
public void onSigningKeyUsed(final String keyIdentifier, final Instant expiration) {
|
||||
LOGGER.debug("Signing Key Used [{}] Expiration [{}]", keyIdentifier, expiration);
|
||||
verificationKeyService.setExpiration(keyIdentifier, expiration);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
/**
|
||||
* Listener handling Verification Key events
|
||||
*/
|
||||
public interface VerificationKeyListener {
|
||||
/**
|
||||
* On Verification Key Generated
|
||||
*
|
||||
* @param keyIdentifier Key Identifier
|
||||
* @param key Key used for Verification
|
||||
*/
|
||||
void onVerificationKeyGenerated(String keyIdentifier, Key key);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.key;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verification Key Selector returns a List of java.security.Key objects for signature verification
|
||||
*/
|
||||
public interface VerificationKeySelector {
|
||||
/**
|
||||
* Get Verification Keys for Key Identifier
|
||||
*
|
||||
* @param keyIdentifier Key Identifier
|
||||
* @return List of found java.security.Key objects
|
||||
*/
|
||||
List<? extends Key> getVerificationKeys(String keyIdentifier);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.key.command;
|
||||
|
||||
import org.apache.nifi.web.security.jwt.key.service.VerificationKeyService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Key Expiration Command removes expired Verification Keys
|
||||
*/
|
||||
public class KeyExpirationCommand implements Runnable {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeyExpirationCommand.class);
|
||||
|
||||
private final VerificationKeyService verificationKeyService;
|
||||
|
||||
public KeyExpirationCommand(final VerificationKeyService verificationKeyService) {
|
||||
this.verificationKeyService = Objects.requireNonNull(verificationKeyService, "Verification Key Service required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run deletes expired Verification Keys
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
LOGGER.debug("Delete Expired Verification Keys Started");
|
||||
verificationKeyService.deleteExpired();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.key.command;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer;
|
||||
import org.apache.nifi.web.security.jwt.jws.SignerListener;
|
||||
import org.apache.nifi.web.security.jwt.key.VerificationKeyListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Key Generation Command produces new RSA Key Pairs and configures a JWS Signer
|
||||
*/
|
||||
public class KeyGenerationCommand implements Runnable {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KeyGenerationCommand.class);
|
||||
|
||||
private static final String KEY_ALGORITHM = "RSA";
|
||||
|
||||
private static final int KEY_SIZE = 4096;
|
||||
|
||||
private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.PS512;
|
||||
|
||||
private final KeyPairGenerator keyPairGenerator;
|
||||
|
||||
private final SignerListener signerListener;
|
||||
|
||||
private final VerificationKeyListener verificationKeyListener;
|
||||
|
||||
public KeyGenerationCommand(final SignerListener signerListener, final VerificationKeyListener verificationKeyListener) {
|
||||
this.signerListener = Objects.requireNonNull(signerListener, "Signer Listener required");
|
||||
this.verificationKeyListener = Objects.requireNonNull(verificationKeyListener, "Verification Key Listener required");
|
||||
try {
|
||||
keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
|
||||
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run generates a new Key Pair and notifies configured listeners
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
final String keyIdentifier = UUID.randomUUID().toString();
|
||||
LOGGER.debug("Generated Key Pair [{}] Key Identifier [{}]", KEY_ALGORITHM, keyIdentifier);
|
||||
|
||||
verificationKeyListener.onVerificationKeyGenerated(keyIdentifier, keyPair.getPublic());
|
||||
|
||||
final JWSSigner jwsSigner = new RSASSASigner(keyPair.getPrivate());
|
||||
signerListener.onSignerUpdated(new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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.key.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.nifi.components.state.Scope;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Standard Verification Key Service implemented using State Manager
|
||||
*/
|
||||
public class StandardVerificationKeyService implements VerificationKeyService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardVerificationKeyService.class);
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
private static final Scope SCOPE = Scope.LOCAL;
|
||||
|
||||
private final StateManager stateManager;
|
||||
|
||||
public StandardVerificationKeyService(final StateManager stateManager) {
|
||||
this.stateManager = stateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Key using specified Key Identifier
|
||||
*
|
||||
* @param id Key Identifier
|
||||
* @return Optional Key
|
||||
*/
|
||||
@Override
|
||||
public Optional<Key> findById(final String id) {
|
||||
final Optional<String> serializedKey = findSerializedKey(id);
|
||||
return serializedKey.map(this::getVerificationKey).map(this::getKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Expired Verification Keys is synchronized to avoid losing updates from other methods
|
||||
*/
|
||||
@Override
|
||||
public synchronized void deleteExpired() {
|
||||
final Map<String, String> state = getStateMap().toMap();
|
||||
|
||||
final Instant now = Instant.now();
|
||||
final Map<String, String> updatedState = state
|
||||
.values()
|
||||
.stream()
|
||||
.map(this::getVerificationKey)
|
||||
.filter(verificationKey -> verificationKey.getExpiration().isAfter(now))
|
||||
.collect(Collectors.toMap(VerificationKey::getId, this::serializeVerificationKey));
|
||||
|
||||
if (updatedState.equals(state)) {
|
||||
LOGGER.debug("Expired Verification Keys not found");
|
||||
} else {
|
||||
try {
|
||||
stateManager.setState(updatedState, SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException("Delete Expired Verification Keys Failed", e);
|
||||
}
|
||||
LOGGER.debug("Delete Expired Verification Keys Completed: Keys Before [{}] Keys After [{}]", state.size(), updatedState.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Verification Key
|
||||
*
|
||||
* @param id Key Identifier
|
||||
* @param key Key
|
||||
* @param expiration Expiration
|
||||
*/
|
||||
@Override
|
||||
public void save(final String id, final Key key, final Instant expiration) {
|
||||
final VerificationKey verificationKey = new VerificationKey();
|
||||
verificationKey.setId(id);
|
||||
verificationKey.setEncoded(key.getEncoded());
|
||||
verificationKey.setAlgorithm(key.getAlgorithm());
|
||||
verificationKey.setExpiration(expiration);
|
||||
setVerificationKey(verificationKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Expiration of Verification Key when found
|
||||
*
|
||||
* @param id Key Identifier
|
||||
* @param expiration Expiration
|
||||
*/
|
||||
@Override
|
||||
public void setExpiration(final String id, final Instant expiration) {
|
||||
final Optional<String> serializedKey = findSerializedKey(id);
|
||||
if (serializedKey.isPresent()) {
|
||||
final VerificationKey verificationKey = getVerificationKey(serializedKey.get());
|
||||
verificationKey.setExpiration(expiration);
|
||||
setVerificationKey(verificationKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Verification Key is synchronized to avoid competing updates to the State Map
|
||||
*
|
||||
* @param verificationKey Verification Key to be stored
|
||||
*/
|
||||
private synchronized void setVerificationKey(final VerificationKey verificationKey) {
|
||||
try {
|
||||
final String serialized = serializeVerificationKey(verificationKey);
|
||||
final Map<String, String> state = new HashMap<>(getStateMap().toMap());
|
||||
state.put(verificationKey.getId(), serialized);
|
||||
stateManager.setState(state, SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException("Set Verification Key State Failed", e);
|
||||
}
|
||||
LOGGER.debug("Stored Verification Key [{}] Expiration [{}]", verificationKey.getId(), verificationKey.getExpiration());
|
||||
}
|
||||
|
||||
private Optional<String> findSerializedKey(final String id) {
|
||||
final StateMap stateMap = getStateMap();
|
||||
return Optional.ofNullable(stateMap.get(id));
|
||||
}
|
||||
|
||||
private String serializeVerificationKey(final VerificationKey verificationKey) {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(verificationKey);
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new UncheckedIOException("Serialize Verification Key Failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private VerificationKey getVerificationKey(final String serialized) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(serialized, VerificationKey.class);
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new UncheckedIOException("Read Verification Key Failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Key getKey(final VerificationKey verificationKey) {
|
||||
final KeySpec keySpec = new X509EncodedKeySpec(verificationKey.getEncoded());
|
||||
final String algorithm = verificationKey.getAlgorithm();
|
||||
try {
|
||||
final KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
|
||||
return keyFactory.generatePublic(keySpec);
|
||||
} catch (final InvalidKeySpecException | NoSuchAlgorithmException e) {
|
||||
final String message = String.format("Parsing Encoded Key [%s] Algorithm [%s] Failed", verificationKey.getId(), algorithm);
|
||||
throw new IllegalStateException(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private StateMap getStateMap() {
|
||||
try {
|
||||
return stateManager.getState(SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException("Get State Failed", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.key.service;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Verification Key used for storing serialized instances
|
||||
*/
|
||||
class VerificationKey {
|
||||
private String id;
|
||||
|
||||
private String algorithm;
|
||||
|
||||
private byte[] encoded;
|
||||
|
||||
private Instant expiration;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(final String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(final String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public byte[] getEncoded() {
|
||||
return encoded;
|
||||
}
|
||||
|
||||
public void setEncoded(final byte[] encoded) {
|
||||
this.encoded = encoded;
|
||||
}
|
||||
|
||||
public Instant getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public void setExpiration(final Instant expiration) {
|
||||
this.expiration = expiration;
|
||||
}
|
||||
}
|
|
@ -14,43 +14,43 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.admin.dao;
|
||||
package org.apache.nifi.web.security.jwt.key.service;
|
||||
|
||||
import org.apache.nifi.key.Key;
|
||||
import java.security.Key;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Key data access.
|
||||
* Verification Key Service for storing and retrieving keys
|
||||
*/
|
||||
public interface KeyDAO {
|
||||
public interface VerificationKeyService {
|
||||
/**
|
||||
* Find Key using specified Key Identifier
|
||||
*
|
||||
* @param id Key Identifier
|
||||
* @return Optional Key
|
||||
*/
|
||||
Optional<Key> findById(String id);
|
||||
|
||||
/**
|
||||
* Gets the key for the specified user identity. Returns null if no key exists for the key id.
|
||||
*
|
||||
* @param id The key id
|
||||
* @return The key or null
|
||||
* Delete Expired Keys
|
||||
*/
|
||||
Key findKeyById(int id);
|
||||
void deleteExpired();
|
||||
|
||||
/**
|
||||
* Gets the latest key for the specified identity. Returns null if no key exists for the user identity.
|
||||
* Save Key with associated expiration
|
||||
*
|
||||
* @param identity The identity
|
||||
* @return The key or null
|
||||
* @param id Key Identifier
|
||||
* @param key Key
|
||||
* @param expiration Expiration
|
||||
*/
|
||||
Key findLatestKeyByIdentity(String identity);
|
||||
void save(String id, Key key, Instant expiration);
|
||||
|
||||
/**
|
||||
* Creates a key for the specified user identity.
|
||||
* Set Expiration for specified Key Identifier
|
||||
*
|
||||
* @param identity The user identity
|
||||
* @return The key
|
||||
* @param id Key Identifier
|
||||
* @param expiration Expiration
|
||||
*/
|
||||
Key createKey(String identity);
|
||||
|
||||
/**
|
||||
* Deletes a key using the key ID.
|
||||
*
|
||||
* @param keyId The key ID
|
||||
*/
|
||||
Integer deleteKey(Integer keyId);
|
||||
void setExpiration(String id, Instant expiration);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.provider;
|
||||
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
|
||||
/**
|
||||
* Bearer Token Provider supports generation of Access Tokens used for Bearer Authentication
|
||||
*/
|
||||
public interface BearerTokenProvider {
|
||||
/**
|
||||
* Get Bearer Token
|
||||
*
|
||||
* @param loginAuthenticationToken Login Authentication Token
|
||||
* @return Bearer Token
|
||||
*/
|
||||
String getBearerToken(LoginAuthenticationToken loginAuthenticationToken);
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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.provider;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSObject;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.Payload;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer;
|
||||
import org.apache.nifi.web.security.jwt.jws.JwsSignerProvider;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Standard Bearer Token Provider supports returning serialized and signed JSON Web Tokens
|
||||
*/
|
||||
public class StandardBearerTokenProvider implements BearerTokenProvider {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardBearerTokenProvider.class);
|
||||
|
||||
private static final String URL_ENCODED_CHARACTER_SET = StandardCharsets.UTF_8.name();
|
||||
|
||||
private final JwsSignerProvider jwsSignerProvider;
|
||||
|
||||
public StandardBearerTokenProvider(final JwsSignerProvider jwsSignerProvider) {
|
||||
this.jwsSignerProvider = jwsSignerProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Signed JSON Web Token using Login Authentication Token
|
||||
*
|
||||
* @param loginAuthenticationToken Login Authentication Token
|
||||
* @return Serialized Signed JSON Web Token
|
||||
*/
|
||||
@Override
|
||||
public String getBearerToken(final LoginAuthenticationToken loginAuthenticationToken) {
|
||||
Objects.requireNonNull(loginAuthenticationToken, "LoginAuthenticationToken required");
|
||||
final String subject = Objects.requireNonNull(loginAuthenticationToken.getPrincipal(), "Principal required").toString();
|
||||
final String username = loginAuthenticationToken.getName();
|
||||
|
||||
final String issuer = getUrlEncoded(loginAuthenticationToken.getIssuer());
|
||||
final Date now = new Date();
|
||||
final Date expirationTime = new Date(loginAuthenticationToken.getExpiration());
|
||||
final JWTClaimsSet claims = new JWTClaimsSet.Builder()
|
||||
.jwtID(UUID.randomUUID().toString())
|
||||
.subject(subject)
|
||||
.issuer(issuer)
|
||||
.audience(issuer)
|
||||
.notBeforeTime(now)
|
||||
.issueTime(now)
|
||||
.expirationTime(expirationTime)
|
||||
.claim(SupportedClaim.PREFERRED_USERNAME.getClaim(), username)
|
||||
.build();
|
||||
return getSignedBearerToken(claims);
|
||||
}
|
||||
|
||||
private String getSignedBearerToken(final JWTClaimsSet claims) {
|
||||
final Date expirationTime = claims.getExpirationTime();
|
||||
final JwsSignerContainer jwsSignerContainer = jwsSignerProvider.getJwsSignerContainer(expirationTime.toInstant());
|
||||
|
||||
final String keyIdentifier = jwsSignerContainer.getKeyIdentifier();
|
||||
final JWSAlgorithm algorithm = jwsSignerContainer.getJwsAlgorithm();
|
||||
final JWSHeader header = new JWSHeader.Builder(algorithm).keyID(keyIdentifier).build();
|
||||
final Payload payload = new Payload(claims.toJSONObject());
|
||||
final JWSObject jwsObject = new JWSObject(header, payload);
|
||||
|
||||
final JWSSigner signer = jwsSignerContainer.getJwsSigner();
|
||||
try {
|
||||
jwsObject.sign(signer);
|
||||
} catch (final JOSEException e) {
|
||||
final String message = String.format("Signing Failed for Algorithm [%s] Key Identifier [%s]", algorithm, keyIdentifier);
|
||||
throw new IllegalArgumentException(message, e);
|
||||
}
|
||||
|
||||
LOGGER.debug("Signed Bearer Token using Key [{}] for Subject [{}]", keyIdentifier, claims.getSubject());
|
||||
return jwsObject.serialize();
|
||||
}
|
||||
|
||||
private String getUrlEncoded(final String string) {
|
||||
try {
|
||||
return URLEncoder.encode(string, URL_ENCODED_CHARACTER_SET);
|
||||
} catch (final UnsupportedEncodingException e) {
|
||||
throw new IllegalArgumentException(String.format("URL Encoding [%s] Failed", string), e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,33 +14,43 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.admin.service.action;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.KeyDAO;
|
||||
import org.apache.nifi.key.Key;
|
||||
package org.apache.nifi.web.security.jwt.provider;
|
||||
|
||||
/**
|
||||
* Gets a key for the specified user identity.
|
||||
* Supported Claim for JSON Web Tokens
|
||||
*/
|
||||
public class GetOrCreateKeyAction implements AdministrationAction<Key> {
|
||||
public enum SupportedClaim {
|
||||
/** RFC 7519 Section 4.1.1 */
|
||||
ISSUER("iss"),
|
||||
|
||||
private final String identity;
|
||||
/** RFC 7519 Section 4.1.2 */
|
||||
SUBJECT("sub"),
|
||||
|
||||
public GetOrCreateKeyAction(String identity) {
|
||||
this.identity = identity;
|
||||
/** RFC 7519 Section 4.1.3 */
|
||||
AUDIENCE("aud"),
|
||||
|
||||
/** RFC 7519 Section 4.1.4 */
|
||||
EXPIRATION("exp"),
|
||||
|
||||
/** RFC 7519 Section 4.1.5 */
|
||||
NOT_BEFORE("nbf"),
|
||||
|
||||
/** RFC 7519 Section 4.1.6 */
|
||||
ISSUED_AT("iat"),
|
||||
|
||||
/** RFC 7519 Section 4.1.7 */
|
||||
JWT_ID("jti"),
|
||||
|
||||
/** Preferred Username defined in OpenID Connect Core 1.0 Standard Claims */
|
||||
PREFERRED_USERNAME("preferred_username");
|
||||
|
||||
private final String claim;
|
||||
|
||||
SupportedClaim(final String claim) {
|
||||
this.claim = claim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key execute(DAOFactory daoFactory) {
|
||||
final KeyDAO keyDao = daoFactory.getKeyDAO();
|
||||
|
||||
Key key = keyDao.findLatestKeyByIdentity(identity);
|
||||
if (key == null) {
|
||||
key = keyDao.createKey(identity);
|
||||
}
|
||||
|
||||
return key;
|
||||
public String getClaim() {
|
||||
return claim;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.resolver;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.http.SecurityHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Bearer Token Resolver prefers the HTTP Authorization Header and then evaluates the Authorization Cookie when found
|
||||
*/
|
||||
public class StandardBearerTokenResolver implements BearerTokenResolver {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardBearerTokenResolver.class);
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
/**
|
||||
* Resolve Bearer Token from HTTP Request checking Authorization Header then Authorization Cookie when found
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return Bearer Token or null when not found
|
||||
*/
|
||||
@Override
|
||||
public String resolve(final HttpServletRequest request) {
|
||||
String bearerToken = null;
|
||||
|
||||
final String header = request.getHeader(SecurityHeader.AUTHORIZATION.getHeader());
|
||||
if (StringUtils.startsWithIgnoreCase(header, BEARER_PREFIX)) {
|
||||
bearerToken = StringUtils.removeStartIgnoreCase(header, BEARER_PREFIX);
|
||||
} else {
|
||||
final Cookie cookie = WebUtils.getCookie(request, SecurityCookieName.AUTHORIZATION_BEARER.getName());
|
||||
if (cookie == null) {
|
||||
LOGGER.trace("Bearer Token not found in Header or Cookie");
|
||||
} else {
|
||||
bearerToken = cookie.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return bearerToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
/**
|
||||
* JSON Web Token Logout Listener
|
||||
*/
|
||||
public interface JwtLogoutListener {
|
||||
/**
|
||||
* Logout Bearer Token
|
||||
*
|
||||
* @param bearerToken Bearer Token
|
||||
*/
|
||||
void logout(String bearerToken);
|
||||
}
|
|
@ -14,36 +14,32 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.admin.service;
|
||||
package org.apache.nifi.web.security.jwt.revocation;
|
||||
|
||||
import org.apache.nifi.key.Key;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Manages NiFi user keys.
|
||||
* JSON Web Token Revocation Service
|
||||
*/
|
||||
public interface KeyService {
|
||||
public interface JwtRevocationService {
|
||||
/**
|
||||
* Delete Expired Revocations
|
||||
*/
|
||||
void deleteExpired();
|
||||
|
||||
/**
|
||||
* Gets a key for the specified user identity. Returns null if the user has not had a key issued
|
||||
* Is JSON Web Token Identifier Revoked
|
||||
*
|
||||
* @param id The key id
|
||||
* @return The key or null
|
||||
* @param id JSON Web Token Identifier
|
||||
* @return Revoked Status
|
||||
*/
|
||||
Key getKey(int id);
|
||||
boolean isRevoked(String id);
|
||||
|
||||
/**
|
||||
* Gets a key for the specified user identity. If a key does not exist, one will be created.
|
||||
* Set Revoked Status using JSON Web Token Identifier
|
||||
*
|
||||
* @param identity The user identity
|
||||
* @return The key
|
||||
* @throws AdministrationException if it failed to get/create the key
|
||||
* @param id JSON Web Token Identifier
|
||||
* @param expiration Expiration of Revocation Status after which the status record can be removed
|
||||
*/
|
||||
Key getOrCreateKey(String identity);
|
||||
|
||||
/**
|
||||
* Deletes keys for the specified identity.
|
||||
*
|
||||
* @param keyId The user's key ID
|
||||
*/
|
||||
void deleteKey(Integer keyId);
|
||||
void setRevoked(String id, Instant expiration);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
|
||||
|
||||
import static org.springframework.security.oauth2.core.OAuth2TokenValidatorResult.success;
|
||||
import static org.springframework.security.oauth2.core.OAuth2TokenValidatorResult.failure;
|
||||
|
||||
/**
|
||||
* JSON Web Token Validator checks the JWT Identifier against a Revocation Service
|
||||
*/
|
||||
public class JwtRevocationValidator implements OAuth2TokenValidator<Jwt> {
|
||||
private static final BearerTokenError REVOKED_ERROR = BearerTokenErrors.invalidToken("Access Token Revoked");
|
||||
|
||||
private static final OAuth2TokenValidatorResult FAILURE_RESULT = failure(REVOKED_ERROR);
|
||||
|
||||
private final JwtRevocationService jwtRevocationService;
|
||||
|
||||
public JwtRevocationValidator(final JwtRevocationService jwtRevocationService) {
|
||||
this.jwtRevocationService = jwtRevocationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate checks JSON Web Token Identifier against Revocation Service
|
||||
*
|
||||
* @param jwt JSON Web Token Identifier
|
||||
* @return Validator Result based on Revoked Status
|
||||
*/
|
||||
@Override
|
||||
public OAuth2TokenValidatorResult validate(final Jwt jwt) {
|
||||
return jwtRevocationService.isRevoked(jwt.getId()) ? FAILURE_RESULT : success();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
|
||||
/**
|
||||
* Standard JSON Web Token Logout Listener handles parsing and revocation
|
||||
*/
|
||||
public class StandardJwtLogoutListener implements JwtLogoutListener {
|
||||
/** JWT Decoder */
|
||||
private final JwtDecoder jwtDecoder;
|
||||
|
||||
/** JWT Revocation Service */
|
||||
private final JwtRevocationService jwtRevocationService;
|
||||
|
||||
public StandardJwtLogoutListener(final JwtDecoder jwtDecoder, final JwtRevocationService jwtRevocationService) {
|
||||
this.jwtDecoder = jwtDecoder;
|
||||
this.jwtRevocationService = jwtRevocationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout Bearer Token sets revoked status using the JSON Web Token Identifier
|
||||
*
|
||||
* @param bearerToken Bearer Token
|
||||
*/
|
||||
@Override
|
||||
public void logout(final String bearerToken) {
|
||||
if (StringUtils.isNotBlank(bearerToken)) {
|
||||
final Jwt jwt = jwtDecoder.decode(bearerToken);
|
||||
jwtRevocationService.setRevoked(jwt.getId(), jwt.getExpiresAt());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
import org.apache.nifi.components.state.Scope;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Standard JSON Web Token Revocation Service using State Manager
|
||||
*/
|
||||
public class StandardJwtRevocationService implements JwtRevocationService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardJwtRevocationService.class);
|
||||
|
||||
private static final Scope SCOPE = Scope.LOCAL;
|
||||
|
||||
private final StateManager stateManager;
|
||||
|
||||
public StandardJwtRevocationService(final StateManager stateManager) {
|
||||
this.stateManager = stateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Expired Revocations is synchronized is avoid losing updates from setRevoked()
|
||||
*/
|
||||
@Override
|
||||
public synchronized void deleteExpired() {
|
||||
final Map<String, String> state = getStateMap().toMap();
|
||||
|
||||
final Instant now = Instant.now();
|
||||
final Map<String, String> updatedState = state
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> Instant.parse(entry.getValue()).isAfter(now))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
if (updatedState.equals(state)) {
|
||||
LOGGER.debug("Expired Revocations not found");
|
||||
} else {
|
||||
try {
|
||||
stateManager.setState(updatedState, SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException("Delete Expired Revocations Failed", e);
|
||||
}
|
||||
LOGGER.debug("Delete Expired Revocations: Before [{}] After [{}]", state.size(), updatedState.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is JSON Web Token Identifier Revoked based on State Map Status
|
||||
*
|
||||
* @param id JSON Web Token Identifier
|
||||
* @return Revoked Status
|
||||
*/
|
||||
@Override
|
||||
public boolean isRevoked(final String id) {
|
||||
final StateMap stateMap = getStateMap();
|
||||
return stateMap.toMap().containsKey(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Revoked Status is synchronized to avoid competing changes to the State Map
|
||||
*
|
||||
* @param id JSON Web Token Identifier
|
||||
* @param expiration Expiration of Revocation Status after which the status record can be removed
|
||||
*/
|
||||
@Override
|
||||
public synchronized void setRevoked(final String id, final Instant expiration) {
|
||||
final StateMap stateMap = getStateMap();
|
||||
final Map<String, String> state = new HashMap<>(stateMap.toMap());
|
||||
state.put(id, expiration.toString());
|
||||
try {
|
||||
stateManager.setState(state, SCOPE);
|
||||
LOGGER.debug("JWT Identifier [{}] Revocation Completed", id);
|
||||
} catch (final IOException e) {
|
||||
LOGGER.error("JWT Identifier [{}] Revocation Failed", id, e);
|
||||
}
|
||||
}
|
||||
|
||||
private StateMap getStateMap() {
|
||||
try {
|
||||
return stateManager.getState(SCOPE);
|
||||
} catch (final IOException e) {
|
||||
throw new UncheckedIOException("Get State Failed", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.revocation.command;
|
||||
|
||||
import org.apache.nifi.web.security.jwt.revocation.JwtRevocationService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Revocation Expiration Command removes expired Revocations
|
||||
*/
|
||||
public class RevocationExpirationCommand implements Runnable {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RevocationExpirationCommand.class);
|
||||
|
||||
private final JwtRevocationService jwtRevocationService;
|
||||
|
||||
public RevocationExpirationCommand(final JwtRevocationService jwtRevocationService) {
|
||||
this.jwtRevocationService = Objects.requireNonNull(jwtRevocationService, "JWT Revocation Service required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Run deletes expired Revocations
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
LOGGER.debug("Delete Expired Revocations Started");
|
||||
jwtRevocationService.deleteExpired();
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ public class KnoxServiceFactoryBean implements FactoryBean<KnoxService> {
|
|||
private NiFiProperties properties = null;
|
||||
|
||||
@Override
|
||||
public KnoxService getObject() throws Exception {
|
||||
public KnoxService getObject() {
|
||||
if (knoxService == null) {
|
||||
// ensure we only allow knox if login and oidc are disabled
|
||||
if (properties.isKnoxSsoEnabled() && (properties.isLoginIdentityProviderEnabled() || properties.isOidcEnabled() || properties.isSamlEnabled())) {
|
||||
|
|
|
@ -68,7 +68,6 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.apache.nifi.authentication.exception.IdentityAccessException;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.jwt.JwtService;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -82,8 +81,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
|||
private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProvider.class);
|
||||
private final String EMAIL_CLAIM = "email";
|
||||
|
||||
private NiFiProperties properties;
|
||||
private JwtService jwtService;
|
||||
private final NiFiProperties properties;
|
||||
private OIDCProviderMetadata oidcProviderMetadata;
|
||||
private int oidcConnectTimeout;
|
||||
private int oidcReadTimeout;
|
||||
|
@ -94,12 +92,10 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
|||
/**
|
||||
* Creates a new StandardOidcIdentityProvider.
|
||||
*
|
||||
* @param jwtService jwt service
|
||||
* @param properties properties
|
||||
*/
|
||||
public StandardOidcIdentityProvider(final JwtService jwtService, final NiFiProperties properties) {
|
||||
public StandardOidcIdentityProvider(final NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -457,10 +453,8 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
|||
final Date expiration = claimsSet.getExpirationTime();
|
||||
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
|
||||
|
||||
// Convert into a NiFi JWT for retrieval later
|
||||
final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(
|
||||
return new LoginAuthenticationToken(
|
||||
identity, identity, expiresIn, claimsSet.getIssuer().getValue());
|
||||
return loginToken;
|
||||
}
|
||||
|
||||
private OIDCTokens getOidcTokens(OIDCTokenResponse response) {
|
||||
|
@ -510,14 +504,13 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
|||
|
||||
private static List<String> getAvailableClaims(JWTClaimsSet claimSet) {
|
||||
// Get the claims available in the ID token response
|
||||
List<String> presentClaims = claimSet.getClaims().entrySet().stream()
|
||||
return claimSet.getClaims().entrySet().stream()
|
||||
// Check claim values are not empty
|
||||
.filter(e -> StringUtils.isNotBlank(e.getValue().toString()))
|
||||
.filter(e -> e.getValue() != null && StringUtils.isNotBlank(e.getValue().toString()))
|
||||
// If not empty, put claim name in a map
|
||||
.map(Map.Entry::getKey)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
return presentClaims;
|
||||
}
|
||||
|
||||
private void validateAccessToken(OIDCTokens oidcTokens) throws Exception {
|
||||
|
|
|
@ -19,7 +19,7 @@ package org.apache.nifi.web.security.saml.impl;
|
|||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.jwt.JwtService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.saml.SAMLStateManager;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.security.util.CacheKey;
|
||||
|
@ -34,7 +34,7 @@ public class StandardSAMLStateManager implements SAMLStateManager {
|
|||
|
||||
private static Logger LOGGER = LoggerFactory.getLogger(StandardSAMLStateManager.class);
|
||||
|
||||
private JwtService jwtService;
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
// identifier from cookie -> state value
|
||||
private final Cache<CacheKey, String> stateLookupForPendingRequests;
|
||||
|
@ -42,12 +42,12 @@ public class StandardSAMLStateManager implements SAMLStateManager {
|
|||
// identifier from cookie -> jwt or identity (and generate jwt on retrieval)
|
||||
private final Cache<CacheKey, String> jwtLookupForCompletedRequests;
|
||||
|
||||
public StandardSAMLStateManager(final JwtService jwtService) {
|
||||
this(jwtService, 60, TimeUnit.SECONDS);
|
||||
public StandardSAMLStateManager(final BearerTokenProvider bearerTokenProvider) {
|
||||
this(bearerTokenProvider, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public StandardSAMLStateManager(final JwtService jwtService, final int cacheExpiration, final TimeUnit units) {
|
||||
this.jwtService = jwtService;
|
||||
public StandardSAMLStateManager(final BearerTokenProvider bearerTokenProvider, final int cacheExpiration, final TimeUnit units) {
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
this.stateLookupForPendingRequests = CacheBuilder.newBuilder().expireAfterWrite(cacheExpiration, units).build();
|
||||
this.jwtLookupForCompletedRequests = CacheBuilder.newBuilder().expireAfterWrite(cacheExpiration, units).build();
|
||||
}
|
||||
|
@ -108,12 +108,12 @@ public class StandardSAMLStateManager implements SAMLStateManager {
|
|||
}
|
||||
|
||||
final CacheKey requestIdentifierKey = new CacheKey(requestIdentifier);
|
||||
final String nifiJwt = jwtService.generateSignedToken(token);
|
||||
final String bearerToken = bearerTokenProvider.getBearerToken(token);
|
||||
try {
|
||||
// cache the jwt for later retrieval
|
||||
synchronized (jwtLookupForCompletedRequests) {
|
||||
final String cachedJwt = jwtLookupForCompletedRequests.get(requestIdentifierKey, () -> nifiJwt);
|
||||
if (!IdentityProviderUtils.timeConstantEqualityCheck(nifiJwt, cachedJwt)) {
|
||||
final String cachedJwt = jwtLookupForCompletedRequests.get(requestIdentifierKey, () -> bearerToken);
|
||||
if (!IdentityProviderUtils.timeConstantEqualityCheck(bearerToken, cachedJwt)) {
|
||||
throw new IllegalStateException("An existing login request is already in progress.");
|
||||
}
|
||||
}
|
||||
|
@ -139,8 +139,4 @@ public class StandardSAMLStateManager implements SAMLStateManager {
|
|||
return jwt;
|
||||
}
|
||||
}
|
||||
|
||||
public void setJwtService(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,8 +35,6 @@ import org.apache.nifi.nar.NarCloseable;
|
|||
import org.apache.nifi.properties.SensitivePropertyProviderFactoryAware;
|
||||
import org.apache.nifi.security.xml.XmlUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.xml.sax.SAXException;
|
||||
|
@ -65,7 +63,6 @@ import java.util.Map;
|
|||
public class LoginIdentityProviderFactoryBean extends SensitivePropertyProviderFactoryAware
|
||||
implements FactoryBean, DisposableBean, LoginIdentityProviderLookup {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactoryBean.class);
|
||||
private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/login-identity-providers.xsd";
|
||||
private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authentication.generated";
|
||||
private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
|
||||
|
||||
<!-- certificate extractor -->
|
||||
<bean id="certificateExtractor" class="org.apache.nifi.web.security.x509.X509CertificateExtractor"/>
|
||||
|
||||
<!-- principal extractor -->
|
||||
<bean id="principalExtractor" class="org.apache.nifi.web.security.x509.SubjectDnX509PrincipalExtractor"/>
|
||||
|
||||
<!-- ocsp validator -->
|
||||
<bean id="ocspValidator" class="org.apache.nifi.web.security.x509.ocsp.OcspCertificateValidator">
|
||||
<constructor-arg ref="nifiProperties"/>
|
||||
</bean>
|
||||
|
||||
<!-- x509 validator -->
|
||||
<bean id="certificateValidator" class="org.apache.nifi.web.security.x509.X509CertificateValidator">
|
||||
<property name="ocspValidator" ref="ocspValidator"/>
|
||||
</bean>
|
||||
|
||||
<!-- x509 identity provider -->
|
||||
<bean id="certificateIdentityProvider" class="org.apache.nifi.web.security.x509.X509IdentityProvider">
|
||||
<property name="principalExtractor" ref="principalExtractor"/>
|
||||
<property name="certificateValidator" ref="certificateValidator"/>
|
||||
</bean>
|
||||
|
||||
<!-- x509 authentication provider -->
|
||||
<bean id="x509AuthenticationProvider" class="org.apache.nifi.web.security.x509.X509AuthenticationProvider">
|
||||
<constructor-arg ref="certificateIdentityProvider" index="0"/>
|
||||
<constructor-arg ref="authorizer" index="1"/>
|
||||
<constructor-arg ref="nifiProperties" index="2"/>
|
||||
</bean>
|
||||
|
||||
<!-- jwt service -->
|
||||
<bean id="jwtService" class="org.apache.nifi.web.security.jwt.JwtService">
|
||||
<constructor-arg ref="keyService"/>
|
||||
</bean>
|
||||
|
||||
<!-- jwt authentication provider -->
|
||||
<bean id="jwtAuthenticationProvider" class="org.apache.nifi.web.security.jwt.JwtAuthenticationProvider">
|
||||
<constructor-arg ref="jwtService" index="0"/>
|
||||
<constructor-arg ref="nifiProperties" index="1"/>
|
||||
<constructor-arg ref="authorizer" index="2"/>
|
||||
<constructor-arg ref="idpUserGroupService" index="3"/>
|
||||
</bean>
|
||||
|
||||
<!-- knox service -->
|
||||
<bean id="knoxService" class="org.apache.nifi.web.security.knox.KnoxServiceFactoryBean">
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
|
||||
<!-- knox authentication provider -->
|
||||
<bean id="knoxAuthenticationProvider" class="org.apache.nifi.web.security.knox.KnoxAuthenticationProvider">
|
||||
<constructor-arg ref="knoxService" index="0"/>
|
||||
<constructor-arg ref="nifiProperties" index="1"/>
|
||||
<constructor-arg ref="authorizer" index="2"/>
|
||||
</bean>
|
||||
|
||||
<!-- Kerberos service -->
|
||||
<bean id="kerberosService" class="org.apache.nifi.web.security.spring.KerberosServiceFactoryBean">
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
|
||||
<!-- login identity provider -->
|
||||
<bean id="loginIdentityProvider" class="org.apache.nifi.web.security.spring.LoginIdentityProviderFactoryBean">
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
<property name="extensionManager" ref="extensionManager" />
|
||||
</bean>
|
||||
|
||||
<!-- oidc -->
|
||||
<bean id="oidcProvider" class="org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider">
|
||||
<constructor-arg ref="jwtService" index="0"/>
|
||||
<constructor-arg ref="nifiProperties" index="1"/>
|
||||
</bean>
|
||||
<bean id="oidcService" class="org.apache.nifi.web.security.oidc.OidcService">
|
||||
<constructor-arg ref="oidcProvider"/>
|
||||
</bean>
|
||||
|
||||
<!-- saml -->
|
||||
<bean id="samlConfigurationFactory" class="org.apache.nifi.web.security.saml.impl.StandardSAMLConfigurationFactory" />
|
||||
|
||||
<bean id="samlService" class="org.apache.nifi.web.security.saml.impl.StandardSAMLService" init-method="initialize" destroy-method="shutdown">
|
||||
<constructor-arg ref="samlConfigurationFactory" index="0"/>
|
||||
<constructor-arg ref="nifiProperties" index="1"/>
|
||||
</bean>
|
||||
|
||||
<bean id="samlStateManager" class="org.apache.nifi.web.security.saml.impl.StandardSAMLStateManager">
|
||||
<constructor-arg ref="jwtService" index="0"/>
|
||||
</bean>
|
||||
|
||||
<bean id="samlCredentialStore" class="org.apache.nifi.web.security.saml.impl.StandardSAMLCredentialStore">
|
||||
<constructor-arg ref="idpCredentialService" index="0"/>
|
||||
</bean>
|
||||
|
||||
<!-- logout -->
|
||||
<bean id="logoutRequestManager" class="org.apache.nifi.web.security.logout.LogoutRequestManager" scope="singleton"/>
|
||||
|
||||
<!-- anonymous -->
|
||||
<bean id="anonymousAuthenticationProvider" class="org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider">
|
||||
<constructor-arg ref="nifiProperties" index="0"/>
|
||||
<constructor-arg ref="authorizer" index="1"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
|
@ -21,13 +21,7 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
|
|||
import com.nimbusds.oauth2.sdk.id.Issuer
|
||||
import com.nimbusds.openid.connect.sdk.SubjectType
|
||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import org.apache.nifi.admin.service.KeyService
|
||||
import org.apache.nifi.key.Key
|
||||
import org.apache.nifi.util.NiFiProperties
|
||||
import org.apache.nifi.web.security.jwt.JwtService
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
|
@ -43,7 +37,6 @@ import java.util.concurrent.TimeUnit
|
|||
class OidcServiceGroovyTest extends GroovyTestCase {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OidcServiceGroovyTest.class)
|
||||
|
||||
private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
|
||||
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
|
||||
"nifi.security.user.oidc.discovery.url" : "https://localhost/oidc",
|
||||
"nifi.security.user.login.identity.provider" : "provider",
|
||||
|
@ -58,7 +51,6 @@ class OidcServiceGroovyTest extends GroovyTestCase {
|
|||
|
||||
// Mock collaborators
|
||||
private static NiFiProperties mockNiFiProperties
|
||||
private static JwtService mockJwtService = [:] as JwtService
|
||||
private static StandardOidcIdentityProvider soip
|
||||
|
||||
private static final String MOCK_REQUEST_IDENTIFIER = "mock-request-identifier"
|
||||
|
@ -79,7 +71,7 @@ class OidcServiceGroovyTest extends GroovyTestCase {
|
|||
@Before
|
||||
void setUp() throws Exception {
|
||||
mockNiFiProperties = buildNiFiProperties()
|
||||
soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -91,34 +83,6 @@ class OidcServiceGroovyTest extends GroovyTestCase {
|
|||
new NiFiProperties(combinedProps)
|
||||
}
|
||||
|
||||
private static JwtService buildJwtService() {
|
||||
def mockJS = new JwtService([:] as KeyService) {
|
||||
@Override
|
||||
String generateSignedToken(LoginAuthenticationToken lat) {
|
||||
signNiFiToken(lat)
|
||||
}
|
||||
}
|
||||
mockJS
|
||||
}
|
||||
|
||||
private static String signNiFiToken(LoginAuthenticationToken lat) {
|
||||
String identity = "mockUser"
|
||||
String USERNAME_CLAIM = "username"
|
||||
String KEY_ID_CLAIM = "keyId"
|
||||
Calendar expiration = Calendar.getInstance()
|
||||
expiration.setTimeInMillis(System.currentTimeMillis() + 10_000)
|
||||
String username = lat.getName()
|
||||
|
||||
return Jwts.builder().setSubject(identity)
|
||||
.setIssuer(lat.getIssuer())
|
||||
.setAudience(lat.getIssuer())
|
||||
.claim(USERNAME_CLAIM, username)
|
||||
.claim(KEY_ID_CLAIM, SIGNING_KEY.getId())
|
||||
.setExpiration(expiration.getTime())
|
||||
.setIssuedAt(Calendar.getInstance().getTime())
|
||||
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY.key.getBytes("UTF-8")).compact()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldStoreJwt() {
|
||||
// Arrange
|
||||
|
@ -189,7 +153,6 @@ class OidcServiceGroovyTest extends GroovyTestCase {
|
|||
}
|
||||
|
||||
private static StandardOidcIdentityProvider buildIdentityProviderWithMockInitializedProvider(Map<String, String> additionalProperties = [:]) {
|
||||
JwtService mockJS = buildJwtService()
|
||||
NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
|
||||
|
||||
// Mock OIDC provider metadata
|
||||
|
@ -197,7 +160,7 @@ class OidcServiceGroovyTest extends GroovyTestCase {
|
|||
URI mockURI = new URI("https://localhost/oidc")
|
||||
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
|
||||
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP) {
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNFP) {
|
||||
@Override
|
||||
void initializeProvider() {
|
||||
soip.oidcProviderMetadata = metadata
|
||||
|
|
|
@ -43,16 +43,10 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
|||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens
|
||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
|
||||
import groovy.json.JsonOutput
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.SignatureAlgorithm
|
||||
import net.minidev.json.JSONObject
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.apache.nifi.admin.service.KeyService
|
||||
import org.apache.nifi.key.Key
|
||||
import org.apache.nifi.util.NiFiProperties
|
||||
import org.apache.nifi.util.StringUtils
|
||||
import org.apache.nifi.web.security.jwt.JwtService
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
|
@ -66,7 +60,6 @@ import org.slf4j.LoggerFactory
|
|||
class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProviderGroovyTest.class)
|
||||
|
||||
private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
|
||||
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
|
||||
"nifi.security.user.oidc.discovery.url" : "https://localhost/oidc",
|
||||
"nifi.security.user.login.identity.provider" : "provider",
|
||||
|
@ -81,7 +74,6 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
|
||||
// Mock collaborators
|
||||
private static NiFiProperties mockNiFiProperties
|
||||
private static JwtService mockJwtService = [:] as JwtService
|
||||
|
||||
private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ" +
|
||||
"SI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZp" +
|
||||
|
@ -109,34 +101,6 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
new NiFiProperties(combinedProps)
|
||||
}
|
||||
|
||||
private static JwtService buildJwtService() {
|
||||
def mockJS = new JwtService([:] as KeyService) {
|
||||
@Override
|
||||
String generateSignedToken(LoginAuthenticationToken lat) {
|
||||
signNiFiToken(lat)
|
||||
}
|
||||
}
|
||||
mockJS
|
||||
}
|
||||
|
||||
private static String signNiFiToken(LoginAuthenticationToken lat) {
|
||||
String identity = "mockUser"
|
||||
String USERNAME_CLAIM = "username"
|
||||
String KEY_ID_CLAIM = "keyId"
|
||||
Calendar expiration = Calendar.getInstance()
|
||||
expiration.setTimeInMillis(System.currentTimeMillis() + 10_000)
|
||||
String username = lat.getName()
|
||||
|
||||
return Jwts.builder().setSubject(identity)
|
||||
.setIssuer(lat.getIssuer())
|
||||
.setAudience(lat.getIssuer())
|
||||
.claim(USERNAME_CLAIM, username)
|
||||
.claim(KEY_ID_CLAIM, SIGNING_KEY.getId())
|
||||
.setExpiration(expiration.getTime())
|
||||
.setIssuedAt(Calendar.getInstance().getTime())
|
||||
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY.key.getBytes("UTF-8")).compact()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldGetAvailableClaims() {
|
||||
// Arrange
|
||||
|
@ -168,7 +132,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
@Test
|
||||
void testShouldCreateClientAuthenticationFromPost() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
@ -205,7 +169,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
void testShouldCreateClientAuthenticationFromBasic() {
|
||||
// Arrange
|
||||
// Mock collaborators
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
@ -241,7 +205,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
@Test
|
||||
void testShouldCreateTokenHTTPRequest() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
// Mock AuthorizationGrant
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
|
@ -280,7 +244,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
@Test
|
||||
void testShouldLookupIdentityInUserInfo() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
@ -304,7 +268,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
@Test
|
||||
void testLookupIdentityUserInfoShouldHandleMissingIdentity() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
@ -329,7 +293,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
@Test
|
||||
void testLookupIdentityUserInfoShouldHandle500() {
|
||||
// Arrange
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNiFiProperties)
|
||||
|
||||
Issuer mockIssuer = new Issuer("https://localhost/oidc")
|
||||
URI mockURI = new URI("https://localhost/oidc")
|
||||
|
@ -698,9 +662,8 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
|||
}
|
||||
|
||||
private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map<String, String> additionalProperties = [:]) {
|
||||
JwtService mockJS = buildJwtService()
|
||||
NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP)
|
||||
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockNFP)
|
||||
|
||||
// Mock OIDC provider metadata
|
||||
Issuer mockIssuer = new Issuer("mockIssuer")
|
||||
|
|
|
@ -1,282 +0,0 @@
|
|||
/*
|
||||
* 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.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.AccessPolicyProvider;
|
||||
import org.apache.nifi.authorization.Authorizer;
|
||||
import org.apache.nifi.authorization.Group;
|
||||
import org.apache.nifi.authorization.ManagedAuthorizer;
|
||||
import org.apache.nifi.authorization.User;
|
||||
import org.apache.nifi.authorization.UserAndGroups;
|
||||
import org.apache.nifi.authorization.UserGroupProvider;
|
||||
import org.apache.nifi.authorization.user.NiFiUser;
|
||||
import org.apache.nifi.authorization.user.NiFiUserDetails;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.idp.IdpUserGroup;
|
||||
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.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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 = "nifiadmin@nifi.apache.org";
|
||||
|
||||
private final static String UNKNOWN_TOKEN = "eyJhbGciOiJIUzI1NiJ9" +
|
||||
".eyJzdWIiOiJ1bmtub3duX3Rva2VuIiwiaXNzIjoiS2VyYmVyb3NQcm9" +
|
||||
"2aWRlciIsImF1ZCI6IktlcmJlcm9zUHJvdmlkZXIiLCJwcmVmZXJyZWR" +
|
||||
"fdXNlcm5hbWUiOiJ1bmtub3duX3Rva2VuIiwia2lkIjoxLCJleHAiOjE" +
|
||||
"2OTI0NTQ2NjcsImlhdCI6MTU5MjQxMTQ2N30.PpOGx3Ul5ydokOOuzKd" +
|
||||
"aRKv1kxy6Q4jGy7rBPU8PqxY";
|
||||
|
||||
private NiFiProperties properties;
|
||||
|
||||
|
||||
private JwtService jwtService;
|
||||
private Authorizer authorizer;
|
||||
private IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private JwtAuthenticationProvider jwtAuthenticationProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
TestKeyService keyService = new TestKeyService();
|
||||
jwtService = new JwtService(keyService);
|
||||
idpUserGroupService = mock(IdpUserGroupService.class);
|
||||
authorizer = mock(Authorizer.class);
|
||||
|
||||
// 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 NiFiProperties(props);
|
||||
|
||||
jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, authorizer, idpUserGroupService);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Collections.emptyList());
|
||||
|
||||
// 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);
|
||||
|
||||
when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Collections.emptyList());
|
||||
|
||||
// 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);
|
||||
|
||||
when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Collections.emptyList());
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdpUserGroupsPresent() {
|
||||
// Arrange
|
||||
LoginAuthenticationToken loginAuthenticationToken =
|
||||
new LoginAuthenticationToken(ADMIN_IDENTITY,
|
||||
EXPIRATION_MILLIS,
|
||||
"MockIdentityProvider");
|
||||
String token = jwtService.generateSignedToken(loginAuthenticationToken);
|
||||
final JwtAuthenticationRequestToken request = new JwtAuthenticationRequestToken(token, CLIENT_ADDRESS);
|
||||
|
||||
final String groupName1 = "group1";
|
||||
final IdpUserGroup idpUserGroup1 = createIdpUserGroup(1, ADMIN_IDENTITY, groupName1, IdpType.SAML);
|
||||
|
||||
final String groupName2 = "group2";
|
||||
final IdpUserGroup idpUserGroup2 = createIdpUserGroup(2, ADMIN_IDENTITY, groupName2, IdpType.SAML);
|
||||
|
||||
when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Arrays.asList(idpUserGroup1, idpUserGroup2));
|
||||
|
||||
// Act
|
||||
final NiFiAuthenticationToken result = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(request);
|
||||
final NiFiUserDetails details = (NiFiUserDetails) result.getPrincipal();
|
||||
|
||||
// Assert details username is correct
|
||||
assertEquals(ADMIN_IDENTITY, details.getUsername());
|
||||
|
||||
final NiFiUser returnedUser = details.getNiFiUser();
|
||||
assertNotNull(returnedUser);
|
||||
|
||||
// Assert user-group-provider groups is empty
|
||||
assertNull(returnedUser.getGroups());
|
||||
|
||||
// Assert identity-provider groups is correct
|
||||
assertEquals(2, returnedUser.getIdentityProviderGroups().size());
|
||||
assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName1));
|
||||
assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName2));
|
||||
|
||||
// Assert combined groups has only idp groups
|
||||
assertEquals(2, returnedUser.getAllGroups().size());
|
||||
assertTrue(returnedUser.getAllGroups().contains(groupName1));
|
||||
assertTrue(returnedUser.getAllGroups().contains(groupName2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCombineUserGroupProviderGroupsAndIdpUserGroups() {
|
||||
// setup IdpUserGroupService...
|
||||
|
||||
final String groupName1 = "group1";
|
||||
final IdpUserGroup idpUserGroup1 = createIdpUserGroup(1, ADMIN_IDENTITY, groupName1, IdpType.SAML);
|
||||
|
||||
final String groupName2 = "group2";
|
||||
final IdpUserGroup idpUserGroup2 = createIdpUserGroup(2, ADMIN_IDENTITY, groupName2, IdpType.SAML);
|
||||
|
||||
idpUserGroupService = mock(IdpUserGroupService.class);
|
||||
when(idpUserGroupService.getUserGroups(ADMIN_IDENTITY)).thenReturn(Arrays.asList(idpUserGroup1, idpUserGroup2));
|
||||
|
||||
// setup ManagedAuthorizer...
|
||||
final String groupName3 = "group3";
|
||||
final Group group3 = new Group.Builder().identifierGenerateRandom().name(groupName3).build();
|
||||
|
||||
final UserGroupProvider userGroupProvider = mock(UserGroupProvider.class);
|
||||
when(userGroupProvider.getUserAndGroups(ADMIN_IDENTITY)).thenReturn(new UserAndGroups() {
|
||||
@Override
|
||||
public User getUser() {
|
||||
return new User.Builder().identifier(ADMIN_IDENTITY).identity(ADMIN_IDENTITY).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Group> getGroups() {
|
||||
return Collections.singleton(group3);
|
||||
}
|
||||
});
|
||||
|
||||
final AccessPolicyProvider accessPolicyProvider = mock(AccessPolicyProvider.class);
|
||||
when(accessPolicyProvider.getUserGroupProvider()).thenReturn(userGroupProvider);
|
||||
|
||||
final ManagedAuthorizer managedAuthorizer = mock(ManagedAuthorizer.class);
|
||||
when(managedAuthorizer.getAccessPolicyProvider()).thenReturn(accessPolicyProvider);
|
||||
|
||||
jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, managedAuthorizer, idpUserGroupService);
|
||||
|
||||
// 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 details username is correct
|
||||
assertEquals(ADMIN_IDENTITY, details.getUsername());
|
||||
|
||||
final NiFiUser returnedUser = details.getNiFiUser();
|
||||
assertNotNull(returnedUser);
|
||||
|
||||
// Assert user-group-provider groups are correct
|
||||
assertEquals(1, returnedUser.getGroups().size());
|
||||
assertTrue(returnedUser.getGroups().contains(groupName3));
|
||||
|
||||
// Assert identity-provider groups are correct
|
||||
assertEquals(2, returnedUser.getIdentityProviderGroups().size());
|
||||
assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName1));
|
||||
assertTrue(returnedUser.getIdentityProviderGroups().contains(groupName2));
|
||||
|
||||
// Assert combined groups are correct
|
||||
assertEquals(3, returnedUser.getAllGroups().size());
|
||||
assertTrue(returnedUser.getAllGroups().contains(groupName1));
|
||||
assertTrue(returnedUser.getAllGroups().contains(groupName2));
|
||||
assertTrue(returnedUser.getAllGroups().contains(groupName3));
|
||||
}
|
||||
|
||||
private IdpUserGroup createIdpUserGroup(int id, String identity, String groupName, IdpType idpType) {
|
||||
final IdpUserGroup userGroup = new IdpUserGroup();
|
||||
userGroup.setId(id);
|
||||
userGroup.setIdentity(identity);
|
||||
userGroup.setGroupName(groupName);
|
||||
userGroup.setType(idpType);
|
||||
return userGroup;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,682 +0,0 @@
|
|||
/*
|
||||
* 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 io.jsonwebtoken.JwtException;
|
||||
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.authorization.util.IdentityMapping;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.key.Key;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
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.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;
|
||||
import java.net.URLEncoder;
|
||||
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 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;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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
|
||||
*/
|
||||
private static final String VALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRl"
|
||||
+ "ciIsImF1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZ"
|
||||
+ "XJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoxLCJleHAiOjI0NDc4MDg3NjEsIm"
|
||||
+ "lhdCI6MTQ0NzgwODcwMX0.r6aGZ6FNNYMOpcXW8BK2VYaQeX1uO0Aw1KJfjB3Q1DU";
|
||||
|
||||
// This token has an empty subject field
|
||||
private static final String INVALID_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZG"
|
||||
+ "VudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvI"
|
||||
+ "iwia2lkIjoxLCJleHAiOjI0NDc4MDg3NjEsImlhdCI6MTQ0NzgwODcwMX0"
|
||||
+ ".x_1p2M6E0vwWHWMujIUnSL3GkFoDqqICllRxo2SMNaw";
|
||||
|
||||
private static final String VALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZC"
|
||||
+ "I6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJl"
|
||||
+ "c3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9";
|
||||
|
||||
// This token has an empty subject field
|
||||
private static final String INVALID_UNSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVu"
|
||||
+ "dGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoi"
|
||||
+ "YWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9";
|
||||
|
||||
// Algorithm field is "none"
|
||||
private static final String VALID_MALSIGNED_TOKEN = "eyJhbGciOiJub25lIn0"
|
||||
+ ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZC"
|
||||
+ "I6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJl"
|
||||
+ "c3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9"
|
||||
+ ".mPO_wMNMl_zjMNevhNvUoXbSJ9Kx6jAe5OxDIAzKQbI";
|
||||
|
||||
// Algorithm field is "none" and no signature is present
|
||||
private static final String VALID_MALSIGNED_NO_SIG_TOKEN = "eyJhbGciOiJub25lIn0"
|
||||
+ ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY"
|
||||
+ "2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIj"
|
||||
+ "oiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.";
|
||||
|
||||
// This token has an empty subject field
|
||||
private static final String INVALID_MALSIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik1vY2tJZGVud"
|
||||
+ "Gl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvcHJlc3RvIiwia2lkIjoiYW"
|
||||
+ "xvcHJlc3RvIiwiZXhwIjoxNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9.WAwmUY4KHKV2oARNodkqDkbZsfRXGZfD2Ccy64GX9QF";
|
||||
|
||||
// This token is signed but expired
|
||||
private static final String EXPIRED_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiIiLCJpc3MiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6Ik"
|
||||
+ "1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxvc"
|
||||
+ "HJlc3RvIiwia2lkIjoxLCJleHAiOjE0NDc4MDg3NjEsImlhdCI6MTQ0NzgwODcw"
|
||||
+ "MX0.ZPDIhNKuL89vTGXcuztOYaGifwcrQy_gid4j8Sspmto";
|
||||
|
||||
// Subject is "mgilman" but signed with "alopresto" key
|
||||
private static final String IMPOSTER_SIGNED_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiJtZ2lsbWFuIiwiaXNzIjoiTW9ja0lkZW50aXR5UHJvdmlkZXIiLCJ"
|
||||
+ "hdWQiOiJNb2NrSWRlbnRpdHlQcm92aWRlciIsInByZWZlcnJlZF91c2VybmFtZSI"
|
||||
+ "6ImFsb3ByZXN0byIsImtpZCI6MSwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc"
|
||||
+ "4MDg3MDF9.aw5OAvLTnb_sHmSQOQzW-A7NImiZgXJ2ngbbNL2Ymkc";
|
||||
|
||||
// Issuer field is set to unknown provider
|
||||
private static final String UNKNOWN_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiJhbG9wcmVzdG8iLCJpc3MiOiJVbmtub3duSWRlbnRpdHlQcm92aWRlciIsIm"
|
||||
+ "F1ZCI6Ik1vY2tJZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxv"
|
||||
+ "cHJlc3RvIiwia2lkIjoiYWxvcHJlc3RvIiwiZXhwIjoyNDQ3ODA4NzYxLCJpYXQiOjE0NDc4MDg3MDF9"
|
||||
+ ".SAd9tyNwSaijWet9wvAWSNmpxmPSK4XQuLx7h3ARqBo";
|
||||
|
||||
// Issuer field is absent
|
||||
private static final String NO_ISSUER_TOKEN = "eyJhbGciOiJIUzI1NiJ9"
|
||||
+ ".eyJzdWIiOiJhbG9wcmVzdG8iLCJhdWQiOiJNb2NrSWRlbnRpdHlQcm92a"
|
||||
+ "WRlciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsb3ByZXN0byIsImtpZCI"
|
||||
+ "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 = "nifiadmin@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);
|
||||
}
|
||||
|
||||
private static String generateHS256Token(String rawHeader, String rawPayload, String hmacSecret, boolean isValid,
|
||||
boolean isSigned) {
|
||||
try {
|
||||
logger.info("Generating token for " + rawHeader + " + " + rawPayload);
|
||||
|
||||
String base64Header = Base64.encodeBase64URLSafeString(rawHeader.getBytes(StandardCharsets.UTF_8));
|
||||
String base64Payload = Base64.encodeBase64URLSafeString(rawPayload.getBytes(StandardCharsets.UTF_8));
|
||||
// TODO: Support valid/invalid manipulation
|
||||
|
||||
final String body = base64Header + TOKEN_DELIMITER + base64Payload;
|
||||
|
||||
String signature = generateHMAC(hmacSecret, body);
|
||||
|
||||
return body + TOKEN_DELIMITER + signature;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
final String errorMessage = "Could not generate the token";
|
||||
logger.error(errorMessage, e);
|
||||
fail(errorMessage);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateHMAC(String hmacSecret, String body) throws NoSuchAlgorithmException,
|
||||
InvalidKeyException {
|
||||
Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secret_key = new SecretKeySpec(hmacSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
hmacSHA256.init(secret_key);
|
||||
return Base64.encodeBase64URLSafeString(hmacSHA256.doFinal(body.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
final Key key = new Key();
|
||||
key.setId(1);
|
||||
key.setIdentity(DEFAULT_IDENTITY);
|
||||
key.setKey(HMAC_SECRET);
|
||||
|
||||
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", Integer.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(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 NiFiProperties(props));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
jwtService = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldGetAuthenticationForValidToken() throws Exception {
|
||||
// Arrange
|
||||
String token = VALID_SIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
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
|
||||
String token = INVALID_SIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForEmptyToken() throws Exception {
|
||||
// Arrange
|
||||
String token = "";
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForUnsignedToken() throws Exception {
|
||||
// Arrange
|
||||
String token = VALID_UNSIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForMalsignedToken() throws Exception {
|
||||
// Arrange
|
||||
String token = VALID_MALSIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithm() throws Exception {
|
||||
// Arrange
|
||||
String token = VALID_MALSIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForTokenWithWrongAlgorithmAndNoSignature() throws Exception {
|
||||
// Arrange
|
||||
String token = VALID_MALSIGNED_NO_SIG_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Ignore("Not yet implemented")
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForTokenFromUnknownIdentityProvider() throws Exception {
|
||||
// Arrange
|
||||
String token = UNKNOWN_ISSUER_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForTokenFromEmptyIdentityProvider() throws Exception {
|
||||
// Arrange
|
||||
String token = NO_ISSUER_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForExpiredToken() throws Exception {
|
||||
// Arrange
|
||||
String token = EXPIRED_SIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGetAuthenticationForImposterToken() throws Exception {
|
||||
// Arrange
|
||||
String token = IMPOSTER_SIGNED_TOKEN;
|
||||
|
||||
// Act
|
||||
String identity = jwtService.getAuthenticationFromToken(token);
|
||||
logger.info("Extracted identity: " + identity);
|
||||
|
||||
// Assert
|
||||
// Should fail
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldGenerateSignedToken() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Token expires in 60 seconds
|
||||
final int EXPIRATION_MILLIS = 60000;
|
||||
LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto",
|
||||
EXPIRATION_MILLIS,
|
||||
"MockIdentityProvider");
|
||||
logger.info("Generating token for " + loginAuthenticationToken);
|
||||
|
||||
final String EXPECTED_HEADER = DEFAULT_HEADER;
|
||||
|
||||
// Convert the expiration time from ms to s
|
||||
final long TOKEN_EXPIRATION_SEC = (long) (loginAuthenticationToken.getExpiration() / 1000.0);
|
||||
|
||||
// Act
|
||||
String token = jwtService.generateSignedToken(loginAuthenticationToken);
|
||||
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
|
||||
final String DECODED_PAYLOAD = new String(Base64.decodeBase64(token.split("\\.")[1].getBytes()));
|
||||
final long ISSUED_AT_SEC = Long.valueOf(DECODED_PAYLOAD.substring(DECODED_PAYLOAD.lastIndexOf(":") + 1,
|
||||
DECODED_PAYLOAD.length() - 1));
|
||||
logger.trace("Actual token was issued at " + ISSUED_AT_SEC);
|
||||
|
||||
// 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");
|
||||
claims.put("aud", "MockIdentityProvider");
|
||||
claims.put("preferred_username", "alopresto");
|
||||
claims.put("kid", 1);
|
||||
claims.put("exp", TOKEN_EXPIRATION_SEC);
|
||||
claims.put("iat", ISSUED_AT_SEC);
|
||||
logger.trace("JSON Object to String: " + new JSONObject(claims).toString());
|
||||
|
||||
final String EXPECTED_PAYLOAD = new JSONObject(claims).toString();
|
||||
final String EXPECTED_TOKEN_STRING = generateHS256Token(EXPECTED_HEADER, EXPECTED_PAYLOAD, true, true);
|
||||
logger.info("Expected JWT: " + EXPECTED_TOKEN_STRING);
|
||||
|
||||
// Assert
|
||||
assertEquals("JWT token", EXPECTED_TOKEN_STRING, token);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldGenerateSignedTokenWithURLEncodedIssuer() throws Exception {
|
||||
// Arrange
|
||||
|
||||
// Token expires in 60 seconds
|
||||
final int EXPIRATION_MILLIS = 60000;
|
||||
final String rawIssuer = "https://accounts.google.com/o/saml2?idpid=acode";
|
||||
final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken("alopresto", EXPIRATION_MILLIS, rawIssuer);
|
||||
logger.info("Generating token for " + loginAuthenticationToken);
|
||||
|
||||
final String EXPECTED_HEADER = DEFAULT_HEADER;
|
||||
|
||||
// Convert the expiration time from ms to s
|
||||
final long TOKEN_EXPIRATION_SEC = (long) (loginAuthenticationToken.getExpiration() / 1000.0);
|
||||
|
||||
// Act
|
||||
String token = jwtService.generateSignedToken(loginAuthenticationToken);
|
||||
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
|
||||
final String DECODED_PAYLOAD = new String(Base64.decodeBase64(token.split("\\.")[1].getBytes()));
|
||||
final long ISSUED_AT_SEC = Long.valueOf(DECODED_PAYLOAD.substring(DECODED_PAYLOAD.lastIndexOf(":") + 1,
|
||||
DECODED_PAYLOAD.length() - 1));
|
||||
logger.trace("Actual token was issued at " + ISSUED_AT_SEC);
|
||||
|
||||
// Always use LinkedHashMap to enforce order of the signingKeys because the signature depends on order
|
||||
final String encodedIssuer = URLEncoder.encode(rawIssuer, "UTF-8");
|
||||
|
||||
final Map<String, Object> claims = new LinkedHashMap<>();
|
||||
claims.put("sub", "alopresto");
|
||||
claims.put("iss", encodedIssuer);
|
||||
claims.put("aud", encodedIssuer);
|
||||
claims.put("preferred_username", "alopresto");
|
||||
claims.put("kid", 1);
|
||||
claims.put("exp", TOKEN_EXPIRATION_SEC);
|
||||
claims.put("iat", ISSUED_AT_SEC);
|
||||
logger.trace("JSON Object to String: " + new JSONObject(claims).toString());
|
||||
|
||||
final String EXPECTED_PAYLOAD = new JSONObject(claims).toString();
|
||||
final String EXPECTED_TOKEN_STRING = generateHS256Token(EXPECTED_HEADER, EXPECTED_PAYLOAD, true, true);
|
||||
logger.info("Expected JWT: " + EXPECTED_TOKEN_STRING);
|
||||
|
||||
// Assert
|
||||
assertEquals("JWT token", EXPECTED_TOKEN_STRING, token);
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testShouldNotGenerateTokenWithNullAuthenticationToken() throws Exception {
|
||||
// Arrange
|
||||
LoginAuthenticationToken nullLoginAuthenticationToken = null;
|
||||
logger.info("Generating token for " + nullLoginAuthenticationToken);
|
||||
|
||||
// Act
|
||||
jwtService.generateSignedToken(nullLoginAuthenticationToken);
|
||||
|
||||
// Assert
|
||||
// Should throw exception
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGenerateTokenWithEmptyIdentity() throws Exception {
|
||||
// Arrange
|
||||
final int EXPIRATION_MILLIS = 60000;
|
||||
LoginAuthenticationToken emptyIdentityLoginAuthenticationToken = new LoginAuthenticationToken("",
|
||||
EXPIRATION_MILLIS, "MockIdentityProvider");
|
||||
logger.info("Generating token for " + emptyIdentityLoginAuthenticationToken);
|
||||
|
||||
// Act
|
||||
jwtService.generateSignedToken(emptyIdentityLoginAuthenticationToken);
|
||||
|
||||
// Assert
|
||||
// Should throw exception
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGenerateTokenWithNullIdentity() throws Exception {
|
||||
// Arrange
|
||||
final int EXPIRATION_MILLIS = 60000;
|
||||
LoginAuthenticationToken nullIdentityLoginAuthenticationToken = new LoginAuthenticationToken(null,
|
||||
EXPIRATION_MILLIS, "MockIdentityProvider");
|
||||
logger.info("Generating token for " + nullIdentityLoginAuthenticationToken);
|
||||
|
||||
// Act
|
||||
jwtService.generateSignedToken(nullIdentityLoginAuthenticationToken);
|
||||
|
||||
// Assert
|
||||
// Should throw exception
|
||||
}
|
||||
|
||||
@Test(expected = JwtException.class)
|
||||
public void testShouldNotGenerateTokenWithMissingKey() throws Exception {
|
||||
// Arrange
|
||||
final int EXPIRATION_MILLIS = 60000;
|
||||
LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(DEFAULT_IDENTITY,
|
||||
EXPIRATION_MILLIS,
|
||||
"MockIdentityProvider");
|
||||
logger.info("Generating token for " + loginAuthenticationToken);
|
||||
|
||||
// Set up the bad key service
|
||||
KeyService missingKeyService = mock(KeyService.class);
|
||||
when(missingKeyService.getOrCreateKey(anyString())).thenThrow(new AdministrationException("Could not find a "
|
||||
+ "key for that user"));
|
||||
jwtService = new JwtService(missingKeyService);
|
||||
|
||||
// Act
|
||||
jwtService.generateSignedToken(loginAuthenticationToken);
|
||||
|
||||
// Assert
|
||||
// Should throw exception
|
||||
}
|
||||
|
||||
@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.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);
|
||||
jwtService.logOut(token);
|
||||
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 testLogoutWhenAuthTokenIsEmptyShouldThrowError() throws Exception {
|
||||
// Arrange
|
||||
expectedException.expect(JwtException.class);
|
||||
expectedException.expectMessage("Unable to validate the access token.");
|
||||
|
||||
// Act
|
||||
jwtService.logOut(null);
|
||||
|
||||
// Assert
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* 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 groovy.json.JsonOutput;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class NiFiBearerTokenResolverTest {
|
||||
|
||||
public static String jwtString;
|
||||
|
||||
@Mock
|
||||
private static HttpServletRequest request;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpOnce() throws Exception {
|
||||
final String ALG_HEADER = "{\"alg\":\"HS256\"}";
|
||||
final int EXPIRATION_SECONDS = 500;
|
||||
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;
|
||||
|
||||
Map<String, String> hashMap = new HashMap<String, String>() {{
|
||||
put("sub", "unknownuser");
|
||||
put("iss", "MockIdentityProvider");
|
||||
put("aud", "MockIdentityProvider");
|
||||
put("preferred_username", "unknownuser");
|
||||
put("kid", String.valueOf(1));
|
||||
put("exp", String.valueOf(TOKEN_EXPIRATION_SECONDS));
|
||||
put("iat", String.valueOf(TOKEN_ISSUED_AT));
|
||||
}};
|
||||
|
||||
// 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
|
||||
final String EXPECTED_PAYLOAD = JsonOutput.toJson(hashMap);
|
||||
|
||||
// Set up our JWT string with a test token
|
||||
jwtString = JwtServiceTest.generateHS256Token(ALG_HEADER, EXPECTED_PAYLOAD, true, true);
|
||||
|
||||
request = mock(HttpServletRequest.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidAuthenticationHeaderString() {
|
||||
String authenticationHeader = "Bearer " + jwtString;
|
||||
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
|
||||
String isValidHeader = new NiFiBearerTokenResolver().resolve(request);
|
||||
|
||||
assertEquals(jwtString, isValidHeader);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingBearer() {
|
||||
String authenticationHeader = jwtString;
|
||||
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
|
||||
String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
|
||||
|
||||
assertNull(resolvedToken);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtraCharactersAtBeginningOfToken() {
|
||||
String authenticationHeader = "xBearer " + jwtString;
|
||||
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
|
||||
String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
|
||||
|
||||
assertNull(resolvedToken);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadTokenFormat() {
|
||||
String[] tokenStrings = jwtString.split("\\.");
|
||||
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.valueOf("Bearer " + tokenStrings[1] + tokenStrings[2]));
|
||||
String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
|
||||
|
||||
assertNull(resolvedToken);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleTokenInvalid() {
|
||||
String authenticationHeader = "Bearer " + jwtString;
|
||||
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(String.format("%s %s", authenticationHeader, authenticationHeader));
|
||||
String resolvedToken = new NiFiBearerTokenResolver().resolve(request);
|
||||
|
||||
assertNull(resolvedToken);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractToken() {
|
||||
String authenticationHeader = "Bearer " + jwtString;
|
||||
when(request.getHeader(eq(NiFiBearerTokenResolver.AUTHORIZATION))).thenReturn(authenticationHeader);
|
||||
String extractedToken = new NiFiBearerTokenResolver().resolve(request);
|
||||
|
||||
assertEquals(jwtString, extractedToken);
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import org.apache.nifi.admin.service.KeyService;
|
||||
import org.apache.nifi.key.Key;
|
||||
|
||||
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 key : signingKeys) {
|
||||
if(key.getIdentity().equals(identity)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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.converter;
|
||||
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.PlainJWT;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.AccessPolicyProvider;
|
||||
import org.apache.nifi.authorization.Group;
|
||||
import org.apache.nifi.authorization.ManagedAuthorizer;
|
||||
import org.apache.nifi.authorization.UserAndGroups;
|
||||
import org.apache.nifi.authorization.UserGroupProvider;
|
||||
import org.apache.nifi.authorization.user.NiFiUser;
|
||||
import org.apache.nifi.authorization.user.NiFiUserDetails;
|
||||
import org.apache.nifi.idp.IdpUserGroup;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardJwtAuthenticationConverterTest {
|
||||
private static final String USERNAME = "NiFi";
|
||||
|
||||
private static final String AUTHORIZER_GROUP = "AuthorizerGroup";
|
||||
|
||||
private static final String PROVIDER_GROUP = "ProviderGroup";
|
||||
|
||||
private static final String TYPE_FIELD = "typ";
|
||||
|
||||
private static final String JWT_TYPE = "JWT";
|
||||
|
||||
@Mock
|
||||
private ManagedAuthorizer authorizer;
|
||||
|
||||
@Mock
|
||||
private AccessPolicyProvider accessPolicyProvider;
|
||||
|
||||
@Mock
|
||||
private UserGroupProvider userGroupProvider;
|
||||
|
||||
@Mock
|
||||
private UserAndGroups userAndGroups;
|
||||
|
||||
@Mock
|
||||
private IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private StandardJwtAuthenticationConverter converter;
|
||||
|
||||
@Before
|
||||
public void setConverter() {
|
||||
final Map<String, String> properties = new HashMap<>();
|
||||
final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(StringUtils.EMPTY, properties);
|
||||
converter = new StandardJwtAuthenticationConverter(authorizer, idpUserGroupService, niFiProperties);
|
||||
|
||||
when(authorizer.getAccessPolicyProvider()).thenReturn(accessPolicyProvider);
|
||||
when(accessPolicyProvider.getUserGroupProvider()).thenReturn(userGroupProvider);
|
||||
when(userGroupProvider.getUserAndGroups(eq(USERNAME))).thenReturn(userAndGroups);
|
||||
|
||||
final Group group = new Group.Builder().name(AUTHORIZER_GROUP).identifier(AUTHORIZER_GROUP).build();
|
||||
when(userAndGroups.getGroups()).thenReturn(Collections.singleton(group));
|
||||
|
||||
final IdpUserGroup idpUserGroup = new IdpUserGroup();
|
||||
idpUserGroup.setGroupName(PROVIDER_GROUP);
|
||||
when(idpUserGroupService.getUserGroups(eq(USERNAME))).thenReturn(Collections.singletonList(idpUserGroup));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvert() {
|
||||
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
|
||||
.subject(USERNAME)
|
||||
.build();
|
||||
final String token = new PlainJWT(claimsSet).serialize();
|
||||
final Jwt jwt = Jwt.withTokenValue(token)
|
||||
.header(TYPE_FIELD, JWT_TYPE)
|
||||
.subject(USERNAME)
|
||||
.build();
|
||||
|
||||
final NiFiAuthenticationToken authenticationToken = converter.convert(jwt);
|
||||
assertNotNull(authenticationToken);
|
||||
assertEquals(USERNAME, authenticationToken.toString());
|
||||
|
||||
final NiFiUserDetails details = (NiFiUserDetails) authenticationToken.getDetails();
|
||||
final NiFiUser user = details.getNiFiUser();
|
||||
|
||||
final Set<String> expectedGroups = Collections.singleton(AUTHORIZER_GROUP);
|
||||
assertEquals(expectedGroups, user.getGroups());
|
||||
|
||||
final Set<String> expectedProviderGroups = Collections.singleton(PROVIDER_GROUP);
|
||||
assertEquals(expectedProviderGroups, user.getIdentityProviderGroups());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.jws;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardJwsSignerProviderTest {
|
||||
private static final String KEY_IDENTIFIER = UUID.randomUUID().toString();
|
||||
|
||||
@Mock
|
||||
private SigningKeyListener signingKeyListener;
|
||||
|
||||
@Mock
|
||||
private JwsSignerContainer jwsSignerContainer;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<String> keyIdentifierCaptor;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Instant> expirationCaptor;
|
||||
|
||||
private StandardJwsSignerProvider provider;
|
||||
|
||||
@Before
|
||||
public void setProvider() {
|
||||
provider = new StandardJwsSignerProvider(signingKeyListener);
|
||||
when(jwsSignerContainer.getKeyIdentifier()).thenReturn(KEY_IDENTIFIER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnSignerUpdated() {
|
||||
provider.onSignerUpdated(jwsSignerContainer);
|
||||
final Instant expiration = Instant.now();
|
||||
final JwsSignerContainer container = provider.getJwsSignerContainer(expiration);
|
||||
|
||||
assertEquals("JWS Signer Container not matched", jwsSignerContainer, container);
|
||||
|
||||
verify(signingKeyListener).onSigningKeyUsed(keyIdentifierCaptor.capture(), expirationCaptor.capture());
|
||||
assertEquals(KEY_IDENTIFIER, keyIdentifierCaptor.getValue());
|
||||
assertEquals(expiration, expirationCaptor.getValue());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.key.command;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer;
|
||||
import org.apache.nifi.web.security.jwt.jws.SignerListener;
|
||||
import org.apache.nifi.web.security.jwt.key.VerificationKeyListener;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.security.Key;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class KeyGenerationCommandTest {
|
||||
private static final String KEY_ALGORITHM = "RSA";
|
||||
|
||||
private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.PS512;
|
||||
|
||||
@Mock
|
||||
private SignerListener signerListener;
|
||||
|
||||
@Mock
|
||||
private VerificationKeyListener verificationKeyListener;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<JwsSignerContainer> signerCaptor;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<String> keyIdentifierCaptor;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Key> keyCaptor;
|
||||
|
||||
private KeyGenerationCommand command;
|
||||
|
||||
@Before
|
||||
public void setCommand() {
|
||||
command = new KeyGenerationCommand(signerListener, verificationKeyListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRun() {
|
||||
command.run();
|
||||
|
||||
verify(signerListener).onSignerUpdated(signerCaptor.capture());
|
||||
final JwsSignerContainer signerContainer = signerCaptor.getValue();
|
||||
assertEquals(JWS_ALGORITHM, signerContainer.getJwsAlgorithm());
|
||||
|
||||
verify(verificationKeyListener).onVerificationKeyGenerated(keyIdentifierCaptor.capture(), keyCaptor.capture());
|
||||
final Key key = keyCaptor.getValue();
|
||||
assertEquals(KEY_ALGORITHM, key.getAlgorithm());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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.key.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.nifi.components.state.Scope;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateMap;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Key;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardVerificationKeyServiceTest {
|
||||
private static final String ID = UUID.randomUUID().toString();
|
||||
|
||||
private static final String ALGORITHM = "RSA";
|
||||
|
||||
private static final byte[] ENCODED = ALGORITHM.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
private static final Scope SCOPE = Scope.LOCAL;
|
||||
|
||||
private static final Instant EXPIRED = Instant.now().minusSeconds(60);
|
||||
|
||||
@Mock
|
||||
private StateManager stateManager;
|
||||
|
||||
@Mock
|
||||
private StateMap stateMap;
|
||||
|
||||
@Mock
|
||||
private Key key;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Map<String, String>> stateCaptor;
|
||||
|
||||
private StandardVerificationKeyService service;
|
||||
|
||||
@Before
|
||||
public void setService() {
|
||||
service = new StandardVerificationKeyService(stateManager);
|
||||
when(key.getAlgorithm()).thenReturn(ALGORITHM);
|
||||
when(key.getEncoded()).thenReturn(ENCODED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteExpired() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
|
||||
final String serialized = getSerializedVerificationKey(EXPIRED);
|
||||
when(stateMap.toMap()).thenReturn(Collections.singletonMap(ID, serialized));
|
||||
|
||||
service.deleteExpired();
|
||||
|
||||
verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE));
|
||||
final Map<String, String> stateSaved = stateCaptor.getValue();
|
||||
assertTrue("Expired Key not deleted", stateSaved.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSave() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(stateMap.toMap()).thenReturn(Collections.emptyMap());
|
||||
|
||||
final Instant expiration = Instant.now();
|
||||
service.save(ID, key, expiration);
|
||||
|
||||
verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE));
|
||||
final Map<String, String> stateSaved = stateCaptor.getValue();
|
||||
final String serialized = stateSaved.get(ID);
|
||||
assertNotNull("Serialized Key not found", serialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetExpiration() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(stateMap.toMap()).thenReturn(Collections.emptyMap());
|
||||
|
||||
final Instant expiration = Instant.now();
|
||||
final String serialized = getSerializedVerificationKey(expiration);
|
||||
when(stateMap.get(eq(ID))).thenReturn(serialized);
|
||||
|
||||
service.setExpiration(ID, expiration);
|
||||
|
||||
verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE));
|
||||
final Map<String, String> stateSaved = stateCaptor.getValue();
|
||||
final String saved = stateSaved.get(ID);
|
||||
assertNotNull("Serialized Key not found", saved);
|
||||
}
|
||||
|
||||
private String getSerializedVerificationKey(final Instant expiration) throws JsonProcessingException {
|
||||
final VerificationKey verificationKey = new VerificationKey();
|
||||
verificationKey.setId(ID);
|
||||
verificationKey.setExpiration(expiration);
|
||||
return OBJECT_MAPPER.writeValueAsString(verificationKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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.provider;
|
||||
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSSigner;
|
||||
import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import org.apache.nifi.web.security.jwt.jws.JwsSignerContainer;
|
||||
import org.apache.nifi.web.security.jwt.jws.JwsSignerProvider;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardBearerTokenProviderTest {
|
||||
private static final String USERNAME = "USERNAME";
|
||||
|
||||
private static final String IDENTITY = "IDENTITY";
|
||||
|
||||
private static final long EXPIRATION = 60;
|
||||
|
||||
private static final String ISSUER = "ISSUER";
|
||||
|
||||
private static final String KEY_ALGORITHM = "RSA";
|
||||
|
||||
private static final int KEY_SIZE = 4096;
|
||||
|
||||
private static final JWSAlgorithm JWS_ALGORITHM = JWSAlgorithm.PS512;
|
||||
|
||||
@Mock
|
||||
private JwsSignerProvider jwsSignerProvider;
|
||||
|
||||
private StandardBearerTokenProvider provider;
|
||||
|
||||
private JWSVerifier jwsVerifier;
|
||||
|
||||
private JWSSigner jwsSigner;
|
||||
|
||||
@Before
|
||||
public void setProvider() throws NoSuchAlgorithmException {
|
||||
provider = new StandardBearerTokenProvider(jwsSignerProvider);
|
||||
|
||||
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
|
||||
keyPairGenerator.initialize(KEY_SIZE);
|
||||
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyPair.getPublic());
|
||||
jwsSigner = new RSASSASigner(keyPair.getPrivate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetBearerToken() throws ParseException, JOSEException {
|
||||
final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(IDENTITY, USERNAME, EXPIRATION, ISSUER);
|
||||
final String keyIdentifier = UUID.randomUUID().toString();
|
||||
final JwsSignerContainer jwsSignerContainer = new JwsSignerContainer(keyIdentifier, JWS_ALGORITHM, jwsSigner);
|
||||
when(jwsSignerProvider.getJwsSignerContainer(isA(Instant.class))).thenReturn(jwsSignerContainer);
|
||||
|
||||
final String bearerToken = provider.getBearerToken(loginAuthenticationToken);
|
||||
|
||||
final SignedJWT signedJwt = SignedJWT.parse(bearerToken);
|
||||
assertTrue("Verification Failed", signedJwt.verify(jwsVerifier));
|
||||
|
||||
final JWTClaimsSet claims = signedJwt.getJWTClaimsSet();
|
||||
assertNotNull("Issue Time not found", claims.getIssueTime());
|
||||
assertNotNull("Not Before Time Time not found", claims.getNotBeforeTime());
|
||||
assertNotNull("Expiration Time Time not found", claims.getExpirationTime());
|
||||
assertEquals(ISSUER, claims.getIssuer());
|
||||
assertEquals(Collections.singletonList(ISSUER), claims.getAudience());
|
||||
assertEquals(IDENTITY, claims.getSubject());
|
||||
assertEquals(USERNAME, claims.getClaim(SupportedClaim.PREFERRED_USERNAME.getClaim()));
|
||||
assertNotNull("JSON Web Token Identifier not found", claims.getJWTID());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.resolver;
|
||||
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
import org.apache.nifi.web.security.http.SecurityHeader;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardBearerTokenResolverTest {
|
||||
private static final String BEARER_TOKEN = "TOKEN";
|
||||
|
||||
private StandardBearerTokenResolver resolver;
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Before
|
||||
public void setResolver() {
|
||||
resolver = new StandardBearerTokenResolver();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveAuthorizationHeaderFound() {
|
||||
setHeader(String.format("Bearer %s", BEARER_TOKEN));
|
||||
assertEquals(BEARER_TOKEN, resolver.resolve(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveAuthorizationHeaderMissingPrefix() {
|
||||
setHeader(BEARER_TOKEN);
|
||||
assertNull(resolver.resolve(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveAuthorizationHeaderIncorrectPrefix() {
|
||||
setHeader(String.format("Basic %s", BEARER_TOKEN));
|
||||
assertNull(resolver.resolve(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveCookieFound() {
|
||||
final Cookie cookie = new Cookie(SecurityCookieName.AUTHORIZATION_BEARER.getName(), BEARER_TOKEN);
|
||||
when(request.getCookies()).thenReturn(new Cookie[]{cookie});
|
||||
assertEquals(BEARER_TOKEN, resolver.resolve(request));
|
||||
}
|
||||
|
||||
private void setHeader(final String header) {
|
||||
when(request.getHeader(eq(SecurityHeader.AUTHORIZATION.getHeader()))).thenReturn(header);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class JwtRevocationValidatorTest {
|
||||
private static final String ID = UUID.randomUUID().toString();
|
||||
|
||||
private static final String TOKEN = "TOKEN";
|
||||
|
||||
private static final String TYPE_FIELD = "typ";
|
||||
|
||||
private static final String JWT_TYPE = "JWT";
|
||||
|
||||
@Mock
|
||||
private JwtRevocationService jwtRevocationService;
|
||||
|
||||
private Jwt jwt;
|
||||
|
||||
private JwtRevocationValidator validator;
|
||||
|
||||
@Before
|
||||
public void setValidator() {
|
||||
validator = new JwtRevocationValidator(jwtRevocationService);
|
||||
jwt = Jwt.withTokenValue(TOKEN).header(TYPE_FIELD, JWT_TYPE).jti(ID).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateSuccess() {
|
||||
when(jwtRevocationService.isRevoked(eq(ID))).thenReturn(false);
|
||||
final OAuth2TokenValidatorResult result = validator.validate(jwt);
|
||||
assertFalse(result.hasErrors());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateFailure() {
|
||||
when(jwtRevocationService.isRevoked(eq(ID))).thenReturn(true);
|
||||
final OAuth2TokenValidatorResult result = validator.validate(jwt);
|
||||
assertTrue(result.hasErrors());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardJwtLogoutListenerTest {
|
||||
private static final String ID = UUID.randomUUID().toString();
|
||||
|
||||
private static final Instant EXPIRES = Instant.now();
|
||||
|
||||
private static final String TOKEN = "TOKEN";
|
||||
|
||||
private static final String TYPE_FIELD = "typ";
|
||||
|
||||
private static final String JWT_TYPE = "JWT";
|
||||
|
||||
@Mock
|
||||
private JwtRevocationService jwtRevocationService;
|
||||
|
||||
@Mock
|
||||
private JwtDecoder jwtDecoder;
|
||||
|
||||
private Jwt jwt;
|
||||
|
||||
private StandardJwtLogoutListener listener;
|
||||
|
||||
@Before
|
||||
public void setListener() {
|
||||
listener = new StandardJwtLogoutListener(jwtDecoder, jwtRevocationService);
|
||||
jwt = Jwt.withTokenValue(TOKEN).header(TYPE_FIELD, JWT_TYPE).jti(ID).expiresAt(EXPIRES).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutBearerTokenNullZeroInteractions() {
|
||||
listener.logout(null);
|
||||
verifyZeroInteractions(jwtDecoder);
|
||||
verifyZeroInteractions(jwtRevocationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutBearerToken() {
|
||||
when(jwtDecoder.decode(eq(TOKEN))).thenReturn(jwt);
|
||||
|
||||
listener.logout(TOKEN);
|
||||
|
||||
verify(jwtRevocationService).setRevoked(eq(ID), eq(EXPIRES));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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.revocation;
|
||||
|
||||
import org.apache.nifi.components.state.Scope;
|
||||
import org.apache.nifi.components.state.StateManager;
|
||||
import org.apache.nifi.components.state.StateMap;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class StandardJwtRevocationServiceTest {
|
||||
private static final String ID = UUID.randomUUID().toString();
|
||||
|
||||
private static final Scope SCOPE = Scope.LOCAL;
|
||||
|
||||
private static final Instant EXPIRED = Instant.now().minusSeconds(60);
|
||||
|
||||
@Mock
|
||||
private StateManager stateManager;
|
||||
|
||||
@Mock
|
||||
private StateMap stateMap;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Map<String, String>> stateCaptor;
|
||||
|
||||
private StandardJwtRevocationService service;
|
||||
|
||||
@Before
|
||||
public void setService() {
|
||||
service = new StandardJwtRevocationService(stateManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteExpired() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(stateMap.toMap()).thenReturn(Collections.singletonMap(ID, EXPIRED.toString()));
|
||||
|
||||
service.deleteExpired();
|
||||
|
||||
verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE));
|
||||
final Map<String, String> stateSaved = stateCaptor.getValue();
|
||||
assertTrue("Expired Key not deleted", stateSaved.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsRevokedFound() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(stateMap.toMap()).thenReturn(Collections.singletonMap(ID, EXPIRED.toString()));
|
||||
|
||||
assertTrue(service.isRevoked(ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsRevokedNotFound() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(stateMap.toMap()).thenReturn(Collections.emptyMap());
|
||||
|
||||
assertFalse(service.isRevoked(ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetRevoked() throws IOException {
|
||||
when(stateManager.getState(eq(SCOPE))).thenReturn(stateMap);
|
||||
when(stateMap.toMap()).thenReturn(Collections.emptyMap());
|
||||
|
||||
final Instant expiration = Instant.now();
|
||||
service.setRevoked(ID, expiration);
|
||||
|
||||
verify(stateManager).setState(stateCaptor.capture(), eq(SCOPE));
|
||||
final Map<String, String> stateSaved = stateCaptor.getValue();
|
||||
final String saved = stateSaved.get(ID);
|
||||
assertEquals("Expiration not matched", expiration.toString(), saved);
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
package org.apache.nifi.web.security.saml.impl;
|
||||
|
||||
import org.apache.nifi.web.security.jwt.JwtService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.saml.SAMLStateManager;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.junit.Before;
|
||||
|
@ -32,13 +32,13 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
public class TestStandardSAMLStateManager {
|
||||
|
||||
private JwtService jwtService;
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
private SAMLStateManager stateManager;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
jwtService = mock(JwtService.class);
|
||||
stateManager = new StandardSAMLStateManager(jwtService);
|
||||
bearerTokenProvider = mock(BearerTokenProvider.class);
|
||||
stateManager = new StandardSAMLStateManager(bearerTokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -76,7 +76,7 @@ public class TestStandardSAMLStateManager {
|
|||
|
||||
// create the jwt and cache it
|
||||
final String fakeJwt = "fake-jwt";
|
||||
when(jwtService.generateSignedToken(token)).thenReturn(fakeJwt);
|
||||
when(bearerTokenProvider.getBearerToken(token)).thenReturn(fakeJwt);
|
||||
stateManager.createJwt(requestId, token);
|
||||
|
||||
// should return the jwt above
|
||||
|
|
|
@ -339,34 +339,22 @@
|
|||
<dependency>
|
||||
<groupId>com.nimbusds</groupId>
|
||||
<artifactId>oauth2-oidc-sdk</artifactId>
|
||||
<version>6.16.2</version>
|
||||
<version>9.10.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.minidev</groupId>
|
||||
<artifactId>json-smart</artifactId>
|
||||
<version>2.3.1</version>
|
||||
<version>2.4.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.nimbusds</groupId>
|
||||
<artifactId>lang-tag</artifactId>
|
||||
<version>1.4.4</version>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.nimbusds</groupId>
|
||||
<artifactId>nimbus-jose-jwt</artifactId>
|
||||
<version>7.9</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<version>9.11.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.jettison</groupId>
|
||||
|
@ -583,6 +571,21 @@
|
|||
<artifactId>spring-context</artifactId>
|
||||
<version>${spring.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-resource-server</artifactId>
|
||||
<version>${spring.security.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-core</artifactId>
|
||||
<version>${spring.security.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
<version>${spring.security.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
|
|
Loading…
Reference in New Issue