mirror of
https://github.com/apache/nifi.git
synced 2025-02-28 14:39:10 +00:00
NIFI-9849 Refactored SAML Support with Spring Security 5
- Updated SAML Authentication Configuration with Spring Security SAML 2 components - Updated Administration Guide with REST Resources - Replaced SAMLAccessResource methods with applicable Spring Security Filters - Removed IDP Credential Service and supporting components - Removed message.logging.enabled, metadata.signing.enabled, and signature.digest.algorithm properties - Added Access Token Expiration resource method - Removed Saml2AccessResource and replaced with Access Token Expiration to avoid unnecessary conflicts with SAML login consumer - Corrected Resource URI handling to support proxy server access Signed-off-by: Nathan Gough <thenatog@gmail.com> This closes #6149.
This commit is contained in:
parent
1465c2c629
commit
0de83292de
@ -18,15 +18,22 @@ package org.apache.nifi.web.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Request URI Builder encapsulates URI construction handling supported HTTP proxy request headers
|
||||
*/
|
||||
public class RequestUriBuilder {
|
||||
private static final String ALLOWED_CONTEXT_PATHS_PARAMETER = "allowedContextPaths";
|
||||
|
||||
private static final String COMMA_SEPARATOR = ",";
|
||||
|
||||
private final String scheme;
|
||||
|
||||
private final String host;
|
||||
@ -44,6 +51,17 @@ public class RequestUriBuilder {
|
||||
this.contextPath = contextPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Builder from HTTP Servlet Request using Scheme, Host, Port, and Context Path reading from headers
|
||||
*
|
||||
* @param httpServletRequest HTTP Servlet Request
|
||||
* @return Request URI Builder
|
||||
*/
|
||||
public static RequestUriBuilder fromHttpServletRequest(final HttpServletRequest httpServletRequest) {
|
||||
final List<String> allowedContextPaths = getAllowedContextPathsConfigured(httpServletRequest);
|
||||
return fromHttpServletRequest(httpServletRequest, allowedContextPaths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Builder from HTTP Servlet Request using Scheme, Host, Port, and Context Path reading from headers
|
||||
*
|
||||
@ -85,4 +103,11 @@ public class RequestUriBuilder {
|
||||
throw new IllegalArgumentException("Build URI Failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> getAllowedContextPathsConfigured(final HttpServletRequest httpServletRequest) {
|
||||
final ServletContext servletContext = httpServletRequest.getServletContext();
|
||||
final String allowedContextPathsParameter = servletContext.getInitParameter(ALLOWED_CONTEXT_PATHS_PARAMETER);
|
||||
final String[] allowedContextPathsParsed = StringUtils.split(allowedContextPathsParameter, COMMA_SEPARATOR);
|
||||
return allowedContextPathsParsed == null ? Collections.emptyList() : Arrays.asList(allowedContextPathsParsed);
|
||||
}
|
||||
}
|
||||
|
@ -468,6 +468,9 @@ JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the di
|
||||
|
||||
To enable authentication via SAML the following properties must be configured in _nifi.properties_.
|
||||
|
||||
Configuring a Metadata URL and an Entity Identifier enables Apache NiFi to act as a SAML 2.0 Relying Party, allowing users
|
||||
to authenticate using an account managed through a SAML 2.0 Asserting Party.
|
||||
|
||||
[options="header"]
|
||||
|==================================================================================================================================================
|
||||
| Property Name | Description
|
||||
@ -475,19 +478,30 @@ To enable authentication via SAML the following properties must be configured in
|
||||
|`nifi.security.user.saml.sp.entity.id`| The entity id of the service provider (i.e. NiFi). This value will be used as the `Issuer` for SAML authentication requests and should be a valid URI. In some cases the service provider entity id must be registered ahead of time with the identity provider.
|
||||
|`nifi.security.user.saml.identity.attribute.name`| The name of a SAML assertion attribute containing the user'sidentity. This property is optional and if not specified, or if the attribute is not found, then the NameID of the Subject will be used.
|
||||
|`nifi.security.user.saml.group.attribute.name`| The name of a SAML assertion attribute containing group names the user belongs to. This property is optional, but if populated the groups will be passed along to the authorization process.
|
||||
|`nifi.security.user.saml.metadata.signing.enabled`| Enables signing of the generated service provider metadata. The default value is `false`.
|
||||
|`nifi.security.user.saml.request.signing.enabled`| Controls the value of `AuthnRequestsSigned` in the generated service provider metadata from `nifi-api/access/saml/metadata`. This indicates that the service provider (i.e. NiFi) should not sign authentication requests sent to the identity provider, but the requests may still need to be signed if the identity provider indicates `WantAuthnRequestSigned=true`. The default value is `false`.
|
||||
|`nifi.security.user.saml.want.assertions.signed`| Controls the value of `WantAssertionsSigned` in the generated service provider metadata from `nifi-api/access/saml/metadata`. This indicates that the identity provider should sign assertions, but some identity providers may provide their own configuration for controlling whether assertions are signed. The default value is `true`.
|
||||
|`nifi.security.user.saml.signature.algorithm`| The algorithm to use when signing SAML messages. Reference the link:https://git.shibboleth.net/view/?p=java-xmltooling.git;a=blob;f=src/main/java/org/opensaml/xml/signature/SignatureConstants.java[Open SAML Signature Constants] for a list of valid values. If not specified, a default of SHA-256 will be used. The default value is `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`.
|
||||
|`nifi.security.user.saml.signature.digest.algorithm`| The digest algorithm to use when signing SAML messages. Reference the link:https://git.shibboleth.net/view/?p=java-xmltooling.git;a=blob;f=src/main/java/org/opensaml/xml/signature/SignatureConstants.java[Open SAML Signature Constants] for a list of valid values. If not specified, a default of SHA-256 will be used. The default value is `http://www.w3.org/2001/04/xmlenc#sha256`.
|
||||
|`nifi.security.user.saml.message.logging.enabled`| Enables logging of SAML messages for debugging purposes. The default value is `false`.
|
||||
|`nifi.security.user.saml.authentication.expiration`| The expiration of the NiFi JWT that will be produced from a successful SAML authentication response. The default value is `12 hours`.
|
||||
|`nifi.security.user.saml.single.logout.enabled`| Enables SAML SingleLogout which causes a logout from NiFi to logout of the identity provider. By default, a logout of NiFi will only remove the NiFi JWT. The default value is `false`.
|
||||
|`nifi.security.user.saml.http.client.truststore.strategy`| The truststore strategy when the IDP metadata URL begins with https. A value of `JDK` indicates to use the JDK's default truststore. A value of`NIFI`indicates to use the truststore specified by `nifi.security.truststore`.
|
||||
|`nifi.security.user.saml.http.client.truststore.strategy`| The truststore strategy when the IDP metadata URL begins with https. A value of `JDK` indicates to use the JDK's default truststore. A value of `NIFI` indicates to use the truststore specified by `nifi.security.truststore`.
|
||||
|`nifi.security.user.saml.http.client.connect.timeout`| The connection timeout when communicating with the SAML IDP. The default value is `30 secs`.
|
||||
|`nifi.security.user.saml.http.client.read.timeout`| The read timeout when communicating with the SAML IDP. The default value is `30 secs`.
|
||||
|==================================================================================================================================================
|
||||
|
||||
==== SAML REST Resources
|
||||
|
||||
SAML authentication enables the following REST API resources for integration with a SAML 2.0 Asserting Party:
|
||||
|
||||
[options="header"]
|
||||
|======================================
|
||||
| Resource Path | Description
|
||||
| /nifi-api/access/saml/local-logout/request | Complete SAML 2.0 Logout processing without communicating with the Asserting Party
|
||||
| /nifi-api/access/saml/login/consumer | Process SAML 2.0 Login Requests assertions using HTTP-POST or HTTP-REDIRECT binding
|
||||
| /nifi-api/access/saml/metadata | Retrieve SAML 2.0 entity descriptor metadata as XML
|
||||
| /nifi-api/access/saml/single-logout/consumer | Process SAML 2.0 Single Logout Request assertions using HTTP-POST or HTTP-REDIRECT binding. Requires Single Logout to be enabled.
|
||||
| /nifi-api/access/saml/single-logout/request | Complete SAML 2.0 Single Logout processing initiating a request to the Asserting Party. Requires Single Logout to be enabled.
|
||||
|======================================
|
||||
|
||||
[[apache_knox]]
|
||||
=== Apache Knox
|
||||
|
||||
|
@ -43,17 +43,6 @@ public class IdpDataSourceFactoryBean implements FactoryBean<JdbcConnectionPool>
|
||||
// idp tables
|
||||
// ----------
|
||||
|
||||
private static final String IDP_CREDENTIAL_TABLE_NAME = "IDENTITY_PROVIDER_CREDENTIAL";
|
||||
|
||||
private static final String CREATE_IDP_CREDENTIAL_TABLE = "CREATE TABLE " + IDP_CREDENTIAL_TABLE_NAME + " ("
|
||||
+ "ID INT NOT NULL PRIMARY KEY AUTO_INCREMENT, "
|
||||
+ "IDENTITY VARCHAR2(4096) NOT NULL, "
|
||||
+ "IDP_TYPE VARCHAR2(200) NOT NULL, "
|
||||
+ "CREDENTIAL BLOB NOT NULL, "
|
||||
+ "CREATED TIMESTAMP NOT NULL, "
|
||||
+ "CONSTRAINT UK__IDENTITY UNIQUE (IDENTITY)"
|
||||
+ ")";
|
||||
|
||||
private static final String IDP_USER_GROUP_TABLE_NAME = "IDENTITY_PROVIDER_USER_GROUP";
|
||||
|
||||
private static final String CREATE_IDP_USER_GROUP_TABLE = "CREATE TABLE " + IDP_USER_GROUP_TABLE_NAME + " ("
|
||||
@ -108,9 +97,8 @@ public class IdpDataSourceFactoryBean implements FactoryBean<JdbcConnectionPool>
|
||||
statement = connection.createStatement();
|
||||
|
||||
// determine if the idp tables need to be created
|
||||
rs = connection.getMetaData().getTables(null, null, IDP_CREDENTIAL_TABLE_NAME, null);
|
||||
rs = connection.getMetaData().getTables(null, null, IDP_USER_GROUP_TABLE_NAME, null);
|
||||
if (!rs.next()) {
|
||||
statement.execute(CREATE_IDP_CREDENTIAL_TABLE);
|
||||
statement.execute(CREATE_IDP_USER_GROUP_TABLE);
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,6 @@ public interface DAOFactory {
|
||||
|
||||
ActionDAO getActionDAO();
|
||||
|
||||
IdpCredentialDAO getIdpCredentialDAO();
|
||||
|
||||
IdpUserGroupDAO getIdpUserGroupDAO();
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ package org.apache.nifi.admin.dao.impl;
|
||||
|
||||
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 java.sql.Connection;
|
||||
@ -39,11 +38,6 @@ public class DAOFactoryImpl implements DAOFactory {
|
||||
return new StandardActionDAO(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredentialDAO getIdpCredentialDAO() {
|
||||
return new StandardIdpCredentialDAO(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpUserGroupDAO getIdpUserGroupDAO() {
|
||||
return new StandardIdpUserGroupDAO(connection);
|
||||
|
@ -1,189 +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 org.apache.nifi.admin.RepositoryUtils;
|
||||
import org.apache.nifi.admin.dao.DataAccessException;
|
||||
import org.apache.nifi.admin.dao.IdpCredentialDAO;
|
||||
import org.apache.nifi.idp.IdpCredential;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Date;
|
||||
|
||||
public class StandardIdpCredentialDAO implements IdpCredentialDAO {
|
||||
|
||||
private static final String INSERT_CREDENTIAL = "INSERT INTO IDENTITY_PROVIDER_CREDENTIAL " +
|
||||
"(IDENTITY, IDP_TYPE, CREDENTIAL, CREATED) VALUES (?, ?, ?, ?)";
|
||||
|
||||
private static final String SELECT_CREDENTIAL_BY_ID = "SELECT ID, IDENTITY, IDP_TYPE, CREDENTIAL, CREATED " +
|
||||
"FROM IDENTITY_PROVIDER_CREDENTIAL " +
|
||||
"WHERE ID = ?";
|
||||
|
||||
private static final String SELECT_CREDENTIAL_BY_IDENTITY = "SELECT ID, IDENTITY, IDP_TYPE, CREDENTIAL, CREATED " +
|
||||
"FROM IDENTITY_PROVIDER_CREDENTIAL " +
|
||||
"WHERE IDENTITY = ?";
|
||||
|
||||
private static final String DELETE_CREDENTIAL_BY_ID = "DELETE FROM IDENTITY_PROVIDER_CREDENTIAL " +
|
||||
"WHERE ID = ?";
|
||||
|
||||
private static final String DELETE_CREDENTIAL_BY_IDENTITY = "DELETE FROM IDENTITY_PROVIDER_CREDENTIAL " +
|
||||
"WHERE IDENTITY = ?";
|
||||
|
||||
private final Connection connection;
|
||||
|
||||
public StandardIdpCredentialDAO(final Connection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential createCredential(final IdpCredential credential) throws DataAccessException {
|
||||
if (credential == null) {
|
||||
throw new IllegalArgumentException("Credential cannot be null");
|
||||
}
|
||||
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// populate the parameters
|
||||
statement = connection.prepareStatement(INSERT_CREDENTIAL, Statement.RETURN_GENERATED_KEYS);
|
||||
statement.setString(1, credential.getIdentity());
|
||||
statement.setString(2, credential.getType().name());
|
||||
statement.setBytes(3, credential.getCredential());
|
||||
statement.setTimestamp(4, new java.sql.Timestamp(credential.getCreated().getTime()));
|
||||
|
||||
// execute the insert
|
||||
int updateCount = statement.executeUpdate();
|
||||
rs = statement.getGeneratedKeys();
|
||||
|
||||
// verify the results
|
||||
if (updateCount == 1 && rs.next()) {
|
||||
credential.setId(rs.getInt(1));
|
||||
return credential;
|
||||
} else {
|
||||
throw new DataAccessException("Unable to save IDP credential.");
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential findCredentialById(final int id) throws DataAccessException {
|
||||
IdpCredential credential = null;
|
||||
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// set parameters
|
||||
statement = connection.prepareStatement(SELECT_CREDENTIAL_BY_ID);
|
||||
statement.setInt(1, id);
|
||||
|
||||
// execute the query
|
||||
rs = statement.executeQuery();
|
||||
|
||||
// if the credential was found, add it
|
||||
if (rs.next()) {
|
||||
credential = new IdpCredential();
|
||||
populateCredential(rs, credential);
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential findCredentialByIdentity(final String identity) throws DataAccessException {
|
||||
IdpCredential credential = null;
|
||||
|
||||
PreparedStatement statement = null;
|
||||
ResultSet rs = null;
|
||||
try {
|
||||
// set parameters
|
||||
statement = connection.prepareStatement(SELECT_CREDENTIAL_BY_IDENTITY);
|
||||
statement.setString(1, identity);
|
||||
|
||||
// execute the query
|
||||
rs = statement.executeQuery();
|
||||
|
||||
// if the credential was found, add it
|
||||
if (rs.next()) {
|
||||
credential = new IdpCredential();
|
||||
populateCredential(rs, credential);
|
||||
}
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(rs);
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteCredentialById(int id) throws DataAccessException {
|
||||
PreparedStatement statement = null;
|
||||
try {
|
||||
statement = connection.prepareStatement(DELETE_CREDENTIAL_BY_ID);
|
||||
statement.setInt(1, id);
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} catch (DataAccessException dae) {
|
||||
throw dae;
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteCredentialByIdentity(String identity) throws DataAccessException {
|
||||
PreparedStatement statement = null;
|
||||
try {
|
||||
statement = connection.prepareStatement(DELETE_CREDENTIAL_BY_IDENTITY);
|
||||
statement.setString(1, identity);
|
||||
return statement.executeUpdate();
|
||||
} catch (SQLException sqle) {
|
||||
throw new DataAccessException(sqle);
|
||||
} catch (DataAccessException dae) {
|
||||
throw dae;
|
||||
} finally {
|
||||
RepositoryUtils.closeQuietly(statement);
|
||||
}
|
||||
}
|
||||
|
||||
private void populateCredential(final ResultSet rs, final IdpCredential credential) throws SQLException {
|
||||
credential.setId(rs.getInt("ID"));
|
||||
credential.setIdentity(rs.getString("IDENTITY"));
|
||||
credential.setType(IdpType.valueOf(rs.getString("IDP_TYPE")));
|
||||
credential.setCredential(rs.getBytes("CREDENTIAL"));
|
||||
credential.setCreated(new Date(rs.getTimestamp("CREATED").getTime()));
|
||||
}
|
||||
}
|
@ -1,57 +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;
|
||||
|
||||
import org.apache.nifi.idp.IdpCredential;
|
||||
|
||||
/**
|
||||
* Manages IDP Credentials.
|
||||
*/
|
||||
public interface IdpCredentialService {
|
||||
|
||||
/**
|
||||
* Creates the given credential.
|
||||
*
|
||||
* @param credential the credential
|
||||
* @return the credential with the id
|
||||
*/
|
||||
IdpCredential createCredential(IdpCredential credential);
|
||||
|
||||
/**
|
||||
* Gets the credential for the given identity.
|
||||
*
|
||||
* @param identity the user identity
|
||||
* @return the credential or null if one does not exist for the given identity
|
||||
*/
|
||||
IdpCredential getCredential(String identity);
|
||||
|
||||
/**
|
||||
* Deletes the credential with the given id.
|
||||
*
|
||||
* @param id the credential id
|
||||
*/
|
||||
void deleteCredential(int id);
|
||||
|
||||
/**
|
||||
* Replaces the credential for the given user identity.
|
||||
*
|
||||
* @param credential the new credential
|
||||
* @return the credential with the id
|
||||
*/
|
||||
IdpCredential replaceCredential(IdpCredential credential);
|
||||
|
||||
}
|
@ -1,213 +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.IdpCredentialService;
|
||||
import org.apache.nifi.admin.service.action.CreateIdpCredentialAction;
|
||||
import org.apache.nifi.admin.service.action.DeleteIdpCredentialByIdAction;
|
||||
import org.apache.nifi.admin.service.action.DeleteIdpCredentialByIdentityAction;
|
||||
import org.apache.nifi.admin.service.action.GetIdpCredentialByIdentity;
|
||||
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.idp.IdpCredential;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* Database implementation of IdpCredentialService.
|
||||
*/
|
||||
public class StandardIdpCredentialService implements IdpCredentialService {
|
||||
|
||||
private static Logger LOGGER = LoggerFactory.getLogger(StandardIdpCredentialService.class);
|
||||
|
||||
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
private final Lock readLock = lock.readLock();
|
||||
private final Lock writeLock = lock.writeLock();
|
||||
|
||||
private TransactionBuilder transactionBuilder;
|
||||
|
||||
@Override
|
||||
public IdpCredential createCredential(final IdpCredential credential) {
|
||||
Transaction transaction = null;
|
||||
IdpCredential createdCredential;
|
||||
|
||||
writeLock.lock();
|
||||
try {
|
||||
// ensure the created date is set
|
||||
if (credential.getCreated() == null) {
|
||||
credential.setCreated(new Date());
|
||||
}
|
||||
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// create the credential
|
||||
final CreateIdpCredentialAction action = new CreateIdpCredentialAction(credential);
|
||||
createdCredential = transaction.execute(action);
|
||||
|
||||
// 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 createdCredential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential getCredential(final String identity) {
|
||||
Transaction transaction = null;
|
||||
IdpCredential credential;
|
||||
|
||||
readLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// get the credential
|
||||
final GetIdpCredentialByIdentity action = new GetIdpCredentialByIdentity(identity);
|
||||
credential = transaction.execute(action);
|
||||
|
||||
// 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 credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteCredential(final int id) {
|
||||
Transaction transaction = null;
|
||||
|
||||
writeLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// delete the credential
|
||||
final DeleteIdpCredentialByIdAction action = new DeleteIdpCredentialByIdAction(id);
|
||||
Integer rowsDeleted = transaction.execute(action);
|
||||
if (rowsDeleted == 0) {
|
||||
LOGGER.warn("No IDP credential was found to delete for id " + id);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential replaceCredential(final IdpCredential credential) {
|
||||
final String identity = credential.getIdentity();
|
||||
if (StringUtils.isBlank(identity)) {
|
||||
throw new IllegalArgumentException("Identity is required");
|
||||
}
|
||||
|
||||
Transaction transaction = null;
|
||||
IdpCredential createdCredential;
|
||||
|
||||
writeLock.lock();
|
||||
try {
|
||||
// start the transaction
|
||||
transaction = transactionBuilder.start();
|
||||
|
||||
// delete the credential
|
||||
final DeleteIdpCredentialByIdentityAction deleteAction = new DeleteIdpCredentialByIdentityAction(identity);
|
||||
Integer rowsDeleted = transaction.execute(deleteAction);
|
||||
if (rowsDeleted == 0) {
|
||||
LOGGER.debug("No IDP credential was found to delete for id " + identity);
|
||||
}
|
||||
|
||||
// ensure the created date is set for the new credential
|
||||
if (credential.getCreated() == null) {
|
||||
credential.setCreated(new Date());
|
||||
}
|
||||
|
||||
// create the new credential
|
||||
final CreateIdpCredentialAction createAction = new CreateIdpCredentialAction(credential);
|
||||
createdCredential = transaction.execute(createAction);
|
||||
|
||||
// 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 createdCredential;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -1,84 +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.idp;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class IdpCredential {
|
||||
|
||||
private int id;
|
||||
private String identity;
|
||||
private IdpType type;
|
||||
private byte[] credential;
|
||||
private Date created;
|
||||
|
||||
public IdpCredential() {
|
||||
|
||||
}
|
||||
|
||||
public IdpCredential(int id, String identity, IdpType type, byte[] credential) {
|
||||
this(id, identity, type, credential, new Date());
|
||||
}
|
||||
|
||||
public IdpCredential(int id, String identity, IdpType type, byte[] credential, Date created) {
|
||||
this.id = id;
|
||||
this.identity = identity;
|
||||
this.type = type;
|
||||
this.credential = credential;
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getIdentity() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
public void setIdentity(String identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
public IdpType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(IdpType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public byte[] getCredential() {
|
||||
return credential;
|
||||
}
|
||||
|
||||
public void setCredential(byte[] credential) {
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
}
|
@ -43,11 +43,6 @@
|
||||
<property name="transactionBuilder" ref="auditTransactionBuilder"/>
|
||||
</bean>
|
||||
|
||||
<!-- idp credential service -->
|
||||
<bean id="idpCredentialService" class="org.apache.nifi.admin.service.impl.StandardIdpCredentialService">
|
||||
<property name="transactionBuilder" ref="idpTransactionBuilder"/>
|
||||
</bean>
|
||||
|
||||
<!-- idp user group service -->
|
||||
<bean id="idpUserGroupService" class="org.apache.nifi.admin.service.impl.StandardIdpUserGroupService">
|
||||
<property name="transactionBuilder" ref="idpTransactionBuilder"/>
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.api.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import org.apache.nifi.web.api.dto.util.InstantAdapter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.time.Instant;
|
||||
|
||||
@XmlRootElement(name = "accessTokenExpiration")
|
||||
public class AccessTokenExpirationDTO {
|
||||
|
||||
private Instant expiration;
|
||||
|
||||
@XmlJavaTypeAdapter(InstantAdapter.class)
|
||||
@ApiModelProperty(
|
||||
value = "Token Expiration",
|
||||
dataType = "string",
|
||||
accessMode = ApiModelProperty.AccessMode.READ_ONLY
|
||||
)
|
||||
public Instant getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public void setExpiration(Instant expiration) {
|
||||
this.expiration = expiration;
|
||||
}
|
||||
}
|
@ -14,22 +14,25 @@
|
||||
* 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.api.dto.util;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.IdpCredentialDAO;
|
||||
import javax.xml.bind.annotation.adapters.XmlAdapter;
|
||||
import java.time.Instant;
|
||||
|
||||
public class DeleteIdpCredentialByIdAction implements AdministrationAction<Integer> {
|
||||
/**
|
||||
* XmlAdapter for (un)marshalling an Instant
|
||||
*/
|
||||
public class InstantAdapter extends XmlAdapter<String, Instant> {
|
||||
|
||||
private final Integer id;
|
||||
public static final String DEFAULT_DATE_TIME_FORMAT = "MM/dd/yyyy HH:mm:ss z";
|
||||
|
||||
public DeleteIdpCredentialByIdAction(final Integer id) {
|
||||
this.id = id;
|
||||
@Override
|
||||
public String marshal(Instant instant) throws Exception {
|
||||
return instant.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer execute(DAOFactory daoFactory) {
|
||||
final IdpCredentialDAO dao = daoFactory.getIdpCredentialDAO();
|
||||
return dao.deleteCredentialById(id);
|
||||
public Instant unmarshal(String instant) throws Exception {
|
||||
return Instant.parse(instant);
|
||||
}
|
||||
}
|
@ -14,22 +14,23 @@
|
||||
* 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.api.entity;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.IdpCredentialDAO;
|
||||
import org.apache.nifi.web.api.dto.AccessTokenExpirationDTO;
|
||||
|
||||
public class DeleteIdpCredentialByIdentityAction implements AdministrationAction<Integer> {
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
|
||||
private final String identity;
|
||||
@XmlRootElement(name = "accessTokenExpirationEntity")
|
||||
public class AccessTokenExpirationEntity extends Entity {
|
||||
|
||||
public DeleteIdpCredentialByIdentityAction(final String identity) {
|
||||
this.identity = identity;
|
||||
private AccessTokenExpirationDTO accessTokenExpiration;
|
||||
|
||||
public AccessTokenExpirationDTO getAccessTokenExpiration() {
|
||||
return accessTokenExpiration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer execute(DAOFactory daoFactory) {
|
||||
final IdpCredentialDAO dao = daoFactory.getIdpCredentialDAO();
|
||||
return dao.deleteCredentialByIdentity(identity);
|
||||
public void setAccessTokenExpiration(AccessTokenExpirationDTO accessTokenExpiration) {
|
||||
this.accessTokenExpiration = accessTokenExpiration;
|
||||
}
|
||||
|
||||
}
|
@ -202,6 +202,10 @@
|
||||
<groupId>log4j</groupId>
|
||||
<artifactId>log4j</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
@ -184,12 +184,9 @@
|
||||
<nifi.security.user.saml.sp.entity.id />
|
||||
<nifi.security.user.saml.identity.attribute.name />
|
||||
<nifi.security.user.saml.group.attribute.name />
|
||||
<nifi.security.user.saml.metadata.signing.enabled>false</nifi.security.user.saml.metadata.signing.enabled>
|
||||
<nifi.security.user.saml.request.signing.enabled>false</nifi.security.user.saml.request.signing.enabled>
|
||||
<nifi.security.user.saml.want.assertions.signed>true</nifi.security.user.saml.want.assertions.signed>
|
||||
<nifi.security.user.saml.signature.algorithm>http://www.w3.org/2001/04/xmldsig-more#rsa-sha256</nifi.security.user.saml.signature.algorithm>
|
||||
<nifi.security.user.saml.signature.digest.algorithm>http://www.w3.org/2001/04/xmlenc#sha256</nifi.security.user.saml.signature.digest.algorithm>
|
||||
<nifi.security.user.saml.message.logging.enabled>false</nifi.security.user.saml.message.logging.enabled>
|
||||
<nifi.security.user.saml.authentication.expiration>12 hours</nifi.security.user.saml.authentication.expiration>
|
||||
<nifi.security.user.saml.single.logout.enabled>false</nifi.security.user.saml.single.logout.enabled>
|
||||
<nifi.security.user.saml.http.client.truststore.strategy>JDK</nifi.security.user.saml.http.client.truststore.strategy>
|
||||
|
@ -215,12 +215,9 @@ nifi.security.user.saml.idp.metadata.url=${nifi.security.user.saml.idp.metadata.
|
||||
nifi.security.user.saml.sp.entity.id=${nifi.security.user.saml.sp.entity.id}
|
||||
nifi.security.user.saml.identity.attribute.name=${nifi.security.user.saml.identity.attribute.name}
|
||||
nifi.security.user.saml.group.attribute.name=${nifi.security.user.saml.group.attribute.name}
|
||||
nifi.security.user.saml.metadata.signing.enabled=${nifi.security.user.saml.metadata.signing.enabled}
|
||||
nifi.security.user.saml.request.signing.enabled=${nifi.security.user.saml.request.signing.enabled}
|
||||
nifi.security.user.saml.want.assertions.signed=${nifi.security.user.saml.want.assertions.signed}
|
||||
nifi.security.user.saml.signature.algorithm=${nifi.security.user.saml.signature.algorithm}
|
||||
nifi.security.user.saml.signature.digest.algorithm=${nifi.security.user.saml.signature.digest.algorithm}
|
||||
nifi.security.user.saml.message.logging.enabled=${nifi.security.user.saml.message.logging.enabled}
|
||||
nifi.security.user.saml.authentication.expiration=${nifi.security.user.saml.authentication.expiration}
|
||||
nifi.security.user.saml.single.logout.enabled=${nifi.security.user.saml.single.logout.enabled}
|
||||
nifi.security.user.saml.http.client.truststore.strategy=${nifi.security.user.saml.http.client.truststore.strategy}
|
||||
|
@ -137,6 +137,8 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
||||
private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
|
||||
private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$";
|
||||
|
||||
private static final String ALLOWED_CONTEXT_PATHS_PARAMETER = "allowedContextPaths";
|
||||
|
||||
private static final String CONTEXT_PATH_ALL = "/*";
|
||||
private static final String CONTEXT_PATH_ROOT = "/";
|
||||
private static final String CONTEXT_PATH_NIFI = "/nifi";
|
||||
@ -296,7 +298,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
||||
webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled()));
|
||||
webUiContext.getInitParams().put("saml-supported", String.valueOf(props.isSamlEnabled()));
|
||||
webUiContext.getInitParams().put("saml-single-logout-supported", String.valueOf(props.isSamlSingleLogoutEnabled()));
|
||||
webUiContext.getInitParams().put("allowedContextPaths", props.getAllowedContextPaths());
|
||||
webAppContextHandlers.addHandler(webUiContext);
|
||||
|
||||
// load the web api app
|
||||
@ -318,7 +319,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
||||
|
||||
// load the web error app
|
||||
final WebAppContext webErrorContext = loadWar(webErrorWar, CONTEXT_PATH_ROOT, frameworkClassLoader);
|
||||
webErrorContext.getInitParams().put("allowedContextPaths", props.getAllowedContextPaths());
|
||||
webAppContextHandlers.addHandler(webErrorContext);
|
||||
|
||||
// deploy the web apps
|
||||
@ -586,6 +586,7 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
|
||||
|
||||
private WebAppContext loadWar(final File warFile, final String contextPath, final ClassLoader parentClassLoader) {
|
||||
final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
|
||||
webappContext.getInitParams().put(ALLOWED_CONTEXT_PATHS_PARAMETER, props.getAllowedContextPaths());
|
||||
webappContext.setContextPath(contextPath);
|
||||
webappContext.setDisplayName(contextPath);
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
package org.apache.nifi.web.server.connector;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.nifi.jetty.configuration.connector.ApplicationLayerProtocol;
|
||||
import org.apache.nifi.jetty.configuration.connector.StandardServerConnectorFactory;
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
package org.apache.nifi.web.server.log;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.jetty.server.CustomRequestLog;
|
||||
import org.eclipse.jetty.server.RequestLog;
|
||||
import org.eclipse.jetty.server.Slf4jRequestLogWriter;
|
||||
|
@ -98,7 +98,6 @@ public class NiFiWebApiResourceConfig extends ResourceConfig {
|
||||
register(ctx.getBean("countersResource"));
|
||||
register(ctx.getBean("systemDiagnosticsResource"));
|
||||
register(ctx.getBean("accessResource"));
|
||||
register(ctx.getBean("samlResource"));
|
||||
register(ctx.getBean("oidcResource"));
|
||||
register(ctx.getBean("accessPolicyResource"));
|
||||
register(ctx.getBean("tenantsResource"));
|
||||
|
@ -21,19 +21,23 @@ import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
|
||||
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
|
||||
import org.apache.nifi.web.security.csrf.CsrfCookieRequestMatcher;
|
||||
import org.apache.nifi.web.security.csrf.StandardCookieCsrfTokenRepository;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
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.log.AuthenticationUserFilter;
|
||||
import org.apache.nifi.web.security.oidc.OIDCEndpoints;
|
||||
import org.apache.nifi.web.security.saml.SAMLEndpoints;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2LocalLogoutFilter;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2SingleLogoutFilter;
|
||||
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.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@ -44,6 +48,11 @@ 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.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
@ -71,6 +80,17 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||
private NiFiAnonymousAuthenticationFilter anonymousAuthenticationFilter;
|
||||
private NiFiAnonymousAuthenticationProvider anonymousAuthenticationProvider;
|
||||
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter;
|
||||
private Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter;
|
||||
private Saml2MetadataFilter saml2MetadataFilter;
|
||||
private Saml2LogoutRequestFilter saml2LogoutRequestFilter;
|
||||
private Saml2LogoutResponseFilter saml2LogoutResponseFilter;
|
||||
private Saml2SingleLogoutFilter saml2SingleLogoutFilter;
|
||||
private Saml2LocalLogoutFilter saml2LocalLogoutFilter;
|
||||
private AuthenticationProvider openSamlAuthenticationProvider;
|
||||
|
||||
public NiFiWebApiSecurityConfiguration() {
|
||||
super(true); // disable defaults
|
||||
}
|
||||
@ -95,15 +115,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||
OIDCEndpoints.LOGOUT_CALLBACK,
|
||||
"/access/knox/callback",
|
||||
"/access/knox/request",
|
||||
SAMLEndpoints.SERVICE_PROVIDER_METADATA,
|
||||
SAMLEndpoints.LOGIN_REQUEST,
|
||||
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 authentication filters
|
||||
SAMLEndpoints.SINGLE_LOGOUT_REQUEST,
|
||||
SAMLEndpoints.SINGLE_LOGOUT_CONSUMER,
|
||||
SAMLEndpoints.LOCAL_LOGOUT,
|
||||
"/access/logout/complete");
|
||||
}
|
||||
|
||||
@ -117,6 +128,21 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||
new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, new CsrfCookieRequestMatcher()))
|
||||
.csrfTokenRepository(new StandardCookieCsrfTokenRepository(properties.getAllowedContextPathsAsList()));
|
||||
|
||||
if (properties.isSamlEnabled()) {
|
||||
http.addFilterBefore(saml2WebSsoAuthenticationFilter, AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(saml2WebSsoAuthenticationRequestFilter, AnonymousAuthenticationFilter.class);
|
||||
|
||||
// Metadata and Logout Filters must be invoked prior to CSRF or other security filtering
|
||||
http.addFilterBefore(saml2MetadataFilter, CsrfFilter.class);
|
||||
http.addFilterBefore(saml2LocalLogoutFilter, CsrfFilter.class);
|
||||
|
||||
if (properties.isSamlSingleLogoutEnabled()) {
|
||||
http.addFilterBefore(saml2SingleLogoutFilter, CsrfFilter.class);
|
||||
http.addFilterBefore(saml2LogoutRequestFilter, CsrfFilter.class);
|
||||
http.addFilterBefore(saml2LogoutResponseFilter, CsrfFilter.class);
|
||||
}
|
||||
}
|
||||
|
||||
http.addFilterBefore(x509FilterBean(), AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(bearerTokenAuthenticationFilter(), AnonymousAuthenticationFilter.class);
|
||||
http.addFilterBefore(knoxFilterBean(), AnonymousAuthenticationFilter.class);
|
||||
@ -141,6 +167,10 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||
.authenticationProvider(jwtAuthenticationProvider)
|
||||
.authenticationProvider(knoxAuthenticationProvider)
|
||||
.authenticationProvider(anonymousAuthenticationProvider);
|
||||
|
||||
if (properties.isSamlEnabled()) {
|
||||
auth.authenticationProvider(openSamlAuthenticationProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -221,4 +251,50 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||
public void setPrincipalExtractor(X509PrincipalExtractor principalExtractor) {
|
||||
this.principalExtractor = principalExtractor;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) {
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2WebSsoAuthenticationFilter(final Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter) {
|
||||
this.saml2WebSsoAuthenticationFilter = saml2WebSsoAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2WebSsoAuthenticationRequestFilter(final Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter) {
|
||||
this.saml2WebSsoAuthenticationRequestFilter = saml2WebSsoAuthenticationRequestFilter;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2MetadataFilter(final Saml2MetadataFilter saml2MetadataFilter) {
|
||||
this.saml2MetadataFilter = saml2MetadataFilter;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2LogoutRequestFilter(final Saml2LogoutRequestFilter saml2LogoutRequestFilter) {
|
||||
this.saml2LogoutRequestFilter = saml2LogoutRequestFilter;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2LogoutResponseFilter(final Saml2LogoutResponseFilter saml2LogoutResponseFilter) {
|
||||
this.saml2LogoutResponseFilter = saml2LogoutResponseFilter;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2SingleLogoutFilter(final Saml2SingleLogoutFilter saml2SingleLogoutFilter) {
|
||||
this.saml2SingleLogoutFilter = saml2SingleLogoutFilter;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setSaml2LocalLogoutFilter(final Saml2LocalLogoutFilter saml2LocalLogoutFilter) {
|
||||
this.saml2LocalLogoutFilter = saml2LocalLogoutFilter;
|
||||
}
|
||||
|
||||
@Qualifier("openSamlAuthenticationProvider")
|
||||
@Autowired
|
||||
public void setOpenSamlAuthenticationProvider(final AuthenticationProvider openSamlAuthenticationProvider) {
|
||||
this.openSamlAuthenticationProvider = openSamlAuthenticationProvider;
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,9 @@ import org.apache.nifi.authorization.user.NiFiUserDetails;
|
||||
import org.apache.nifi.authorization.user.NiFiUserUtils;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.api.dto.AccessTokenExpirationDTO;
|
||||
import org.apache.nifi.web.api.entity.AccessTokenExpirationEntity;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
|
||||
import org.apache.nifi.web.api.dto.AccessStatusDTO;
|
||||
import org.apache.nifi.web.api.entity.AccessConfigurationEntity;
|
||||
@ -58,6 +60,8 @@ 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.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
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;
|
||||
@ -78,6 +82,7 @@ import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.net.URI;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -102,6 +107,7 @@ public class AccessResource extends ApplicationResource {
|
||||
private LoginIdentityProvider loginIdentityProvider;
|
||||
private JwtAuthenticationProvider jwtAuthenticationProvider;
|
||||
private JwtLogoutListener jwtLogoutListener;
|
||||
private JwtDecoder jwtDecoder;
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
private BearerTokenResolver bearerTokenResolver;
|
||||
private KnoxService knoxService;
|
||||
@ -432,6 +438,36 @@ public class AccessResource extends ApplicationResource {
|
||||
return generateCreatedResponse(uri, bearerToken).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/token/expiration")
|
||||
@ApiOperation(
|
||||
value = "Get expiration for current Access Token",
|
||||
notes = NON_GUARANTEED_ENDPOINT,
|
||||
response = AccessTokenExpirationEntity.class
|
||||
)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(code = 200, message = "Access Token Expiration found"),
|
||||
@ApiResponse(code = 401, message = "Access Token not authorized"),
|
||||
@ApiResponse(code = 409, message = "Access Token not resolved"),
|
||||
}
|
||||
)
|
||||
public Response getAccessTokenExpiration() {
|
||||
final String bearerToken = bearerTokenResolver.resolve(httpServletRequest);
|
||||
if (bearerToken == null) {
|
||||
throw new IllegalStateException("Access Token not found");
|
||||
} else {
|
||||
final Jwt jwt = jwtDecoder.decode(bearerToken);
|
||||
final Instant expiration = jwt.getExpiresAt();
|
||||
final AccessTokenExpirationDTO accessTokenExpiration = new AccessTokenExpirationDTO();
|
||||
accessTokenExpiration.setExpiration(expiration);
|
||||
final AccessTokenExpirationEntity accessTokenExpirationEntity = new AccessTokenExpirationEntity();
|
||||
accessTokenExpirationEntity.setAccessTokenExpiration(accessTokenExpiration);
|
||||
return Response.ok(accessTokenExpirationEntity).build();
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@ -459,15 +495,15 @@ public class AccessResource extends ApplicationResource {
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("Logout Started [{}]", mappedUserIdentity);
|
||||
logger.debug("Removing Authorization Cookie [{}]", mappedUserIdentity);
|
||||
final String requestIdentifier = UUID.randomUUID().toString();
|
||||
logger.info("Logout Request [{}] Identity [{}] started", requestIdentifier, mappedUserIdentity);
|
||||
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
|
||||
|
||||
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);
|
||||
final LogoutRequest logoutRequest = new LogoutRequest(requestIdentifier, mappedUserIdentity);
|
||||
logoutRequestManager.start(logoutRequest);
|
||||
|
||||
applicationCookieService.addCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER, logoutRequest.getRequestIdentifier());
|
||||
@ -560,6 +596,10 @@ public class AccessResource extends ApplicationResource {
|
||||
this.jwtAuthenticationProvider = jwtAuthenticationProvider;
|
||||
}
|
||||
|
||||
public void setJwtDecoder(final JwtDecoder jwtDecoder) {
|
||||
this.jwtDecoder = jwtDecoder;
|
||||
}
|
||||
|
||||
public void setJwtLogoutListener(final JwtLogoutListener jwtLogoutListener) {
|
||||
this.jwtLogoutListener = jwtLogoutListener;
|
||||
}
|
||||
|
@ -50,9 +50,9 @@ import org.apache.nifi.util.ComponentIdGenerator;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.NiFiServiceFacade;
|
||||
import org.apache.nifi.web.Revision;
|
||||
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.api.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.api.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.api.dto.ControllerServiceDTO;
|
||||
import org.apache.nifi.web.api.dto.ControllerServiceReferencingComponentDTO;
|
||||
import org.apache.nifi.web.api.dto.RevisionDTO;
|
||||
|
@ -42,7 +42,7 @@ import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
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;
|
||||
|
@ -1,554 +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.api;
|
||||
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException;
|
||||
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.api.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.saml.SAMLCredentialStore;
|
||||
import org.apache.nifi.web.security.saml.SAMLEndpoints;
|
||||
import org.apache.nifi.web.security.saml.SAMLService;
|
||||
import org.apache.nifi.web.security.saml.SAMLStateManager;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path(SAMLEndpoints.SAML_ACCESS_ROOT)
|
||||
@Api(
|
||||
value = SAMLEndpoints.SAML_ACCESS_ROOT,
|
||||
description = "Endpoints for authenticating, obtaining an access token or logging out of a configured SAML authentication provider."
|
||||
)
|
||||
public class SAMLAccessResource extends ApplicationResource {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SAMLAccessResource.class);
|
||||
private static final String SAML_METADATA_MEDIA_TYPE = "application/samlmetadata+xml";
|
||||
private static final String LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND = "The logout request identifier was not found in the request. Unable to continue.";
|
||||
private static final String LOGOUT_REQUEST_NOT_FOUND_FOR_GIVEN_IDENTIFIER = "No logout request was found for the given identifier. Unable to continue.";
|
||||
private static final boolean LOGGING_IN = true;
|
||||
|
||||
private SAMLService samlService;
|
||||
private SAMLStateManager samlStateManager;
|
||||
private SAMLCredentialStore samlCredentialStore;
|
||||
private IdpUserGroupService idpUserGroupService;
|
||||
private LogoutRequestManager logoutRequestManager;
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(SAML_METADATA_MEDIA_TYPE)
|
||||
@Path(SAMLEndpoints.SERVICE_PROVIDER_METADATA_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Retrieves the service provider metadata.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public Response samlMetadata(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
|
||||
// only consider user specific access over https
|
||||
if (!httpServletRequest.isSecure()) {
|
||||
throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
|
||||
}
|
||||
|
||||
// ensure saml is enabled
|
||||
if (!samlService.isSamlEnabled()) {
|
||||
logger.debug(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
|
||||
return Response.status(Response.Status.CONFLICT).entity(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED).build();
|
||||
}
|
||||
|
||||
// ensure saml service provider is initialized
|
||||
initializeSamlServiceProvider();
|
||||
|
||||
final String metadataXml = samlService.getServiceProviderMetadata();
|
||||
return Response.ok(metadataXml, SAML_METADATA_MEDIA_TYPE).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.LOGIN_REQUEST_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Initiates an SSO request to the configured SAML identity provider.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlLoginRequest(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, LOGGING_IN));
|
||||
|
||||
// ensure saml service provider is initialized
|
||||
initializeSamlServiceProvider();
|
||||
|
||||
final String samlRequestIdentifier = UUID.randomUUID().toString();
|
||||
|
||||
// generate a cookie to associate this login sequence
|
||||
applicationCookieService.addCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.SAML_REQUEST_IDENTIFIER, samlRequestIdentifier);
|
||||
|
||||
// get the state for this request
|
||||
final String relayState = samlStateManager.createState(samlRequestIdentifier);
|
||||
|
||||
// initiate the login request
|
||||
try {
|
||||
samlService.initiateLogin(httpServletRequest, httpServletResponse, relayState);
|
||||
} catch (Exception e) {
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.LOGIN_CONSUMER_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Processes the SSO response from the SAML identity provider for HTTP-POST binding.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlLoginHttpPostConsumer(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse,
|
||||
MultivaluedMap<String, String> formParams) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, LOGGING_IN));
|
||||
|
||||
// process the response from the idp...
|
||||
final Map<String, String> parameters = getParameterMap(formParams);
|
||||
samlLoginConsumer(httpServletRequest, httpServletResponse, parameters);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.LOGIN_CONSUMER_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Processes the SSO response from the SAML identity provider for HTTP-REDIRECT binding.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlLoginHttpRedirectConsumer(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse,
|
||||
@Context UriInfo uriInfo) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, LOGGING_IN));
|
||||
|
||||
// process the response from the idp...
|
||||
final Map<String, String> parameters = getParameterMap(uriInfo.getQueryParameters());
|
||||
samlLoginConsumer(httpServletRequest, httpServletResponse, parameters);
|
||||
}
|
||||
|
||||
private void samlLoginConsumer(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Map<String, String> parameters) throws Exception {
|
||||
// ensure saml service provider is initialized
|
||||
initializeSamlServiceProvider();
|
||||
|
||||
// ensure the request has the cookie with the request id
|
||||
final Optional<String> requestIdentifier = getSamlRequestIdentifier();
|
||||
if (!requestIdentifier.isPresent()) {
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was not found in the request. Unable to continue.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure a RelayState value was sent back
|
||||
final String requestState = parameters.get("RelayState");
|
||||
if (requestState == null) {
|
||||
removeSamlRequestCookie(httpServletResponse);
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "The RelayState parameter was not found in the request. Unable to continue.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure the RelayState value in the request matches the store state
|
||||
final String samlRequestIdentifier = requestIdentifier.get();
|
||||
if (!samlStateManager.isStateValid(samlRequestIdentifier, requestState)) {
|
||||
logger.error("The RelayState value returned by the SAML IDP does not match the stored state. Unable to continue login process.");
|
||||
removeSamlRequestCookie(httpServletResponse);
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Purposed RelayState does not match the stored state. Unable to continue login process.");
|
||||
return;
|
||||
}
|
||||
|
||||
// process the SAML response
|
||||
final SAMLCredential samlCredential;
|
||||
try {
|
||||
samlCredential = samlService.processLogin(httpServletRequest, httpServletResponse, parameters);
|
||||
} catch (Exception e) {
|
||||
removeSamlRequestCookie(httpServletResponse);
|
||||
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// create the login token
|
||||
final String rawIdentity = samlService.getUserIdentity(samlCredential);
|
||||
final String mappedIdentity = IdentityMappingUtil.mapIdentity(rawIdentity, IdentityMappingUtil.getIdentityMappings(properties));
|
||||
final long expiration = samlService.getAuthExpiration();
|
||||
final String issuer = samlCredential.getRemoteEntityID();
|
||||
|
||||
final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(mappedIdentity, mappedIdentity, expiration, issuer);
|
||||
|
||||
// create and cache a NiFi JWT that can be retrieved later from the exchange end-point
|
||||
samlStateManager.createJwt(samlRequestIdentifier, loginToken);
|
||||
|
||||
// store the SAMLCredential for retrieval during logout
|
||||
samlCredentialStore.save(mappedIdentity, samlCredential);
|
||||
|
||||
// get the user's groups from the assertions if the exist and store them for later retrieval
|
||||
final Set<String> userGroups = samlService.getUserGroups(samlCredential);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("SAML User '{}' belongs to the unmapped groups {}", mappedIdentity, StringUtils.join(userGroups));
|
||||
}
|
||||
|
||||
final List<IdentityMapping> groupIdentityMappings = IdentityMappingUtil.getGroupMappings(properties);
|
||||
final Set<String> mappedGroups = userGroups.stream()
|
||||
.map(g -> IdentityMappingUtil.mapIdentity(g, groupIdentityMappings))
|
||||
.collect(Collectors.toSet());
|
||||
logger.info("SAML User '{}' belongs to the mapped groups {}", mappedIdentity, StringUtils.join(mappedGroups));
|
||||
|
||||
idpUserGroupService.replaceUserGroups(mappedIdentity, IdpType.SAML, mappedGroups);
|
||||
|
||||
// redirect to the name page
|
||||
httpServletResponse.sendRedirect(getNiFiUri());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Path(SAMLEndpoints.LOGIN_EXCHANGE_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Retrieves a JWT following a successful login sequence using the configured SAML identity provider.",
|
||||
response = String.class,
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public Response samlLoginExchange(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse) {
|
||||
|
||||
// only consider user specific access over https
|
||||
if (!httpServletRequest.isSecure()) {
|
||||
throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
|
||||
}
|
||||
|
||||
// ensure saml is enabled
|
||||
if (!samlService.isSamlEnabled()) {
|
||||
logger.debug(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
|
||||
return Response.status(Response.Status.CONFLICT).entity(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED).build();
|
||||
}
|
||||
|
||||
// ensure saml service provider is initialized
|
||||
initializeSamlServiceProvider();
|
||||
|
||||
// ensure the request has the cookie with the request identifier
|
||||
final Optional<String> requestIdentifier = getSamlRequestIdentifier();
|
||||
if (!requestIdentifier.isPresent()) {
|
||||
final String message = "The login request identifier was not found in the request. Unable to continue.";
|
||||
logger.warn(message);
|
||||
return Response.status(Response.Status.BAD_REQUEST).entity(message).build();
|
||||
}
|
||||
|
||||
removeSamlRequestCookie(httpServletResponse);
|
||||
final String samlRequestIdentifier = requestIdentifier.get();
|
||||
final String jwt = samlStateManager.getJwt(samlRequestIdentifier);
|
||||
if (jwt == null) {
|
||||
throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue.");
|
||||
}
|
||||
|
||||
logger.info("SAML Login Request [{}] Completed", samlRequestIdentifier);
|
||||
setBearerToken(httpServletResponse, jwt);
|
||||
return generateOkResponse(jwt).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.SINGLE_LOGOUT_REQUEST_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Initiates a logout request using the SingleLogout service of the configured SAML identity provider.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlSingleLogoutRequest(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, !LOGGING_IN));
|
||||
|
||||
// ensure the logout request identifier is present
|
||||
final Optional<String> cookieValue = getLogoutRequestIdentifier();
|
||||
if (!cookieValue.isPresent()) {
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure there is a logout request in progress for the given identifier
|
||||
final String logoutRequestIdentifier = cookieValue.get();
|
||||
final LogoutRequest logoutRequest = logoutRequestManager.get(logoutRequestIdentifier);
|
||||
if (logoutRequest == null) {
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_NOT_FOUND_FOR_GIVEN_IDENTIFIER);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure saml service provider is initialized
|
||||
initializeSamlServiceProvider();
|
||||
|
||||
final String userIdentity = logoutRequest.getMappedUserIdentity();
|
||||
logger.info("Attempting to performing SAML Single Logout for {}", userIdentity);
|
||||
|
||||
// retrieve the credential that was stored during the login sequence
|
||||
final SAMLCredential samlCredential = samlCredentialStore.get(userIdentity);
|
||||
if (samlCredential == null) {
|
||||
throw new IllegalStateException("Unable to find a stored SAML credential for " + userIdentity);
|
||||
}
|
||||
|
||||
// initiate the logout
|
||||
try {
|
||||
logger.info("Initiating SAML Single Logout with IDP...");
|
||||
samlService.initiateLogout(httpServletRequest, httpServletResponse, samlCredential);
|
||||
} catch (final Exception e) {
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.SINGLE_LOGOUT_CONSUMER_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Processes a SingleLogout message from the configured SAML identity provider using the HTTP-REDIRECT binding.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlSingleLogoutHttpRedirectConsumer(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse,
|
||||
@Context UriInfo uriInfo) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, !LOGGING_IN));
|
||||
|
||||
// process the SLO request
|
||||
final Map<String, String> parameters = getParameterMap(uriInfo.getQueryParameters());
|
||||
samlSingleLogoutConsumer(httpServletRequest, httpServletResponse, parameters);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.SINGLE_LOGOUT_CONSUMER_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Processes a SingleLogout message from the configured SAML identity provider using the HTTP-POST binding.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlSingleLogoutHttpPostConsumer(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse,
|
||||
MultivaluedMap<String, String> formParams) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, !LOGGING_IN));
|
||||
|
||||
// process the SLO request
|
||||
final Map<String, String> parameters = getParameterMap(formParams);
|
||||
samlSingleLogoutConsumer(httpServletRequest, httpServletResponse, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common logic for consuming SAML Single Logout messages from either HTTP-POST or HTTP-REDIRECT.
|
||||
*
|
||||
* @param httpServletRequest the request
|
||||
* @param httpServletResponse the response
|
||||
* @param parameters additional parameters
|
||||
* @throws Exception if an error occurs
|
||||
*/
|
||||
private void samlSingleLogoutConsumer(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
|
||||
Map<String, String> parameters) throws Exception {
|
||||
|
||||
// ensure saml service provider is initialized
|
||||
initializeSamlServiceProvider();
|
||||
|
||||
// ensure the logout request identifier is present
|
||||
final Optional<String> requestIdentifier = getLogoutRequestIdentifier();
|
||||
if (!requestIdentifier.isPresent()) {
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure there is a logout request in progress for the given identifier
|
||||
final String logoutRequestIdentifier = requestIdentifier.get();
|
||||
final LogoutRequest logoutRequest = logoutRequestManager.get(logoutRequestIdentifier);
|
||||
if (logoutRequest == null) {
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_NOT_FOUND_FOR_GIVEN_IDENTIFIER);
|
||||
return;
|
||||
}
|
||||
|
||||
// complete the logout request so it is no longer cached
|
||||
logoutRequestManager.complete(logoutRequestIdentifier);
|
||||
|
||||
// remove the cookie with the logout request identifier
|
||||
removeLogoutRequestCookie(httpServletResponse);
|
||||
|
||||
// get the user identity from the logout request
|
||||
final String identity = logoutRequest.getMappedUserIdentity();
|
||||
logger.info("Consuming SAML Single Logout for {}", identity);
|
||||
|
||||
// remove the saved credential
|
||||
samlCredentialStore.delete(identity);
|
||||
|
||||
// delete any stored groups
|
||||
idpUserGroupService.deleteUserGroups(identity);
|
||||
|
||||
// process the Single Logout SAML message
|
||||
try {
|
||||
samlService.processLogout(httpServletRequest, httpServletResponse, parameters);
|
||||
logger.info("Completed SAML Single Logout for {}", identity);
|
||||
} catch (Exception e) {
|
||||
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to the logout landing page
|
||||
httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
@Produces(MediaType.WILDCARD)
|
||||
@Path(SAMLEndpoints.LOCAL_LOGOUT_RELATIVE)
|
||||
@ApiOperation(
|
||||
value = "Local logout when SAML is enabled, does not communicate with the IDP.",
|
||||
notes = NON_GUARANTEED_ENDPOINT
|
||||
)
|
||||
public void samlLocalLogout(@Context HttpServletRequest httpServletRequest,
|
||||
@Context HttpServletResponse httpServletResponse) throws Exception {
|
||||
|
||||
assert(isSamlEnabled(httpServletRequest, httpServletResponse, !LOGGING_IN));
|
||||
|
||||
// complete the logout request if one exists
|
||||
final Optional<String> cookieValue = getLogoutRequestIdentifier();
|
||||
if (cookieValue.isPresent()) {
|
||||
final String logoutRequestIdentifier = cookieValue.get();
|
||||
final LogoutRequest logoutRequest = logoutRequestManager.complete(logoutRequestIdentifier);
|
||||
|
||||
final String mappedUserIdentity = logoutRequest.getMappedUserIdentity();
|
||||
samlCredentialStore.delete(mappedUserIdentity);
|
||||
idpUserGroupService.deleteUserGroups(mappedUserIdentity);
|
||||
|
||||
logger.info("Logout Request [{}] Identity [{}] SAML Local Logout Completed", logoutRequestIdentifier, mappedUserIdentity);
|
||||
} else {
|
||||
logger.warn("Logout Request Cookie [{}] not found", ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName());
|
||||
}
|
||||
|
||||
removeLogoutRequestCookie(httpServletResponse);
|
||||
// redirect to logout landing page
|
||||
httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri());
|
||||
}
|
||||
|
||||
private void initializeSamlServiceProvider() {
|
||||
if (!samlService.isServiceProviderInitialized()) {
|
||||
final String samlMetadataUri = generateResourceUri("saml", "metadata");
|
||||
final String baseUri = samlMetadataUri.replace("/saml/metadata", "");
|
||||
samlService.initializeServiceProvider(baseUri);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String,String> getParameterMap(final MultivaluedMap<String, String> formParams) {
|
||||
final Map<String,String> params = new HashMap<>();
|
||||
for (final String paramKey : formParams.keySet()) {
|
||||
params.put(paramKey, formParams.getFirst(paramKey));
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private void removeSamlRequestCookie(final HttpServletResponse httpServletResponse) {
|
||||
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
private boolean isSamlEnabled(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, boolean isLogin) throws Exception {
|
||||
final String pageTitle = getForwardPageTitle(isLogin);
|
||||
|
||||
// only consider user specific access over https
|
||||
if (!httpServletRequest.isSecure()) {
|
||||
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ensure saml is enabled
|
||||
if (!samlService.isSamlEnabled()) {
|
||||
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getForwardPageTitle(boolean isLogin) {
|
||||
return isLogin ? ApplicationResource.LOGIN_ERROR_TITLE : ApplicationResource.LOGOUT_ERROR_TITLE;
|
||||
}
|
||||
|
||||
private Optional<String> getSamlRequestIdentifier() {
|
||||
return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
private void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) {
|
||||
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
private Optional<String> getLogoutRequestIdentifier() {
|
||||
return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
}
|
||||
|
||||
private String getNiFiLogoutCompleteUri() {
|
||||
return getNiFiUri() + "logout-complete";
|
||||
}
|
||||
|
||||
public void setSamlService(SAMLService samlService) {
|
||||
this.samlService = samlService;
|
||||
}
|
||||
|
||||
public void setSamlStateManager(SAMLStateManager samlStateManager) {
|
||||
this.samlStateManager = samlStateManager;
|
||||
}
|
||||
|
||||
public void setSamlCredentialStore(SAMLCredentialStore samlCredentialStore) {
|
||||
this.samlCredentialStore = samlCredentialStore;
|
||||
}
|
||||
|
||||
public void setIdpUserGroupService(IdpUserGroupService idpUserGroupService) {
|
||||
this.idpUserGroupService = idpUserGroupService;
|
||||
}
|
||||
|
||||
public void setProperties(final NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
protected NiFiProperties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public void setLogoutRequestManager(LogoutRequestManager logoutRequestManager) {
|
||||
this.logoutRequestManager = logoutRequestManager;
|
||||
}
|
||||
}
|
@ -585,6 +585,7 @@
|
||||
<property name="certificateExtractor" ref="certificateExtractor"/>
|
||||
<property name="principalExtractor" ref="principalExtractor"/>
|
||||
<property name="jwtAuthenticationProvider" ref="jwtAuthenticationProvider"/>
|
||||
<property name="jwtDecoder" ref="jwtDecoder" />
|
||||
<property name="jwtLogoutListener" ref="jwtLogoutListener"/>
|
||||
<property name="bearerTokenProvider" ref="bearerTokenProvider"/>
|
||||
<property name="bearerTokenResolver" ref="bearerTokenResolver"/>
|
||||
@ -594,14 +595,6 @@
|
||||
<property name="requestReplicator" ref="requestReplicator" />
|
||||
<property name="flowController" ref="flowController" />
|
||||
</bean>
|
||||
<bean id="samlResource" class="org.apache.nifi.web.api.SAMLAccessResource" scope="singleton">
|
||||
<property name="logoutRequestManager" ref="logoutRequestManager" />
|
||||
<property name="samlService" ref="samlService" />
|
||||
<property name="samlStateManager" ref="samlStateManager"/>
|
||||
<property name="samlCredentialStore" ref="samlCredentialStore"/>
|
||||
<property name="idpUserGroupService" ref="idpUserGroupService" />
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
</bean>
|
||||
<bean id="oidcResource" class="org.apache.nifi.web.api.OIDCAccessResource" scope="singleton">
|
||||
<property name="oidcService" ref="oidcService"/>
|
||||
<property name="properties" ref="nifiProperties"/>
|
||||
|
@ -36,7 +36,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.apache.nifi.web.api.cookie.ApplicationCookieName.OIDC_REQUEST_IDENTIFIER;
|
||||
import static org.apache.nifi.web.security.cookie.ApplicationCookieName.OIDC_REQUEST_IDENTIFIER;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
|
@ -196,14 +196,25 @@
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.extensions</groupId>
|
||||
<artifactId>spring-security-saml2-core</artifactId>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-saml2-service-provider</artifactId>
|
||||
<exclusions>
|
||||
<!-- Exclude Velocity from OpenSAML -->
|
||||
<exclusion>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.kerberos</groupId>
|
||||
@ -245,6 +256,15 @@
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.nifi</groupId>
|
||||
<artifactId>nifi-property-protection-factory</artifactId>
|
||||
|
@ -16,57 +16,392 @@
|
||||
*/
|
||||
package org.apache.nifi.web.security.configuration;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpCredentialService;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
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.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.apache.nifi.web.security.saml2.registration.EntityDescriptorCustomizer;
|
||||
import org.apache.nifi.web.security.saml2.service.authentication.ResponseAuthenticationConverter;
|
||||
import org.apache.nifi.web.security.saml2.registration.Saml2RegistrationProperty;
|
||||
import org.apache.nifi.web.security.saml2.service.web.StandardRelyingPartyRegistrationResolver;
|
||||
import org.apache.nifi.web.security.saml2.service.web.StandardSaml2AuthenticationRequestRepository;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.Saml2AuthenticationSuccessHandler;
|
||||
import org.apache.nifi.web.security.saml2.registration.StandardRelyingPartyRegistrationRepository;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.identity.AttributeNameIdentityConverter;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2LocalLogoutFilter;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2SingleLogoutFilter;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2SingleLogoutHandler;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2LogoutSuccessHandler;
|
||||
import org.apache.nifi.web.security.saml2.web.authentication.logout.StandardSaml2LogoutRequestRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.caffeine.CaffeineCache;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
|
||||
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml3AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
|
||||
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* SAML Configuration for Authentication Security
|
||||
*/
|
||||
@Configuration
|
||||
public class SamlAuthenticationSecurityConfiguration {
|
||||
private final NiFiProperties niFiProperties;
|
||||
private static final Duration REQUEST_EXPIRATION = Duration.ofSeconds(60);
|
||||
|
||||
private static final long REQUEST_MAXIMUM_CACHE_SIZE = 1000;
|
||||
|
||||
private final NiFiProperties properties;
|
||||
|
||||
private final BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private final IdpCredentialService idpCredentialService;
|
||||
private final LogoutRequestManager logoutRequestManager;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
@Autowired
|
||||
public SamlAuthenticationSecurityConfiguration(
|
||||
final NiFiProperties niFiProperties,
|
||||
final NiFiProperties properties,
|
||||
final BearerTokenProvider bearerTokenProvider,
|
||||
final IdpCredentialService idpCredentialService
|
||||
final LogoutRequestManager logoutRequestManager,
|
||||
final IdpUserGroupService idpUserGroupService
|
||||
) {
|
||||
this.niFiProperties = niFiProperties;
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
this.idpCredentialService = idpCredentialService;
|
||||
}
|
||||
|
||||
@Bean(initMethod = "initialize", destroyMethod = "shutdown")
|
||||
public SAMLService samlService() {
|
||||
return new StandardSAMLService(samlConfigurationFactory(), niFiProperties);
|
||||
this.properties = Objects.requireNonNull(properties, "Properties required");
|
||||
this.bearerTokenProvider = Objects.requireNonNull(bearerTokenProvider, "Bearer Token Provider required");
|
||||
this.logoutRequestManager = Objects.requireNonNull(logoutRequestManager, "Logout Request Manager required");
|
||||
this.idpUserGroupService = Objects.requireNonNull(idpUserGroupService, "User Group Service required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Metadata Filter returns SAML 2 Metadata XML
|
||||
*
|
||||
* @return SAML 2 Metadata Filter
|
||||
*/
|
||||
@Bean
|
||||
public StandardSAMLStateManager samlStateManager() {
|
||||
return new StandardSAMLStateManager(bearerTokenProvider);
|
||||
public Saml2MetadataFilter saml2MetadataFilter() {
|
||||
final Saml2MetadataFilter filter = new Saml2MetadataFilter(relyingPartyRegistrationResolver(), saml2MetadataResolver());
|
||||
filter.setRequestMatcher(new AntPathRequestMatcher(SamlUrlPath.METADATA.getPath()));
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Web SSO Authentication Request Filter for SAML 2 initial login sending to an IDP
|
||||
*
|
||||
* @return SAML 2 Authentication Request Filter
|
||||
*/
|
||||
@Bean
|
||||
public StandardSAMLCredentialStore samlCredentialStore() {
|
||||
return new StandardSAMLCredentialStore(idpCredentialService);
|
||||
public Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter() {
|
||||
final Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(saml2AuthenticationRequestResolver());
|
||||
filter.setAuthenticationRequestRepository(saml2AuthenticationRequestRepository());
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Web SSO Authentication Filter for SAML 2 login response processing from an IDP
|
||||
*
|
||||
* @param authenticationManager Spring Security Authentication Manager
|
||||
* @return SAML 2 Authentication Filter
|
||||
*/
|
||||
@Bean
|
||||
public StandardSAMLConfigurationFactory samlConfigurationFactory() {
|
||||
return new StandardSAMLConfigurationFactory();
|
||||
public Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter(final AuthenticationManager authenticationManager) {
|
||||
final Saml2AuthenticationTokenConverter authenticationTokenConverter = new Saml2AuthenticationTokenConverter(relyingPartyRegistrationResolver());
|
||||
final Saml2WebSsoAuthenticationFilter filter = new Saml2WebSsoAuthenticationFilter(authenticationTokenConverter, SamlUrlPath.LOGIN_RESPONSE_REGISTRATION_ID.getPath());
|
||||
filter.setAuthenticationManager(authenticationManager);
|
||||
filter.setAuthenticationSuccessHandler(getAuthenticationSuccessHandler());
|
||||
filter.setAuthenticationRequestRepository(saml2AuthenticationRequestRepository());
|
||||
// Disable HTTP Sessions
|
||||
filter.setAllowSessionCreation(false);
|
||||
filter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security Single Logout Filter for initiating Single Logout Requests sending to an IDP
|
||||
*
|
||||
* @return SAML 2 Single Logout Filter
|
||||
*/
|
||||
@Bean
|
||||
public Saml2SingleLogoutFilter saml2SingleLogoutFilter() {
|
||||
return new Saml2SingleLogoutFilter(logoutRequestManager, saml2SingleLogoutSuccessHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Single Logout Request Filter processing from an IDP
|
||||
*
|
||||
* @return SAML 2 Logout Request Filter
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LogoutRequestFilter saml2LogoutRequestFilter() {
|
||||
final Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(
|
||||
relyingPartyRegistrationResolver(),
|
||||
saml2LogoutRequestValidator(),
|
||||
saml2LogoutResponseResolver(),
|
||||
saml2SingleLogoutHandler()
|
||||
);
|
||||
filter.setLogoutRequestMatcher(new AntPathRequestMatcher(SamlUrlPath.SINGLE_LOGOUT_RESPONSE.getPath()));
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Single Logout Response Filter processing from an IDP
|
||||
*
|
||||
* @return SAML 2 Logout Response Filter
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LogoutResponseFilter saml2LogoutResponseFilter() {
|
||||
final Saml2LogoutResponseFilter saml2LogoutResponseFilter = new Saml2LogoutResponseFilter(
|
||||
relyingPartyRegistrationResolver(),
|
||||
saml2LogoutResponseValidator(),
|
||||
saml2LogoutSuccessHandler()
|
||||
);
|
||||
saml2LogoutResponseFilter.setLogoutRequestRepository(saml2LogoutRequestRepository());
|
||||
saml2LogoutResponseFilter.setLogoutRequestMatcher(new AntPathRequestMatcher(SamlUrlPath.SINGLE_LOGOUT_RESPONSE.getPath()));
|
||||
return saml2LogoutResponseFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard SAML 2 Single Logout Handler
|
||||
*
|
||||
* @return SAML 2 Single Logout Handler
|
||||
*/
|
||||
@Bean
|
||||
public Saml2SingleLogoutHandler saml2SingleLogoutHandler() {
|
||||
return new Saml2SingleLogoutHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML 2 Local Logout Filter for clearing application caches on Logout requests
|
||||
*
|
||||
* @return SAML 2 Local Logout Filter
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LocalLogoutFilter saml2LocalLogoutFilter() {
|
||||
return new Saml2LocalLogoutFilter(saml2LogoutSuccessHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security OpenSAML Authentication Provider for processing SAML 2 login responses
|
||||
*
|
||||
* @return OpenSAML 3 Authentication Provider required for compatibility with Java 8
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public OpenSamlAuthenticationProvider openSamlAuthenticationProvider() {
|
||||
final OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
final ResponseAuthenticationConverter responseAuthenticationConverter = new ResponseAuthenticationConverter(properties.getSamlGroupAttributeName());
|
||||
provider.setResponseAuthenticationConverter(responseAuthenticationConverter);
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Authentication Request Resolver uses OpenSAML 3 for compatibility with Java 8
|
||||
*
|
||||
* @return OpenSAML 3 version of SAML 2 Authentication Request Resolver
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public Saml2AuthenticationRequestResolver saml2AuthenticationRequestResolver() {
|
||||
return new OpenSaml3AuthenticationRequestResolver(relyingPartyRegistrationResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Logout Request Validator
|
||||
*
|
||||
* @return OpenSAML Logout Request Validator
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LogoutRequestValidator saml2LogoutRequestValidator() {
|
||||
return new OpenSamlLogoutRequestValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Logout Response Validator
|
||||
*
|
||||
* @return OpenSAML Logout Response Validator
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LogoutResponseValidator saml2LogoutResponseValidator() {
|
||||
return new OpenSamlLogoutResponseValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Logout Request Resolver uses OpenSAML 3 for compatibility with Java 8
|
||||
*
|
||||
* @return OpenSAML 3 version of SAML 2 Logout Request Resolver
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public Saml2LogoutRequestResolver saml2LogoutRequestResolver() {
|
||||
return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Logout Response Resolver uses OpenSAML 3 for compatibility with Java 8
|
||||
*
|
||||
* @return OpenSAML 3 version of SAML 2 Logout Response Resolver
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Bean
|
||||
public Saml2LogoutResponseResolver saml2LogoutResponseResolver() {
|
||||
return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security Saml 2 Authentication Request Repository for tracking SAML 2 across multiple HTTP requests
|
||||
*
|
||||
* @return SAML 2 Authentication Request Repository
|
||||
*/
|
||||
@Bean
|
||||
public Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> saml2AuthenticationRequestRepository() {
|
||||
final Cache<Object, Object> caffeineCache = Caffeine.newBuilder()
|
||||
.maximumSize(REQUEST_MAXIMUM_CACHE_SIZE)
|
||||
.expireAfterWrite(REQUEST_EXPIRATION)
|
||||
.build();
|
||||
final CaffeineCache cache = new CaffeineCache(Saml2AuthenticationRequestRepository.class.getSimpleName(), caffeineCache);
|
||||
return new StandardSaml2AuthenticationRequestRepository(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Relying Party Registration Resolver for SAML 2 initial login processing
|
||||
*
|
||||
* @return Default Relying Party Registration Resolver
|
||||
*/
|
||||
@Bean
|
||||
public RelyingPartyRegistrationResolver relyingPartyRegistrationResolver() {
|
||||
return new StandardRelyingPartyRegistrationResolver(relyingPartyRegistrationRepository(), properties.getAllowedContextPathsAsList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Relying Party Registration Repository generated using NiFi Properties
|
||||
*
|
||||
* @return Standard Relying Party Registration Repository or placeholder repository when SAML is disabled
|
||||
*/
|
||||
@Bean
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
|
||||
return properties.isSamlEnabled() ? new StandardRelyingPartyRegistrationRepository(properties) : getDisabledRelyingPartyRegistrationRepository();
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring Security SAML 2 Metadata Resolver
|
||||
*
|
||||
* @return OpenSAML SAML 2 Metadata Resolver
|
||||
*/
|
||||
@Bean
|
||||
public Saml2MetadataResolver saml2MetadataResolver() {
|
||||
final OpenSamlMetadataResolver resolver = new OpenSamlMetadataResolver();
|
||||
final EntityDescriptorCustomizer customizer = new EntityDescriptorCustomizer(
|
||||
properties.isSamlWantAssertionsSigned(),
|
||||
properties.isSamlRequestSigningEnabled()
|
||||
);
|
||||
resolver.setEntityDescriptorCustomizer(customizer);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard SAML 2 Logout Success Handler for Logout processing after Single or Local Logout success
|
||||
*
|
||||
* @return SAML 2 Logout Success Handler
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LogoutSuccessHandler saml2LogoutSuccessHandler() {
|
||||
return new Saml2LogoutSuccessHandler(logoutRequestManager, idpUserGroupService);
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML 2 Logout Success Handler for Single Logout processing
|
||||
*
|
||||
* @return Spring Security SAML 2 Logout Success Handler
|
||||
*/
|
||||
@Bean
|
||||
public Saml2RelyingPartyInitiatedLogoutSuccessHandler saml2SingleLogoutSuccessHandler() {
|
||||
final Saml2RelyingPartyInitiatedLogoutSuccessHandler handler = new Saml2RelyingPartyInitiatedLogoutSuccessHandler(saml2LogoutRequestResolver());
|
||||
handler.setLogoutRequestRepository(saml2LogoutRequestRepository());
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML 2 Logout Request Repository for tracking Single Logout requests
|
||||
*
|
||||
* @return SAML 2 Logout Request Repository
|
||||
*/
|
||||
@Bean
|
||||
public Saml2LogoutRequestRepository saml2LogoutRequestRepository() {
|
||||
final Cache<Object, Object> caffeineCache = Caffeine.newBuilder()
|
||||
.maximumSize(REQUEST_MAXIMUM_CACHE_SIZE)
|
||||
.expireAfterWrite(REQUEST_EXPIRATION)
|
||||
.build();
|
||||
final CaffeineCache cache = new CaffeineCache(Saml2LogoutRequestRepository.class.getSimpleName(), caffeineCache);
|
||||
return new StandardSaml2LogoutRequestRepository(cache);
|
||||
}
|
||||
|
||||
private Saml2AuthenticationSuccessHandler getAuthenticationSuccessHandler() {
|
||||
final long authenticationExpiration = (long) FormatUtils.getPreciseTimeDuration(properties.getSamlAuthenticationExpiration(), TimeUnit.MILLISECONDS);
|
||||
final Duration expiration = Duration.ofMillis(authenticationExpiration);
|
||||
final String entityId = properties.getSamlServiceProviderEntityId();
|
||||
final String issuer = entityId == null ? Saml2RegistrationProperty.REGISTRATION_ID.getProperty() : entityId;
|
||||
final Saml2AuthenticationSuccessHandler handler = new Saml2AuthenticationSuccessHandler(
|
||||
bearerTokenProvider,
|
||||
idpUserGroupService,
|
||||
IdentityMappingUtil.getIdentityMappings(properties),
|
||||
IdentityMappingUtil.getGroupMappings(properties),
|
||||
expiration,
|
||||
issuer
|
||||
);
|
||||
|
||||
final String identityAttributeName = properties.getSamlIdentityAttributeName();
|
||||
if (StringUtils.isNotBlank(identityAttributeName)) {
|
||||
final AttributeNameIdentityConverter identityConverter = new AttributeNameIdentityConverter(identityAttributeName);
|
||||
handler.setIdentityConverter(identityConverter);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private RelyingPartyRegistrationRepository getDisabledRelyingPartyRegistrationRepository() {
|
||||
final RelyingPartyRegistration registration = RelyingPartyRegistration
|
||||
.withRegistrationId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.entityId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.assertingPartyDetails(assertingPartyDetails -> {
|
||||
assertingPartyDetails.entityId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
assertingPartyDetails.singleSignOnServiceLocation(SamlUrlPath.LOGIN_RESPONSE_REGISTRATION_ID.getPath());
|
||||
})
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(registration);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.api.cookie;
|
||||
package org.apache.nifi.web.security.cookie;
|
||||
|
||||
import org.apache.nifi.web.security.http.SecurityCookieName;
|
||||
|
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.api.cookie;
|
||||
package org.apache.nifi.web.security.cookie;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.api.cookie;
|
||||
package org.apache.nifi.web.security.cookie;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
@ -1,57 +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.saml;
|
||||
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||
import org.springframework.security.saml.context.SAMLMessageContext;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Specialized interface to add functionality to {@link org.springframework.security.saml.context.SAMLContextProvider}
|
||||
*/
|
||||
public interface NiFiSAMLContextProvider {
|
||||
|
||||
/**
|
||||
* Creates a SAMLContext with local entity values filled. Also request and response must be stored in the context
|
||||
* as message transports. Local entity ID is populated from data in the request object.
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @param parameters additional parameters
|
||||
* @return context
|
||||
* @throws MetadataProviderException in case of metadata problems
|
||||
*/
|
||||
SAMLMessageContext getLocalEntity(HttpServletRequest request, HttpServletResponse response, Map<String,String> parameters)
|
||||
throws MetadataProviderException;
|
||||
|
||||
/**
|
||||
* Creates a SAMLContext with local entity and peer values filled. Also request and response must be stored in the context
|
||||
* as message transports. Local and peer entity IDs are populated from data in the request object.
|
||||
*
|
||||
* @param request request
|
||||
* @param response response
|
||||
* @param parameters additional parameters
|
||||
* @return context
|
||||
* @throws MetadataProviderException in case of metadata problems
|
||||
*/
|
||||
SAMLMessageContext getLocalAndPeerEntity(HttpServletRequest request, HttpServletResponse response, Map<String,String> parameters)
|
||||
throws MetadataProviderException;
|
||||
|
||||
}
|
@ -1,73 +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.saml;
|
||||
|
||||
import org.springframework.security.saml.key.KeyManager;
|
||||
import org.springframework.security.saml.log.SAMLLogger;
|
||||
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
||||
import org.springframework.security.saml.metadata.MetadataManager;
|
||||
import org.springframework.security.saml.processor.SAMLProcessor;
|
||||
import org.springframework.security.saml.websso.SingleLogoutProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileOptions;
|
||||
|
||||
import java.util.Timer;
|
||||
|
||||
public interface SAMLConfiguration {
|
||||
|
||||
String getSpEntityId();
|
||||
|
||||
SAMLProcessor getProcessor();
|
||||
|
||||
NiFiSAMLContextProvider getContextProvider();
|
||||
|
||||
SAMLLogger getLogger();
|
||||
|
||||
WebSSOProfileOptions getWebSSOProfileOptions();
|
||||
|
||||
WebSSOProfile getWebSSOProfile();
|
||||
|
||||
WebSSOProfile getWebSSOProfileECP();
|
||||
|
||||
WebSSOProfile getWebSSOProfileHoK();
|
||||
|
||||
WebSSOProfileConsumer getWebSSOProfileConsumer();
|
||||
|
||||
WebSSOProfileConsumer getWebSSOProfileHoKConsumer();
|
||||
|
||||
SingleLogoutProfile getSingleLogoutProfile();
|
||||
|
||||
ExtendedMetadata getExtendedMetadata();
|
||||
|
||||
MetadataManager getMetadataManager();
|
||||
|
||||
KeyManager getKeyManager();
|
||||
|
||||
Timer getBackgroundTaskTimer();
|
||||
|
||||
long getAuthExpiration();
|
||||
|
||||
String getIdentityAttributeName();
|
||||
|
||||
String getGroupAttributeName();
|
||||
|
||||
boolean isRequestSigningEnabled();
|
||||
|
||||
boolean isWantAssertionsSigned();
|
||||
|
||||
}
|
@ -1,49 +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.saml;
|
||||
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
|
||||
/**
|
||||
* Manages storage of SAML credentials.
|
||||
*/
|
||||
public interface SAMLCredentialStore {
|
||||
|
||||
/**
|
||||
* Saves the given SAML credential so it can later be retrieved by user identity.
|
||||
*
|
||||
* @param identity the identity of the user the credential belongs to
|
||||
* @param credential the credential
|
||||
*/
|
||||
void save(String identity, SAMLCredential credential);
|
||||
|
||||
/**
|
||||
* Retrieves the credential for the given user identity.
|
||||
*
|
||||
* @param identity the user identity
|
||||
* @return the credential, or null if none exists
|
||||
*/
|
||||
SAMLCredential get(String identity);
|
||||
|
||||
/**
|
||||
* Deletes the credential for the given user identity.
|
||||
*
|
||||
* @param identity the user identity
|
||||
*/
|
||||
void delete(String identity);
|
||||
|
||||
}
|
@ -1,44 +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.saml;
|
||||
|
||||
public interface SAMLEndpoints {
|
||||
|
||||
String SAML_ACCESS_ROOT = "/access/saml";
|
||||
|
||||
String SERVICE_PROVIDER_METADATA_RELATIVE = "/metadata";
|
||||
String SERVICE_PROVIDER_METADATA = SAML_ACCESS_ROOT + SERVICE_PROVIDER_METADATA_RELATIVE;
|
||||
|
||||
String LOGIN_REQUEST_RELATIVE = "login/request";
|
||||
String LOGIN_REQUEST = SAML_ACCESS_ROOT + LOGIN_REQUEST_RELATIVE;
|
||||
|
||||
String LOGIN_CONSUMER_RELATIVE = "/login/consumer";
|
||||
String LOGIN_CONSUMER = SAML_ACCESS_ROOT + LOGIN_CONSUMER_RELATIVE;
|
||||
|
||||
String LOGIN_EXCHANGE_RELATIVE = "/login/exchange";
|
||||
String LOGIN_EXCHANGE = SAML_ACCESS_ROOT + LOGIN_EXCHANGE_RELATIVE;
|
||||
|
||||
String LOCAL_LOGOUT_RELATIVE = "/local-logout";
|
||||
String LOCAL_LOGOUT = SAML_ACCESS_ROOT + LOCAL_LOGOUT_RELATIVE;
|
||||
|
||||
String SINGLE_LOGOUT_REQUEST_RELATIVE = "/single-logout/request";
|
||||
String SINGLE_LOGOUT_REQUEST = SAML_ACCESS_ROOT + SINGLE_LOGOUT_REQUEST_RELATIVE;
|
||||
|
||||
String SINGLE_LOGOUT_CONSUMER_RELATIVE = "/single-logout/consumer";
|
||||
String SINGLE_LOGOUT_CONSUMER = SAML_ACCESS_ROOT + SINGLE_LOGOUT_CONSUMER_RELATIVE;
|
||||
|
||||
}
|
@ -1,128 +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.saml;
|
||||
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public interface SAMLService {
|
||||
|
||||
String SAML_SUPPORT_IS_NOT_CONFIGURED = "SAML support is not configured";
|
||||
|
||||
/**
|
||||
* Initializes the service.
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* @return whether SAML support is enabled
|
||||
*/
|
||||
boolean isSamlEnabled();
|
||||
|
||||
/**
|
||||
* @return true if the service provider metadata has been initialized, false otherwise
|
||||
*/
|
||||
boolean isServiceProviderInitialized();
|
||||
|
||||
/**
|
||||
* Initializes the service provider metadata.
|
||||
*
|
||||
* This method must be called before using the service to perform any other SAML operations.
|
||||
*
|
||||
* @param baseUrl the baseUrl of the service provider
|
||||
*/
|
||||
void initializeServiceProvider(String baseUrl);
|
||||
|
||||
/**
|
||||
* Retrieves the service provider metadata XML.
|
||||
*/
|
||||
String getServiceProviderMetadata();
|
||||
|
||||
/**
|
||||
* Retrieves the expiration time in milliseconds for a SAML authentication.
|
||||
*
|
||||
* @return the authentication
|
||||
*/
|
||||
long getAuthExpiration();
|
||||
|
||||
/**
|
||||
* Initiates a login sequence with the SAML identity provider.
|
||||
*
|
||||
* @param request servlet request
|
||||
* @param response servlet response
|
||||
*/
|
||||
void initiateLogin(HttpServletRequest request, HttpServletResponse response, String relayState);
|
||||
|
||||
/**
|
||||
* Processes the assertions coming back from the identity provider and returns a NiFi JWT.
|
||||
*
|
||||
* @param request servlet request
|
||||
* @param response servlet request
|
||||
* @param parameters a map of parameters
|
||||
* @return a NiFi JWT
|
||||
*/
|
||||
SAMLCredential processLogin(HttpServletRequest request, HttpServletResponse response, Map<String,String> parameters);
|
||||
|
||||
/**
|
||||
* Returns the identity of the user based on the given credential.
|
||||
*
|
||||
* If no identity attribute is specified in nifi.properties, then the NameID of the Subject will be used.
|
||||
*
|
||||
* Otherwise the value of the given identity attribute will be used.
|
||||
*
|
||||
* @param samlCredential the SAML credential returned from a successful authentication
|
||||
* @return the user identity
|
||||
*/
|
||||
String getUserIdentity(SAMLCredential samlCredential);
|
||||
|
||||
/**
|
||||
* Returns the names of the groups the user belongs from looking at the assertions in the credential.
|
||||
*
|
||||
* Requires configuring the name of the group attribute in nifi.properties, otherwise an empty set will be returned.
|
||||
*
|
||||
* @param credential the SAML credential returned from a successful authentication
|
||||
* @return the set of groups the user belongs to, or empty set if none exist or if nifi has not been configured with a group attribute name
|
||||
*/
|
||||
Set<String> getUserGroups(SAMLCredential credential);
|
||||
|
||||
/**
|
||||
* Initiates a logout sequence with the SAML identity provider.
|
||||
*
|
||||
* @param request servlet request
|
||||
* @param response servlet response
|
||||
*/
|
||||
void initiateLogout(HttpServletRequest request, HttpServletResponse response, SAMLCredential credential);
|
||||
|
||||
/**
|
||||
* Processes a logout, typically a response from previously initiating a logout, but may be an IDP initiated logout.
|
||||
*
|
||||
* @param request servlet request
|
||||
* @param response servlet response
|
||||
* @param parameters a map of parameters
|
||||
*/
|
||||
void processLogout(HttpServletRequest request, HttpServletResponse response, Map<String,String> parameters);
|
||||
|
||||
/**
|
||||
* Shuts down the service.
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
}
|
@ -1,61 +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.saml;
|
||||
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
|
||||
/**
|
||||
* Manages the state of active SAML requests.
|
||||
*/
|
||||
public interface SAMLStateManager {
|
||||
|
||||
/**
|
||||
* Creates the initial state for starting a SAML login sequence.
|
||||
*
|
||||
* @param requestIdentifier a unique identifier for the current request/login-sequence
|
||||
* @return a state value for the given request
|
||||
*/
|
||||
String createState(String requestIdentifier);
|
||||
|
||||
/**
|
||||
* Determines if the proposed state matches the stored state for the given request.
|
||||
*
|
||||
* @param requestIdentifier the request identifier
|
||||
* @param proposedState the proposed state for the given request
|
||||
* @return true if the proposed state matches the actual state
|
||||
*/
|
||||
boolean isStateValid(String requestIdentifier, String proposedState);
|
||||
|
||||
/**
|
||||
* Creates a NiFi JWT from the token and caches the JWT for future retrieval.
|
||||
*
|
||||
* @param requestIdentifier the request identifier
|
||||
* @param token the login authentication token to create the JWT from
|
||||
*/
|
||||
void createJwt(String requestIdentifier, LoginAuthenticationToken token);
|
||||
|
||||
/**
|
||||
* Retrieves the JWT for the given request identifier that was created by previously calling {@method createJwt}.
|
||||
*
|
||||
* The JWT will be removed from the state cache upon retrieval.
|
||||
*
|
||||
* @param requestIdentifier the request identifier
|
||||
* @return the NiFi JWT for the given request
|
||||
*/
|
||||
String getJwt(String requestIdentifier);
|
||||
|
||||
}
|
@ -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.saml.impl;
|
||||
|
||||
import org.apache.nifi.web.security.saml.NiFiSAMLContextProvider;
|
||||
import org.apache.nifi.web.security.saml.impl.http.HttpServletRequestWithParameters;
|
||||
import org.apache.nifi.web.security.saml.impl.http.ProxyAwareHttpServletRequestWrapper;
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||
import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
|
||||
import org.opensaml.ws.transport.http.HttpServletResponseAdapter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml.context.SAMLContextProviderImpl;
|
||||
import org.springframework.security.saml.context.SAMLMessageContext;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of NiFiSAMLContextProvider that inherits from the standard SAMLContextProviderImpl.
|
||||
*/
|
||||
public class NiFiSAMLContextProviderImpl extends SAMLContextProviderImpl implements NiFiSAMLContextProvider {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(NiFiSAMLContextProviderImpl.class);
|
||||
|
||||
@Override
|
||||
public SAMLMessageContext getLocalEntity(HttpServletRequest request, HttpServletResponse response, Map<String, String> parameters)
|
||||
throws MetadataProviderException {
|
||||
|
||||
SAMLMessageContext context = new SAMLMessageContext();
|
||||
populateGenericContext(request, response, parameters, context);
|
||||
populateLocalEntityId(context, request.getRequestURI());
|
||||
populateLocalContext(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLMessageContext getLocalAndPeerEntity(HttpServletRequest request, HttpServletResponse response, Map<String, String> parameters)
|
||||
throws MetadataProviderException {
|
||||
|
||||
SAMLMessageContext context = new SAMLMessageContext();
|
||||
populateGenericContext(request, response, parameters, context);
|
||||
populateLocalEntityId(context, request.getRequestURI());
|
||||
populateLocalContext(context);
|
||||
populatePeerEntityId(context);
|
||||
populatePeerContext(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
protected void populateGenericContext(HttpServletRequest request, HttpServletResponse response, Map<String, String> parameters, SAMLMessageContext context) {
|
||||
HttpServletRequestWrapper requestWrapper = new ProxyAwareHttpServletRequestWrapper(request);
|
||||
LOGGER.debug("Populating SAMLContext - request wrapper URL is [{}]", requestWrapper.getRequestURL().toString());
|
||||
|
||||
HttpServletRequestAdapter inTransport = new HttpServletRequestWithParameters(requestWrapper, parameters);
|
||||
HttpServletResponseAdapter outTransport = new HttpServletResponseAdapter(response, requestWrapper.isSecure());
|
||||
|
||||
// Store attribute which cannot be located from InTransport directly
|
||||
requestWrapper.setAttribute(org.springframework.security.saml.SAMLConstants.LOCAL_CONTEXT_PATH, requestWrapper.getContextPath());
|
||||
|
||||
context.setMetadataProvider(metadata);
|
||||
context.setInboundMessageTransport(inTransport);
|
||||
context.setOutboundMessageTransport(outTransport);
|
||||
|
||||
context.setMessageStorage(storageFactory.getMessageStorage(requestWrapper));
|
||||
}
|
||||
|
||||
}
|
@ -1,328 +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.saml.impl;
|
||||
|
||||
import org.apache.nifi.web.security.saml.NiFiSAMLContextProvider;
|
||||
import org.apache.nifi.web.security.saml.SAMLConfiguration;
|
||||
import org.springframework.security.saml.key.KeyManager;
|
||||
import org.springframework.security.saml.log.SAMLLogger;
|
||||
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
||||
import org.springframework.security.saml.metadata.MetadataManager;
|
||||
import org.springframework.security.saml.processor.SAMLProcessor;
|
||||
import org.springframework.security.saml.websso.SingleLogoutProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileOptions;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Timer;
|
||||
|
||||
public class StandardSAMLConfiguration implements SAMLConfiguration {
|
||||
|
||||
private final String spEntityId;
|
||||
|
||||
private final SAMLProcessor processor;
|
||||
private final NiFiSAMLContextProvider contextProvider;
|
||||
private final SAMLLogger logger;
|
||||
|
||||
private final WebSSOProfileOptions webSSOProfileOptions;
|
||||
|
||||
private final WebSSOProfile webSSOProfile;
|
||||
private final WebSSOProfile webSSOProfileECP;
|
||||
private final WebSSOProfile webSSOProfileHoK;
|
||||
|
||||
private final WebSSOProfileConsumer webSSOProfileConsumer;
|
||||
private final WebSSOProfileConsumer webSSOProfileHoKConsumer;
|
||||
|
||||
private final SingleLogoutProfile singleLogoutProfile;
|
||||
|
||||
private final ExtendedMetadata extendedMetadata;
|
||||
private final MetadataManager metadataManager;
|
||||
|
||||
private final KeyManager keyManager;
|
||||
private final Timer backgroundTaskTimer;
|
||||
|
||||
private final long authExpiration;
|
||||
private final String identityAttributeName;
|
||||
private final String groupAttributeName;
|
||||
|
||||
private final boolean requestSigningEnabled;
|
||||
private final boolean wantAssertionsSigned;
|
||||
|
||||
private StandardSAMLConfiguration(final Builder builder) {
|
||||
this.spEntityId = Objects.requireNonNull(builder.spEntityId);
|
||||
this.processor = Objects.requireNonNull(builder.processor);
|
||||
this.contextProvider = Objects.requireNonNull(builder.contextProvider);
|
||||
this.logger = Objects.requireNonNull(builder.logger);
|
||||
this.webSSOProfileOptions = Objects.requireNonNull(builder.webSSOProfileOptions);
|
||||
this.webSSOProfile = Objects.requireNonNull(builder.webSSOProfile);
|
||||
this.webSSOProfileECP = Objects.requireNonNull(builder.webSSOProfileECP);
|
||||
this.webSSOProfileHoK = Objects.requireNonNull(builder.webSSOProfileHoK);
|
||||
this.webSSOProfileConsumer = Objects.requireNonNull(builder.webSSOProfileConsumer);
|
||||
this.webSSOProfileHoKConsumer = Objects.requireNonNull(builder.webSSOProfileHoKConsumer);
|
||||
this.singleLogoutProfile = Objects.requireNonNull(builder.singleLogoutProfile);
|
||||
this.extendedMetadata = Objects.requireNonNull(builder.extendedMetadata);
|
||||
this.metadataManager = Objects.requireNonNull(builder.metadataManager);
|
||||
this.keyManager = Objects.requireNonNull(builder.keyManager);
|
||||
this.backgroundTaskTimer = Objects.requireNonNull(builder.backgroundTaskTimer);
|
||||
this.authExpiration = builder.authExpiration;
|
||||
this.identityAttributeName = builder.identityAttributeName;
|
||||
this.groupAttributeName = builder.groupAttributeName;
|
||||
this.requestSigningEnabled = builder.requestSigningEnabled;
|
||||
this.wantAssertionsSigned = builder.wantAssertionsSigned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSpEntityId() {
|
||||
return spEntityId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLProcessor getProcessor() {
|
||||
return processor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NiFiSAMLContextProvider getContextProvider() {
|
||||
return contextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLLogger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSSOProfileOptions getWebSSOProfileOptions() {
|
||||
return webSSOProfileOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSSOProfile getWebSSOProfile() {
|
||||
return webSSOProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSSOProfile getWebSSOProfileECP() {
|
||||
return webSSOProfileECP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSSOProfile getWebSSOProfileHoK() {
|
||||
return webSSOProfileHoK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSSOProfileConsumer getWebSSOProfileConsumer() {
|
||||
return webSSOProfileConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSSOProfileConsumer getWebSSOProfileHoKConsumer() {
|
||||
return webSSOProfileHoKConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SingleLogoutProfile getSingleLogoutProfile() {
|
||||
return singleLogoutProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtendedMetadata getExtendedMetadata() {
|
||||
return extendedMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetadataManager getMetadataManager() {
|
||||
return metadataManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyManager getKeyManager() {
|
||||
return keyManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timer getBackgroundTaskTimer() {
|
||||
return backgroundTaskTimer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAuthExpiration() {
|
||||
return authExpiration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIdentityAttributeName() {
|
||||
return identityAttributeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupAttributeName() {
|
||||
return groupAttributeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequestSigningEnabled() {
|
||||
return requestSigningEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWantAssertionsSigned() {
|
||||
return wantAssertionsSigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for SAMLConfiguration.
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private String spEntityId;
|
||||
|
||||
private SAMLProcessor processor;
|
||||
private NiFiSAMLContextProvider contextProvider;
|
||||
private SAMLLogger logger;
|
||||
|
||||
private WebSSOProfileOptions webSSOProfileOptions;
|
||||
|
||||
private WebSSOProfile webSSOProfile;
|
||||
private WebSSOProfile webSSOProfileECP;
|
||||
private WebSSOProfile webSSOProfileHoK;
|
||||
|
||||
private WebSSOProfileConsumer webSSOProfileConsumer;
|
||||
private WebSSOProfileConsumer webSSOProfileHoKConsumer;
|
||||
|
||||
private SingleLogoutProfile singleLogoutProfile;
|
||||
|
||||
private ExtendedMetadata extendedMetadata;
|
||||
private MetadataManager metadataManager;
|
||||
|
||||
private KeyManager keyManager;
|
||||
private Timer backgroundTaskTimer;
|
||||
|
||||
private long authExpiration;
|
||||
private String groupAttributeName;
|
||||
private String identityAttributeName;
|
||||
|
||||
private boolean requestSigningEnabled;
|
||||
private boolean wantAssertionsSigned;
|
||||
|
||||
public Builder spEntityId(String spEntityId) {
|
||||
this.spEntityId = spEntityId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder processor(SAMLProcessor processor) {
|
||||
this.processor = processor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder contextProvider(NiFiSAMLContextProvider contextProvider) {
|
||||
this.contextProvider = contextProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder logger(SAMLLogger logger) {
|
||||
this.logger = logger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webSSOProfileOptions(WebSSOProfileOptions webSSOProfileOptions) {
|
||||
this.webSSOProfileOptions = webSSOProfileOptions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webSSOProfile(WebSSOProfile webSSOProfile) {
|
||||
this.webSSOProfile = webSSOProfile;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webSSOProfileECP(WebSSOProfile webSSOProfileECP) {
|
||||
this.webSSOProfileECP = webSSOProfileECP;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webSSOProfileHoK(WebSSOProfile webSSOProfileHoK) {
|
||||
this.webSSOProfileHoK = webSSOProfileHoK;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webSSOProfileConsumer(WebSSOProfileConsumer webSSOProfileConsumer) {
|
||||
this.webSSOProfileConsumer = webSSOProfileConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webSSOProfileHoKConsumer(WebSSOProfileConsumer webSSOProfileHoKConsumer) {
|
||||
this.webSSOProfileHoKConsumer = webSSOProfileHoKConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder singleLogoutProfile(SingleLogoutProfile singleLogoutProfile) {
|
||||
this.singleLogoutProfile = singleLogoutProfile;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder extendedMetadata(ExtendedMetadata extendedMetadata) {
|
||||
this.extendedMetadata = extendedMetadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder metadataManager(MetadataManager metadataManager) {
|
||||
this.metadataManager = metadataManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder keyManager(KeyManager keyManager) {
|
||||
this.keyManager = keyManager;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder backgroundTaskTimer(Timer backgroundTaskTimer) {
|
||||
this.backgroundTaskTimer = backgroundTaskTimer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder authExpiration(long authExpiration) {
|
||||
this.authExpiration = authExpiration;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder identityAttributeName(String identityAttributeName) {
|
||||
this.identityAttributeName = identityAttributeName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder groupAttributeName(String groupAttributeName) {
|
||||
this.groupAttributeName = groupAttributeName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder requestSigningEnabled(boolean requestSigningEnabled) {
|
||||
this.requestSigningEnabled = requestSigningEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder wantAssertionsSigned(boolean wantAssertionsSigned) {
|
||||
this.wantAssertionsSigned = wantAssertionsSigned;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SAMLConfiguration build() {
|
||||
return new StandardSAMLConfiguration(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,510 +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.saml.impl;
|
||||
|
||||
import org.apache.commons.httpclient.HttpClient;
|
||||
import org.apache.commons.httpclient.params.HttpClientParams;
|
||||
import org.apache.commons.httpclient.params.HttpConnectionParams;
|
||||
import org.apache.commons.httpclient.protocol.Protocol;
|
||||
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
|
||||
import org.apache.nifi.security.util.KeyStoreUtils;
|
||||
import org.apache.nifi.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.saml.NiFiSAMLContextProvider;
|
||||
import org.apache.nifi.web.security.saml.SAMLConfiguration;
|
||||
import org.apache.nifi.web.security.saml.SAMLConfigurationFactory;
|
||||
import org.apache.nifi.web.security.saml.impl.tls.CompositeKeyManager;
|
||||
import org.apache.nifi.web.security.saml.impl.tls.CustomTLSProtocolSocketFactory;
|
||||
import org.apache.nifi.web.security.saml.impl.tls.TruststoreStrategy;
|
||||
import org.apache.velocity.app.VelocityEngine;
|
||||
import org.opensaml.Configuration;
|
||||
import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
|
||||
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProvider;
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||
import org.opensaml.xml.parse.ParserPool;
|
||||
import org.opensaml.xml.parse.StaticBasicParserPool;
|
||||
import org.opensaml.xml.parse.XMLParserException;
|
||||
import org.opensaml.xml.security.BasicSecurityConfiguration;
|
||||
import org.opensaml.xml.security.SecurityHelper;
|
||||
import org.opensaml.xml.security.credential.Credential;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml.SAMLBootstrap;
|
||||
import org.springframework.security.saml.key.JKSKeyManager;
|
||||
import org.springframework.security.saml.key.KeyManager;
|
||||
import org.springframework.security.saml.log.SAMLDefaultLogger;
|
||||
import org.springframework.security.saml.log.SAMLLogger;
|
||||
import org.springframework.security.saml.metadata.CachingMetadataManager;
|
||||
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
||||
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
|
||||
import org.springframework.security.saml.metadata.MetadataManager;
|
||||
import org.springframework.security.saml.processor.HTTPArtifactBinding;
|
||||
import org.springframework.security.saml.processor.HTTPPAOS11Binding;
|
||||
import org.springframework.security.saml.processor.HTTPPostBinding;
|
||||
import org.springframework.security.saml.processor.HTTPRedirectDeflateBinding;
|
||||
import org.springframework.security.saml.processor.HTTPSOAP11Binding;
|
||||
import org.springframework.security.saml.processor.SAMLBinding;
|
||||
import org.springframework.security.saml.processor.SAMLProcessor;
|
||||
import org.springframework.security.saml.processor.SAMLProcessorImpl;
|
||||
import org.springframework.security.saml.storage.EmptyStorageFactory;
|
||||
import org.springframework.security.saml.util.VelocityFactory;
|
||||
import org.springframework.security.saml.websso.ArtifactResolutionProfileImpl;
|
||||
import org.springframework.security.saml.websso.SingleLogoutProfile;
|
||||
import org.springframework.security.saml.websso.SingleLogoutProfileImpl;
|
||||
import org.springframework.security.saml.websso.WebSSOProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileConsumerHoKImpl;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileECPImpl;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileHoKImpl;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileImpl;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileOptions;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StandardSAMLConfigurationFactory implements SAMLConfigurationFactory {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardSAMLConfigurationFactory.class);
|
||||
|
||||
public SAMLConfiguration create(final NiFiProperties properties) throws Exception {
|
||||
// ensure we only configure SAML when OIDC/KnoxSSO/LoginIdentityProvider are not enabled
|
||||
if (properties.isOidcEnabled() || properties.isKnoxSsoEnabled() || properties.isLoginIdentityProviderEnabled()) {
|
||||
throw new RuntimeException("SAML cannot be enabled if the Login Identity Provider or OpenId Connect or KnoxSSO is configured.");
|
||||
}
|
||||
|
||||
LOGGER.info("Initializing SAML configuration...");
|
||||
|
||||
// Load and validate config from nifi.properties...
|
||||
|
||||
final String rawEntityId = properties.getSamlServiceProviderEntityId();
|
||||
if (StringUtils.isBlank(rawEntityId)) {
|
||||
throw new RuntimeException("Entity ID is required when configuring SAML");
|
||||
}
|
||||
|
||||
final String spEntityId = rawEntityId;
|
||||
LOGGER.info("Service Provider Entity ID = '{}'", spEntityId);
|
||||
|
||||
final String rawIdpMetadataUrl = properties.getSamlIdentityProviderMetadataUrl();
|
||||
|
||||
if (StringUtils.isBlank(rawIdpMetadataUrl)) {
|
||||
throw new RuntimeException("IDP Metadata URL is required when configuring SAML");
|
||||
}
|
||||
|
||||
if (!rawIdpMetadataUrl.startsWith("file://")
|
||||
&& !rawIdpMetadataUrl.startsWith("http://")
|
||||
&& !rawIdpMetadataUrl.startsWith("https://")) {
|
||||
throw new RuntimeException("IDP Medata URL must start with file://, http://, or https://");
|
||||
}
|
||||
|
||||
final URI idpMetadataLocation = URI.create(rawIdpMetadataUrl);
|
||||
LOGGER.info("Identity Provider Metadata Location = '{}'", idpMetadataLocation);
|
||||
|
||||
final String authExpirationFromProperties = properties.getSamlAuthenticationExpiration();
|
||||
LOGGER.info("Authentication Expiration = '{}'", authExpirationFromProperties);
|
||||
|
||||
final long authExpiration;
|
||||
try {
|
||||
authExpiration = Math.round(FormatUtils.getPreciseTimeDuration(authExpirationFromProperties, TimeUnit.MILLISECONDS));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new RuntimeException("Invalid SAML authentication expiration: " + authExpirationFromProperties);
|
||||
}
|
||||
|
||||
final String identityAttributeName = properties.getSamlIdentityAttributeName();
|
||||
if (!StringUtils.isBlank(identityAttributeName)) {
|
||||
LOGGER.info("Identity Attribute Name = '{}'", identityAttributeName);
|
||||
}
|
||||
|
||||
final String groupAttributeName = properties.getSamlGroupAttributeName();
|
||||
if (!StringUtils.isBlank(groupAttributeName)) {
|
||||
LOGGER.info("Group Attribute Name = '{}'", groupAttributeName);
|
||||
}
|
||||
|
||||
final TruststoreStrategy truststoreStrategy;
|
||||
try {
|
||||
truststoreStrategy = TruststoreStrategy.valueOf(properties.getSamlHttpClientTruststoreStrategy());
|
||||
LOGGER.info("HttpClient Truststore Strategy = `{}`", truststoreStrategy.name());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Truststore Strategy must be one of " + TruststoreStrategy.NIFI.name() + " or " + TruststoreStrategy.JDK.name());
|
||||
}
|
||||
|
||||
int connectTimeout;
|
||||
final String rawConnectTimeout = properties.getSamlHttpClientConnectTimeout();
|
||||
try {
|
||||
connectTimeout = (int) FormatUtils.getPreciseTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch (final Exception e) {
|
||||
LOGGER.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
|
||||
NiFiProperties.SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT, rawConnectTimeout, NiFiProperties.DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT);
|
||||
connectTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
int readTimeout;
|
||||
final String rawReadTimeout = properties.getSamlHttpClientReadTimeout();
|
||||
try {
|
||||
readTimeout = (int) FormatUtils.getPreciseTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch (final Exception e) {
|
||||
LOGGER.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
|
||||
NiFiProperties.SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT, rawReadTimeout, NiFiProperties.DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT);
|
||||
readTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_SAML_HTTP_CLIENT_READ_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
// Initialize spring-security-saml/OpenSAML objects...
|
||||
|
||||
final SAMLBootstrap samlBootstrap = new SAMLBootstrap();
|
||||
samlBootstrap.postProcessBeanFactory(null);
|
||||
|
||||
final ParserPool parserPool = createParserPool();
|
||||
final VelocityEngine velocityEngine = VelocityFactory.getEngine();
|
||||
|
||||
final TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties);
|
||||
final KeyManager keyManager = createKeyManager(tlsConfiguration);
|
||||
|
||||
final HttpClient httpClient = createHttpClient(connectTimeout, readTimeout);
|
||||
if (truststoreStrategy == TruststoreStrategy.NIFI) {
|
||||
configureCustomTLSSocketFactory(tlsConfiguration);
|
||||
}
|
||||
|
||||
final boolean signMetadata = properties.isSamlMetadataSigningEnabled();
|
||||
final String signatureAlgorithm = properties.getSamlSignatureAlgorithm();
|
||||
final String signatureDigestAlgorithm = properties.getSamlSignatureDigestAlgorithm();
|
||||
configureGlobalSecurityDefaults(keyManager, signatureAlgorithm, signatureDigestAlgorithm);
|
||||
|
||||
final ExtendedMetadata extendedMetadata = createExtendedMetadata(signatureAlgorithm, signMetadata);
|
||||
|
||||
final Timer backgroundTaskTimer = new Timer(true);
|
||||
final MetadataProvider idpMetadataProvider = createIdpMetadataProvider(idpMetadataLocation, httpClient, backgroundTaskTimer, parserPool);
|
||||
|
||||
final MetadataManager metadataManager = createMetadataManager(idpMetadataProvider, extendedMetadata, keyManager);
|
||||
|
||||
final SAMLProcessor processor = createSAMLProcessor(parserPool, velocityEngine, httpClient);
|
||||
final NiFiSAMLContextProvider contextProvider = createContextProvider(metadataManager, keyManager);
|
||||
|
||||
// Build the configuration instance...
|
||||
|
||||
return new StandardSAMLConfiguration.Builder()
|
||||
.spEntityId(spEntityId)
|
||||
.processor(processor)
|
||||
.contextProvider(contextProvider)
|
||||
.logger(createSAMLLogger(properties))
|
||||
.webSSOProfileOptions(createWebSSOProfileOptions())
|
||||
.webSSOProfile(createWebSSOProfile(metadataManager, processor))
|
||||
.webSSOProfileECP(createWebSSOProfileECP(metadataManager, processor))
|
||||
.webSSOProfileHoK(createWebSSOProfileHok(metadataManager, processor))
|
||||
.webSSOProfileConsumer(createWebSSOProfileConsumer(metadataManager, processor))
|
||||
.webSSOProfileHoKConsumer(createWebSSOProfileHokConsumer(metadataManager, processor))
|
||||
.singleLogoutProfile(createSingeLogoutProfile(metadataManager, processor))
|
||||
.metadataManager(metadataManager)
|
||||
.extendedMetadata(extendedMetadata)
|
||||
.backgroundTaskTimer(backgroundTaskTimer)
|
||||
.keyManager(keyManager)
|
||||
.authExpiration(authExpiration)
|
||||
.identityAttributeName(identityAttributeName)
|
||||
.groupAttributeName(groupAttributeName)
|
||||
.requestSigningEnabled(properties.isSamlRequestSigningEnabled())
|
||||
.wantAssertionsSigned(properties.isSamlWantAssertionsSigned())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static ParserPool createParserPool() throws XMLParserException {
|
||||
final StaticBasicParserPool parserPool = new StaticBasicParserPool();
|
||||
parserPool.initialize();
|
||||
return parserPool;
|
||||
}
|
||||
|
||||
private static HttpClient createHttpClient(final int connectTimeout, final int readTimeout) {
|
||||
final HttpClientParams clientParams = new HttpClientParams();
|
||||
clientParams.setParameter(HttpConnectionParams.CONNECTION_TIMEOUT, connectTimeout);
|
||||
clientParams.setParameter(HttpConnectionParams.SO_TIMEOUT, readTimeout);
|
||||
|
||||
final HttpClient httpClient = new HttpClient(clientParams);
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private static void configureCustomTLSSocketFactory(final TlsConfiguration tlsConfiguration) throws TlsException {
|
||||
final SSLSocketFactory sslSocketFactory = SslContextFactory.createSSLSocketFactory(tlsConfiguration);
|
||||
final ProtocolSocketFactory socketFactory = new CustomTLSProtocolSocketFactory(sslSocketFactory);
|
||||
|
||||
// Consider not using global registration of protocol here as it would potentially impact other uses of commons http client
|
||||
// with in nifi-framework-nar, currently there are no other usages, see https://hc.apache.org/httpclient-3.x/sslguide.html
|
||||
final Protocol p = new Protocol("https", socketFactory, 443);
|
||||
Protocol.registerProtocol(p.getScheme(), p);
|
||||
}
|
||||
|
||||
private static SAMLProcessor createSAMLProcessor(final ParserPool parserPool, final VelocityEngine velocityEngine, final HttpClient httpClient) {
|
||||
final HTTPSOAP11Binding httpsoap11Binding = new HTTPSOAP11Binding(parserPool);
|
||||
final HTTPPAOS11Binding httppaos11Binding = new HTTPPAOS11Binding(parserPool);
|
||||
final HTTPPostBinding httpPostBinding = new HTTPPostBinding(parserPool, velocityEngine);
|
||||
final HTTPRedirectDeflateBinding httpRedirectDeflateBinding = new HTTPRedirectDeflateBinding(parserPool);
|
||||
|
||||
final ArtifactResolutionProfileImpl artifactResolutionProfile = new ArtifactResolutionProfileImpl(httpClient);
|
||||
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(httpsoap11Binding));
|
||||
|
||||
final HTTPArtifactBinding httpArtifactBinding = new HTTPArtifactBinding(
|
||||
parserPool, velocityEngine, artifactResolutionProfile);
|
||||
|
||||
final Collection<SAMLBinding> bindings = new ArrayList<>();
|
||||
bindings.add(httpRedirectDeflateBinding);
|
||||
bindings.add(httpPostBinding);
|
||||
bindings.add(httpArtifactBinding);
|
||||
bindings.add(httpsoap11Binding);
|
||||
bindings.add(httppaos11Binding);
|
||||
|
||||
return new SAMLProcessorImpl(bindings);
|
||||
}
|
||||
|
||||
private static NiFiSAMLContextProvider createContextProvider(final MetadataManager metadataManager, final KeyManager keyManager) throws ServletException {
|
||||
final NiFiSAMLContextProviderImpl contextProvider = new NiFiSAMLContextProviderImpl();
|
||||
contextProvider.setMetadata(metadataManager);
|
||||
contextProvider.setKeyManager(keyManager);
|
||||
|
||||
// Note - the default is HttpSessionStorageFactory, but since we don't use HttpSessions we can't rely on that,
|
||||
// setting this to the EmptyStorageFactory simply disables checking of the InResponseTo field, if we ever want
|
||||
// to bring that back we could possibly implement our own in-memory storage factory
|
||||
// https://docs.spring.io/spring-security-saml/docs/current/reference/html/chapter-troubleshooting.html#d5e1935
|
||||
contextProvider.setStorageFactory(new EmptyStorageFactory());
|
||||
|
||||
contextProvider.afterPropertiesSet();
|
||||
return contextProvider;
|
||||
}
|
||||
|
||||
private static WebSSOProfileOptions createWebSSOProfileOptions() {
|
||||
final WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
|
||||
webSSOProfileOptions.setIncludeScoping(false);
|
||||
return webSSOProfileOptions;
|
||||
}
|
||||
|
||||
private static WebSSOProfile createWebSSOProfile(final MetadataManager metadataManager, final SAMLProcessor processor) throws Exception {
|
||||
final WebSSOProfileImpl webSSOProfile = new WebSSOProfileImpl(processor, metadataManager);
|
||||
webSSOProfile.afterPropertiesSet();
|
||||
return webSSOProfile;
|
||||
}
|
||||
|
||||
private static WebSSOProfile createWebSSOProfileECP(final MetadataManager metadataManager, final SAMLProcessor processor) throws Exception {
|
||||
final WebSSOProfileECPImpl webSSOProfileECP = new WebSSOProfileECPImpl();
|
||||
webSSOProfileECP.setProcessor(processor);
|
||||
webSSOProfileECP.setMetadata(metadataManager);
|
||||
webSSOProfileECP.afterPropertiesSet();
|
||||
return webSSOProfileECP;
|
||||
}
|
||||
|
||||
private static WebSSOProfile createWebSSOProfileHok(final MetadataManager metadataManager, final SAMLProcessor processor) throws Exception {
|
||||
final WebSSOProfileHoKImpl webSSOProfileHok = new WebSSOProfileHoKImpl();
|
||||
webSSOProfileHok.setProcessor(processor);
|
||||
webSSOProfileHok.setMetadata(metadataManager);
|
||||
webSSOProfileHok.afterPropertiesSet();
|
||||
return webSSOProfileHok;
|
||||
}
|
||||
|
||||
private static WebSSOProfileConsumer createWebSSOProfileConsumer(final MetadataManager metadataManager, final SAMLProcessor processor) throws Exception {
|
||||
final WebSSOProfileConsumerImpl webSSOProfileConsumer = new WebSSOProfileConsumerImpl();
|
||||
webSSOProfileConsumer.setProcessor(processor);
|
||||
webSSOProfileConsumer.setMetadata(metadataManager);
|
||||
webSSOProfileConsumer.afterPropertiesSet();
|
||||
return webSSOProfileConsumer;
|
||||
}
|
||||
|
||||
private static WebSSOProfileConsumer createWebSSOProfileHokConsumer(final MetadataManager metadataManager, final SAMLProcessor processor) throws Exception {
|
||||
final WebSSOProfileConsumerHoKImpl webSSOProfileHoKConsumer = new WebSSOProfileConsumerHoKImpl();
|
||||
webSSOProfileHoKConsumer.setProcessor(processor);
|
||||
webSSOProfileHoKConsumer.setMetadata(metadataManager);
|
||||
webSSOProfileHoKConsumer.afterPropertiesSet();
|
||||
return webSSOProfileHoKConsumer;
|
||||
}
|
||||
|
||||
private static SingleLogoutProfile createSingeLogoutProfile(final MetadataManager metadataManager, final SAMLProcessor processor) throws Exception {
|
||||
final SingleLogoutProfileImpl singleLogoutProfile = new SingleLogoutProfileImpl();
|
||||
singleLogoutProfile.setProcessor(processor);
|
||||
singleLogoutProfile.setMetadata(metadataManager);
|
||||
singleLogoutProfile.afterPropertiesSet();
|
||||
return singleLogoutProfile;
|
||||
}
|
||||
|
||||
private static SAMLLogger createSAMLLogger(final NiFiProperties properties) {
|
||||
final SAMLDefaultLogger samlLogger = new SAMLDefaultLogger();
|
||||
if (properties.isSamlMessageLoggingEnabled()) {
|
||||
samlLogger.setLogAllMessages(true);
|
||||
samlLogger.setLogErrors(true);
|
||||
samlLogger.setLogMessagesOnException(true);
|
||||
} else {
|
||||
samlLogger.setLogAllMessages(false);
|
||||
samlLogger.setLogErrors(false);
|
||||
samlLogger.setLogMessagesOnException(false);
|
||||
}
|
||||
return samlLogger;
|
||||
}
|
||||
|
||||
private static KeyManager createKeyManager(final TlsConfiguration tlsConfiguration) throws TlsException, KeyStoreException {
|
||||
final String keystorePath = tlsConfiguration.getKeystorePath();
|
||||
final char[] keystorePasswordChars = tlsConfiguration.getKeystorePassword().toCharArray();
|
||||
final String keystoreType = tlsConfiguration.getKeystoreType().getType();
|
||||
|
||||
final String truststorePath = tlsConfiguration.getTruststorePath();
|
||||
final char[] truststorePasswordChars = tlsConfiguration.getTruststorePassword().toCharArray();
|
||||
final String truststoreType = tlsConfiguration.getTruststoreType().getType();
|
||||
|
||||
final KeyStore keyStore = KeyStoreUtils.loadKeyStore(keystorePath, keystorePasswordChars, keystoreType);
|
||||
final KeyStore trustStore = KeyStoreUtils.loadTrustStore(truststorePath, truststorePasswordChars, truststoreType);
|
||||
|
||||
final String keyAlias = getPrivateKeyAlias(keyStore, keystorePath);
|
||||
LOGGER.info("Default key alias = {}", keyAlias);
|
||||
|
||||
// if no key password was provided, then assume the keystore password is the same as the key password.
|
||||
final String keyPassword = StringUtils.isBlank(tlsConfiguration.getKeyPassword()) ? tlsConfiguration.getKeystorePassword() : tlsConfiguration.getKeyPassword();
|
||||
|
||||
final Map<String,String> keyPasswords = new HashMap<>();
|
||||
if (!StringUtils.isBlank(keyPassword)) {
|
||||
keyPasswords.put(keyAlias, keyPassword);
|
||||
}
|
||||
|
||||
final KeyManager keystoreKeyManager = new JKSKeyManager(keyStore, keyPasswords, keyAlias);
|
||||
final KeyManager truststoreKeyManager = new JKSKeyManager(trustStore, Collections.emptyMap(), null);
|
||||
return new CompositeKeyManager(keystoreKeyManager, truststoreKeyManager);
|
||||
}
|
||||
|
||||
private static String getPrivateKeyAlias(final KeyStore keyStore, final String keystorePath) throws KeyStoreException {
|
||||
final Set<String> keyAliases = getKeyAliases(keyStore);
|
||||
|
||||
int privateKeyAliases = 0;
|
||||
for (final String keyAlias : keyAliases) {
|
||||
if (keyStore.isKeyEntry(keyAlias)) {
|
||||
privateKeyAliases++;
|
||||
}
|
||||
}
|
||||
|
||||
if (privateKeyAliases == 0) {
|
||||
throw new RuntimeException("Unable to determine signing key, the keystore '" + keystorePath + "' does not contain any private keys");
|
||||
}
|
||||
|
||||
if (privateKeyAliases > 1) {
|
||||
throw new RuntimeException("Unable to determine signing key, the keystore '" + keystorePath + "' contains more than one private key");
|
||||
}
|
||||
|
||||
String firstPrivateKeyAlias = null;
|
||||
for (final String keyAlias : keyAliases) {
|
||||
if (keyStore.isKeyEntry(keyAlias)) {
|
||||
firstPrivateKeyAlias = keyAlias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstPrivateKeyAlias;
|
||||
}
|
||||
|
||||
private static Set<String> getKeyAliases(final KeyStore keyStore) throws KeyStoreException {
|
||||
final Set<String> availableKeys = new HashSet<String>();
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
availableKeys.add(aliases.nextElement());
|
||||
}
|
||||
return availableKeys;
|
||||
}
|
||||
|
||||
private static ExtendedMetadata createExtendedMetadata(final String signingAlgorithm, final boolean signMetadata) {
|
||||
final ExtendedMetadata extendedMetadata = new ExtendedMetadata();
|
||||
extendedMetadata.setIdpDiscoveryEnabled(true);
|
||||
extendedMetadata.setSigningAlgorithm(signingAlgorithm);
|
||||
extendedMetadata.setSignMetadata(signMetadata);
|
||||
extendedMetadata.setEcpEnabled(true);
|
||||
return extendedMetadata;
|
||||
}
|
||||
|
||||
private static MetadataProvider createIdpMetadataProvider(final URI idpMetadataLocation, final HttpClient httpClient,
|
||||
final Timer timer, final ParserPool parserPool) throws Exception {
|
||||
if (idpMetadataLocation.getScheme().startsWith("http")) {
|
||||
return createHttpIdpMetadataProvider(idpMetadataLocation, httpClient, timer, parserPool);
|
||||
} else {
|
||||
return createFileIdpMetadataProvider(idpMetadataLocation, parserPool);
|
||||
}
|
||||
}
|
||||
|
||||
private static MetadataProvider createFileIdpMetadataProvider(final URI idpMetadataLocation, final ParserPool parserPool)
|
||||
throws MetadataProviderException {
|
||||
final String idpMetadataFilePath = idpMetadataLocation.getPath();
|
||||
final File idpMetadataFile = new File(idpMetadataFilePath);
|
||||
LOGGER.info("Loading IDP metadata from file located at: " + idpMetadataFile.getAbsolutePath());
|
||||
|
||||
final FilesystemMetadataProvider filesystemMetadataProvider = new FilesystemMetadataProvider(idpMetadataFile);
|
||||
filesystemMetadataProvider.setParserPool(parserPool);
|
||||
filesystemMetadataProvider.initialize();
|
||||
return filesystemMetadataProvider;
|
||||
}
|
||||
|
||||
private static MetadataProvider createHttpIdpMetadataProvider(final URI idpMetadataLocation, final HttpClient httpClient,
|
||||
final Timer timer, final ParserPool parserPool) throws Exception {
|
||||
final String idpMetadataUrl = idpMetadataLocation.toString();
|
||||
final HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(timer, httpClient, idpMetadataUrl);
|
||||
httpMetadataProvider.setParserPool(parserPool);
|
||||
httpMetadataProvider.initialize();
|
||||
return httpMetadataProvider;
|
||||
}
|
||||
|
||||
private static MetadataManager createMetadataManager(final MetadataProvider idpMetadataProvider, final ExtendedMetadata extendedMetadata, final KeyManager keyManager)
|
||||
throws MetadataProviderException {
|
||||
final ExtendedMetadataDelegate idpExtendedMetadataDelegate = new ExtendedMetadataDelegate(idpMetadataProvider, extendedMetadata);
|
||||
idpExtendedMetadataDelegate.setMetadataTrustCheck(true);
|
||||
idpExtendedMetadataDelegate.setMetadataRequireSignature(false);
|
||||
|
||||
final MetadataManager metadataManager = new CachingMetadataManager(Arrays.asList(idpExtendedMetadataDelegate));
|
||||
metadataManager.setKeyManager(keyManager);
|
||||
metadataManager.afterPropertiesSet();
|
||||
return metadataManager;
|
||||
}
|
||||
|
||||
private static void configureGlobalSecurityDefaults(final KeyManager keyManager, final String signingAlgorithm, final String digestAlgorithm) {
|
||||
final BasicSecurityConfiguration securityConfiguration = (BasicSecurityConfiguration) Configuration.getGlobalSecurityConfiguration();
|
||||
|
||||
if (!StringUtils.isBlank(signingAlgorithm)) {
|
||||
final Credential defaultCredential = keyManager.getDefaultCredential();
|
||||
final Key signingKey = SecurityHelper.extractSigningKey(defaultCredential);
|
||||
|
||||
// ensure that the requested signature algorithm can be produced by the type of key we have (i.e. RSA key -> rsa-sha1 signature)
|
||||
final String keyAlgorithm = signingKey.getAlgorithm();
|
||||
if (!signingAlgorithm.contains(keyAlgorithm.toLowerCase())) {
|
||||
throw new IllegalStateException("Key algorithm '" + keyAlgorithm + "' cannot be used to create signatures of type '" + signingAlgorithm + "'");
|
||||
}
|
||||
|
||||
securityConfiguration.registerSignatureAlgorithmURI(keyAlgorithm, signingAlgorithm);
|
||||
}
|
||||
|
||||
if (!StringUtils.isBlank(digestAlgorithm)) {
|
||||
securityConfiguration.setSignatureReferenceDigestMethod(digestAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.saml.impl;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpCredentialService;
|
||||
import org.apache.nifi.idp.IdpCredential;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.saml.SAMLCredentialStore;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
/**
|
||||
* Standard implementation of SAMLCredentialStore that uses Java serialization to store
|
||||
* SAMLCredential objects as BLOBs in a relational database.
|
||||
*/
|
||||
public class StandardSAMLCredentialStore implements SAMLCredentialStore {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardSAMLCredentialStore.class);
|
||||
|
||||
private final IdpCredentialService idpCredentialService;
|
||||
|
||||
public StandardSAMLCredentialStore(final IdpCredentialService idpCredentialService) {
|
||||
this.idpCredentialService = idpCredentialService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(final String identity, final SAMLCredential credential) {
|
||||
if (StringUtils.isBlank(identity)) {
|
||||
throw new IllegalArgumentException("Identity cannot be null");
|
||||
}
|
||||
|
||||
if (credential == null) {
|
||||
throw new IllegalArgumentException("Credential cannot be null");
|
||||
}
|
||||
|
||||
try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
final ObjectOutputStream objOut = new ObjectOutputStream(baos)) {
|
||||
|
||||
objOut.writeObject(credential);
|
||||
objOut.flush();
|
||||
baos.flush();
|
||||
|
||||
final IdpCredential idpCredential = new IdpCredential();
|
||||
idpCredential.setIdentity(identity);
|
||||
idpCredential.setType(IdpType.SAML);
|
||||
idpCredential.setCredential(baos.toByteArray());
|
||||
|
||||
// replace issues a delete first in case the user already has a stored credential that wasn't properly cleaned up on logout
|
||||
final IdpCredential createdIdpCredential = idpCredentialService.replaceCredential(idpCredential);
|
||||
LOGGER.debug("Successfully saved SAMLCredential for {} with id {}", identity, createdIdpCredential.getId());
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to serialize SAMLCredential for user with identity " + identity, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLCredential get(final String identity) {
|
||||
final IdpCredential idpCredential = idpCredentialService.getCredential(identity);
|
||||
if (idpCredential == null) {
|
||||
LOGGER.debug("No SAMLCredential exists for {}", identity);
|
||||
return null;
|
||||
}
|
||||
|
||||
final IdpType idpType = idpCredential.getType();
|
||||
if (idpType != IdpType.SAML) {
|
||||
LOGGER.debug("Stored credential for {} was not a SAML credential, type was {}", identity, idpType);
|
||||
return null;
|
||||
}
|
||||
|
||||
final byte[] serializedCredential = idpCredential.getCredential();
|
||||
|
||||
try (final ByteArrayInputStream bais = new ByteArrayInputStream(serializedCredential);
|
||||
final ObjectInputStream objIn = new ObjectInputStream(bais)) {
|
||||
|
||||
final SAMLCredential samlCredential = (SAMLCredential) objIn.readObject();
|
||||
return samlCredential;
|
||||
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new RuntimeException("Unable to deserialize SAMLCredential for user with identity " + identity, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(final String identity) {
|
||||
final IdpCredential idpCredential = idpCredentialService.getCredential(identity);
|
||||
|
||||
if (idpCredential == null) {
|
||||
LOGGER.debug("No SAMLCredential exists for {}", identity);
|
||||
return;
|
||||
}
|
||||
|
||||
final IdpType idpType = idpCredential.getType();
|
||||
if (idpType != IdpType.SAML) {
|
||||
LOGGER.debug("Stored credential for {} was not a SAML credential, type was {}", identity, idpType);
|
||||
return;
|
||||
}
|
||||
|
||||
idpCredentialService.deleteCredential(idpCredential.getId());
|
||||
}
|
||||
|
||||
}
|
@ -1,534 +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.saml.impl;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.apache.nifi.web.security.saml.NiFiSAMLContextProvider;
|
||||
import org.apache.nifi.web.security.saml.SAMLConfiguration;
|
||||
import org.apache.nifi.web.security.saml.SAMLConfigurationFactory;
|
||||
import org.apache.nifi.web.security.saml.SAMLEndpoints;
|
||||
import org.apache.nifi.web.security.saml.SAMLService;
|
||||
import org.opensaml.common.SAMLException;
|
||||
import org.opensaml.common.SAMLRuntimeException;
|
||||
import org.opensaml.common.binding.decoding.URIComparator;
|
||||
import org.opensaml.saml2.core.Attribute;
|
||||
import org.opensaml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml2.core.LogoutResponse;
|
||||
import org.opensaml.saml2.metadata.Endpoint;
|
||||
import org.opensaml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProvider;
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||
import org.opensaml.xml.XMLObject;
|
||||
import org.opensaml.xml.encryption.DecryptionException;
|
||||
import org.opensaml.xml.schema.XSString;
|
||||
import org.opensaml.xml.schema.impl.XSAnyImpl;
|
||||
import org.opensaml.xml.validation.ValidationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml.SAMLConstants;
|
||||
import org.springframework.security.saml.SAMLCredential;
|
||||
import org.springframework.security.saml.SAMLLogoutProcessingFilter;
|
||||
import org.springframework.security.saml.SAMLProcessingFilter;
|
||||
import org.springframework.security.saml.context.SAMLMessageContext;
|
||||
import org.springframework.security.saml.key.KeyManager;
|
||||
import org.springframework.security.saml.log.SAMLLogger;
|
||||
import org.springframework.security.saml.metadata.ExtendedMetadata;
|
||||
import org.springframework.security.saml.metadata.ExtendedMetadataDelegate;
|
||||
import org.springframework.security.saml.metadata.MetadataGenerator;
|
||||
import org.springframework.security.saml.metadata.MetadataManager;
|
||||
import org.springframework.security.saml.metadata.MetadataMemoryProvider;
|
||||
import org.springframework.security.saml.processor.SAMLProcessor;
|
||||
import org.springframework.security.saml.util.DefaultURLComparator;
|
||||
import org.springframework.security.saml.util.SAMLUtil;
|
||||
import org.springframework.security.saml.websso.SingleLogoutProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfile;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileConsumer;
|
||||
import org.springframework.security.saml.websso.WebSSOProfileOptions;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class StandardSAMLService implements SAMLService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StandardSAMLService.class);
|
||||
|
||||
private final NiFiProperties properties;
|
||||
private final SAMLConfigurationFactory samlConfigurationFactory;
|
||||
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
private final AtomicBoolean spMetadataInitialized = new AtomicBoolean(false);
|
||||
private final AtomicReference<String> spBaseUrl = new AtomicReference<>(null);
|
||||
private final URIComparator uriComparator = new DefaultURLComparator();
|
||||
|
||||
private SAMLConfiguration samlConfiguration;
|
||||
|
||||
|
||||
public StandardSAMLService(final SAMLConfigurationFactory samlConfigurationFactory, final NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
this.samlConfigurationFactory = samlConfigurationFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void initialize() {
|
||||
// this method will always be called so if SAML is not configured just return, don't throw an exception
|
||||
if (!properties.isSamlEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// already initialized so return
|
||||
if (initialized.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info("Initializing SAML Service...");
|
||||
samlConfiguration = samlConfigurationFactory.create(properties);
|
||||
initialized.set(true);
|
||||
LOGGER.info("Finished initializing SAML Service");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to initialize SAML configuration due to: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// this method will always be called so if SAML is not configured just return, don't throw an exception
|
||||
if (!properties.isSamlEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Shutting down SAML Service...");
|
||||
|
||||
if (samlConfiguration != null) {
|
||||
try {
|
||||
final Timer backgroundTimer = samlConfiguration.getBackgroundTaskTimer();
|
||||
backgroundTimer.purge();
|
||||
backgroundTimer.cancel();
|
||||
} catch (final Exception e) {
|
||||
LOGGER.warn("Error shutting down background timer: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
final MetadataManager metadataManager = samlConfiguration.getMetadataManager();
|
||||
metadataManager.destroy();
|
||||
} catch (final Exception e) {
|
||||
LOGGER.warn("Error shutting down metadata manager: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
samlConfiguration = null;
|
||||
initialized.set(false);
|
||||
spMetadataInitialized.set(false);
|
||||
spBaseUrl.set(null);
|
||||
|
||||
LOGGER.info("Finished shutting down SAML Service");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSamlEnabled() {
|
||||
return properties.isSamlEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServiceProviderInitialized() {
|
||||
return spMetadataInitialized.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void initializeServiceProvider(final String baseUrl) {
|
||||
if (!isSamlEnabled()) {
|
||||
throw new IllegalStateException(SAML_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(baseUrl)) {
|
||||
throw new IllegalArgumentException("baseUrl is required when initializing the service provider");
|
||||
}
|
||||
|
||||
if (isServiceProviderInitialized()) {
|
||||
final String existingBaseUrl = spBaseUrl.get();
|
||||
LOGGER.info("Service provider already initialized with baseUrl = '{}'", new Object[]{existingBaseUrl});
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Initializing SAML service provider with baseUrl = '{}'", new Object[]{baseUrl});
|
||||
try {
|
||||
initializeServiceProviderMetadata(baseUrl);
|
||||
spBaseUrl.set(baseUrl);
|
||||
spMetadataInitialized.set(true);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to initialize SAML service provider: " + e.getMessage(), e);
|
||||
}
|
||||
LOGGER.info("Done initializing SAML service provider");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceProviderMetadata() {
|
||||
verifyReadyForSamlOperations();
|
||||
try {
|
||||
final KeyManager keyManager = samlConfiguration.getKeyManager();
|
||||
final MetadataManager metadataManager = samlConfiguration.getMetadataManager();
|
||||
|
||||
final String spEntityId = samlConfiguration.getSpEntityId();
|
||||
final EntityDescriptor descriptor = metadataManager.getEntityDescriptor(spEntityId);
|
||||
|
||||
final String metadataString = SAMLUtil.getMetadataAsString(metadataManager, keyManager, descriptor, null);
|
||||
return metadataString;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to obtain SAML service provider metadata", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAuthExpiration() {
|
||||
verifyReadyForSamlOperations();
|
||||
return samlConfiguration.getAuthExpiration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initiateLogin(final HttpServletRequest request, final HttpServletResponse response, final String relayState) {
|
||||
verifyReadyForSamlOperations();
|
||||
|
||||
final SAMLLogger samlLogger = samlConfiguration.getLogger();
|
||||
final NiFiSAMLContextProvider contextProvider = samlConfiguration.getContextProvider();
|
||||
|
||||
final SAMLMessageContext context;
|
||||
try {
|
||||
context = contextProvider.getLocalAndPeerEntity(request, response, Collections.emptyMap());
|
||||
} catch (final MetadataProviderException e) {
|
||||
throw new IllegalStateException("Unable to create SAML Message Context: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Generate options for the current SSO request
|
||||
final WebSSOProfileOptions options = samlConfiguration.getWebSSOProfileOptions().clone();
|
||||
options.setRelayState(relayState);
|
||||
|
||||
// Send WebSSO AuthN request
|
||||
final WebSSOProfile webSSOProfile = samlConfiguration.getWebSSOProfile();
|
||||
try {
|
||||
webSSOProfile.sendAuthenticationRequest(context, options);
|
||||
samlLogger.log(SAMLConstants.AUTH_N_REQUEST, SAMLConstants.SUCCESS, context);
|
||||
} catch (Exception e) {
|
||||
samlLogger.log(SAMLConstants.AUTH_N_REQUEST, SAMLConstants.FAILURE, context);
|
||||
throw new RuntimeException("Unable to initiate SAML authentication request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SAMLCredential processLogin(final HttpServletRequest request, final HttpServletResponse response, final Map<String,String> parameters) {
|
||||
verifyReadyForSamlOperations();
|
||||
|
||||
LOGGER.info("Attempting SAML2 authentication using profile {}", SAMLConstants.SAML2_WEBSSO_PROFILE_URI);
|
||||
|
||||
final SAMLMessageContext context;
|
||||
try {
|
||||
final NiFiSAMLContextProvider contextProvider = samlConfiguration.getContextProvider();
|
||||
context = contextProvider.getLocalEntity(request, response, parameters);
|
||||
} catch (MetadataProviderException e) {
|
||||
throw new IllegalStateException("Unable to create SAML Message Context: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
final SAMLProcessor samlProcessor = samlConfiguration.getProcessor();
|
||||
try {
|
||||
samlProcessor.retrieveMessage(context);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to load SAML message: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Override set values
|
||||
context.setCommunicationProfileId(SAMLConstants.SAML2_WEBSSO_PROFILE_URI);
|
||||
try {
|
||||
context.setLocalEntityEndpoint(getLocalEntityEndpoint(context));
|
||||
} catch (SAMLException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (!SAMLConstants.SAML2_WEBSSO_PROFILE_URI.equals(context.getCommunicationProfileId())) {
|
||||
throw new IllegalStateException("Unsupported profile encountered in the context: " + context.getCommunicationProfileId());
|
||||
}
|
||||
|
||||
final SAMLLogger samlLogger = samlConfiguration.getLogger();
|
||||
final WebSSOProfileConsumer webSSOProfileConsumer = samlConfiguration.getWebSSOProfileConsumer();
|
||||
|
||||
try {
|
||||
final SAMLCredential credential = webSSOProfileConsumer.processAuthenticationResponse(context);
|
||||
LOGGER.debug("SAML Response contains successful authentication for NameID: " + credential.getNameID().getValue());
|
||||
samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.SUCCESS, context);
|
||||
return credential;
|
||||
} catch (SAMLException | SAMLRuntimeException e) {
|
||||
LOGGER.error("Error validating SAML message", e);
|
||||
samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
|
||||
throw new RuntimeException("Error validating SAML message: " + e.getMessage(), e);
|
||||
} catch (org.opensaml.xml.security.SecurityException | ValidationException e) {
|
||||
LOGGER.error("Error validating signature", e);
|
||||
samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
|
||||
throw new RuntimeException("Error validating SAML message signature: " + e.getMessage(), e);
|
||||
} catch (DecryptionException e) {
|
||||
LOGGER.error("Error decrypting SAML message", e);
|
||||
samlLogger.log(SAMLConstants.AUTH_N_RESPONSE, SAMLConstants.FAILURE, context, e);
|
||||
throw new RuntimeException("Error decrypting SAML message: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserIdentity(final SAMLCredential credential) {
|
||||
verifyReadyForSamlOperations();
|
||||
|
||||
if (credential == null) {
|
||||
throw new IllegalArgumentException("SAML Credential is required");
|
||||
}
|
||||
|
||||
String userIdentity = null;
|
||||
|
||||
final String identityAttributeName = samlConfiguration.getIdentityAttributeName();
|
||||
if (StringUtils.isBlank(identityAttributeName)) {
|
||||
userIdentity = credential.getNameID().getValue();
|
||||
LOGGER.info("No identity attribute specified, using NameID for user identity: {}", userIdentity);
|
||||
} else {
|
||||
LOGGER.debug("Looking for SAML attribute {} ...", identityAttributeName);
|
||||
|
||||
final List<Attribute> attributes = credential.getAttributes();
|
||||
if (attributes == null || attributes.isEmpty()) {
|
||||
userIdentity = credential.getNameID().getValue();
|
||||
LOGGER.warn("No attributes returned in SAML response, using NameID for user identity: {}", userIdentity);
|
||||
} else {
|
||||
for (final Attribute attribute : attributes) {
|
||||
if (!identityAttributeName.equals(attribute.getName())) {
|
||||
LOGGER.trace("Skipping SAML attribute {}", attribute.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final XMLObject value : attribute.getAttributeValues()) {
|
||||
if (value instanceof XSString) {
|
||||
final XSString valueXSString = (XSString) value;
|
||||
userIdentity = valueXSString.getValue();
|
||||
break;
|
||||
} else {
|
||||
LOGGER.debug("Value was not XSString, but was " + value.getClass().getCanonicalName());
|
||||
}
|
||||
}
|
||||
|
||||
if (userIdentity != null) {
|
||||
LOGGER.info("Found user identity {} in attribute {}", userIdentity, attribute.getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userIdentity == null) {
|
||||
userIdentity = credential.getNameID().getValue();
|
||||
LOGGER.warn("No attribute found named {}, using NameID for user identity: {}", identityAttributeName, userIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
return userIdentity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getUserGroups(final SAMLCredential credential) {
|
||||
verifyReadyForSamlOperations();
|
||||
|
||||
if (credential == null) {
|
||||
throw new IllegalArgumentException("SAML Credential is required");
|
||||
}
|
||||
|
||||
final String userIdentity = credential.getNameID().getValue();
|
||||
final String groupAttributeName = samlConfiguration.getGroupAttributeName();
|
||||
if (StringUtils.isBlank(groupAttributeName)) {
|
||||
LOGGER.warn("Cannot obtain groups for {} because no group attribute name has been configured", userIdentity);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
final Set<String> groups = new HashSet<>();
|
||||
if (credential.getAttributes() != null) {
|
||||
for (final Attribute attribute : credential.getAttributes()) {
|
||||
if (!groupAttributeName.equals(attribute.getName())) {
|
||||
LOGGER.debug("Skipping SAML attribute {}", attribute.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final XMLObject value : attribute.getAttributeValues()) {
|
||||
if (value instanceof XSString) {
|
||||
final XSString valueXSString = (XSString) value;
|
||||
final String groupName = valueXSString.getValue();
|
||||
LOGGER.debug("Found group {} for {}", groupName, userIdentity);
|
||||
groups.add(groupName);
|
||||
} else if (value instanceof XSAnyImpl) {
|
||||
final XSAnyImpl valueXSAnyImpl = (XSAnyImpl) value;
|
||||
final String groupName = valueXSAnyImpl.getTextContent();
|
||||
LOGGER.debug("Found group {} for {}", groupName, userIdentity);
|
||||
groups.add(groupName);
|
||||
} else {
|
||||
LOGGER.debug("Value was not XSString and XSAnyImpl, but was " + value.getClass().getCanonicalName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initiateLogout(final HttpServletRequest request, final HttpServletResponse response, final SAMLCredential credential) {
|
||||
verifyReadyForSamlOperations();
|
||||
|
||||
final SAMLMessageContext context;
|
||||
try {
|
||||
final NiFiSAMLContextProvider contextProvider = samlConfiguration.getContextProvider();
|
||||
context = contextProvider.getLocalAndPeerEntity(request, response, Collections.emptyMap());
|
||||
} catch (MetadataProviderException e) {
|
||||
throw new IllegalStateException("Unable to create SAML Message Context: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
final SAMLLogger samlLogger = samlConfiguration.getLogger();
|
||||
final SingleLogoutProfile singleLogoutProfile = samlConfiguration.getSingleLogoutProfile();
|
||||
|
||||
try {
|
||||
singleLogoutProfile.sendLogoutRequest(context, credential);
|
||||
samlLogger.log(SAMLConstants.LOGOUT_REQUEST, SAMLConstants.SUCCESS, context);
|
||||
} catch (Exception e) {
|
||||
samlLogger.log(SAMLConstants.LOGOUT_REQUEST, SAMLConstants.FAILURE, context);
|
||||
throw new RuntimeException("Unable to initiate SAML logout request: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLogout(final HttpServletRequest request, final HttpServletResponse response, final Map<String, String> parameters) {
|
||||
verifyReadyForSamlOperations();
|
||||
|
||||
final SAMLMessageContext context;
|
||||
try {
|
||||
final NiFiSAMLContextProvider contextProvider = samlConfiguration.getContextProvider();
|
||||
context = contextProvider.getLocalAndPeerEntity(request, response, parameters);
|
||||
} catch (MetadataProviderException e) {
|
||||
throw new IllegalStateException("Unable to create SAML Message Context: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
final SAMLProcessor samlProcessor = samlConfiguration.getProcessor();
|
||||
try {
|
||||
samlProcessor.retrieveMessage(context);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to load SAML message: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Override set values
|
||||
context.setCommunicationProfileId(SAMLConstants.SAML2_SLO_PROFILE_URI);
|
||||
try {
|
||||
context.setLocalEntityEndpoint(getLocalEntityEndpoint(context));
|
||||
} catch (SAMLException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Determine if the incoming SAML messages is a response to a logout we initiated, or a request initiated by the IDP
|
||||
if (context.getInboundSAMLMessage() instanceof LogoutResponse) {
|
||||
processLogoutResponse(context);
|
||||
} else if (context.getInboundSAMLMessage() instanceof LogoutRequest) {
|
||||
processLogoutRequest(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void processLogoutResponse(final SAMLMessageContext context) {
|
||||
final SAMLLogger samlLogger = samlConfiguration.getLogger();
|
||||
final SingleLogoutProfile logoutProfile = samlConfiguration.getSingleLogoutProfile();
|
||||
|
||||
try {
|
||||
logoutProfile.processLogoutResponse(context);
|
||||
samlLogger.log(SAMLConstants.LOGOUT_RESPONSE, SAMLConstants.SUCCESS, context);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Received logout response is invalid", e);
|
||||
samlLogger.log(SAMLConstants.LOGOUT_RESPONSE, SAMLConstants.FAILURE, context, e);
|
||||
throw new RuntimeException("Received logout response is invalid: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processLogoutRequest(final SAMLMessageContext context) {
|
||||
throw new UnsupportedOperationException("Apache NiFi currently does not support IDP initiated logout");
|
||||
}
|
||||
|
||||
private Endpoint getLocalEntityEndpoint(final SAMLMessageContext context) throws SAMLException {
|
||||
return SAMLUtil.getEndpoint(
|
||||
context.getLocalEntityRoleMetadata().getEndpoints(),
|
||||
context.getInboundSAMLBinding(),
|
||||
context.getInboundMessageTransport(),
|
||||
uriComparator);
|
||||
}
|
||||
|
||||
private void initializeServiceProviderMetadata(final String baseUrl) throws MetadataProviderException {
|
||||
// Create filters so MetadataGenerator can get URLs, but we don't actually use the filters, the filter
|
||||
// paths are the URLs from AccessResource that match up with the corresponding SAML endpoint
|
||||
final SAMLProcessingFilter ssoProcessingFilter = new SAMLProcessingFilter();
|
||||
ssoProcessingFilter.setFilterProcessesUrl(SAMLEndpoints.LOGIN_CONSUMER);
|
||||
|
||||
final LogoutHandler noOpLogoutHandler = (request, response, authentication) -> {
|
||||
return;
|
||||
};
|
||||
final SAMLLogoutProcessingFilter sloProcessingFilter = new SAMLLogoutProcessingFilter("/nifi", noOpLogoutHandler);
|
||||
sloProcessingFilter.setFilterProcessesUrl(SAMLEndpoints.SINGLE_LOGOUT_CONSUMER);
|
||||
|
||||
// Create the MetadataGenerator...
|
||||
final MetadataGenerator metadataGenerator = new MetadataGenerator();
|
||||
metadataGenerator.setEntityId(samlConfiguration.getSpEntityId());
|
||||
metadataGenerator.setEntityBaseURL(baseUrl);
|
||||
metadataGenerator.setExtendedMetadata(samlConfiguration.getExtendedMetadata());
|
||||
metadataGenerator.setIncludeDiscoveryExtension(false);
|
||||
metadataGenerator.setKeyManager(samlConfiguration.getKeyManager());
|
||||
metadataGenerator.setSamlWebSSOFilter(ssoProcessingFilter);
|
||||
metadataGenerator.setSamlLogoutProcessingFilter(sloProcessingFilter);
|
||||
metadataGenerator.setRequestSigned(samlConfiguration.isRequestSigningEnabled());
|
||||
metadataGenerator.setWantAssertionSigned(samlConfiguration.isWantAssertionsSigned());
|
||||
|
||||
// Generate service provider metadata...
|
||||
final EntityDescriptor descriptor = metadataGenerator.generateMetadata();
|
||||
final ExtendedMetadata extendedMetadata = metadataGenerator.generateExtendedMetadata();
|
||||
|
||||
// Create the MetadataProvider to hold SP metadata
|
||||
final MetadataMemoryProvider memoryProvider = new MetadataMemoryProvider(descriptor);
|
||||
memoryProvider.initialize();
|
||||
|
||||
final MetadataProvider spMetadataProvider = new ExtendedMetadataDelegate(memoryProvider, extendedMetadata);
|
||||
|
||||
// Update the MetadataManager with the service provider MetadataProvider
|
||||
final MetadataManager metadataManager = samlConfiguration.getMetadataManager();
|
||||
metadataManager.addMetadataProvider(spMetadataProvider);
|
||||
metadataManager.setHostedSPName(descriptor.getEntityID());
|
||||
metadataManager.refreshMetadata();
|
||||
}
|
||||
|
||||
private void verifyReadyForSamlOperations() {
|
||||
if (!isSamlEnabled()) {
|
||||
throw new IllegalStateException(SAML_SUPPORT_IS_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
if (!initialized.get()) {
|
||||
throw new IllegalStateException("StandardSAMLService has not been initialized");
|
||||
}
|
||||
|
||||
if (!isServiceProviderInitialized()) {
|
||||
throw new IllegalStateException("Service Provider is not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,133 +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.saml.impl;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
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;
|
||||
import org.apache.nifi.web.security.util.IdentityProviderUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StandardSAMLStateManager implements SAMLStateManager {
|
||||
|
||||
private static Logger LOGGER = LoggerFactory.getLogger(StandardSAMLStateManager.class);
|
||||
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
// identifier from cookie -> state value
|
||||
private final Cache<CacheKey, String> stateLookupForPendingRequests;
|
||||
|
||||
// identifier from cookie -> jwt or identity (and generate jwt on retrieval)
|
||||
private final Cache<CacheKey, String> jwtLookupForCompletedRequests;
|
||||
|
||||
public StandardSAMLStateManager(final BearerTokenProvider bearerTokenProvider) {
|
||||
this(bearerTokenProvider, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public StandardSAMLStateManager(final BearerTokenProvider bearerTokenProvider, final int cacheExpiration, final TimeUnit units) {
|
||||
this.bearerTokenProvider = bearerTokenProvider;
|
||||
this.stateLookupForPendingRequests = Caffeine.newBuilder().expireAfterWrite(cacheExpiration, units).build();
|
||||
this.jwtLookupForCompletedRequests = Caffeine.newBuilder().expireAfterWrite(cacheExpiration, units).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createState(final String requestIdentifier) {
|
||||
if (StringUtils.isBlank(requestIdentifier)) {
|
||||
throw new IllegalArgumentException("Request identifier is required");
|
||||
}
|
||||
|
||||
final CacheKey requestIdentifierKey = new CacheKey(requestIdentifier);
|
||||
final String state = IdentityProviderUtils.generateStateValue();
|
||||
|
||||
synchronized (stateLookupForPendingRequests) {
|
||||
final String cachedState = stateLookupForPendingRequests.get(requestIdentifierKey, key -> state);
|
||||
if (!IdentityProviderUtils.timeConstantEqualityCheck(state, cachedState)) {
|
||||
throw new IllegalStateException("An existing login request is already in progress.");
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateValid(final String requestIdentifier, final String proposedState) {
|
||||
if (StringUtils.isBlank(requestIdentifier)) {
|
||||
throw new IllegalArgumentException("Request identifier is required");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(proposedState)) {
|
||||
throw new IllegalArgumentException("Proposed state must be specified.");
|
||||
}
|
||||
|
||||
final CacheKey requestIdentifierKey = new CacheKey(requestIdentifier);
|
||||
|
||||
synchronized (stateLookupForPendingRequests) {
|
||||
final String state = stateLookupForPendingRequests.getIfPresent(requestIdentifierKey);
|
||||
if (state != null) {
|
||||
stateLookupForPendingRequests.invalidate(requestIdentifierKey);
|
||||
}
|
||||
|
||||
return state != null && IdentityProviderUtils.timeConstantEqualityCheck(state, proposedState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createJwt(final String requestIdentifier, final LoginAuthenticationToken token) {
|
||||
if (StringUtils.isBlank(requestIdentifier)) {
|
||||
throw new IllegalStateException("Request identifier is required");
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
throw new IllegalArgumentException("Token is required");
|
||||
}
|
||||
|
||||
final CacheKey requestIdentifierKey = new CacheKey(requestIdentifier);
|
||||
final String bearerToken = bearerTokenProvider.getBearerToken(token);
|
||||
// cache the jwt for later retrieval
|
||||
synchronized (jwtLookupForCompletedRequests) {
|
||||
final String cachedJwt = jwtLookupForCompletedRequests.get(requestIdentifierKey, key -> bearerToken);
|
||||
if (!IdentityProviderUtils.timeConstantEqualityCheck(bearerToken, cachedJwt)) {
|
||||
throw new IllegalStateException("An existing login request is already in progress.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJwt(final String requestIdentifier) {
|
||||
if (StringUtils.isBlank(requestIdentifier)) {
|
||||
throw new IllegalStateException("Request identifier is required");
|
||||
}
|
||||
|
||||
final CacheKey requestIdentifierKey = new CacheKey(requestIdentifier);
|
||||
|
||||
synchronized (jwtLookupForCompletedRequests) {
|
||||
final String jwt = jwtLookupForCompletedRequests.getIfPresent(requestIdentifierKey);
|
||||
if (jwt != null) {
|
||||
jwtLookupForCompletedRequests.invalidate(requestIdentifierKey);
|
||||
}
|
||||
|
||||
return jwt;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +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.saml.impl.http;
|
||||
|
||||
import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Extends the HttpServletRequestAdapter with a provided set of parameters.
|
||||
*/
|
||||
public class HttpServletRequestWithParameters extends HttpServletRequestAdapter {
|
||||
|
||||
private final Map<String, String> providedParameters;
|
||||
|
||||
public HttpServletRequestWithParameters(final HttpServletRequest request, final Map<String, String> providedParameters) {
|
||||
super(request);
|
||||
this.providedParameters = providedParameters == null ? Collections.emptyMap() : providedParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameterValue(final String name) {
|
||||
String value = super.getParameterValue(name);
|
||||
if (value == null) {
|
||||
value = providedParameters.get(name);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getParameterValues(final String name) {
|
||||
List<String> combinedValues = new ArrayList<>();
|
||||
|
||||
List<String> initialValues = super.getParameterValues(name);
|
||||
if (initialValues != null) {
|
||||
combinedValues.addAll(initialValues);
|
||||
}
|
||||
|
||||
String providedValue = providedParameters.get(name);
|
||||
if (providedValue != null) {
|
||||
combinedValues.add(providedValue);
|
||||
}
|
||||
|
||||
return combinedValues;
|
||||
}
|
||||
}
|
@ -1,96 +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.saml.impl.http;
|
||||
|
||||
import org.apache.nifi.web.util.WebUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
/**
|
||||
* Extension of HttpServletRequestWrapper that respects proxied/forwarded header values for scheme, host, port, and context path.
|
||||
* <p>
|
||||
* If NiFi generates a SAML request using proxied values so that the IDP redirects back through the proxy, then this is needed
|
||||
* so that when Open SAML checks the Destination in the SAML response, it will match with the values here.
|
||||
* <p>
|
||||
* This class is based on SAMLContextProviderLB from spring-security-saml.
|
||||
*/
|
||||
public class ProxyAwareHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final String scheme;
|
||||
private final String serverName;
|
||||
private final int serverPort;
|
||||
private final String proxyContextPath;
|
||||
private final String contextPath;
|
||||
|
||||
public ProxyAwareHttpServletRequestWrapper(final HttpServletRequest request) {
|
||||
super(request);
|
||||
this.scheme = WebUtils.determineProxiedScheme(request);
|
||||
this.serverName = WebUtils.determineProxiedHost(request);
|
||||
this.serverPort = Integer.valueOf(WebUtils.determineProxiedPort(request));
|
||||
|
||||
final String tempProxyContextPath = WebUtils.normalizeContextPath(WebUtils.determineContextPath(request));
|
||||
this.proxyContextPath = tempProxyContextPath.equals("/") ? "" : tempProxyContextPath;
|
||||
|
||||
this.contextPath = request.getContextPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContextPath() {
|
||||
return contextPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getScheme() {
|
||||
return scheme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerName() {
|
||||
return serverName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getServerPort() {
|
||||
return serverPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequestURI() {
|
||||
StringBuilder sb = new StringBuilder(contextPath);
|
||||
sb.append(getServletPath());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringBuffer getRequestURL() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(scheme).append("://").append(serverName);
|
||||
sb.append(":").append(serverPort);
|
||||
sb.append(proxyContextPath);
|
||||
sb.append(contextPath);
|
||||
sb.append(getServletPath());
|
||||
if (getPathInfo() != null) sb.append(getPathInfo());
|
||||
return sb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return "https".equalsIgnoreCase(scheme);
|
||||
}
|
||||
|
||||
}
|
@ -1,107 +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.saml.impl.tls;
|
||||
|
||||
import org.opensaml.xml.security.CriteriaSet;
|
||||
import org.opensaml.xml.security.SecurityException;
|
||||
import org.opensaml.xml.security.credential.Credential;
|
||||
import org.springframework.security.saml.key.KeyManager;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* KeyManager implementation that combines two KeyManager instances where one instance represents a keystore containing
|
||||
* the service provider's private key (i.e. nifi's keystore.jks) and the other represents a keystore containing the
|
||||
* trusted certificates (i.e. nifi's truststore.jks).
|
||||
*
|
||||
* During any call that requires resolution of a Credential, the server KeyManager is always checked first, if nothing
|
||||
* is found then the trust KeyManager is checked.
|
||||
*
|
||||
* The default Credential is considered that default Credential from the server KeyManager.
|
||||
*/
|
||||
public class CompositeKeyManager implements KeyManager {
|
||||
|
||||
private final KeyManager serverKeyManager;
|
||||
private final KeyManager trustKeyManager;
|
||||
|
||||
public CompositeKeyManager(final KeyManager serverKeyManager, final KeyManager trustKeyManager) {
|
||||
this.serverKeyManager = Objects.requireNonNull(serverKeyManager);
|
||||
this.trustKeyManager = Objects.requireNonNull(trustKeyManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Credential getCredential(String keyName) {
|
||||
if (keyName == null) {
|
||||
return serverKeyManager.getDefaultCredential();
|
||||
}
|
||||
|
||||
Credential credential = serverKeyManager.getCredential(keyName);
|
||||
if (credential == null) {
|
||||
credential = trustKeyManager.getCredential(keyName);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Credential getDefaultCredential() {
|
||||
return serverKeyManager.getDefaultCredential();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultCredentialName() {
|
||||
return serverKeyManager.getDefaultCredentialName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getAvailableCredentials() {
|
||||
final Set<String> allCredentials = new HashSet<>();
|
||||
allCredentials.addAll(serverKeyManager.getAvailableCredentials());
|
||||
allCredentials.addAll(trustKeyManager.getAvailableCredentials());
|
||||
return allCredentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate getCertificate(String alias) {
|
||||
X509Certificate certificate = serverKeyManager.getCertificate(alias);
|
||||
if (certificate == null) {
|
||||
certificate = trustKeyManager.getCertificate(alias);
|
||||
}
|
||||
return certificate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Credential> resolve(CriteriaSet criteria) throws SecurityException {
|
||||
Iterable<Credential> credentials = serverKeyManager.resolve(criteria);
|
||||
if (credentials == null || !credentials.iterator().hasNext()) {
|
||||
credentials = trustKeyManager.resolve(criteria);
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Credential resolveSingle(CriteriaSet criteria) throws SecurityException {
|
||||
Credential credential = serverKeyManager.resolveSingle(criteria);
|
||||
if (credential == null) {
|
||||
trustKeyManager.resolveSingle(criteria);
|
||||
}
|
||||
return credential;
|
||||
}
|
||||
}
|
@ -1,69 +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.saml.impl.tls;
|
||||
|
||||
import org.apache.commons.httpclient.params.HttpConnectionParams;
|
||||
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
public class CustomTLSProtocolSocketFactory implements SecureProtocolSocketFactory {
|
||||
|
||||
private final SSLSocketFactory sslSocketFactory;
|
||||
|
||||
public CustomTLSProtocolSocketFactory(final SSLSocketFactory sslSocketFactory) {
|
||||
this.sslSocketFactory = sslSocketFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
|
||||
return sslSocketFactory.createSocket(socket, host, port, autoClose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return sslSocketFactory.createSocket(host, port, localAddress, localPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort, HttpConnectionParams params) throws IOException {
|
||||
if (params == null) {
|
||||
throw new IllegalArgumentException("Parameters may not be null");
|
||||
}
|
||||
int timeout = params.getConnectionTimeout();
|
||||
if (timeout == 0) {
|
||||
return sslSocketFactory.createSocket(host, port, localAddress, localPort);
|
||||
} else {
|
||||
Socket socket = sslSocketFactory.createSocket();
|
||||
SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
|
||||
SocketAddress remoteaddr = new InetSocketAddress(host, port);
|
||||
socket.bind(localaddr);
|
||||
socket.connect(remoteaddr, timeout);
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return sslSocketFactory.createSocket(host, port);
|
||||
}
|
||||
}
|
@ -14,20 +14,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.security.saml.impl.tls;
|
||||
package org.apache.nifi.web.security.saml2;
|
||||
|
||||
/**
|
||||
* Indicates which truststore should be used when creating an HttpClient for an https URL.
|
||||
* SAML Configuration Exception
|
||||
*/
|
||||
public enum TruststoreStrategy {
|
||||
public class SamlConfigurationException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Use the JDK truststore.
|
||||
*/
|
||||
JDK,
|
||||
public SamlConfigurationException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use NiFi's truststore specified in nifi.properties.
|
||||
*/
|
||||
NIFI;
|
||||
public SamlConfigurationException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -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.saml2;
|
||||
|
||||
import static org.apache.nifi.web.security.saml2.registration.Saml2RegistrationProperty.REGISTRATION_ID;
|
||||
|
||||
/**
|
||||
* Shared configuration for SAML URL Paths
|
||||
*/
|
||||
public enum SamlUrlPath {
|
||||
METADATA("/access/saml/metadata"),
|
||||
|
||||
LOCAL_LOGOUT_REQUEST("/access/saml/local-logout/request"),
|
||||
|
||||
LOGIN_RESPONSE(String.format("/access/saml/login/%s", REGISTRATION_ID.getProperty())),
|
||||
|
||||
LOGIN_RESPONSE_REGISTRATION_ID("/access/saml/login/{registrationId}"),
|
||||
|
||||
SINGLE_LOGOUT_REQUEST("/access/saml/single-logout/request"),
|
||||
|
||||
SINGLE_LOGOUT_RESPONSE(String.format("/access/saml/single-logout/%s", REGISTRATION_ID.getProperty()));
|
||||
|
||||
private final String path;
|
||||
|
||||
SamlUrlPath(final String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.saml2.registration;
|
||||
|
||||
import org.opensaml.saml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
|
||||
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Entity Descriptor Customizer sets configuration properties
|
||||
*/
|
||||
public class EntityDescriptorCustomizer implements Consumer<OpenSamlMetadataResolver.EntityDescriptorParameters> {
|
||||
private boolean wantAssertionsSigned;
|
||||
|
||||
private boolean requestsSigned;
|
||||
|
||||
/**
|
||||
* Entity Descriptor Customizer with configuration properties
|
||||
*
|
||||
* @param wantAssertionsSigned Enable or disable indication of want assertions signed on SP SSO Descriptor
|
||||
* @param requestsSigned Enable or disable indication of authentication requests signed on SP SSO Descriptor
|
||||
*/
|
||||
public EntityDescriptorCustomizer(
|
||||
final boolean wantAssertionsSigned,
|
||||
final boolean requestsSigned
|
||||
) {
|
||||
this.wantAssertionsSigned = wantAssertionsSigned;
|
||||
this.requestsSigned = requestsSigned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(final OpenSamlMetadataResolver.EntityDescriptorParameters entityDescriptorParameters) {
|
||||
final EntityDescriptor entityDescriptor = entityDescriptorParameters.getEntityDescriptor();
|
||||
final SPSSODescriptor spssoDescriptor = entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS);
|
||||
spssoDescriptor.setWantAssertionsSigned(wantAssertionsSigned);
|
||||
spssoDescriptor.setAuthnRequestsSigned(requestsSigned);
|
||||
}
|
||||
}
|
@ -14,19 +14,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.security.saml;
|
||||
package org.apache.nifi.web.security.saml2.registration;
|
||||
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
|
||||
public interface SAMLConfigurationFactory {
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
/**
|
||||
* Provider interface for Relying Party Registration Builder abstracting access to metadata from a configurable location
|
||||
*/
|
||||
interface RegistrationBuilderProvider {
|
||||
/**
|
||||
* Creates a SAMLConfiguration instance from the given NiFiProperties.
|
||||
* Get Relying Party Registration Builder
|
||||
*
|
||||
* @param properties the NiFiProperties instance
|
||||
* @return the configuration instance
|
||||
* @throws Exception if the configuration can't be created
|
||||
* @return Relying Party Registration Builder
|
||||
*/
|
||||
SAMLConfiguration create(final NiFiProperties properties) throws Exception;
|
||||
|
||||
RelyingPartyRegistration.Builder getRegistrationBuilder();
|
||||
}
|
@ -14,23 +14,23 @@
|
||||
* 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.saml2.registration;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.IdpCredentialDAO;
|
||||
import org.apache.nifi.idp.IdpCredential;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
|
||||
public class CreateIdpCredentialAction implements AdministrationAction<IdpCredential> {
|
||||
import java.security.KeyStore;
|
||||
import java.util.Collection;
|
||||
|
||||
private final IdpCredential credential;
|
||||
|
||||
public CreateIdpCredentialAction(final IdpCredential credential) {
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential execute(DAOFactory daoFactory) {
|
||||
final IdpCredentialDAO dao = daoFactory.getIdpCredentialDAO();
|
||||
return dao.createCredential(credential);
|
||||
}
|
||||
/**
|
||||
* SAML2 Credentials Provider translates Certificate and Key entries to SAML2 X.509 Credentials
|
||||
*/
|
||||
public interface Saml2CredentialProvider {
|
||||
/**
|
||||
* Get SAML2 X.509 Credentials from Key Store entries
|
||||
*
|
||||
* @param keyStore Key Store containing credentials
|
||||
* @param keyPassword Optional key password for loading Private Keys
|
||||
* @return Collection of SAML2 Credentials
|
||||
*/
|
||||
Collection<Saml2X509Credential> getCredentials(KeyStore keyStore, char[] keyPassword);
|
||||
}
|
@ -14,20 +14,22 @@
|
||||
* 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.saml2.registration;
|
||||
|
||||
import org.apache.nifi.idp.IdpCredential;
|
||||
/**
|
||||
* SAML 2 configuration for Registration information
|
||||
*/
|
||||
public enum Saml2RegistrationProperty {
|
||||
/** Registration Identifier to maintain compatibility with initial SAML 2 implementation */
|
||||
REGISTRATION_ID("consumer");
|
||||
|
||||
public interface IdpCredentialDAO {
|
||||
private final String property;
|
||||
|
||||
IdpCredential createCredential(IdpCredential credential) throws DataAccessException;
|
||||
|
||||
IdpCredential findCredentialById(int id) throws DataAccessException;
|
||||
|
||||
IdpCredential findCredentialByIdentity(String identity) throws DataAccessException;
|
||||
|
||||
int deleteCredentialById(int id) throws DataAccessException;
|
||||
|
||||
int deleteCredentialByIdentity(String identity) throws DataAccessException;
|
||||
Saml2RegistrationProperty(final String property) {
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
public String getProperty() {
|
||||
return property;
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.saml2.registration;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.apache.nifi.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.saml2.SamlConfigurationException;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Standard Registration Builder Provider implementation based on NiFi Application Properties
|
||||
*/
|
||||
class StandardRegistrationBuilderProvider implements RegistrationBuilderProvider {
|
||||
static final String NIFI_TRUST_STORE_STRATEGY = "NIFI";
|
||||
|
||||
private static final String HTTP_SCHEME_PREFIX = "http";
|
||||
|
||||
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
private final NiFiProperties properties;
|
||||
|
||||
public StandardRegistrationBuilderProvider(final NiFiProperties properties) {
|
||||
this.properties = Objects.requireNonNull(properties, "Properties required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Registration Builder from configured location supporting local files or HTTP services
|
||||
*
|
||||
* @return Registration Builder
|
||||
*/
|
||||
@Override
|
||||
public RelyingPartyRegistration.Builder getRegistrationBuilder() {
|
||||
final String metadataUrl = Objects.requireNonNull(properties.getSamlIdentityProviderMetadataUrl(), "Metadata URL required");
|
||||
|
||||
try (final InputStream inputStream = getInputStream(metadataUrl)) {
|
||||
return RelyingPartyRegistrations.fromMetadata(inputStream);
|
||||
} catch (final IOException e) {
|
||||
throw new SamlConfigurationException(String.format("SAML Metadata loading failed [%s]", metadataUrl), e);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getInputStream(final String metadataUrl) throws IOException {
|
||||
final InputStream inputStream;
|
||||
if (metadataUrl.startsWith(HTTP_SCHEME_PREFIX)) {
|
||||
inputStream = getRemoteInputStream(metadataUrl);
|
||||
} else {
|
||||
inputStream = resourceLoader.getResource(metadataUrl).getInputStream();
|
||||
}
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
private InputStream getRemoteInputStream(final String metadataUrl) {
|
||||
final OkHttpClient client = getHttpClient();
|
||||
|
||||
final Request request = new Request.Builder().get().url(metadataUrl).build();
|
||||
final Call call = client.newCall(request);
|
||||
try {
|
||||
final Response response = call.execute();
|
||||
if (response.isSuccessful()) {
|
||||
final ResponseBody body = Objects.requireNonNull(response.body(), "SAML Metadata response not found");
|
||||
return body.byteStream();
|
||||
} else {
|
||||
response.close();
|
||||
throw new SamlConfigurationException(String.format("SAML Metadata retrieval failed [%s] HTTP %d", metadataUrl, response.code()));
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
throw new SamlConfigurationException(String.format("SAML Metadata retrieval failed [%s]", metadataUrl), e);
|
||||
}
|
||||
}
|
||||
|
||||
private OkHttpClient getHttpClient() {
|
||||
final Duration connectTimeout = Duration.ofMillis(
|
||||
(long) FormatUtils.getPreciseTimeDuration(properties.getSamlHttpClientConnectTimeout(), TimeUnit.MILLISECONDS)
|
||||
);
|
||||
final Duration readTimeout = Duration.ofMillis(
|
||||
(long) FormatUtils.getPreciseTimeDuration(properties.getSamlHttpClientReadTimeout(), TimeUnit.MILLISECONDS)
|
||||
);
|
||||
|
||||
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||
.connectTimeout(connectTimeout)
|
||||
.readTimeout(readTimeout);
|
||||
|
||||
if (NIFI_TRUST_STORE_STRATEGY.equals(properties.getSamlHttpClientTruststoreStrategy())) {
|
||||
setSslSocketFactory(builder);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void setSslSocketFactory(final OkHttpClient.Builder builder) {
|
||||
final TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties);
|
||||
|
||||
try {
|
||||
final X509TrustManager trustManager = Objects.requireNonNull(SslContextFactory.getX509TrustManager(tlsConfiguration), "TrustManager required");
|
||||
final TrustManager[] trustManagers = new TrustManager[] { trustManager };
|
||||
final SSLContext sslContext = Objects.requireNonNull(SslContextFactory.createSslContext(tlsConfiguration, trustManagers), "SSLContext required");
|
||||
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager);
|
||||
} catch (final TlsException e) {
|
||||
throw new SamlConfigurationException("SAML Metadata HTTP TLS configuration failed", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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.saml2.registration;
|
||||
|
||||
import org.apache.nifi.security.util.KeyStoreUtils;
|
||||
import org.apache.nifi.security.util.StandardTlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Standard implementation of Relying Party Registration Repository based on NiFi Properties
|
||||
*/
|
||||
public class StandardRelyingPartyRegistrationRepository implements RelyingPartyRegistrationRepository {
|
||||
static final String BASE_URL_FORMAT = "{baseUrl}%s";
|
||||
|
||||
static final String LOGIN_RESPONSE_LOCATION = String.format(BASE_URL_FORMAT, SamlUrlPath.LOGIN_RESPONSE.getPath());
|
||||
|
||||
static final String SINGLE_LOGOUT_RESPONSE_SERVICE_LOCATION = String.format(BASE_URL_FORMAT, SamlUrlPath.SINGLE_LOGOUT_RESPONSE.getPath());
|
||||
|
||||
private static final char[] BLANK_PASSWORD = new char[0];
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardRelyingPartyRegistrationRepository.class);
|
||||
|
||||
private final Saml2CredentialProvider saml2CredentialProvider = new StandardSaml2CredentialProvider();
|
||||
|
||||
private final NiFiProperties properties;
|
||||
|
||||
private final RelyingPartyRegistration relyingPartyRegistration;
|
||||
|
||||
/**
|
||||
* Standard implementation builds a Registration based on NiFi Properties and returns the same instance for all queries
|
||||
*
|
||||
* @param properties NiFi Application Properties
|
||||
*/
|
||||
public StandardRelyingPartyRegistrationRepository(final NiFiProperties properties) {
|
||||
this.properties = properties;
|
||||
this.relyingPartyRegistration = getRelyingPartyRegistration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelyingPartyRegistration findByRegistrationId(final String registrationId) {
|
||||
return relyingPartyRegistration;
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration getRelyingPartyRegistration() {
|
||||
final RegistrationBuilderProvider registrationBuilderProvider = new StandardRegistrationBuilderProvider(properties);
|
||||
final RelyingPartyRegistration.Builder builder = registrationBuilderProvider.getRegistrationBuilder();
|
||||
|
||||
builder.registrationId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
|
||||
final String entityId = properties.getSamlServiceProviderEntityId();
|
||||
builder.entityId(entityId);
|
||||
|
||||
builder.assertionConsumerServiceLocation(LOGIN_RESPONSE_LOCATION);
|
||||
|
||||
if (properties.isSamlSingleLogoutEnabled()) {
|
||||
builder.singleLogoutServiceLocation(SINGLE_LOGOUT_RESPONSE_SERVICE_LOCATION);
|
||||
builder.singleLogoutServiceResponseLocation(SINGLE_LOGOUT_RESPONSE_SERVICE_LOCATION);
|
||||
}
|
||||
|
||||
final Collection<Saml2X509Credential> configuredCredentials = getCredentials();
|
||||
final List<Saml2X509Credential> signingCredentials = configuredCredentials.stream()
|
||||
.filter(Saml2X509Credential::isSigningCredential)
|
||||
.collect(Collectors.toList());
|
||||
logger.debug("Loaded SAML2 Signing Credentials [{}]", signingCredentials.size());
|
||||
|
||||
builder.signingX509Credentials(credentials -> credentials.addAll(signingCredentials));
|
||||
builder.decryptionX509Credentials(credentials -> credentials.addAll(signingCredentials));
|
||||
|
||||
final List<Saml2X509Credential> verificationCredentials = configuredCredentials.stream()
|
||||
.filter(Saml2X509Credential::isVerificationCredential)
|
||||
.collect(Collectors.toList());
|
||||
logger.debug("Loaded SAML2 Verification Credentials [{}]", verificationCredentials.size());
|
||||
|
||||
builder.assertingPartyDetails(assertingPartyDetails -> assertingPartyDetails
|
||||
.signingAlgorithms(signingAlgorithms -> signingAlgorithms.add(properties.getSamlSignatureAlgorithm()))
|
||||
.verificationX509Credentials(credentials -> credentials.addAll(verificationCredentials))
|
||||
.encryptionX509Credentials(credentials -> credentials.addAll(verificationCredentials))
|
||||
);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Collection<Saml2X509Credential> getCredentials() {
|
||||
final TlsConfiguration tlsConfiguration = StandardTlsConfiguration.fromNiFiProperties(properties);
|
||||
|
||||
final List<Saml2X509Credential> credentials = new ArrayList<>();
|
||||
|
||||
if (tlsConfiguration.isKeystorePopulated()) {
|
||||
final KeyStore keyStore = getKeyStore(tlsConfiguration);
|
||||
final char[] keyPassword = tlsConfiguration.getKeyPassword() == null
|
||||
? tlsConfiguration.getKeystorePassword().toCharArray()
|
||||
: tlsConfiguration.getKeyPassword().toCharArray();
|
||||
final Collection<Saml2X509Credential> keyStoreCredentials = saml2CredentialProvider.getCredentials(keyStore, keyPassword);
|
||||
|
||||
credentials.addAll(keyStoreCredentials);
|
||||
}
|
||||
|
||||
if (tlsConfiguration.isTruststorePopulated()) {
|
||||
final KeyStore trustStore = getTrustStore(tlsConfiguration);
|
||||
final Collection<Saml2X509Credential> trustStoreCredentials = saml2CredentialProvider.getCredentials(trustStore, BLANK_PASSWORD);
|
||||
credentials.addAll(trustStoreCredentials);
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private KeyStore getTrustStore(final TlsConfiguration tlsConfiguration) {
|
||||
try {
|
||||
return KeyStoreUtils.loadKeyStore(
|
||||
tlsConfiguration.getTruststorePath(),
|
||||
tlsConfiguration.getTruststorePassword().toCharArray(),
|
||||
tlsConfiguration.getTruststoreType().getType()
|
||||
);
|
||||
} catch (final TlsException e) {
|
||||
throw new Saml2Exception("Trust Store loading failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore getKeyStore(final TlsConfiguration tlsConfiguration) {
|
||||
try {
|
||||
return KeyStoreUtils.loadKeyStore(
|
||||
tlsConfiguration.getKeystorePath(),
|
||||
tlsConfiguration.getKeystorePassword().toCharArray(),
|
||||
tlsConfiguration.getKeystoreType().getType()
|
||||
);
|
||||
} catch (final TlsException e) {
|
||||
throw new Saml2Exception("Key Store loading failed", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.saml2.registration;
|
||||
|
||||
import org.springframework.security.saml2.Saml2Exception;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Standard implementation of SAML2 Credential Provider capable of reading Key Store and Trust Store entries
|
||||
*/
|
||||
public class StandardSaml2CredentialProvider implements Saml2CredentialProvider {
|
||||
/**
|
||||
* Get Credentials from Key Store
|
||||
*
|
||||
* @param keyStore Key Store containing credentials
|
||||
* @param keyPassword Optional key password for loading Private Keys
|
||||
* @return Collection of SAML2 X.509 Credentials
|
||||
*/
|
||||
@Override
|
||||
public Collection<Saml2X509Credential> getCredentials(final KeyStore keyStore, final char[] keyPassword) {
|
||||
Objects.requireNonNull(keyStore, "Key Store required");
|
||||
|
||||
final List<Saml2X509Credential> credentials = new ArrayList<>();
|
||||
|
||||
try {
|
||||
final Enumeration<String> aliases = keyStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
final String alias = aliases.nextElement();
|
||||
if (keyStore.isKeyEntry(alias)) {
|
||||
processKeyEntry(keyStore, alias, keyPassword, credentials);
|
||||
} else if (keyStore.isCertificateEntry(alias)) {
|
||||
processCertificateEntry(keyStore, alias, credentials);
|
||||
}
|
||||
}
|
||||
} catch (final KeyStoreException e) {
|
||||
throw new Saml2Exception("Loading SAML Credentials failed", e);
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
private Key getKey(final KeyStore keyStore, final String alias, final char[] keyPassword) {
|
||||
try {
|
||||
return keyStore.getKey(alias, keyPassword);
|
||||
} catch (final GeneralSecurityException e) {
|
||||
throw new Saml2Exception(String.format("Loading Key [%s] failed", alias));
|
||||
}
|
||||
}
|
||||
|
||||
private void processKeyEntry(
|
||||
final KeyStore keyStore,
|
||||
final String alias,
|
||||
final char[] keyPassword,
|
||||
final List<Saml2X509Credential> credentials
|
||||
) throws KeyStoreException {
|
||||
final Key key = getKey(keyStore, alias, keyPassword);
|
||||
if (key instanceof PrivateKey) {
|
||||
final PrivateKey privateKey = (PrivateKey) key;
|
||||
final Certificate certificateEntry = keyStore.getCertificate(alias);
|
||||
if (certificateEntry instanceof X509Certificate) {
|
||||
final X509Certificate certificate = (X509Certificate) certificateEntry;
|
||||
final Saml2X509Credential credential = new Saml2X509Credential(
|
||||
privateKey,
|
||||
certificate,
|
||||
Saml2X509Credential.Saml2X509CredentialType.SIGNING,
|
||||
Saml2X509Credential.Saml2X509CredentialType.DECRYPTION
|
||||
);
|
||||
credentials.add(credential);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processCertificateEntry(
|
||||
final KeyStore keyStore,
|
||||
final String alias,
|
||||
final List<Saml2X509Credential> credentials
|
||||
) throws KeyStoreException {
|
||||
final Certificate certificateEntry = keyStore.getCertificate(alias);
|
||||
if (certificateEntry instanceof X509Certificate) {
|
||||
final X509Certificate certificate = (X509Certificate) certificateEntry;
|
||||
final Saml2X509Credential credential = new Saml2X509Credential(
|
||||
certificate,
|
||||
Saml2X509Credential.Saml2X509CredentialType.VERIFICATION,
|
||||
Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION
|
||||
);
|
||||
credentials.add(credential);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.saml2.service.authentication;
|
||||
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.schema.XSAny;
|
||||
import org.opensaml.core.xml.schema.XSString;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Converter from SAML 2 Response Token to SAML 2 Authentication for Spring Security
|
||||
*/
|
||||
public class ResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
|
||||
@SuppressWarnings("deprecation")
|
||||
private static final Converter<ResponseToken, Saml2Authentication> defaultConverter = OpenSamlAuthenticationProvider.createDefaultResponseAuthenticationConverter();
|
||||
|
||||
private final String groupAttributeName;
|
||||
|
||||
/**
|
||||
* Response Authentication Converter with optional Group Attribute Name
|
||||
*
|
||||
* @param groupAttributeName Group Attribute Name is not required
|
||||
*/
|
||||
public ResponseAuthenticationConverter(final String groupAttributeName) {
|
||||
this.groupAttributeName = groupAttributeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SAML 2 Response Token using default Converter and process authorities based on Group Attribute Name
|
||||
*
|
||||
* @param responseToken SAML 2 Response Token
|
||||
* @return SAML 2 Authentication
|
||||
*/
|
||||
@Override
|
||||
public Saml2Authentication convert(final ResponseToken responseToken) {
|
||||
Objects.requireNonNull(responseToken, "Response Token required");
|
||||
final List<Assertion> assertions = responseToken.getResponse().getAssertions();
|
||||
final Saml2Authentication authentication = Objects.requireNonNull(defaultConverter.convert(responseToken), "Authentication required");
|
||||
final Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal();
|
||||
return new Saml2Authentication(principal, authentication.getSaml2Response(), getAuthorities(assertions));
|
||||
}
|
||||
|
||||
private Collection<? extends GrantedAuthority> getAuthorities(final List<Assertion> assertions) {
|
||||
final Collection<? extends GrantedAuthority> authorities;
|
||||
|
||||
if (StringUtils.isBlank(groupAttributeName)) {
|
||||
authorities = Collections.emptyList();
|
||||
} else {
|
||||
// Stream Assertions to Attributes and filter based on Group Attribute Name
|
||||
authorities = assertions.stream()
|
||||
.flatMap(assertion -> assertion.getAttributeStatements().stream())
|
||||
.flatMap(attributeStatement -> attributeStatement.getAttributes().stream())
|
||||
.filter(attribute -> groupAttributeName.equals(attribute.getName()))
|
||||
.flatMap(attribute -> attribute.getAttributeValues().stream())
|
||||
.map(this::getAttributeValue)
|
||||
.filter(Objects::nonNull)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return authorities;
|
||||
}
|
||||
|
||||
private String getAttributeValue(final XMLObject xmlObject) {
|
||||
final String attributeValue;
|
||||
|
||||
if (xmlObject instanceof XSAny) {
|
||||
final XSAny any = (XSAny) xmlObject;
|
||||
attributeValue = any.getTextContent();
|
||||
} else if (xmlObject instanceof XSString) {
|
||||
final XSString string = (XSString) xmlObject;
|
||||
attributeValue = string.getValue();
|
||||
} else {
|
||||
attributeValue = null;
|
||||
}
|
||||
|
||||
return attributeValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.saml2.service.web;
|
||||
|
||||
import org.apache.nifi.web.security.saml2.registration.Saml2RegistrationProperty;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Standard Relying Party Registration Resolver with support for proxy headers
|
||||
*/
|
||||
public class StandardRelyingPartyRegistrationResolver implements Converter<HttpServletRequest, RelyingPartyRegistration>, RelyingPartyRegistrationResolver {
|
||||
private static final String BASE_URL_KEY = "baseUrl";
|
||||
|
||||
private static final String REGISTRATION_ID_KEY = "registrationId";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardRelyingPartyRegistrationResolver.class);
|
||||
|
||||
private final RelyingPartyRegistrationRepository repository;
|
||||
|
||||
private final List<String> allowedContextPaths;
|
||||
|
||||
/**
|
||||
* Standard Resolver with Registration Repository and Allowed Context Paths from application properties
|
||||
*
|
||||
* @param repository Relying Party Registration Repository required
|
||||
* @param allowedContextPaths Allowed Context Paths required
|
||||
*/
|
||||
public StandardRelyingPartyRegistrationResolver(final RelyingPartyRegistrationRepository repository, final List<String> allowedContextPaths) {
|
||||
this.repository = Objects.requireNonNull(repository, "Repository required");
|
||||
this.allowedContextPaths = Objects.requireNonNull(allowedContextPaths, "Allowed Context Paths required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Request to Relying Party Registration using internal default Registration Identifier
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return Relying Party Registration
|
||||
*/
|
||||
@Override
|
||||
public RelyingPartyRegistration convert(final HttpServletRequest request) {
|
||||
return resolve(request, Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Registration from Repository and resolve template locations based on allowed proxy headers
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param relyingPartyRegistrationId Registration Identifier requested
|
||||
* @return Relying Party Registration or null when identifier not found
|
||||
*/
|
||||
@Override
|
||||
public RelyingPartyRegistration resolve(final HttpServletRequest request, final String relyingPartyRegistrationId) {
|
||||
Objects.requireNonNull(request, "Request required");
|
||||
final RelyingPartyRegistration registration = repository.findByRegistrationId(relyingPartyRegistrationId);
|
||||
|
||||
final RelyingPartyRegistration resolved;
|
||||
if (registration == null) {
|
||||
resolved = null;
|
||||
logger.warn("Relying Party Registration [{}] not found", relyingPartyRegistrationId);
|
||||
} else {
|
||||
final String baseUrl = getBaseUrl(request);
|
||||
|
||||
final String assertionConsumerServiceLocation = resolveUrl(registration.getAssertionConsumerServiceLocation(), baseUrl, registration);
|
||||
final String singleLogoutServiceLocation = resolveUrl(registration.getSingleLogoutServiceLocation(), baseUrl, registration);
|
||||
final String singleLogoutServiceResponseLocation = resolveUrl(registration.getSingleLogoutServiceResponseLocation(), baseUrl, registration);
|
||||
|
||||
resolved = RelyingPartyRegistration.withRelyingPartyRegistration(registration)
|
||||
.assertionConsumerServiceLocation(assertionConsumerServiceLocation)
|
||||
.singleLogoutServiceLocation(singleLogoutServiceLocation)
|
||||
.singleLogoutServiceResponseLocation(singleLogoutServiceResponseLocation)
|
||||
.build();
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String resolveUrl(final String templateUrl, final String baseUrl, final RelyingPartyRegistration registration) {
|
||||
final String resolved;
|
||||
if (templateUrl == null) {
|
||||
resolved = null;
|
||||
} else {
|
||||
final Map<String, String> uriVariables = new HashMap<>();
|
||||
uriVariables.put(BASE_URL_KEY, baseUrl);
|
||||
uriVariables.put(REGISTRATION_ID_KEY, registration.getRegistrationId());
|
||||
resolved = UriComponentsBuilder.fromUriString(templateUrl).buildAndExpand(uriVariables).toUriString();
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String getBaseUrl(final HttpServletRequest request) {
|
||||
final URI requestUri = RequestUriBuilder.fromHttpServletRequest(request, allowedContextPaths).build();
|
||||
final String httpUrl = requestUri.toString();
|
||||
final String contextPath = request.getContextPath();
|
||||
return UriComponentsBuilder.fromHttpUrl(httpUrl).path(contextPath).replaceQuery(null).fragment(null).build().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.security.saml2.service.web;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Standard implementation of SAML 2 Authentication Request Repository using cookies
|
||||
*/
|
||||
public class StandardSaml2AuthenticationRequestRepository implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardSaml2AuthenticationRequestRepository.class);
|
||||
|
||||
private static final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
/**
|
||||
* Standard SAML 2 Authentication Request Repository with Spring Cache abstraction
|
||||
*
|
||||
* @param cache Spring Cache for Authentication Requests
|
||||
*/
|
||||
public StandardSaml2AuthenticationRequestRepository(final Cache cache) {
|
||||
this.cache = Objects.requireNonNull(cache, "Cache required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Authentication Request based on SAML Request Identifier cookies
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return SAML 2 Authentication Request or null when not found
|
||||
*/
|
||||
@Override
|
||||
public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(final HttpServletRequest request) {
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
|
||||
|
||||
final AbstractSaml2AuthenticationRequest authenticationRequest;
|
||||
if (requestIdentifier.isPresent()) {
|
||||
final String identifier = requestIdentifier.get();
|
||||
authenticationRequest = cache.get(identifier, AbstractSaml2AuthenticationRequest.class);
|
||||
if (authenticationRequest == null) {
|
||||
logger.warn("SAML Authentication Request [{}] not found", identifier);
|
||||
}
|
||||
} else {
|
||||
logger.warn("SAML Authentication Request Identifier cookie not found");
|
||||
authenticationRequest = null;
|
||||
}
|
||||
return authenticationRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Authentication Request in cache and add cookies to HTTP responses
|
||||
*
|
||||
* @param authenticationRequest Authentication Request to be saved
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
*/
|
||||
@Override
|
||||
public void saveAuthenticationRequest(final AbstractSaml2AuthenticationRequest authenticationRequest, final HttpServletRequest request, final HttpServletResponse response) {
|
||||
if (authenticationRequest == null) {
|
||||
removeAuthenticationRequest(request, response);
|
||||
} else {
|
||||
final String identifier = UUID.randomUUID().toString();
|
||||
cache.put(identifier, authenticationRequest);
|
||||
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
applicationCookieService.addCookie(resourceUri, response, ApplicationCookieName.SAML_REQUEST_IDENTIFIER, identifier);
|
||||
logger.debug("SAML Authentication Request [{}] saved", identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Authentication Request from cache and remove cookies
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @return SAML 2 Authentication Request removed or null when not found
|
||||
*/
|
||||
@Override
|
||||
public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(final HttpServletRequest request, final HttpServletResponse response) {
|
||||
final AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
|
||||
if (authenticationRequest == null) {
|
||||
logger.warn("SAML Authentication Request not found");
|
||||
} else {
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
applicationCookieService.removeCookie(resourceUri, response, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
|
||||
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.SAML_REQUEST_IDENTIFIER);
|
||||
requestIdentifier.ifPresent(cache::evict);
|
||||
}
|
||||
return authenticationRequest;
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||
import org.apache.nifi.authorization.util.IdentityMappingUtil;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* SAML 2 Authentication Success Handler redirects to the user interface and sets a Session Cookie with a Bearer Token
|
||||
*/
|
||||
public class Saml2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
|
||||
private static final String UI_PATH = "/nifi/";
|
||||
|
||||
private final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
private final List<IdentityMapping> userIdentityMappings;
|
||||
|
||||
private final List<IdentityMapping> groupIdentityMappings;
|
||||
|
||||
private final Duration expiration;
|
||||
|
||||
private final String issuer;
|
||||
|
||||
private Converter<Saml2AuthenticatedPrincipal, String> identityConverter = Saml2AuthenticatedPrincipal::getName;
|
||||
|
||||
/**
|
||||
* SAML 2 Authentication Success Handler requires Bearer Token Provider and expiration for generated tokens
|
||||
*
|
||||
* @param bearerTokenProvider Bearer Token Provider
|
||||
* @param idpUserGroupService User Group Service for persisting groups from the Identity Provider
|
||||
* @param userIdentityMappings User Identity Mappings
|
||||
* @param groupIdentityMappings Group Identity Mappings
|
||||
* @param expiration Expiration for generated tokens
|
||||
* @param issuer Token Issuer
|
||||
*/
|
||||
public Saml2AuthenticationSuccessHandler(
|
||||
final BearerTokenProvider bearerTokenProvider,
|
||||
final IdpUserGroupService idpUserGroupService,
|
||||
final List<IdentityMapping> userIdentityMappings,
|
||||
final List<IdentityMapping> groupIdentityMappings,
|
||||
final Duration expiration,
|
||||
final String issuer
|
||||
) {
|
||||
this.bearerTokenProvider = Objects.requireNonNull(bearerTokenProvider, "Bearer Token Provider required");
|
||||
this.idpUserGroupService = Objects.requireNonNull(idpUserGroupService, "User Group Service required");
|
||||
this.userIdentityMappings = Objects.requireNonNull(userIdentityMappings, "User Identity Mappings required");
|
||||
this.groupIdentityMappings = Objects.requireNonNull(groupIdentityMappings, "Group Identity Mappings required");
|
||||
this.expiration = Objects.requireNonNull(expiration, "Expiration required");
|
||||
this.issuer = Objects.requireNonNull(issuer, "Issuer required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Identity Converter for customized mapping of SAML 2 Authenticated Principal to user identity
|
||||
*
|
||||
* @param identityConverter Identity Converter required
|
||||
*/
|
||||
public void setIdentityConverter(final Converter<Saml2AuthenticatedPrincipal, String> identityConverter) {
|
||||
this.identityConverter = Objects.requireNonNull(identityConverter, "Converter required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine Redirect Target URL based on Request URL and add Session Cookie containing a Bearer Token
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param authentication SAML 2 Authentication
|
||||
* @return Redirect Target URL
|
||||
*/
|
||||
@Override
|
||||
public String determineTargetUrl(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) {
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
processAuthentication(response, authentication, resourceUri);
|
||||
|
||||
final URI targetUri = RequestUriBuilder.fromHttpServletRequest(request).path(UI_PATH).build();
|
||||
return targetUri.toString();
|
||||
}
|
||||
|
||||
private void processAuthentication(final HttpServletResponse response, final Authentication authentication, final URI resourceUri) {
|
||||
final String identity = getIdentity(authentication);
|
||||
final Set<String> groups = getGroups(authentication);
|
||||
idpUserGroupService.replaceUserGroups(identity, IdpType.SAML, groups);
|
||||
|
||||
final String bearerToken = getBearerToken(identity);
|
||||
applicationCookieService.addSessionCookie(resourceUri, response, ApplicationCookieName.AUTHORIZATION_BEARER, bearerToken);
|
||||
}
|
||||
|
||||
private String getBearerToken(final String identity) {
|
||||
final LoginAuthenticationToken loginAuthenticationToken = new LoginAuthenticationToken(identity, identity, expiration.toMillis(), issuer);
|
||||
return bearerTokenProvider.getBearerToken(loginAuthenticationToken);
|
||||
}
|
||||
|
||||
private String getIdentity(final Authentication authentication) {
|
||||
final Object principal = authentication.getPrincipal();
|
||||
|
||||
final String identity;
|
||||
if (principal instanceof Saml2AuthenticatedPrincipal) {
|
||||
final Saml2AuthenticatedPrincipal authenticatedPrincipal = (Saml2AuthenticatedPrincipal) principal;
|
||||
identity = identityConverter.convert(authenticatedPrincipal);
|
||||
} else {
|
||||
identity = authentication.getName();
|
||||
}
|
||||
return IdentityMappingUtil.mapIdentity(identity, userIdentityMappings);
|
||||
}
|
||||
|
||||
private Set<String> getGroups(final Authentication authentication) {
|
||||
return authentication.getAuthorities()
|
||||
.stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.map(group -> IdentityMappingUtil.mapIdentity(group, groupIdentityMappings))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
@ -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.saml2.web.authentication.identity;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Converter for customized User Identity using SAML Attribute Value
|
||||
*/
|
||||
public class AttributeNameIdentityConverter implements Converter<Saml2AuthenticatedPrincipal, String> {
|
||||
private String attributeName;
|
||||
|
||||
public AttributeNameIdentityConverter(final String attributeName) {
|
||||
this.attributeName = Objects.requireNonNull(attributeName, "Attribute Name required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Principal to identity using configured attribute name when found
|
||||
*
|
||||
* @param principal SAML 2 Authenticated Principal
|
||||
* @return Attribute Value or Principal Name when attribute not found
|
||||
*/
|
||||
@Override
|
||||
public String convert(final Saml2AuthenticatedPrincipal principal) {
|
||||
final Object attribute = principal.getFirstAttribute(attributeName);
|
||||
return attribute == null ? principal.getName() : attribute.toString();
|
||||
}
|
||||
}
|
@ -14,23 +14,31 @@
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.admin.dao.DAOFactory;
|
||||
import org.apache.nifi.admin.dao.IdpCredentialDAO;
|
||||
import org.apache.nifi.idp.IdpCredential;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
|
||||
public class GetIdpCredentialByIdentity implements AdministrationAction<IdpCredential> {
|
||||
import java.util.Objects;
|
||||
|
||||
private final String identity;
|
||||
/**
|
||||
* Logout Authentication Token for processing Logout Requests using Spring Security SAML 2 handlers
|
||||
*/
|
||||
public class LogoutAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private final String name;
|
||||
|
||||
public GetIdpCredentialByIdentity(final String identity) {
|
||||
this.identity = identity;
|
||||
public LogoutAuthenticationToken(final String name) {
|
||||
super(AuthorityUtils.NO_AUTHORITIES);
|
||||
this.name = Objects.requireNonNull(name, "Name required");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdpCredential execute(DAOFactory daoFactory) {
|
||||
final IdpCredentialDAO dao = daoFactory.getIdpCredentialDAO();
|
||||
return dao.findCredentialByIdentity(identity);
|
||||
public Object getCredentials() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* SAML 2 Logout Filter completes application Logout Requests
|
||||
*/
|
||||
public class Saml2LocalLogoutFilter extends OncePerRequestFilter {
|
||||
private final AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(SamlUrlPath.LOCAL_LOGOUT_REQUEST.getPath());
|
||||
|
||||
private final LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
public Saml2LocalLogoutFilter(
|
||||
final LogoutSuccessHandler logoutSuccessHandler
|
||||
) {
|
||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Logout Success Handler when request path matches
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param filterChain Filter Chain
|
||||
* @throws ServletException Thrown on FilterChain.doFilter() failures
|
||||
* @throws IOException Thrown on FilterChain.doFilter() failures
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
|
||||
if (requestMatcher.matches(request)) {
|
||||
final SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
final Authentication authentication = securityContext.getAuthentication();
|
||||
logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* SAML 2 Logout Success Handler implementation for completing application Logout Requests
|
||||
*/
|
||||
public class Saml2LogoutSuccessHandler implements LogoutSuccessHandler {
|
||||
private static final String LOGOUT_COMPLETE_PATH = "/nifi/logout-complete";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Saml2LogoutSuccessHandler.class);
|
||||
|
||||
private final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final LogoutRequestManager logoutRequestManager;
|
||||
|
||||
private final IdpUserGroupService idpUserGroupService;
|
||||
|
||||
public Saml2LogoutSuccessHandler(
|
||||
final LogoutRequestManager logoutRequestManager,
|
||||
final IdpUserGroupService idpUserGroupService
|
||||
) {
|
||||
this.logoutRequestManager = Objects.requireNonNull(logoutRequestManager, "Logout Request Manager required");
|
||||
this.idpUserGroupService = Objects.requireNonNull(idpUserGroupService, "User Group Service required");
|
||||
}
|
||||
|
||||
/**
|
||||
* On Logout Success complete Logout Request based on Logout Request Identifier found in cookies
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param authentication Authentication is not used
|
||||
* @throws IOException Thrown on HttpServletResponse.sendRedirect() failures
|
||||
*/
|
||||
@Override
|
||||
public void onLogoutSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
|
||||
final Optional<String> logoutRequestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
if (logoutRequestIdentifier.isPresent()) {
|
||||
final String requestIdentifier = logoutRequestIdentifier.get();
|
||||
final LogoutRequest logoutRequest = logoutRequestManager.complete(requestIdentifier);
|
||||
|
||||
if (logoutRequest == null) {
|
||||
logger.warn("Logout Request [{}] not found", requestIdentifier);
|
||||
} else {
|
||||
final String mappedUserIdentity = logoutRequest.getMappedUserIdentity();
|
||||
|
||||
idpUserGroupService.deleteUserGroups(mappedUserIdentity);
|
||||
logger.info("Logout Request [{}] Identity [{}] completed", requestIdentifier, mappedUserIdentity);
|
||||
}
|
||||
|
||||
final URI logoutCompleteUri = RequestUriBuilder.fromHttpServletRequest(request).path(LOGOUT_COMPLETE_PATH).build();
|
||||
final String targetUrl = logoutCompleteUri.toString();
|
||||
response.sendRedirect(targetUrl);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* SAML 2 Single Local Filter processes Single Logout Requests
|
||||
*/
|
||||
public class Saml2SingleLogoutFilter extends OncePerRequestFilter {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Saml2SingleLogoutFilter.class);
|
||||
|
||||
private static final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher(SamlUrlPath.SINGLE_LOGOUT_REQUEST.getPath());
|
||||
|
||||
private final LogoutRequestManager logoutRequestManager;
|
||||
|
||||
private final LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
public Saml2SingleLogoutFilter(
|
||||
final LogoutRequestManager logoutRequestManager,
|
||||
final LogoutSuccessHandler logoutSuccessHandler
|
||||
) {
|
||||
this.logoutRequestManager = Objects.requireNonNull(logoutRequestManager, "Request Manager require");
|
||||
this.logoutSuccessHandler = Objects.requireNonNull(logoutSuccessHandler, "Success Handler required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Logout Request Identifier cookies and find Logout Request then call Logout Success Handler
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @param filterChain Filter Chain
|
||||
* @throws ServletException Thrown on FilterChain.doFilter() failures
|
||||
* @throws IOException Thrown on FilterChain.doFilter() failures
|
||||
*/
|
||||
@Override
|
||||
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
|
||||
if (requestMatcher.matches(request)) {
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
if (requestIdentifier.isPresent()) {
|
||||
final String identifier = requestIdentifier.get();
|
||||
final LogoutRequest logoutRequest = logoutRequestManager.get(identifier);
|
||||
if (logoutRequest == null) {
|
||||
logger.warn("SAML 2 Logout Request [{}] not found", identifier);
|
||||
} else {
|
||||
final String userIdentity = logoutRequest.getMappedUserIdentity();
|
||||
final Authentication authentication = new LogoutAuthenticationToken(userIdentity);
|
||||
logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
|
||||
}
|
||||
} else {
|
||||
logger.warn("SAML 2 Logout Request cookie not found");
|
||||
}
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Standard SAML 2 Single Logout Handler for tracking Logout Requests with Spring Saml2LogoutRequestFilter
|
||||
*/
|
||||
public class Saml2SingleLogoutHandler implements LogoutHandler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Saml2SingleLogoutHandler.class);
|
||||
|
||||
@Override
|
||||
public void logout(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) {
|
||||
logger.info("SAML 2 Single Logout completed");
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieService;
|
||||
import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Standard implementation of SAML 2 Logout Request Repository using cookies
|
||||
*/
|
||||
public class StandardSaml2LogoutRequestRepository implements Saml2LogoutRequestRepository {
|
||||
private static final Logger logger = LoggerFactory.getLogger(StandardSaml2LogoutRequestRepository.class);
|
||||
|
||||
private static final ApplicationCookieService applicationCookieService = new StandardApplicationCookieService();
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
public StandardSaml2LogoutRequestRepository(final Cache cache) {
|
||||
this.cache = Objects.requireNonNull(cache, "Cache required");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Logout Request
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @return SAML 2 Logout Request or null when not found
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest loadLogoutRequest(final HttpServletRequest request) {
|
||||
Objects.requireNonNull(request, "Request required");
|
||||
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
|
||||
final Saml2LogoutRequest logoutRequest;
|
||||
if (requestIdentifier.isPresent()) {
|
||||
final String identifier = requestIdentifier.get();
|
||||
logoutRequest = cache.get(identifier, Saml2LogoutRequest.class);
|
||||
if (logoutRequest == null) {
|
||||
logger.warn("SAML Logout Request [{}] not found", identifier);
|
||||
}
|
||||
} else {
|
||||
logger.warn("SAML Logout Request Identifier cookie not found");
|
||||
logoutRequest = null;
|
||||
}
|
||||
return logoutRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Logout Request in cache and set cookies
|
||||
*
|
||||
* @param logoutRequest Logout Request to be saved
|
||||
* @param request HTTP Servlet Request required
|
||||
* @param response HTTP Servlet Response required
|
||||
*/
|
||||
@Override
|
||||
public void saveLogoutRequest(final Saml2LogoutRequest logoutRequest, final HttpServletRequest request, final HttpServletResponse response) {
|
||||
Objects.requireNonNull(request, "Request required");
|
||||
Objects.requireNonNull(response, "Response required");
|
||||
if (logoutRequest == null) {
|
||||
removeLogoutRequest(request, response);
|
||||
} else {
|
||||
Objects.requireNonNull(logoutRequest.getRelayState(), "Relay State required");
|
||||
|
||||
// Get current Logout Request Identifier or generate when not found
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
final String identifier = requestIdentifier.orElse(UUID.randomUUID().toString());
|
||||
cache.put(identifier, logoutRequest);
|
||||
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
applicationCookieService.addCookie(resourceUri, response, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER, identifier);
|
||||
logger.debug("SAML Logout Request [{}] saved", identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove Logout Request
|
||||
*
|
||||
* @param request HTTP Servlet Request
|
||||
* @param response HTTP Servlet Response
|
||||
* @return SAML 2 Logout Request removed or null when not found
|
||||
*/
|
||||
@Override
|
||||
public Saml2LogoutRequest removeLogoutRequest(final HttpServletRequest request, final HttpServletResponse response) {
|
||||
Objects.requireNonNull(request, "Request required");
|
||||
Objects.requireNonNull(response, "Response required");
|
||||
final Saml2LogoutRequest logoutRequest = loadLogoutRequest(request);
|
||||
if (logoutRequest == null) {
|
||||
logger.warn("SAML Logout Request not found");
|
||||
} else {
|
||||
final URI resourceUri = RequestUriBuilder.fromHttpServletRequest(request).build();
|
||||
applicationCookieService.removeCookie(resourceUri, response, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
|
||||
final Optional<String> requestIdentifier = applicationCookieService.getCookieValue(request, ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER);
|
||||
requestIdentifier.ifPresent(cache::evict);
|
||||
}
|
||||
return logoutRequest;
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.api.cookie;
|
||||
package org.apache.nifi.web.security.cookie;
|
||||
|
||||
import org.apache.nifi.util.StringUtils;
|
||||
import org.junit.Before;
|
@ -1,123 +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.saml.impl;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.saml.SAMLConfigurationFactory;
|
||||
import org.apache.nifi.web.security.saml.SAMLService;
|
||||
import org.apache.nifi.web.security.saml.impl.tls.TruststoreStrategy;
|
||||
import org.junit.After;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TestStandardSAMLService {
|
||||
|
||||
private NiFiProperties properties;
|
||||
private SAMLConfigurationFactory samlConfigurationFactory;
|
||||
private SAMLService samlService;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpSuite() {
|
||||
Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
properties = mock(NiFiProperties.class);
|
||||
samlConfigurationFactory = new StandardSAMLConfigurationFactory();
|
||||
samlService = new StandardSAMLService(samlConfigurationFactory, properties);
|
||||
}
|
||||
|
||||
@After
|
||||
public void teardown() {
|
||||
samlService.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSamlEnabledWithFileBasedIdpMetadata() throws GeneralSecurityException, IOException {
|
||||
final String spEntityId = "org:apache:nifi";
|
||||
final File idpMetadataFile = new File("src/test/resources/saml/sso-circle-meta.xml");
|
||||
final String baseUrl = "https://localhost:8443/nifi-api";
|
||||
|
||||
final TlsConfiguration tlsConfiguration = new TemporaryKeyStoreBuilder().build();
|
||||
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_KEYSTORE)).thenReturn(tlsConfiguration.getKeystorePath());
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD)).thenReturn(tlsConfiguration.getKeystorePassword());
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD)).thenReturn(tlsConfiguration.getKeyPassword());
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE)).thenReturn(tlsConfiguration.getKeystoreType().getType());
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)).thenReturn(tlsConfiguration.getTruststorePath());
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)).thenReturn(tlsConfiguration.getTruststorePassword());
|
||||
when(properties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)).thenReturn(tlsConfiguration.getTruststoreType().getType());
|
||||
when(properties.getPropertyKeys()).thenReturn(new HashSet<>(Arrays.asList(
|
||||
NiFiProperties.SECURITY_KEYSTORE,
|
||||
NiFiProperties.SECURITY_KEYSTORE_PASSWD,
|
||||
NiFiProperties.SECURITY_KEY_PASSWD,
|
||||
NiFiProperties.SECURITY_KEYSTORE_TYPE,
|
||||
NiFiProperties.SECURITY_TRUSTSTORE,
|
||||
NiFiProperties.SECURITY_TRUSTSTORE_PASSWD,
|
||||
NiFiProperties.SECURITY_TRUSTSTORE_TYPE
|
||||
)));
|
||||
|
||||
when(properties.isSamlEnabled()).thenReturn(true);
|
||||
when(properties.getSamlServiceProviderEntityId()).thenReturn(spEntityId);
|
||||
when(properties.getSamlIdentityProviderMetadataUrl()).thenReturn("file://" + idpMetadataFile.getAbsolutePath());
|
||||
when(properties.getSamlAuthenticationExpiration()).thenReturn("12 hours");
|
||||
when(properties.getSamlHttpClientTruststoreStrategy()).thenReturn(TruststoreStrategy.JDK.name());
|
||||
|
||||
// initialize the saml service
|
||||
samlService.initialize();
|
||||
assertTrue(samlService.isSamlEnabled());
|
||||
|
||||
// initialize the service provider
|
||||
assertFalse(samlService.isServiceProviderInitialized());
|
||||
samlService.initializeServiceProvider(baseUrl);
|
||||
assertTrue(samlService.isServiceProviderInitialized());
|
||||
|
||||
// obtain the service provider metadata xml
|
||||
final String spMetadataXml = samlService.getServiceProviderMetadata();
|
||||
assertTrue(spMetadataXml.contains("entityID=\"org:apache:nifi\""));
|
||||
assertTrue(spMetadataXml.contains("<md:AssertionConsumerService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://localhost:8443/nifi-api/access/saml/login/consumer\""));
|
||||
assertTrue(spMetadataXml.contains("<md:SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" Location=\"https://localhost:8443/nifi-api/access/saml/single-logout/consumer\"/>"));
|
||||
assertTrue(spMetadataXml.contains("<md:SingleLogoutService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" Location=\"https://localhost:8443/nifi-api/access/saml/single-logout/consumer\"/>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitializeWhenSamlNotEnabled() {
|
||||
when(properties.isSamlEnabled()).thenReturn(false);
|
||||
samlService.initialize();
|
||||
assertFalse(samlService.isSamlEnabled());
|
||||
assertThrows(IllegalStateException.class, () -> samlService.initializeServiceProvider("https://localhost:8443/nifi-api"));
|
||||
assertThrows(IllegalStateException.class, () -> samlService.getServiceProviderMetadata());
|
||||
}
|
||||
}
|
@ -1,90 +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.saml.impl;
|
||||
|
||||
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;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class TestStandardSAMLStateManager {
|
||||
|
||||
private BearerTokenProvider bearerTokenProvider;
|
||||
private SAMLStateManager stateManager;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
bearerTokenProvider = mock(BearerTokenProvider.class);
|
||||
stateManager = new StandardSAMLStateManager(bearerTokenProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateStateAndCheckIsValid() {
|
||||
final String requestId = "request1";
|
||||
|
||||
// create state
|
||||
final String state = stateManager.createState(requestId);
|
||||
assertNotNull(state);
|
||||
|
||||
// should be valid
|
||||
assertTrue(stateManager.isStateValid(requestId, state));
|
||||
|
||||
// should have been invalidated by checking if is valid above
|
||||
assertFalse(stateManager.isStateValid(requestId, state));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void testCreateStateWhenExisting() {
|
||||
final String requestId = "request1";
|
||||
stateManager.createState(requestId);
|
||||
stateManager.createState(requestId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsValidWhenDoesNotExist() {
|
||||
final String requestId = "request1";
|
||||
assertFalse(stateManager.isStateValid(requestId, "some-state-value"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndGetJwt() {
|
||||
final String requestId = "request1";
|
||||
final LoginAuthenticationToken token = new LoginAuthenticationToken("user1", "user1", 10000, "nifi");
|
||||
|
||||
// create the jwt and cache it
|
||||
final String fakeJwt = "fake-jwt";
|
||||
when(bearerTokenProvider.getBearerToken(token)).thenReturn(fakeJwt);
|
||||
stateManager.createJwt(requestId, token);
|
||||
|
||||
// should return the jwt above
|
||||
final String jwt = stateManager.getJwt(requestId);
|
||||
assertEquals(fakeJwt, jwt);
|
||||
|
||||
// should no longer exist after retrieving above
|
||||
assertNull(stateManager.getJwt(requestId));
|
||||
}
|
||||
|
||||
}
|
@ -1,98 +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.saml.impl.http;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TestHttpServletRequestWithParameters {
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Test
|
||||
public void testGetParameterValueWhenNoExtraParameters() {
|
||||
final String paramName = "fooParam";
|
||||
final String paramValue = "fooValue";
|
||||
when(request.getParameter(eq(paramName))).thenReturn(paramValue);
|
||||
|
||||
final HttpServletRequestAdapter requestAdapter = new HttpServletRequestWithParameters(request, Collections.emptyMap());
|
||||
final String result = requestAdapter.getParameterValue(paramName);
|
||||
assertEquals(paramValue, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetParameterValueWhenExtraParameters() {
|
||||
final String paramName = "fooParam";
|
||||
final String paramValue = "fooValue";
|
||||
|
||||
final Map<String,String> extraParams = new HashMap<>();
|
||||
extraParams.put(paramName, paramValue);
|
||||
|
||||
when(request.getParameter(any())).thenReturn(null);
|
||||
|
||||
final HttpServletRequestAdapter requestAdapter = new HttpServletRequestWithParameters(request, extraParams);
|
||||
final String result = requestAdapter.getParameterValue(paramName);
|
||||
assertEquals(paramValue, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetParameterValuesWhenNoExtraParameters() {
|
||||
final String paramName = "fooParam";
|
||||
final String paramValue = "fooValue";
|
||||
when(request.getParameterValues(eq(paramName))).thenReturn(new String[] {paramValue});
|
||||
|
||||
final HttpServletRequestAdapter requestAdapter = new HttpServletRequestWithParameters(request, Collections.emptyMap());
|
||||
final List<String> results = requestAdapter.getParameterValues(paramName);
|
||||
assertEquals(1, results.size());
|
||||
assertEquals(paramValue, results.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetParameterValuesWhenExtraParameters() {
|
||||
final String paramName = "fooParam";
|
||||
final String paramValue1 = "fooValue1";
|
||||
when(request.getParameterValues(eq(paramName))).thenReturn(new String[] {paramValue1});
|
||||
|
||||
final String paramValue2 = "fooValue2";
|
||||
final Map<String,String> extraParams = new HashMap<>();
|
||||
extraParams.put(paramName, paramValue2);
|
||||
|
||||
final HttpServletRequestAdapter requestAdapter = new HttpServletRequestWithParameters(request, extraParams);
|
||||
final List<String> results = requestAdapter.getParameterValues(paramName);
|
||||
assertEquals(2, results.size());
|
||||
assertTrue(results.contains(paramValue1));
|
||||
assertTrue(results.contains(paramValue2));
|
||||
}
|
||||
}
|
@ -1,77 +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.saml.impl.http;
|
||||
|
||||
import org.apache.nifi.web.util.WebUtils;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class TestProxyAwareHttpServletRequestWrapper {
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
|
||||
@Test
|
||||
public void testWhenNotProxied() {
|
||||
when(request.getScheme()).thenReturn("https");
|
||||
when(request.getServerName()).thenReturn("localhost");
|
||||
when(request.getServerPort()).thenReturn(8443);
|
||||
when(request.getContextPath()).thenReturn("/nifi-api");
|
||||
when(request.getServletPath()).thenReturn("/access/saml/metadata");
|
||||
when(request.getHeader(any())).thenReturn(null);
|
||||
|
||||
final HttpServletRequestWrapper requestWrapper = new ProxyAwareHttpServletRequestWrapper(request);
|
||||
assertEquals("https://localhost:8443/nifi-api/access/saml/metadata", requestWrapper.getRequestURL().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenProxied() {
|
||||
when(request.getHeader(eq(WebUtils.PROXY_SCHEME_HTTP_HEADER))).thenReturn("https");
|
||||
when(request.getHeader(eq(WebUtils.PROXY_HOST_HTTP_HEADER))).thenReturn("proxy-host");
|
||||
when(request.getHeader(eq(WebUtils.PROXY_PORT_HTTP_HEADER))).thenReturn("443");
|
||||
when(request.getHeader(eq(WebUtils.PROXY_CONTEXT_PATH_HTTP_HEADER))).thenReturn("/proxy-context");
|
||||
when(request.getContextPath()).thenReturn("/nifi-api");
|
||||
when(request.getServletPath()).thenReturn("/access/saml/metadata");
|
||||
|
||||
final HttpServletRequestWrapper requestWrapper = new ProxyAwareHttpServletRequestWrapper(request);
|
||||
assertEquals("https://proxy-host:443/proxy-context/nifi-api/access/saml/metadata", requestWrapper.getRequestURL().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenProxiedWithEmptyProxyContextPath() {
|
||||
when(request.getHeader(eq(WebUtils.PROXY_SCHEME_HTTP_HEADER))).thenReturn("https");
|
||||
when(request.getHeader(eq(WebUtils.PROXY_HOST_HTTP_HEADER))).thenReturn("proxy-host");
|
||||
when(request.getHeader(eq(WebUtils.PROXY_PORT_HTTP_HEADER))).thenReturn("443");
|
||||
when(request.getHeader(eq(WebUtils.PROXY_CONTEXT_PATH_HTTP_HEADER))).thenReturn("/");
|
||||
when(request.getContextPath()).thenReturn("/nifi-api");
|
||||
when(request.getServletPath()).thenReturn("/access/saml/metadata");
|
||||
|
||||
final HttpServletRequestWrapper requestWrapper = new ProxyAwareHttpServletRequestWrapper(request);
|
||||
assertEquals("https://proxy-host:443/nifi-api/access/saml/metadata", requestWrapper.getRequestURL().toString());
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.saml2.registration;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.nifi.security.util.SslContextFactory;
|
||||
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.security.util.TlsException;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.apache.nifi.web.security.saml2.SamlConfigurationException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class StandardRegistrationBuilderProviderTest {
|
||||
private static final String LOCALHOST = "localhost";
|
||||
|
||||
private static final String METADATA_PATH = "/saml/sso-circle-meta.xml";
|
||||
|
||||
private static final int HTTP_NOT_FOUND = 404;
|
||||
|
||||
private static final boolean PROXY_DISABLED = false;
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
@BeforeEach
|
||||
void startServer() throws IOException {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void shutdownServer() throws IOException {
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRegistrationBuilderFileUrl() {
|
||||
final NiFiProperties properties = getProperties(getFileMetadataUrl());
|
||||
|
||||
assertRegistrationFound(properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRegistrationBuilderHttpUrl() throws IOException {
|
||||
final String metadata = getMetadata();
|
||||
final MockResponse response = new MockResponse().setBody(metadata);
|
||||
mockWebServer.enqueue(response);
|
||||
final String metadataUrl = getMetadataUrl();
|
||||
|
||||
final NiFiProperties properties = getProperties(metadataUrl);
|
||||
|
||||
assertRegistrationFound(properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRegistrationBuilderHttpUrlNotFound() {
|
||||
final MockResponse response = new MockResponse().setResponseCode(HTTP_NOT_FOUND);
|
||||
mockWebServer.enqueue(response);
|
||||
final String metadataUrl = getMetadataUrl();
|
||||
|
||||
final NiFiProperties properties = getProperties(metadataUrl);
|
||||
|
||||
final StandardRegistrationBuilderProvider provider = new StandardRegistrationBuilderProvider(properties);
|
||||
|
||||
final SamlConfigurationException exception = assertThrows(SamlConfigurationException.class, provider::getRegistrationBuilder);
|
||||
assertTrue(exception.getMessage().contains(Integer.toString(HTTP_NOT_FOUND)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRegistrationBuilderHttpsUrl() throws IOException, TlsException {
|
||||
final TlsConfiguration tlsConfiguration = new TemporaryKeyStoreBuilder().build();
|
||||
final SSLSocketFactory sslSocketFactory = Objects.requireNonNull(SslContextFactory.createSSLSocketFactory(tlsConfiguration));
|
||||
mockWebServer.useHttps(sslSocketFactory, PROXY_DISABLED);
|
||||
|
||||
final String metadata = getMetadata();
|
||||
final MockResponse response = new MockResponse().setBody(metadata);
|
||||
mockWebServer.enqueue(response);
|
||||
final String metadataUrl = getMetadataUrl();
|
||||
|
||||
final NiFiProperties properties = getProperties(metadataUrl, tlsConfiguration);
|
||||
|
||||
assertRegistrationFound(properties);
|
||||
}
|
||||
|
||||
private String getMetadataUrl() {
|
||||
final HttpUrl url = mockWebServer.url(METADATA_PATH).newBuilder().host(LOCALHOST).build();
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private void assertRegistrationFound(final NiFiProperties properties) {
|
||||
final StandardRegistrationBuilderProvider provider = new StandardRegistrationBuilderProvider(properties);
|
||||
final RelyingPartyRegistration.Builder builder = provider.getRegistrationBuilder();
|
||||
|
||||
final RelyingPartyRegistration registration = builder.build();
|
||||
assertEquals(Saml2MessageBinding.POST, registration.getAssertionConsumerServiceBinding());
|
||||
}
|
||||
|
||||
private NiFiProperties getProperties(final String metadataUrl) {
|
||||
final Properties properties = new Properties();
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_IDP_METADATA_URL, metadataUrl);
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
|
||||
private NiFiProperties getProperties(final String metadataUrl, final TlsConfiguration tlsConfiguration) {
|
||||
final Properties properties = new Properties();
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_IDP_METADATA_URL, metadataUrl);
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_HTTP_CLIENT_TRUSTSTORE_STRATEGY, StandardRegistrationBuilderProvider.NIFI_TRUST_STORE_STRATEGY);
|
||||
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEYSTORE, tlsConfiguration.getKeystorePath());
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE, tlsConfiguration.getKeystoreType().getType());
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD, tlsConfiguration.getKeystorePassword());
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEY_PASSWD, tlsConfiguration.getKeyPassword());
|
||||
properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE, tlsConfiguration.getTruststorePath());
|
||||
properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, tlsConfiguration.getTruststoreType().getType());
|
||||
properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD, tlsConfiguration.getTruststorePassword());
|
||||
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
|
||||
final String getMetadata() throws IOException {
|
||||
try (final InputStream inputStream = Objects.requireNonNull(getClass().getResourceAsStream(METADATA_PATH))) {
|
||||
return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private String getFileMetadataUrl() {
|
||||
final URL resource = Objects.requireNonNull(getClass().getResource(METADATA_PATH));
|
||||
return resource.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.saml2.registration;
|
||||
|
||||
import org.apache.nifi.security.util.TemporaryKeyStoreBuilder;
|
||||
import org.apache.nifi.security.util.TlsConfiguration;
|
||||
import org.apache.nifi.util.NiFiProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.net.URL;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class StandardRelyingPartyRegistrationRepositoryTest {
|
||||
private static final String METADATA_PATH = "/saml/sso-circle-meta.xml";
|
||||
|
||||
private static final String ENTITY_ID = "nifi";
|
||||
|
||||
private static final X500Principal CERTIFICATE_PRINCIPAL = new X500Principal("CN=localhost");
|
||||
|
||||
@Test
|
||||
void testFindByRegistrationId() {
|
||||
final NiFiProperties properties = getProperties();
|
||||
final StandardRelyingPartyRegistrationRepository repository = new StandardRelyingPartyRegistrationRepository(properties);
|
||||
|
||||
final RelyingPartyRegistration registration = repository.findByRegistrationId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
|
||||
assertRegistrationPropertiesFound(registration);
|
||||
|
||||
assertNull(registration.getSingleLogoutServiceLocation());
|
||||
assertNull(registration.getSingleLogoutServiceResponseLocation());
|
||||
|
||||
final RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = registration.getAssertingPartyDetails();
|
||||
assertFalse(assertingPartyDetails.getWantAuthnRequestsSigned());
|
||||
assertTrue(assertingPartyDetails.getSigningAlgorithms().contains(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256));
|
||||
|
||||
final Collection<Saml2X509Credential> signingCredentials = registration.getSigningX509Credentials();
|
||||
assertTrue(signingCredentials.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByRegistrationIdSingleLogoutEnabled() {
|
||||
final TlsConfiguration tlsConfiguration = new TemporaryKeyStoreBuilder().build();
|
||||
|
||||
final NiFiProperties properties = getSingleLogoutProperties(tlsConfiguration);
|
||||
final StandardRelyingPartyRegistrationRepository repository = new StandardRelyingPartyRegistrationRepository(properties);
|
||||
|
||||
final RelyingPartyRegistration registration = repository.findByRegistrationId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
|
||||
assertRegistrationPropertiesFound(registration);
|
||||
|
||||
assertEquals(StandardRelyingPartyRegistrationRepository.SINGLE_LOGOUT_RESPONSE_SERVICE_LOCATION, registration.getSingleLogoutServiceLocation());
|
||||
assertEquals(StandardRelyingPartyRegistrationRepository.SINGLE_LOGOUT_RESPONSE_SERVICE_LOCATION, registration.getSingleLogoutServiceResponseLocation());
|
||||
|
||||
final RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails = registration.getAssertingPartyDetails();
|
||||
assertFalse(assertingPartyDetails.getWantAuthnRequestsSigned());
|
||||
assertTrue(assertingPartyDetails.getSigningAlgorithms().contains(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512));
|
||||
|
||||
assertSigningCredentialsFound(registration);
|
||||
assertEncryptionCredentialsFound(assertingPartyDetails);
|
||||
}
|
||||
|
||||
private void assertSigningCredentialsFound(final RelyingPartyRegistration registration) {
|
||||
final Collection<Saml2X509Credential> signingCredentials = registration.getSigningX509Credentials();
|
||||
assertFalse(signingCredentials.isEmpty());
|
||||
final Saml2X509Credential credential = signingCredentials.iterator().next();
|
||||
final X509Certificate certificate = credential.getCertificate();
|
||||
assertEquals(CERTIFICATE_PRINCIPAL, certificate.getSubjectX500Principal());
|
||||
assertEquals(CERTIFICATE_PRINCIPAL, certificate.getIssuerX500Principal());
|
||||
}
|
||||
|
||||
private void assertEncryptionCredentialsFound(final RelyingPartyRegistration.AssertingPartyDetails assertingPartyDetails) {
|
||||
final Collection<Saml2X509Credential> encryptionCredentials = assertingPartyDetails.getEncryptionX509Credentials();
|
||||
assertFalse(encryptionCredentials.isEmpty());
|
||||
final Optional<Saml2X509Credential> certificateCredential = encryptionCredentials.stream().filter(
|
||||
credential -> CERTIFICATE_PRINCIPAL.equals(credential.getCertificate().getSubjectX500Principal())
|
||||
).findFirst();
|
||||
assertTrue(certificateCredential.isPresent(), "Trust Store certificate credential not found");
|
||||
}
|
||||
|
||||
private void assertRegistrationPropertiesFound(final RelyingPartyRegistration registration) {
|
||||
assertNotNull(registration);
|
||||
assertEquals(Saml2RegistrationProperty.REGISTRATION_ID.getProperty(), registration.getRegistrationId());
|
||||
assertEquals(ENTITY_ID, registration.getEntityId());
|
||||
assertEquals(StandardRelyingPartyRegistrationRepository.LOGIN_RESPONSE_LOCATION, registration.getAssertionConsumerServiceLocation());
|
||||
}
|
||||
|
||||
private NiFiProperties getProperties() {
|
||||
final Properties properties = getStandardProperties();
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
|
||||
private NiFiProperties getSingleLogoutProperties(final TlsConfiguration tlsConfiguration) {
|
||||
final Properties properties = getStandardProperties();
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_SINGLE_LOGOUT_ENABLED, Boolean.TRUE.toString());
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_SIGNATURE_ALGORITHM, SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512);
|
||||
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEYSTORE, tlsConfiguration.getKeystorePath());
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE, tlsConfiguration.getKeystoreType().getType());
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD, tlsConfiguration.getKeystorePassword());
|
||||
properties.setProperty(NiFiProperties.SECURITY_KEY_PASSWD, tlsConfiguration.getKeyPassword());
|
||||
properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE, tlsConfiguration.getTruststorePath());
|
||||
properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, tlsConfiguration.getTruststoreType().getType());
|
||||
properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD, tlsConfiguration.getTruststorePassword());
|
||||
|
||||
return NiFiProperties.createBasicNiFiProperties(null, properties);
|
||||
}
|
||||
|
||||
private Properties getStandardProperties() {
|
||||
final Properties properties = new Properties();
|
||||
final String metadataUrl = getFileMetadataUrl();
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_IDP_METADATA_URL, metadataUrl);
|
||||
properties.setProperty(NiFiProperties.SECURITY_USER_SAML_SP_ENTITY_ID, ENTITY_ID);
|
||||
return properties;
|
||||
}
|
||||
|
||||
private String getFileMetadataUrl() {
|
||||
final URL resource = Objects.requireNonNull(getClass().getResource(METADATA_PATH));
|
||||
return resource.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.saml2.service.web;
|
||||
|
||||
import org.apache.nifi.web.security.saml2.registration.Saml2RegistrationProperty;
|
||||
import org.apache.nifi.web.util.WebUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardRelyingPartyRegistrationResolverTest {
|
||||
private static final String SERVICE_LOCATION = "{baseUrl}/login/saml2/sso/{registrationId}";
|
||||
|
||||
private static final String SINGLE_LOGOUT_LOCATION = "{baseUrl}/saml2/slo/{registrationId}";
|
||||
|
||||
private static final String CONTEXT_PATH = "/nifi-api";
|
||||
|
||||
private static final String REQUEST_URI = "/nifi-api/access";
|
||||
|
||||
private static final String FORWARDED_PATH = "/forwarded";
|
||||
|
||||
private static final int SERVER_PORT = 8080;
|
||||
|
||||
private static final String EXPECTED_CONSUMER_SERVICE_LOCATION = "http://localhost:8080/nifi-api/login/saml2/sso/consumer";
|
||||
|
||||
private static final String EXPECTED_FORWARDED_CONSUMER_SERVICE_LOCATION = "http://localhost:8080/forwarded/nifi-api/login/saml2/sso/consumer";
|
||||
|
||||
private static final String EXPECTED_SINGLE_LOGOUT_SERVICE_LOCATION = "http://localhost:8080/forwarded/nifi-api/saml2/slo/consumer";
|
||||
|
||||
private static final String REGISTRATION_ID = Saml2RegistrationProperty.REGISTRATION_ID.getProperty();
|
||||
|
||||
@Mock
|
||||
RelyingPartyRegistrationRepository repository;
|
||||
|
||||
MockHttpServletRequest request;
|
||||
|
||||
@BeforeEach
|
||||
void setResolver() {
|
||||
request = new MockHttpServletRequest();
|
||||
request.setServerPort(SERVER_PORT);
|
||||
request.setRequestURI(REQUEST_URI);
|
||||
request.setPathInfo(REQUEST_URI);
|
||||
request.setContextPath(CONTEXT_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveNotFound() {
|
||||
final StandardRelyingPartyRegistrationResolver resolver = new StandardRelyingPartyRegistrationResolver(repository, Collections.emptyList());
|
||||
|
||||
final RelyingPartyRegistration registration = resolver.resolve(request, REGISTRATION_ID);
|
||||
|
||||
assertNull(registration);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveFound() {
|
||||
final StandardRelyingPartyRegistrationResolver resolver = new StandardRelyingPartyRegistrationResolver(repository, Collections.emptyList());
|
||||
|
||||
final RelyingPartyRegistration registration = getRegistrationBuilder().build();
|
||||
when(repository.findByRegistrationId(eq(REGISTRATION_ID))).thenReturn(registration);
|
||||
|
||||
final RelyingPartyRegistration resolved = resolver.resolve(request, REGISTRATION_ID);
|
||||
|
||||
assertNotNull(resolved);
|
||||
assertEquals(EXPECTED_CONSUMER_SERVICE_LOCATION, resolved.getAssertionConsumerServiceLocation());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResolveSingleLogoutForwardedPathFound() {
|
||||
final StandardRelyingPartyRegistrationResolver resolver = new StandardRelyingPartyRegistrationResolver(repository, Collections.singletonList(FORWARDED_PATH));
|
||||
|
||||
final RelyingPartyRegistration registration = getSingleLogoutRegistration();
|
||||
when(repository.findByRegistrationId(eq(REGISTRATION_ID))).thenReturn(registration);
|
||||
|
||||
request.addHeader(WebUtils.PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_PATH);
|
||||
|
||||
final RelyingPartyRegistration resolved = resolver.resolve(request, REGISTRATION_ID);
|
||||
|
||||
assertNotNull(resolved);
|
||||
assertEquals(EXPECTED_FORWARDED_CONSUMER_SERVICE_LOCATION, resolved.getAssertionConsumerServiceLocation());
|
||||
assertEquals(EXPECTED_SINGLE_LOGOUT_SERVICE_LOCATION, resolved.getSingleLogoutServiceLocation());
|
||||
assertEquals(EXPECTED_SINGLE_LOGOUT_SERVICE_LOCATION, resolved.getSingleLogoutServiceResponseLocation());
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration.Builder getRegistrationBuilder() {
|
||||
return RelyingPartyRegistration.withRegistrationId(REGISTRATION_ID)
|
||||
.entityId(REGISTRATION_ID)
|
||||
.assertionConsumerServiceLocation(SERVICE_LOCATION)
|
||||
.assertingPartyDetails(assertingPartyDetails -> {
|
||||
assertingPartyDetails.entityId(REGISTRATION_ID);
|
||||
assertingPartyDetails.singleSignOnServiceLocation(SERVICE_LOCATION);
|
||||
});
|
||||
}
|
||||
|
||||
private RelyingPartyRegistration getSingleLogoutRegistration() {
|
||||
return getRegistrationBuilder()
|
||||
.singleLogoutServiceLocation(SINGLE_LOGOUT_LOCATION)
|
||||
.singleLogoutServiceResponseLocation(SINGLE_LOGOUT_LOCATION)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.saml2.service.web;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.saml2.registration.Saml2RegistrationProperty;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardSaml2AuthenticationRequestRepositoryTest {
|
||||
private static final String REQUEST_IDENTIFIER = UUID.randomUUID().toString();
|
||||
|
||||
private static final String LOCATION = "http://localhost/nifi-api";
|
||||
|
||||
private static final String SAML_REQUEST = "<LoginRequest/>";
|
||||
|
||||
@Mock
|
||||
Cache cache;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
private StandardSaml2AuthenticationRequestRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setRepository() {
|
||||
repository = new StandardSaml2AuthenticationRequestRepository(cache);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthenticationRequestCookieNotFound() {
|
||||
final AbstractSaml2AuthenticationRequest request = repository.loadAuthenticationRequest(httpServletRequest);
|
||||
|
||||
assertNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthenticationRequestCacheNotFound() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.SAML_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
|
||||
final AbstractSaml2AuthenticationRequest request = repository.loadAuthenticationRequest(httpServletRequest);
|
||||
|
||||
assertNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadAuthenticationRequestFound() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.SAML_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
|
||||
final AbstractSaml2AuthenticationRequest cachedRequest = getRequest();
|
||||
when(cache.get(eq(REQUEST_IDENTIFIER), eq(AbstractSaml2AuthenticationRequest.class))).thenReturn(cachedRequest);
|
||||
|
||||
final AbstractSaml2AuthenticationRequest request = repository.loadAuthenticationRequest(httpServletRequest);
|
||||
|
||||
assertNotNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveAuthenticationRequest() {
|
||||
httpServletRequest.setRequestURI(LOCATION);
|
||||
final AbstractSaml2AuthenticationRequest request = getRequest();
|
||||
|
||||
repository.saveAuthenticationRequest(request, httpServletRequest, httpServletResponse);
|
||||
|
||||
final Cookie cookie = httpServletResponse.getCookie(ApplicationCookieName.SAML_REQUEST_IDENTIFIER.getCookieName());
|
||||
assertNotNull(cookie);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAuthenticationRequestCookieNotFound() {
|
||||
final AbstractSaml2AuthenticationRequest request = repository.removeAuthenticationRequest(httpServletRequest, httpServletResponse);
|
||||
|
||||
assertNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveAuthenticationRequestFound() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.SAML_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
httpServletRequest.setRequestURI(LOCATION);
|
||||
|
||||
final AbstractSaml2AuthenticationRequest cachedRequest = getRequest();
|
||||
when(cache.get(eq(REQUEST_IDENTIFIER), eq(AbstractSaml2AuthenticationRequest.class))).thenReturn(cachedRequest);
|
||||
|
||||
final AbstractSaml2AuthenticationRequest request = repository.removeAuthenticationRequest(httpServletRequest, httpServletResponse);
|
||||
|
||||
assertNotNull(request);
|
||||
}
|
||||
|
||||
private AbstractSaml2AuthenticationRequest getRequest() {
|
||||
final RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.entityId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.assertingPartyDetails(assertingPartyDetails -> {
|
||||
assertingPartyDetails.entityId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
assertingPartyDetails.singleSignOnServiceLocation(LOCATION);
|
||||
})
|
||||
.build();
|
||||
return Saml2PostAuthenticationRequest.withRelyingPartyRegistration(registration).samlRequest(SAML_REQUEST).build();
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.authorization.util.IdentityMapping;
|
||||
import org.apache.nifi.idp.IdpType;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class Saml2AuthenticationSuccessHandlerTest {
|
||||
private static final String ISSUER = Saml2AuthenticationSuccessHandlerTest.class.getSimpleName();
|
||||
|
||||
private static final Duration EXPIRATION = Duration.ofMinutes(1);
|
||||
|
||||
private static final String IDENTITY = Authentication.class.getSimpleName();
|
||||
|
||||
private static final String IDENTITY_UPPER = IDENTITY.toUpperCase();
|
||||
|
||||
private static final String AUTHORITY = GrantedAuthority.class.getSimpleName();
|
||||
|
||||
private static final String AUTHORITY_LOWER = AUTHORITY.toLowerCase();
|
||||
|
||||
private static final String REQUEST_URI = "/nifi-api";
|
||||
|
||||
private static final int SERVER_PORT = 8080;
|
||||
|
||||
private static final String TARGET_URL = "http://localhost:8080/nifi/";
|
||||
|
||||
private static final String FIRST_GROUP = "$1";
|
||||
|
||||
private static final Pattern MATCH_PATTERN = Pattern.compile("(.*)");
|
||||
|
||||
private static final IdentityMapping UPPER_IDENTITY_MAPPING = new IdentityMapping(
|
||||
IdentityMapping.Transform.UPPER.toString(),
|
||||
MATCH_PATTERN,
|
||||
FIRST_GROUP,
|
||||
IdentityMapping.Transform.UPPER
|
||||
);
|
||||
|
||||
private static final IdentityMapping LOWER_IDENTITY_MAPPING = new IdentityMapping(
|
||||
IdentityMapping.Transform.LOWER.toString(),
|
||||
MATCH_PATTERN,
|
||||
FIRST_GROUP,
|
||||
IdentityMapping.Transform.LOWER
|
||||
);
|
||||
|
||||
@Mock
|
||||
BearerTokenProvider bearerTokenProvider;
|
||||
|
||||
@Mock
|
||||
IdpUserGroupService idpUserGroupService;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
Saml2AuthenticationSuccessHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setHandler() {
|
||||
handler = new Saml2AuthenticationSuccessHandler(
|
||||
bearerTokenProvider,
|
||||
idpUserGroupService,
|
||||
Collections.singletonList(UPPER_IDENTITY_MAPPING),
|
||||
Collections.singletonList(LOWER_IDENTITY_MAPPING),
|
||||
EXPIRATION,
|
||||
ISSUER
|
||||
);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletRequest.setServerPort(SERVER_PORT);
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDetermineTargetUrl() {
|
||||
httpServletRequest.setRequestURI(REQUEST_URI);
|
||||
|
||||
final Authentication authentication = new TestingAuthenticationToken(IDENTITY, IDENTITY, AUTHORITY);
|
||||
|
||||
final String targetUrl = handler.determineTargetUrl(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
assertEquals(TARGET_URL, targetUrl);
|
||||
|
||||
verify(idpUserGroupService).replaceUserGroups(eq(IDENTITY_UPPER), eq(IdpType.SAML), eq(Collections.singleton(AUTHORITY_LOWER)));
|
||||
|
||||
final Cookie bearerCookie = httpServletResponse.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
|
||||
assertNotNull(bearerCookie);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.apache.nifi.web.security.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class Saml2LocalLogoutFilterTest {
|
||||
@Mock
|
||||
LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
MockFilterChain filterChain;
|
||||
|
||||
Saml2LocalLogoutFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setFilter() {
|
||||
filter = new Saml2LocalLogoutFilter(logoutSuccessHandler);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
filterChain = new MockFilterChain();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterInternalNotMatched() throws ServletException, IOException {
|
||||
filter.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
|
||||
|
||||
verifyNoInteractions(logoutSuccessHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterInternal() throws ServletException, IOException {
|
||||
httpServletRequest.setPathInfo(SamlUrlPath.LOCAL_LOGOUT_REQUEST.getPath());
|
||||
filter.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
|
||||
|
||||
verify(logoutSuccessHandler).onLogoutSuccess(eq(httpServletRequest), eq(httpServletResponse), isNull());
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.admin.service.IdpUserGroupService;
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class Saml2LogoutSuccessHandlerTest {
|
||||
private static final String REQUEST_IDENTIFIER = UUID.randomUUID().toString();
|
||||
|
||||
private static final String USER_IDENTITY = LogoutRequest.class.getSimpleName();
|
||||
|
||||
private static final String REQUEST_URI = "/nifi-api";
|
||||
|
||||
private static final int SERVER_PORT = 8080;
|
||||
|
||||
private static final String REDIRECTED_URL = "http://localhost:8080/nifi/logout-complete";
|
||||
|
||||
@Mock
|
||||
IdpUserGroupService idpUserGroupService;
|
||||
|
||||
@Mock
|
||||
Authentication authentication;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
LogoutRequestManager logoutRequestManager;
|
||||
|
||||
Saml2LogoutSuccessHandler handler;
|
||||
|
||||
@BeforeEach
|
||||
void setHandler() {
|
||||
logoutRequestManager = new LogoutRequestManager();
|
||||
handler = new Saml2LogoutSuccessHandler(logoutRequestManager, idpUserGroupService);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletRequest.setServerPort(SERVER_PORT);
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnLogoutSuccessRequestNotFound() throws IOException {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
httpServletRequest.setRequestURI(REQUEST_URI);
|
||||
|
||||
handler.onLogoutSuccess(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
final String redirectedUrl = httpServletResponse.getRedirectedUrl();
|
||||
|
||||
assertEquals(REDIRECTED_URL, redirectedUrl);
|
||||
|
||||
verifyNoInteractions(idpUserGroupService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnLogoutSuccessRequestFound() throws IOException {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
httpServletRequest.setRequestURI(REQUEST_URI);
|
||||
|
||||
final LogoutRequest logoutRequest = new LogoutRequest(REQUEST_IDENTIFIER, USER_IDENTITY);
|
||||
logoutRequestManager.start(logoutRequest);
|
||||
|
||||
handler.onLogoutSuccess(httpServletRequest, httpServletResponse, authentication);
|
||||
|
||||
final String redirectedUrl = httpServletResponse.getRedirectedUrl();
|
||||
|
||||
assertEquals(REDIRECTED_URL, redirectedUrl);
|
||||
verify(idpUserGroupService).deleteUserGroups(eq(USER_IDENTITY));
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequest;
|
||||
import org.apache.nifi.web.security.logout.LogoutRequestManager;
|
||||
import org.apache.nifi.web.security.saml2.SamlUrlPath;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class Saml2SingleLogoutFilterTest {
|
||||
private static final String REQUEST_IDENTIFIER = UUID.randomUUID().toString();
|
||||
|
||||
private static final String USER_IDENTITY = LogoutRequest.class.getSimpleName();
|
||||
|
||||
@Mock
|
||||
LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
MockFilterChain filterChain;
|
||||
|
||||
LogoutRequestManager logoutRequestManager;
|
||||
|
||||
Saml2SingleLogoutFilter filter;
|
||||
|
||||
@BeforeEach
|
||||
void setFilter() {
|
||||
logoutRequestManager = new LogoutRequestManager();
|
||||
filter = new Saml2SingleLogoutFilter(logoutRequestManager, logoutSuccessHandler);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
filterChain = new MockFilterChain();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterInternalNotMatched() throws ServletException, IOException {
|
||||
filter.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
|
||||
|
||||
verifyNoInteractions(logoutSuccessHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoFilterInternal() throws ServletException, IOException {
|
||||
httpServletRequest.setPathInfo(SamlUrlPath.SINGLE_LOGOUT_REQUEST.getPath());
|
||||
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
|
||||
final LogoutRequest logoutRequest = new LogoutRequest(REQUEST_IDENTIFIER, USER_IDENTITY);
|
||||
logoutRequestManager.start(logoutRequest);
|
||||
|
||||
filter.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
|
||||
|
||||
verify(logoutSuccessHandler).onLogoutSuccess(eq(httpServletRequest), eq(httpServletResponse), isA(LogoutAuthenticationToken.class));
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.saml2.web.authentication.logout;
|
||||
|
||||
import org.apache.nifi.web.security.cookie.ApplicationCookieName;
|
||||
import org.apache.nifi.web.security.saml2.registration.Saml2RegistrationProperty;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StandardSaml2LogoutRequestRepositoryTest {
|
||||
private static final String REQUEST_IDENTIFIER = UUID.randomUUID().toString();
|
||||
|
||||
private static final String RELAY_STATE = Saml2LogoutRequest.class.getSimpleName();
|
||||
|
||||
private static final String LOCATION = "http://localhost/nifi-api";
|
||||
|
||||
private static final String SAML_REQUEST = "<LoginRequest/>";
|
||||
|
||||
@Mock
|
||||
Cache cache;
|
||||
|
||||
MockHttpServletRequest httpServletRequest;
|
||||
|
||||
MockHttpServletResponse httpServletResponse;
|
||||
|
||||
private StandardSaml2LogoutRequestRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setRepository() {
|
||||
repository = new StandardSaml2LogoutRequestRepository(cache);
|
||||
httpServletRequest = new MockHttpServletRequest();
|
||||
httpServletResponse = new MockHttpServletResponse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadLogoutRequestCookieNotFound() {
|
||||
final Saml2LogoutRequest request = repository.loadLogoutRequest(httpServletRequest);
|
||||
|
||||
assertNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadLogoutRequestCacheNotFound() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
|
||||
final Saml2LogoutRequest request = repository.loadLogoutRequest(httpServletRequest);
|
||||
|
||||
assertNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadLogoutRequestFound() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
|
||||
final Saml2LogoutRequest cachedRequest = getRequest();
|
||||
when(cache.get(eq(REQUEST_IDENTIFIER), eq(Saml2LogoutRequest.class))).thenReturn(cachedRequest);
|
||||
|
||||
final Saml2LogoutRequest request = repository.loadLogoutRequest(httpServletRequest);
|
||||
|
||||
assertNotNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveLogoutRequest() {
|
||||
httpServletRequest.setRequestURI(LOCATION);
|
||||
final Saml2LogoutRequest request = getRequest();
|
||||
|
||||
repository.saveLogoutRequest(request, httpServletRequest, httpServletResponse);
|
||||
|
||||
final Cookie cookie = httpServletResponse.getCookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName());
|
||||
assertNotNull(cookie);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveLogoutRequestCookieNotFound() {
|
||||
final Saml2LogoutRequest request = repository.removeLogoutRequest(httpServletRequest, httpServletResponse);
|
||||
|
||||
assertNull(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveLogoutRequestFound() {
|
||||
final Cookie cookie = new Cookie(ApplicationCookieName.LOGOUT_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER);
|
||||
httpServletRequest.setCookies(cookie);
|
||||
httpServletRequest.setRequestURI(LOCATION);
|
||||
|
||||
final Saml2LogoutRequest cachedRequest = getRequest();
|
||||
when(cache.get(eq(REQUEST_IDENTIFIER), eq(Saml2LogoutRequest.class))).thenReturn(cachedRequest);
|
||||
|
||||
final Saml2LogoutRequest request = repository.removeLogoutRequest(httpServletRequest, httpServletResponse);
|
||||
|
||||
assertNotNull(request);
|
||||
}
|
||||
|
||||
private Saml2LogoutRequest getRequest() {
|
||||
final RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.entityId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty())
|
||||
.assertingPartyDetails(assertingPartyDetails -> {
|
||||
assertingPartyDetails.entityId(Saml2RegistrationProperty.REGISTRATION_ID.getProperty());
|
||||
assertingPartyDetails.singleSignOnServiceLocation(LOCATION);
|
||||
})
|
||||
.build();
|
||||
return Saml2LogoutRequest.withRelyingPartyRegistration(registration).samlRequest(SAML_REQUEST).relayState(RELAY_STATE).build();
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package org.apache.nifi.web.filter;
|
||||
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
@ -23,12 +25,16 @@ import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Filter for determining appropriate login location.
|
||||
*/
|
||||
public class LoginFilter implements Filter {
|
||||
private static final String SAML2_AUTHENTICATE_FILTER_PATH = "/nifi-api/saml2/authenticate/consumer";
|
||||
|
||||
private ServletContext servletContext;
|
||||
|
||||
@ -50,8 +56,11 @@ public class LoginFilter implements Filter {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/knox/request").forward(request, response);
|
||||
} else if (supportsSAML) {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/saml/login/request").forward(request, response);
|
||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
final URI authenticateUri = RequestUriBuilder.fromHttpServletRequest(httpServletRequest).path(SAML2_AUTHENTICATE_FILTER_PATH).build();
|
||||
// Redirect to request consumer URL defined in Spring Security OpenSamlAuthenticationRequestResolver.requestMatcher
|
||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
httpServletResponse.sendRedirect(authenticateUri.toString());
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
*/
|
||||
package org.apache.nifi.web.filter;
|
||||
|
||||
import org.apache.nifi.web.util.RequestUriBuilder;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
@ -23,7 +25,10 @@ import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Filter for determining appropriate logout location.
|
||||
@ -58,12 +63,13 @@ public class LogoutFilter implements Filter {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/knox/logout").forward(request, response);
|
||||
} else if (supportsSaml) {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
if (supportsSamlSingleLogout) {
|
||||
apiContext.getRequestDispatcher("/access/saml/single-logout/request").forward(request, response);
|
||||
} else {
|
||||
apiContext.getRequestDispatcher("/access/saml/local-logout").forward(request, response);
|
||||
}
|
||||
// Redirect to request URL defined in nifi-web-api security filter configuration
|
||||
final String logoutUrl = supportsSamlSingleLogout ? "/nifi-api/access/saml/single-logout/request" : "/nifi-api/access/saml/local-logout/request";
|
||||
|
||||
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
final URI targetUri = RequestUriBuilder.fromHttpServletRequest(httpServletRequest).path(logoutUrl).build();
|
||||
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
httpServletResponse.sendRedirect(targetUri.toString());
|
||||
} else {
|
||||
final ServletContext apiContext = servletContext.getContext("/nifi-api");
|
||||
apiContext.getRequestDispatcher("/access/logout/complete").forward(request, response);
|
||||
|
@ -109,11 +109,11 @@
|
||||
urls: {
|
||||
api: '../nifi-api',
|
||||
accessConfig: '../nifi-api/access/config',
|
||||
accessTokenExpiration: '../nifi-api/access/token/expiration',
|
||||
currentUser: '../nifi-api/flow/current-user',
|
||||
controllerBulletins: '../nifi-api/flow/controller/bulletins',
|
||||
kerberos: '../nifi-api/access/kerberos',
|
||||
oidc: '../nifi-api/access/oidc/exchange',
|
||||
saml: '../nifi-api/access/saml/login/exchange',
|
||||
revision: '../nifi-api/flow/revision',
|
||||
banners: '../nifi-api/flow/banners'
|
||||
}
|
||||
@ -911,11 +911,17 @@
|
||||
successfulAuthentication(jwt)
|
||||
}).fail(function () {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: config.urls.saml,
|
||||
dataType: 'text'
|
||||
}).done(function (jwt) {
|
||||
successfulAuthentication(jwt)
|
||||
type: 'GET',
|
||||
url: config.urls.accessTokenExpiration,
|
||||
dataType: 'json'
|
||||
}).done(function (accessTokenExpirationEntity) {
|
||||
var accessTokenExpiration = accessTokenExpirationEntity.accessTokenExpiration;
|
||||
// Convert ISO 8601 string to session expiration in seconds
|
||||
var expiration = Date.parse(accessTokenExpiration.expiration);
|
||||
var expirationSeconds = expiration / 1000;
|
||||
var sessionExpiration = Math.round(expirationSeconds);
|
||||
nfAuthorizationStorage.setToken(sessionExpiration);
|
||||
deferred.resolve();
|
||||
}).fail(function () {
|
||||
deferred.reject();
|
||||
});
|
||||
|
@ -429,35 +429,23 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.extensions</groupId>
|
||||
<artifactId>spring-security-saml2-core</artifactId>
|
||||
<version>1.0.10.RELEASE</version>
|
||||
<exclusions>
|
||||
<!-- Excluded to avoid inclusion of different version of Bouncy Castle Provider -->
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-ext-jdk15on</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.io7m.xom</groupId>
|
||||
<artifactId>xom</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>xalan</groupId>
|
||||
<artifactId>xalan</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.kerberos</groupId>
|
||||
<artifactId>spring-security-kerberos-core</artifactId>
|
||||
<version>1.0.1.RELEASE</version>
|
||||
</dependency>
|
||||
<!-- Override xmlsec from spring-security-saml2-service-provider -->
|
||||
<dependency>
|
||||
<groupId>org.apache.santuario</groupId>
|
||||
<artifactId>xmlsec</artifactId>
|
||||
<version>2.3.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.fasterxml.woodstox</groupId>
|
||||
<artifactId>woodstox-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
|
@ -71,6 +71,11 @@ language governing permissions and limitations under the License. -->
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</exclusion>
|
||||
<!-- Exclude SAML 2 unnecessary dependency -->
|
||||
<exclusion>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-saml2-service-provider</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
Loading…
x
Reference in New Issue
Block a user