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:
exceptionfactory 2021-07-28 16:45:39 -05:00 committed by Nathan Gough
parent 9bcbf83e5a
commit a652280fbb
85 changed files with 3045 additions and 3144 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}
}
}
}

View File

@ -23,8 +23,6 @@ public interface DAOFactory {
ActionDAO getActionDAO();
KeyDAO getKeyDAO();
IdpCredentialDAO getIdpCredentialDAO();
IdpUserGroupDAO getIdpUserGroupDAO();

View File

@ -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);

View File

@ -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);
}
}
}

View File

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

View File

@ -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() {
}
}

View File

@ -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"/>

View File

@ -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");

View File

@ -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 />

View File

@ -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}

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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();
}

View File

@ -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) {

View File

@ -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"/>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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) {

View File

@ -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);
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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());
}
}

View File

@ -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>";
}
}

View File

@ -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);
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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());
}
}

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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())) {

View File

@ -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 {

View File

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

View File

@ -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();

View File

@ -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>

View File

@ -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

View File

@ -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")

View File

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

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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>