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:
exceptionfactory 2022-06-13 14:06:07 -05:00 committed by Nathan Gough
parent 1465c2c629
commit 0de83292de
87 changed files with 3349 additions and 4162 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,83 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.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));
}
}

View File

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

View File

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

View File

@ -1,124 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.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;
}
}

View File

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

View File

@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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