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]
