thenatog commented on a change in pull request #4614:
URL: https://github.com/apache/nifi/pull/4614#discussion_r525633489



##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/tls/CustomTLSProtocolSocketFactory.java
##########
@@ -0,0 +1,69 @@
+/*
+ * 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 {

Review comment:
       Just to verify, this class is here to allow using NiFi's 
SSLContextFactory?

##########
File path: nifi-docs/src/main/asciidoc/administration-guide.adoc
##########
@@ -375,12 +375,32 @@ JSON Web Key (JWK) provided through the jwks_uri in the 
metadata found at the di
 |`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the 
user to be logged in; default is `email`. May need to be requested via the 
`nifi.security.user.oidc.additional.scopes` before usage.
 
|==================================================================================================================================================
 
+[[saml]]
+=== SAML
+
+To enable authentication via SAML the following properties must be configured 
in _nifi.properties_.
+
+[options="header"]
+|==================================================================================================================================================
+| Property Name | Description
+|`nifi.security.user.saml.idp.metadata.url` | The URL for obtaining the 
identity provider's metadata. The metadata can be retrieved from the identity 
provider via `http://` or `https://`, or a local file can be referenced using 
`file://` .
+|`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.signing.key.alias`| The alias of the key within 
`nifi.security.keystore` that will be used for signing SAML messages.
+|`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 the 
default of SHA-1 will be used.

Review comment:
       Is it possible for us to specify a default other than SHA-1?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/tls/CompositeKeyManager.java
##########
@@ -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.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 {

Review comment:
       I think we discussed this somewhere but this CompositeKeyManager is 
because SAML expects a single KeyManager which contains a key and trusted certs?

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLConfigurationFactory.java
##########
@@ -0,0 +1,502 @@
+/*
+ * 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.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);

Review comment:
       What does registering the protocol here actually do? Is this simply how 
we tell HTTPClient what socket factory to use?

##########
File path: nifi-docs/src/main/asciidoc/administration-guide.adoc
##########
@@ -375,12 +375,32 @@ JSON Web Key (JWK) provided through the jwks_uri in the 
metadata found at the di
 |`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the 
user to be logged in; default is `email`. May need to be requested via the 
`nifi.security.user.oidc.additional.scopes` before usage.
 
|==================================================================================================================================================
 
+[[saml]]
+=== SAML
+
+To enable authentication via SAML the following properties must be configured 
in _nifi.properties_.
+
+[options="header"]
+|==================================================================================================================================================
+| Property Name | Description
+|`nifi.security.user.saml.idp.metadata.url` | The URL for obtaining the 
identity provider's metadata. The metadata can be retrieved from the identity 
provider via `http://` or `https://`, or a local file can be referenced using 
`file://` .
+|`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.signing.key.alias`| The alias of the key within 
`nifi.security.keystore` that will be used for signing SAML messages.
+|`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 the 
default of SHA-1 will be used.

Review comment:
       I used http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 successfully 
with a SHA256WITHRSA signed X.509 key/cert. We should encourage users to use 
SHA256 by making that the default if we can. The tls-toolkit and other cert 
tools default is to use RSA-256, so certificates generated using these tools 
will most likely support this signing. 

##########
File path: 
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
##########
@@ -171,6 +204,516 @@ public Response getLoginConfig(@Context 
HttpServletRequest httpServletRequest) {
         return generateOkResponse(entity).build();
     }
 
+    @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) throws 
Exception {
+        // only consider user specific access over https
+        if (!httpServletRequest.isSecure()) {
+            throw new 
AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
+        }
+
+        // ensure saml is enabled
+        if (!samlService.isSamlEnabled()) {
+            logger.warn(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 {
+
+        // only consider user specific access over https
+        if (!httpServletRequest.isSecure()) {
+            forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
AUTHENTICATION_NOT_ENABLED_MSG);
+            return;
+        }
+
+        // ensure saml is enabled
+        if (!samlService.isSamlEnabled()) {
+            forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
+            return;
+        }
+
+        // ensure saml service provider is initialized
+        initializeSamlServiceProvider();
+
+        final String samlRequestIdentifier = UUID.randomUUID().toString();
+
+        // generate a cookie to associate this login sequence
+        final Cookie cookie = new Cookie(SAML_REQUEST_IDENTIFIER, 
samlRequestIdentifier);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(60);
+        cookie.setSecure(true);
+        httpServletResponse.addCookie(cookie);
+
+        // 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());
+            return;
+        }
+    }
+
+    @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 {
+
+        // only consider user specific access over https
+        if (!httpServletRequest.isSecure()) {
+            forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
AUTHENTICATION_NOT_ENABLED_MSG);
+            return;
+        }
+
+        // ensure saml is enabled
+        if (!samlService.isSamlEnabled()) {
+            forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
+            return;
+        }
+
+        // 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 {
+
+        // only consider user specific access over https
+        if (!httpServletRequest.isSecure()) {
+            forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
AUTHENTICATION_NOT_ENABLED_MSG);
+            return;
+        }
+
+        // ensure saml is enabled
+        if (!samlService.isSamlEnabled()) {
+            forwardToLoginMessagePage(httpServletRequest, httpServletResponse, 
SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
+            return;
+        }
+
+        // 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 String samlRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), SAML_REQUEST_IDENTIFIER);
+        if (samlRequestIdentifier == null) {
+            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
+        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 = samlCredential.getNameID().getValue();
+        final String mappedIdentity = 
IdentityMappingUtil.mapIdentity(rawIdentity, 
IdentityMappingUtil.getIdentityMappings(properties));
+        final long expiration = 
validateTokenExpiration(samlService.getAuthExpiration(), mappedIdentity);
+        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
+        // issue a delete first in case the user already had a stored 
credential that somehow wasn't properly cleaned up on logout
+        samlCredentialStore.delete(mappedIdentity);
+        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);
+        storeIdpGroups(mappedIdentity, IdpType.SAML, userGroups);
+
+        // redirect to the name page
+        httpServletResponse.sendRedirect(getNiFiUri());
+    }
+
+    private void storeIdpGroups(final String userIdentity, final IdpType 
idpType, final Set<String> groupNames) {

Review comment:
       The AccessResource class has definitely been getting bigger. Might be 
something we need to look at more generally.




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to