You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2021/06/21 21:39:54 UTC
[nifi] branch main updated: NIFI-8025 - Refactored SAML and OIDC
Resources to separate classes
This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 9744644 NIFI-8025 - Refactored SAML and OIDC Resources to separate classes
9744644 is described below
commit 9744644b9d643dada81566f58dc8a78f008e8fe6
Author: Nathan Gough <th...@gmail.com>
AuthorDate: Fri Apr 30 14:38:07 2021 -0400
NIFI-8025 - Refactored SAML and OIDC Resources to separate classes
This closes #5079
Signed-off-by: David Handermann <ex...@apache.org>
---
.../apache/nifi/web/NiFiWebApiResourceConfig.java | 7 +-
.../nifi/web/NiFiWebApiSecurityConfiguration.java | 9 +-
.../org/apache/nifi/web/api/AccessResource.java | 1115 +-------------------
.../apache/nifi/web/api/ApplicationResource.java | 56 +-
.../apache/nifi/web/api/OIDCAccessResource.java | 573 ++++++++++
.../apache/nifi/web/api/SAMLAccessResource.java | 540 ++++++++++
.../src/main/resources/nifi-web-api-context.xml | 18 +-
.../nifi/web/security/oidc/OIDCEndpoints.java | 37 +
.../nifi/web/security/saml/SAMLEndpoints.java | 30 +-
.../saml/impl/StandardSAMLStateManager.java | 5 +-
.../main/resources/nifi-web-security-context.xml | 2 +-
11 files changed, 1255 insertions(+), 1137 deletions(-)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
index 0ff68bb..f17809d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiResourceConfig.java
@@ -16,8 +16,6 @@
*/
package org.apache.nifi.web;
-import javax.servlet.ServletContext;
-import javax.ws.rs.core.Context;
import org.apache.nifi.web.api.config.AccessDeniedExceptionMapper;
import org.apache.nifi.web.api.config.AdministrationExceptionMapper;
import org.apache.nifi.web.api.config.AuthenticationCredentialsNotFoundExceptionMapper;
@@ -59,6 +57,9 @@ import org.glassfish.jersey.server.filter.EncodingFilter;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Context;
+
public class NiFiWebApiResourceConfig extends ResourceConfig {
public NiFiWebApiResourceConfig(@Context ServletContext servletContext) {
@@ -97,6 +98,8 @@ 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"));
register(ctx.getBean("versionsResource"));
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
index ceaa256..c4e6304 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
@@ -24,6 +24,7 @@ import org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter;
import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider;
+import org.apache.nifi.web.security.oidc.OIDCEndpoints;
import org.apache.nifi.web.security.otp.OtpAuthenticationFilter;
import org.apache.nifi.web.security.otp.OtpAuthenticationProvider;
import org.apache.nifi.web.security.saml.SAMLEndpoints;
@@ -100,10 +101,10 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
"/access/config",
"/access/token",
"/access/kerberos",
- "/access/oidc/exchange",
- "/access/oidc/callback",
- "/access/oidc/logoutCallback",
- "/access/oidc/request",
+ OIDCEndpoints.TOKEN_EXCHANGE,
+ OIDCEndpoints.LOGIN_REQUEST,
+ OIDCEndpoints.LOGIN_CALLBACK,
+ OIDCEndpoints.LOGOUT_CALLBACK,
"/access/knox/callback",
"/access/knox/request",
SAMLEndpoints.SERVICE_PROVIDER_METADATA,
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index 6103d3c..6dd1c26 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -16,31 +16,13 @@
*/
package org.apache.nifi.web.api;
-import com.nimbusds.oauth2.sdk.AuthorizationCode;
-import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
-import com.nimbusds.oauth2.sdk.AuthorizationGrant;
-import com.nimbusds.oauth2.sdk.ParseException;
-import com.nimbusds.oauth2.sdk.http.HTTPResponse;
-import com.nimbusds.oauth2.sdk.id.State;
-import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
-import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
-import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
import io.jsonwebtoken.JwtException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.apache.commons.lang3.StringUtils;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.message.BasicNameValuePair;
import org.apache.nifi.admin.service.AdministrationException;
-import org.apache.nifi.admin.service.IdpUserGroupService;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
import org.apache.nifi.authentication.LoginIdentityProvider;
@@ -51,9 +33,7 @@ import org.apache.nifi.authorization.AccessDeniedException;
import org.apache.nifi.authorization.user.NiFiUser;
import org.apache.nifi.authorization.user.NiFiUserDetails;
import org.apache.nifi.authorization.user.NiFiUserUtils;
-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.FormatUtils;
import org.apache.nifi.web.api.dto.AccessConfigurationDTO;
import org.apache.nifi.web.api.dto.AccessStatusDTO;
@@ -71,31 +51,21 @@ import org.apache.nifi.web.security.kerberos.KerberosService;
import org.apache.nifi.web.security.knox.KnoxService;
import org.apache.nifi.web.security.logout.LogoutRequest;
import org.apache.nifi.web.security.logout.LogoutRequestManager;
-import org.apache.nifi.web.security.oidc.OidcService;
import org.apache.nifi.web.security.otp.OtpService;
-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.apache.nifi.web.security.token.NiFiAuthenticationToken;
import org.apache.nifi.web.security.token.OtpAuthenticationToken;
import org.apache.nifi.web.security.x509.X509AuthenticationProvider;
import org.apache.nifi.web.security.x509.X509AuthenticationRequestToken;
import org.apache.nifi.web.security.x509.X509CertificateExtractor;
-import org.eclipse.jetty.http.HttpCookie;
-import org.eclipse.jetty.http.HttpHeader;
-import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.slf4j.Logger;
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.saml.SAMLCredential;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
import org.springframework.web.util.WebUtils;
-import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -108,24 +78,12 @@ 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.Response.ResponseBuilder;
import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import java.io.IOException;
import java.net.URI;
import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
/**
* RESTful endpoint for managing access.
@@ -138,29 +96,8 @@ import java.util.stream.Collectors;
public class AccessResource extends ApplicationResource {
private static final Logger logger = LoggerFactory.getLogger(AccessResource.class);
-
- private static final String OIDC_REQUEST_IDENTIFIER = "oidc-request-identifier";
- private static final String OIDC_ID_TOKEN_AUTHN_ERROR = "Unable to exchange authorization for ID token: ";
- private static final String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG = "OpenId Connect support is not configured";
- private static final String REVOKE_ACCESS_TOKEN_LOGOUT = "oidc_access_token_logout";
- private static final String ID_TOKEN_LOGOUT = "oidc_id_token_logout";
- private static final String STANDARD_LOGOUT = "oidc_standard_logout";
- private static final Pattern REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT = Pattern.compile("(\\.google\\.com)");
- private static final Pattern ID_TOKEN_LOGOUT_FORMAT = Pattern.compile("(\\.okta)");
- private static final int msTimeout = 30_000;
- private static final int VALID_FOR_SESSION_ONLY = -1;
-
- private static final String SAML_REQUEST_IDENTIFIER = "saml-request-identifier";
- private static final String SAML_METADATA_MEDIA_TYPE = "application/samlmetadata+xml";
-
- private static final String LOGIN_ERROR_TITLE = "Unable to continue login sequence";
- private static final String LOGOUT_ERROR_TITLE = "Unable to continue logout sequence";
- private static final String LOGOUT_REQUEST_IDENTIFIER = "nifi-logout-request-identifier";
-
-
- private static final String AUTHENTICATION_NOT_ENABLED_MSG = "User authentication/authorization is only supported when running over HTTPS.";
- 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.";
+ protected static final String AUTHENTICATION_NOT_ENABLED_MSG = "User authentication/authorization is only supported when running over HTTPS.";
+ static final String LOGOUT_REQUEST_IDENTIFIER = "nifi-logout-request-identifier";
private X509CertificateExtractor certificateExtractor;
private X509AuthenticationProvider x509AuthenticationProvider;
@@ -170,16 +107,9 @@ public class AccessResource extends ApplicationResource {
private JwtAuthenticationProvider jwtAuthenticationProvider;
private JwtService jwtService;
private OtpService otpService;
- private OidcService oidcService;
private KnoxService knoxService;
private KerberosService kerberosService;
-
- private SAMLService samlService;
- private SAMLStateManager samlStateManager;
- private SAMLCredentialStore samlCredentialStore;
-
- private IdpUserGroupService idpUserGroupService;
- private LogoutRequestManager logoutRequestManager;
+ protected LogoutRequestManager logoutRequestManager;
/**
* Retrieves the access configuration for this NiFi.
@@ -212,860 +142,6 @@ public class AccessResource extends ApplicationResource {
@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.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 {
-
- // 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 = WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
- 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 = samlService.getUserIdentity(samlCredential);
- 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
- 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) 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.debug(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
- return Response.status(Response.Status.CONFLICT).entity(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED).build();
- }
-
- logger.info("Attempting to exchange SAML login request for a NiFi JWT...");
-
- // ensure saml service provider is initialized
- initializeSamlServiceProvider();
-
- // ensure the request has the cookie with the request identifier
- final String samlRequestIdentifier = WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
- if (samlRequestIdentifier == null) {
- 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();
- }
-
- // remove the saml request cookie
- removeSamlRequestCookie(httpServletResponse);
-
- // get the jwt
- 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.");
- }
-
- // generate the response
- logger.info("SAML login exchange complete");
- 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 {
-
- // only consider user specific access over https
- if (!httpServletRequest.isSecure()) {
- throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
- }
-
- // ensure saml is enabled
- if (!samlService.isSamlEnabled()) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
- return;
- }
-
- // ensure the logout request identifier is present
- final String logoutRequestIdentifier = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
- if (StringUtils.isBlank(logoutRequestIdentifier)) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
- return;
- }
-
- // ensure there is a logout request in progress for the given identifier
- 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 (Exception e) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
- return;
- }
- }
-
- @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 {
-
- // only consider user specific access over https
- if (!httpServletRequest.isSecure()) {
- throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
- }
-
- // ensure saml is enabled
- if (!samlService.isSamlEnabled()) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
- return;
- }
-
- // 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 {
-
- // only consider user specific access over https
- if (!httpServletRequest.isSecure()) {
- throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
- }
-
- // ensure saml is enabled
- if (!samlService.isSamlEnabled()) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
- return;
- }
-
- // 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 String logoutRequestIdentifier = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
- if (StringUtils.isBlank(logoutRequestIdentifier)) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
- return;
- }
-
- // ensure there is a logout request in progress for the given identifier
- 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 {
-
- // only consider user specific access over https
- if (!httpServletRequest.isSecure()) {
- throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
- }
-
- // ensure saml is enabled
- if (!samlService.isSamlEnabled()) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
- return;
- }
-
- // complete the logout request if one exists
- final LogoutRequest completedLogoutRequest = completeLogoutRequest(httpServletResponse);
-
- // if a logout request was completed, then delete the stored SAMLCredential for that user
- if (completedLogoutRequest != null) {
- final String userIdentity = completedLogoutRequest.getMappedUserIdentity();
-
- logger.info("Removing cached SAML information for " + userIdentity);
- samlCredentialStore.delete(userIdentity);
-
- logger.info("Removing cached SAML Groups for " + userIdentity);
- idpUserGroupService.deleteUserGroups(userIdentity);
- }
-
- // redirect to logout landing page
- httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri());
- }
-
- private void initializeSamlServiceProvider() throws MetadataProviderException {
- 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;
- }
-
- @GET
- @Consumes(MediaType.WILDCARD)
- @Produces(MediaType.WILDCARD)
- @Path("oidc/request")
- @ApiOperation(
- value = "Initiates a request to authenticate through the configured OpenId Connect provider.",
- notes = NON_GUARANTEED_ENDPOINT
- )
- public void oidcRequest(@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 oidc is enabled
- if (!oidcService.isOidcEnabled()) {
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
- return;
- }
-
- // generate the authorization uri
- URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcCallback());
-
- // generate the response
- httpServletResponse.sendRedirect(authorizationURI.toString());
- }
-
- @GET
- @Consumes(MediaType.WILDCARD)
- @Produces(MediaType.WILDCARD)
- @Path("oidc/callback")
- @ApiOperation(
- value = "Redirect/callback URI for processing the result of the OpenId Connect login sequence.",
- notes = NON_GUARANTEED_ENDPOINT
- )
- public void oidcCallback(@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 oidc is enabled
- if (!oidcService.isOidcEnabled()) {
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
- return;
- }
-
- final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
- if (oidcRequestIdentifier == null) {
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was " +
- "not found in the request. Unable to continue.");
- return;
- }
-
- final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse;
- try {
- oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
- } catch (final ParseException e) {
- logger.error("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login process.");
-
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // forward to the error page
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Unable to parse the redirect URI " +
- "from the OpenId Connect Provider. Unable to continue login process.");
- return;
- }
-
- if (oidcResponse.indicatesSuccess()) {
- final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
-
- // confirm state
- final State state = successfulOidcResponse.getState();
- if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
- logger.error("The state value returned by the OpenId Connect Provider does not match the stored state. " +
- "Unable to continue login process.");
-
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // forward to the error page
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Purposed state does not match " +
- "the stored state. Unable to continue login process.");
- return;
- }
-
- try {
- // exchange authorization code for id token
- final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
- final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcCallback()));
-
- // get the oidc token
- LoginAuthenticationToken oidcToken = oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(authorizationGrant);
-
- // exchange the oidc token for the NiFi token
- String nifiJwt = jwtService.generateSignedToken(oidcToken);
-
- // store the NiFi token
- oidcService.storeJwt(oidcRequestIdentifier, nifiJwt);
- } catch (final Exception e) {
- logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
-
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // forward to the error page
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
- return;
- }
-
- // redirect to the name page
- httpServletResponse.sendRedirect(getNiFiUri());
- } else {
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // report the unsuccessful login
- final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
- forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt: "
- + errorOidcResponse.getErrorObject().getDescription());
- }
- }
-
- @POST
- @Consumes(MediaType.WILDCARD)
- @Produces(MediaType.TEXT_PLAIN)
- @Path("oidc/exchange")
- @ApiOperation(
- value = "Retrieves a JWT following a successful login sequence using the configured OpenId Connect provider.",
- response = String.class,
- notes = NON_GUARANTEED_ENDPOINT
- )
- public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
- // only consider user specific access over https
- if (!httpServletRequest.isSecure()) {
- throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
- }
-
- // ensure oidc is enabled
- if (!oidcService.isOidcEnabled()) {
- logger.debug(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
- return Response.status(Response.Status.CONFLICT).entity(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG).build();
- }
-
- final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
- if (oidcRequestIdentifier == null) {
- 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();
- }
-
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // get the jwt
- final String jwt = oidcService.getJwt(oidcRequestIdentifier);
- if (jwt == null) {
- throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue.");
- }
-
- return generateTokenResponse(generateOkResponse(jwt), jwt);
- }
-
- @GET
- @Consumes(MediaType.WILDCARD)
- @Produces(MediaType.WILDCARD)
- @Path("oidc/logout")
- @ApiOperation(
- value = "Performs a logout in the OpenId Provider.",
- notes = NON_GUARANTEED_ENDPOINT
- )
- public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
- if (!httpServletRequest.isSecure()) {
- throw new IllegalStateException(AUTHENTICATION_NOT_ENABLED_MSG);
- }
-
- if (!oidcService.isOidcEnabled()) {
- throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
- }
-
- final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
- removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
- logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
-
- // Get the oidc discovery url
- String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
-
- // Determine the logout method
- String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
-
- switch (logoutMethod) {
- case REVOKE_ACCESS_TOKEN_LOGOUT:
- case ID_TOKEN_LOGOUT:
- // Make a request to the IdP
- URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcLogoutCallback());
- httpServletResponse.sendRedirect(authorizationURI.toString());
- break;
- case STANDARD_LOGOUT:
- default:
- // Get the OIDC end session endpoint
- URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
- String postLogoutRedirectUri = generateResourceUri( "..", "nifi", "logout-complete");
-
- if (endSessionEndpoint == null) {
- httpServletResponse.sendRedirect(postLogoutRedirectUri);
- } else {
- URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
- .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
- .build();
- httpServletResponse.sendRedirect(logoutUri.toString());
- }
- break;
- }
- }
-
- @GET
- @Consumes(MediaType.WILDCARD)
- @Produces(MediaType.WILDCARD)
- @Path("oidc/logoutCallback")
- @ApiOperation(
- value = "Redirect/callback URI for processing the result of the OpenId Connect logout sequence.",
- notes = NON_GUARANTEED_ENDPOINT
- )
- public void oidcLogoutCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
- // only consider user specific access over https
- if (!httpServletRequest.isSecure()) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG);
- return;
- }
-
- // ensure oidc is enabled
- if (!oidcService.isOidcEnabled()) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
- return;
- }
-
- final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
- if (oidcRequestIdentifier == null) {
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was " +
- "not found in the request. Unable to continue.");
- return;
- }
-
- final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse;
- try {
- oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
- } catch (final ParseException e) {
- logger.error("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue " +
- "logout process: " + e.getMessage(), e);
-
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // forward to the error page
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Unable to parse the redirect URI " +
- "from the OpenId Connect Provider. Unable to continue logout process.");
- return;
- }
-
- if (oidcResponse.indicatesSuccess()) {
- final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
-
- // confirm state
- final State state = successfulOidcResponse.getState();
- if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
- logger.error("The state value returned by the OpenId Connect Provider does not match the stored " +
- "state. Unable to continue login process.");
-
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // forward to the error page
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Purposed state does not match " +
- "the stored state. Unable to continue login process.");
- return;
- }
-
- // Get the oidc discovery url
- String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
-
- // Determine which logout method to use
- String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
-
- // Get the authorization code and grant
- final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
- final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcLogoutCallback()));
-
- switch (logoutMethod) {
- case REVOKE_ACCESS_TOKEN_LOGOUT:
- // Use the Revocation endpoint + access token
- final String accessToken;
- try {
- // Return the access token
- accessToken = oidcService.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
- } catch (final Exception e) {
- logger.error("Unable to exchange authorization for the Access token: " + e.getMessage(), e);
-
- // Remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // Forward to the error page
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
- return;
- }
-
- // Build the revoke URI and send the POST request
- URI revokeEndpoint = getRevokeEndpoint();
-
- if (revokeEndpoint != null) {
- try {
- // Logout with the revoke endpoint
- revokeEndpointRequest(httpServletResponse, accessToken, revokeEndpoint);
-
- } catch (final IOException e) {
- logger.error("There was an error logging out of the OpenId Connect Provider: "
- + e.getMessage(), e);
-
- // Remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // Forward to the error page
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse,
- "There was an error logging out of the OpenId Connect Provider: "
- + e.getMessage());
- }
- }
- break;
- case ID_TOKEN_LOGOUT:
- // Use the end session endpoint + ID Token
- final String idToken;
- try {
- // Return the ID Token
- idToken = oidcService.exchangeAuthorizationCodeForIdToken(authorizationGrant);
- } catch (final Exception e) {
- logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
-
- // Remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // Forward to the error page
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
- return;
- }
-
- // Get the OIDC end session endpoint
- URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
- String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
-
- if (endSessionEndpoint == null) {
- logger.debug("Unable to log out of the OpenId Connect Provider. The end session endpoint is: null." +
- " Redirecting to the logout page.");
- httpServletResponse.sendRedirect(postLogoutRedirectUri);
- } else {
- URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
- .queryParam("id_token_hint", idToken)
- .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
- .build();
- httpServletResponse.sendRedirect(logoutUri.toString());
- }
- break;
- }
- } else {
- // remove the oidc request cookie
- removeOidcRequestCookie(httpServletResponse);
-
- // report the unsuccessful logout
- final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
- forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful logout attempt: "
- + errorOidcResponse.getErrorObject().getDescription());
- }
- }
-
- @GET
- @Consumes(MediaType.WILDCARD)
@Produces(MediaType.WILDCARD)
@Path("knox/request")
@ApiOperation(
@@ -1553,7 +629,7 @@ public class AccessResource extends ApplicationResource {
httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri());
}
- private LogoutRequest completeLogoutRequest(final HttpServletResponse httpServletResponse) {
+ LogoutRequest completeLogoutRequest(final HttpServletResponse httpServletResponse) {
LogoutRequest logoutRequest = null;
// check if a logout request identifier is present and if so complete the request
@@ -1575,7 +651,7 @@ public class AccessResource extends ApplicationResource {
return logoutRequest;
}
- private long validateTokenExpiration(long proposedTokenExpiration, String identity) {
+ long validateTokenExpiration(long proposedTokenExpiration, String identity) {
final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
@@ -1592,162 +668,15 @@ public class AccessResource extends ApplicationResource {
return proposedTokenExpiration;
}
- private String getOidcCallback() {
- return generateResourceUri("access", "oidc", "callback");
- }
-
- private String getOidcLogoutCallback() {
- return generateResourceUri("access", "oidc", "logoutCallback");
- }
-
- private URI getRevokeEndpoint() {
- return oidcService.getRevocationEndpoint();
- }
-
- private String getNiFiUri() {
- final String nifiApiUrl = generateResourceUri();
- final String baseUrl = StringUtils.substringBeforeLast(nifiApiUrl, "/nifi-api");
- // Note: if the URL does not end with a / then Jetty will end up doing a redirect which can cause
- // a problem when being behind a proxy b/c Jetty's redirect doesn't consider proxy headers
- return baseUrl + "/nifi/";
- }
-
- private String getNiFiLogoutCompleteUri() {
+ String getNiFiLogoutCompleteUri() {
return getNiFiUri() + "logout-complete";
}
- private void removeOidcRequestCookie(final HttpServletResponse httpServletResponse) {
- removeCookie(httpServletResponse, OIDC_REQUEST_IDENTIFIER);
- }
-
- private void removeSamlRequestCookie(final HttpServletResponse httpServletResponse) {
- removeCookie(httpServletResponse, SAML_REQUEST_IDENTIFIER);
- }
-
- private void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) {
+ void removeLogoutRequestCookie(final HttpServletResponse httpServletResponse) {
removeCookie(httpServletResponse, LOGOUT_REQUEST_IDENTIFIER);
}
- private void removeCookie(final HttpServletResponse httpServletResponse, final String cookieName) {
- final Cookie cookie = new Cookie(cookieName, null);
- cookie.setPath("/");
- cookie.setHttpOnly(true);
- cookie.setMaxAge(0);
- cookie.setSecure(true);
- httpServletResponse.addCookie(cookie);
- }
-
- private void forwardToLoginMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception {
- forwardToMessagePage(httpServletRequest, httpServletResponse, LOGIN_ERROR_TITLE, message);
- }
-
- private void forwardToLogoutMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception {
- forwardToMessagePage(httpServletRequest, httpServletResponse, LOGOUT_ERROR_TITLE, message);
- }
-
- private void forwardToMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse,
- final String title, final String message) throws Exception {
- httpServletRequest.setAttribute("title", title);
- httpServletRequest.setAttribute("messages", message);
-
- final ServletContext uiContext = httpServletRequest.getServletContext().getContext("/nifi");
- uiContext.getRequestDispatcher("/WEB-INF/pages/message-page.jsp").forward(httpServletRequest, httpServletResponse);
- }
-
- private String determineLogoutMethod(String oidcDiscoveryUrl) {
- Matcher accessTokenMatcher = REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT.matcher(oidcDiscoveryUrl);
- Matcher idTokenMatcher = ID_TOKEN_LOGOUT_FORMAT.matcher(oidcDiscoveryUrl);
-
- if (accessTokenMatcher.find()) {
- return REVOKE_ACCESS_TOKEN_LOGOUT;
- } else if (idTokenMatcher.find()) {
- return ID_TOKEN_LOGOUT;
- } else {
- return STANDARD_LOGOUT;
- }
- }
-
- /**
- * Generates the request Authorization URI for the OpenID Connect Provider. Returns an authorization
- * URI using the provided callback URI.
- *
- * @param httpServletResponse the servlet response
- * @param callback the OIDC callback URI
- * @return the authorization URI
- */
- private URI oidcRequestAuthorizationCode(@Context HttpServletResponse httpServletResponse, String callback) {
-
- final String oidcRequestIdentifier = UUID.randomUUID().toString();
-
- // generate a cookie to associate this login sequence
- final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
- cookie.setPath("/");
- cookie.setHttpOnly(true);
- cookie.setMaxAge(60);
- cookie.setSecure(true);
- httpServletResponse.addCookie(cookie);
-
- // get the state for this request
- final State state = oidcService.createState(oidcRequestIdentifier);
-
- // build the authorization uri
- final URI authorizationUri = UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
- .queryParam("client_id", oidcService.getClientId())
- .queryParam("response_type", "code")
- .queryParam("scope", oidcService.getScope().toString())
- .queryParam("state", state.getValue())
- .queryParam("redirect_uri", callback)
- .build();
-
- // return Authorization URI
- return authorizationUri;
- }
-
- /**
- * Sends a POST request to the revoke endpoint to log out of the ID Provider.
- *
- * @param httpServletResponse the servlet response
- * @param accessToken the OpenID Connect Provider access token
- * @param revokeEndpoint the name of the cookie
- * @throws IOException exceptional case for communication error with the OpenId Connect Provider
- */
-
- private void revokeEndpointRequest(@Context HttpServletResponse httpServletResponse, String accessToken, URI revokeEndpoint) throws IOException {
-
- RequestConfig config = RequestConfig.custom()
- .setConnectTimeout(msTimeout)
- .setConnectionRequestTimeout(msTimeout)
- .setSocketTimeout(msTimeout)
- .build();
-
- CloseableHttpClient httpClient = HttpClientBuilder
- .create()
- .setDefaultRequestConfig(config)
- .build();
- HttpPost httpPost = new HttpPost(revokeEndpoint);
-
- List<NameValuePair> params = new ArrayList<>();
- // Append a query param with the access token
- params.add(new BasicNameValuePair("token", accessToken));
- httpPost.setEntity(new UrlEncodedFormEntity(params));
-
- try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
- httpClient.close();
-
- if (response.getStatusLine().getStatusCode() == HTTPResponse.SC_OK) {
- // Redirect to logout page
- logger.debug("You are logged out of the OpenId Connect Provider.");
- String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
- httpServletResponse.sendRedirect(postLogoutRedirectUri);
- } else {
- logger.error("There was an error logging out of the OpenId Connect Provider. " +
- "Response status: " + response.getStatusLine().getStatusCode());
- }
- }
- }
-
// setters
-
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
this.loginIdentityProvider = loginIdentityProvider;
}
@@ -1780,42 +709,16 @@ public class AccessResource extends ApplicationResource {
this.otpService = otpService;
}
- public void setOidcService(OidcService oidcService) {
- this.oidcService = oidcService;
- }
-
public void setKnoxService(KnoxService knoxService) {
this.knoxService = knoxService;
}
- 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 setLogoutRequestManager(LogoutRequestManager logoutRequestManager) {
- this.logoutRequestManager = logoutRequestManager;
- }
-
private void logOutUser(HttpServletRequest httpServletRequest) {
final String jwt = new NiFiBearerTokenResolver().resolve(httpServletRequest);
jwtService.logOut(jwt);
}
- private Response generateTokenResponse(ResponseBuilder builder, String token) {
- // currently there is no way to use javax.servlet-api to set SameSite=Strict, so we do this using Jetty
- HttpCookie jwtCookie = new HttpCookie(NiFiBearerTokenResolver.JWT_COOKIE_NAME, token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
- return builder.header(HttpHeader.SET_COOKIE.asString(), jwtCookie.getRFC6265SetCookie()).build();
+ public void setLogoutRequestManager(LogoutRequestManager logoutRequestManager) {
+ this.logoutRequestManager = logoutRequestManager;
}
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index b24b9a3..3f482a0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -55,11 +55,16 @@ import org.apache.nifi.web.api.entity.ComponentEntity;
import org.apache.nifi.web.api.entity.Entity;
import org.apache.nifi.web.api.entity.TransactionResultEntity;
import org.apache.nifi.web.security.ProxiedEntitiesUtils;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
import org.apache.nifi.web.security.util.CacheKey;
import org.apache.nifi.web.util.WebUtils;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.CacheControl;
@@ -92,12 +97,12 @@ import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_NAME;
import static org.apache.nifi.remote.protocol.http.HttpHeaders.LOCATION_URI_INTENT_VALUE;
-import static org.apache.nifi.web.util.WebUtils.PROXY_SCHEME_HTTP_HEADER;
-import static org.apache.nifi.web.util.WebUtils.PROXY_HOST_HTTP_HEADER;
-import static org.apache.nifi.web.util.WebUtils.PROXY_PORT_HTTP_HEADER;
-import static org.apache.nifi.web.util.WebUtils.FORWARDED_PROTO_HTTP_HEADER;
import static org.apache.nifi.web.util.WebUtils.FORWARDED_HOST_HTTP_HEADER;
import static org.apache.nifi.web.util.WebUtils.FORWARDED_PORT_HTTP_HEADER;
+import static org.apache.nifi.web.util.WebUtils.FORWARDED_PROTO_HTTP_HEADER;
+import static org.apache.nifi.web.util.WebUtils.PROXY_HOST_HTTP_HEADER;
+import static org.apache.nifi.web.util.WebUtils.PROXY_PORT_HTTP_HEADER;
+import static org.apache.nifi.web.util.WebUtils.PROXY_SCHEME_HTTP_HEADER;
/**
* Base class for controllers.
@@ -107,6 +112,9 @@ public abstract class ApplicationResource {
public static final String VERSION = "version";
public static final String CLIENT_ID = "clientId";
public static final String DISCONNECTED_NODE_ACKNOWLEDGED = "disconnectedNodeAcknowledged";
+ static final String LOGIN_ERROR_TITLE = "Unable to continue login sequence";
+ static final String LOGOUT_ERROR_TITLE = "Unable to continue logout sequence";
+ private static final int VALID_FOR_SESSION_ONLY = -1;
protected static final String NON_GUARANTEED_ENDPOINT = "Note: This endpoint is subject to change as NiFi and it's REST API evolve.";
@@ -128,6 +136,23 @@ public abstract class ApplicationResource {
private static final int MAX_CACHE_SOFT_LIMIT = 500;
private final Cache<CacheKey, Request<? extends Entity>> twoPhaseCommitCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
+ protected void forwardToLoginMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, LOGIN_ERROR_TITLE, message);
+ }
+
+ protected void forwardToLogoutMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, LOGOUT_ERROR_TITLE, message);
+ }
+
+ protected void forwardToMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse,
+ final String title, final String message) throws Exception {
+ httpServletRequest.setAttribute("title", title);
+ httpServletRequest.setAttribute("messages", message);
+
+ final ServletContext uiContext = httpServletRequest.getServletContext().getContext("/nifi");
+ uiContext.getRequestDispatcher("/WEB-INF/pages/message-page.jsp").forward(httpServletRequest, httpServletResponse);
+ }
+
/**
* Generate a resource uri based off of the specified parameters.
*
@@ -1257,4 +1282,27 @@ public abstract class ApplicationResource {
}
}
+
+ protected Response generateTokenResponse(ResponseBuilder builder, String token) {
+ // currently there is no way to use javax.servlet-api to set SameSite=Strict, so we do this using Jetty
+ HttpCookie jwtCookie = new HttpCookie(NiFiBearerTokenResolver.JWT_COOKIE_NAME, token, null, "/", VALID_FOR_SESSION_ONLY, true, true, null, 0, HttpCookie.SameSite.STRICT);
+ return builder.header(HttpHeader.SET_COOKIE.asString(), jwtCookie.getRFC6265SetCookie()).build();
+ }
+
+ protected void removeCookie(final HttpServletResponse httpServletResponse, final String cookieName) {
+ final Cookie cookie = new Cookie(cookieName, null);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(0);
+ cookie.setSecure(true);
+ httpServletResponse.addCookie(cookie);
+ }
+
+ protected String getNiFiUri() {
+ final String nifiApiUrl = generateResourceUri();
+ final String baseUrl = StringUtils.substringBeforeLast(nifiApiUrl, "/nifi-api");
+ // Note: if the URL does not end with a / then Jetty will end up doing a redirect which can cause
+ // a problem when being behind a proxy b/c Jetty's redirect doesn't consider proxy headers
+ return baseUrl + "/nifi/";
+ }
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java
new file mode 100644
index 0000000..df6a1cb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java
@@ -0,0 +1,573 @@
+/*
+ * 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 com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
+import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
+import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
+import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.security.jwt.JwtService;
+import org.apache.nifi.web.security.jwt.NiFiBearerTokenResolver;
+import org.apache.nifi.web.security.oidc.OIDCEndpoints;
+import org.apache.nifi.web.security.oidc.OidcService;
+import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.util.WebUtils;
+
+import javax.annotation.PreDestroy;
+import javax.servlet.http.Cookie;
+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.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+@Path(OIDCEndpoints.OIDC_ACCESS_ROOT)
+@Api(
+ value = OIDCEndpoints.OIDC_ACCESS_ROOT,
+ description = "Endpoints for obtaining an access token or checking access status."
+)
+public class OIDCAccessResource extends AccessResource {
+
+ private static final Logger logger = LoggerFactory.getLogger(OIDCAccessResource.class);
+ private static final String OIDC_REQUEST_IDENTIFIER = "oidc-request-identifier";
+ private static final String OIDC_ID_TOKEN_AUTHN_ERROR = "Unable to exchange authorization for ID token: ";
+ private static final String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG = "OpenId Connect support is not configured";
+ private static final String REVOKE_ACCESS_TOKEN_LOGOUT = "oidc_access_token_logout";
+ private static final String ID_TOKEN_LOGOUT = "oidc_id_token_logout";
+ private static final String STANDARD_LOGOUT = "oidc_standard_logout";
+ private static final Pattern REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT = Pattern.compile("(\\.google\\.com)");
+ private static final Pattern ID_TOKEN_LOGOUT_FORMAT = Pattern.compile("(\\.okta)");
+ private static final int msTimeout = 30_000;
+ private static final boolean LOGGING_IN = true;
+
+ private OidcService oidcService;
+ private JwtService jwtService;
+ private CloseableHttpClient httpClient;
+
+ public OIDCAccessResource() {
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(msTimeout)
+ .setConnectionRequestTimeout(msTimeout)
+ .setSocketTimeout(msTimeout)
+ .build();
+
+ httpClient = HttpClientBuilder
+ .create()
+ .setDefaultRequestConfig(config)
+ .build();
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.WILDCARD)
+ @Path(OIDCEndpoints.LOGIN_REQUEST_RELATIVE)
+ @ApiOperation(
+ value = "Initiates a request to authenticate through the configured OpenId Connect provider.",
+ notes = NON_GUARANTEED_ENDPOINT
+ )
+ public void oidcRequest(@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 oidc is enabled
+ if (!oidcService.isOidcEnabled()) {
+ forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
+ return;
+ }
+
+ // generate the authorization uri
+ URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcCallback());
+
+ // generate the response
+ httpServletResponse.sendRedirect(authorizationURI.toString());
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.WILDCARD)
+ @Path(OIDCEndpoints.LOGIN_CALLBACK_RELATIVE)
+ @ApiOperation(
+ value = "Redirect/callback URI for processing the result of the OpenId Connect login sequence.",
+ notes = NON_GUARANTEED_ENDPOINT
+ )
+ public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
+ final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, LOGGING_IN);
+
+ final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
+
+ if (oidcResponse != null && oidcResponse.indicatesSuccess()) {
+ final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
+
+ checkOidcState(httpServletResponse, oidcRequestIdentifier, successfulOidcResponse, LOGGING_IN);
+
+ try {
+ // exchange authorization code for id token
+ final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
+ final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcCallback()));
+
+ // get the oidc token
+ LoginAuthenticationToken oidcToken = oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(authorizationGrant);
+
+ // exchange the oidc token for the NiFi token
+ String nifiJwt = jwtService.generateSignedToken(oidcToken);
+
+ // store the NiFi token
+ oidcService.storeJwt(oidcRequestIdentifier, nifiJwt);
+ } catch (final Exception e) {
+ logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
+
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // forward to the error page
+ forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
+ return;
+ }
+
+ // redirect to the name page
+ httpServletResponse.sendRedirect(getNiFiUri());
+ } else {
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // report the unsuccessful login
+ final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
+ forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt: "
+ + errorOidcResponse.getErrorObject().getDescription());
+ }
+ }
+
+ @POST
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @Path(OIDCEndpoints.TOKEN_EXCHANGE_RELATIVE)
+ @ApiOperation(
+ value = "Retrieves a JWT following a successful login sequence using the configured OpenId Connect provider.",
+ response = String.class,
+ notes = NON_GUARANTEED_ENDPOINT
+ )
+ public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
+ // only consider user specific access over https
+ if (!httpServletRequest.isSecure()) {
+ throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
+ }
+
+ // ensure oidc is enabled
+ if (!oidcService.isOidcEnabled()) {
+ logger.debug(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
+ return Response.status(Response.Status.CONFLICT).entity(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG).build();
+ }
+
+ final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
+ if (oidcRequestIdentifier == null) {
+ 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();
+ }
+
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // get the jwt
+ final String jwt = oidcService.getJwt(oidcRequestIdentifier);
+ if (jwt == null) {
+ throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue.");
+ }
+
+ return generateTokenResponse(generateOkResponse(jwt), jwt);
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.WILDCARD)
+ @Path(OIDCEndpoints.LOGOUT_REQUEST_RELATIVE)
+ @ApiOperation(
+ value = "Performs a logout in the OpenId Provider.",
+ notes = NON_GUARANTEED_ENDPOINT
+ )
+ public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
+ if (!httpServletRequest.isSecure()) {
+ throw new IllegalStateException(AUTHENTICATION_NOT_ENABLED_MSG);
+ }
+
+ if (!oidcService.isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
+ }
+
+ final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
+ removeCookie(httpServletResponse, NiFiBearerTokenResolver.JWT_COOKIE_NAME);
+ logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity);
+
+ // Get the oidc discovery url
+ String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
+
+ // Determine the logout method
+ String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
+
+ switch (logoutMethod) {
+ case REVOKE_ACCESS_TOKEN_LOGOUT:
+ case ID_TOKEN_LOGOUT:
+ // Make a request to the IdP
+ URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcLogoutCallback());
+ httpServletResponse.sendRedirect(authorizationURI.toString());
+ break;
+ case STANDARD_LOGOUT:
+ default:
+ // Get the OIDC end session endpoint
+ URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
+ String postLogoutRedirectUri = generateResourceUri( "..", "nifi", "logout-complete");
+
+ if (endSessionEndpoint == null) {
+ httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ } else {
+ URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
+ .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
+ .build();
+ httpServletResponse.sendRedirect(logoutUri.toString());
+ }
+ break;
+ }
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.WILDCARD)
+ @Path(OIDCEndpoints.LOGOUT_CALLBACK_RELATIVE)
+ @ApiOperation(
+ value = "Redirect/callback URI for processing the result of the OpenId Connect logout sequence.",
+ notes = NON_GUARANTEED_ENDPOINT
+ )
+ public void oidcLogoutCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
+ final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, !LOGGING_IN);
+
+ final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
+
+ if (oidcResponse != null && oidcResponse.indicatesSuccess()) {
+ final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
+
+ // confirm state
+ checkOidcState(httpServletResponse, oidcRequestIdentifier, successfulOidcResponse, false);
+
+ // Get the oidc discovery url
+ String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
+
+ // Determine which logout method to use
+ String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
+
+ // Get the authorization code and grant
+ final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
+ final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcLogoutCallback()));
+
+ switch (logoutMethod) {
+ case REVOKE_ACCESS_TOKEN_LOGOUT:
+ // Use the Revocation endpoint + access token
+ final String accessToken;
+ try {
+ // Return the access token
+ accessToken = oidcService.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
+ } catch (final Exception e) {
+ logger.error("Unable to exchange authorization for the Access token: " + e.getMessage(), e);
+
+ // Remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // Forward to the error page
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
+ return;
+ }
+
+ // Build the revoke URI and send the POST request
+ URI revokeEndpoint = getRevokeEndpoint();
+
+ if (revokeEndpoint != null) {
+ try {
+ // Logout with the revoke endpoint
+ revokeEndpointRequest(httpServletResponse, accessToken, revokeEndpoint);
+
+ } catch (final IOException e) {
+ logger.error("There was an error logging out of the OpenId Connect Provider: "
+ + e.getMessage(), e);
+
+ // Remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // Forward to the error page
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse,
+ "There was an error logging out of the OpenId Connect Provider: "
+ + e.getMessage());
+ }
+ }
+ break;
+ case ID_TOKEN_LOGOUT:
+ // Use the end session endpoint + ID Token
+ final String idToken;
+ try {
+ // Return the ID Token
+ idToken = oidcService.exchangeAuthorizationCodeForIdToken(authorizationGrant);
+ } catch (final Exception e) {
+ logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
+
+ // Remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // Forward to the error page
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
+ return;
+ }
+
+ // Get the OIDC end session endpoint
+ URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
+ String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
+
+ if (endSessionEndpoint == null) {
+ logger.debug("Unable to log out of the OpenId Connect Provider. The end session endpoint is: null." +
+ " Redirecting to the logout page.");
+ httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ } else {
+ URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
+ .queryParam("id_token_hint", idToken)
+ .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
+ .build();
+ httpServletResponse.sendRedirect(logoutUri.toString());
+ }
+ break;
+ }
+ } else {
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // report the unsuccessful logout
+ final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful logout attempt: "
+ + errorOidcResponse.getErrorObject().getDescription());
+ }
+ }
+
+ /**
+ * Generates the request Authorization URI for the OpenID Connect Provider. Returns an authorization
+ * URI using the provided callback URI.
+ *
+ * @param httpServletResponse the servlet response
+ * @param callback the OIDC callback URI
+ * @return the authorization URI
+ */
+ private URI oidcRequestAuthorizationCode(@Context HttpServletResponse httpServletResponse, String callback) {
+
+ final String oidcRequestIdentifier = UUID.randomUUID().toString();
+
+ // generate a cookie to associate this login sequence
+ final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(60);
+ cookie.setSecure(true);
+ httpServletResponse.addCookie(cookie);
+
+ // get the state for this request
+ final State state = oidcService.createState(oidcRequestIdentifier);
+
+ // build the authorization uri
+ final URI authorizationUri = UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
+ .queryParam("client_id", oidcService.getClientId())
+ .queryParam("response_type", "code")
+ .queryParam("scope", oidcService.getScope().toString())
+ .queryParam("state", state.getValue())
+ .queryParam("redirect_uri", callback)
+ .build();
+
+ // return Authorization URI
+ return authorizationUri;
+ }
+
+ private String determineLogoutMethod(String oidcDiscoveryUrl) {
+ Matcher accessTokenMatcher = REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT.matcher(oidcDiscoveryUrl);
+ Matcher idTokenMatcher = ID_TOKEN_LOGOUT_FORMAT.matcher(oidcDiscoveryUrl);
+
+ if (accessTokenMatcher.find()) {
+ return REVOKE_ACCESS_TOKEN_LOGOUT;
+ } else if (idTokenMatcher.find()) {
+ return ID_TOKEN_LOGOUT;
+ } else {
+ return STANDARD_LOGOUT;
+ }
+ }
+
+ /**
+ * Sends a POST request to the revoke endpoint to log out of the ID Provider.
+ *
+ * @param httpServletResponse the servlet response
+ * @param accessToken the OpenID Connect Provider access token
+ * @param revokeEndpoint the name of the cookie
+ * @throws IOException exceptional case for communication error with the OpenId Connect Provider
+ */
+ private void revokeEndpointRequest(@Context HttpServletResponse httpServletResponse, String accessToken, URI revokeEndpoint) throws IOException {
+
+ HttpPost httpPost = new HttpPost(revokeEndpoint);
+
+ List<NameValuePair> params = new ArrayList<>();
+ // Append a query param with the access token
+ params.add(new BasicNameValuePair("token", accessToken));
+ httpPost.setEntity(new UrlEncodedFormEntity(params));
+
+ try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+ if (response.getStatusLine().getStatusCode() == HTTPResponse.SC_OK) {
+ // Redirect to logout page
+ logger.debug("You are logged out of the OpenId Connect Provider.");
+ String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
+ httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ } else {
+ logger.error("There was an error logging out of the OpenId Connect Provider. " +
+ "Response status: " + response.getStatusLine().getStatusCode());
+ }
+ }
+ }
+
+ @PreDestroy
+ private final void closeClient() throws IOException {
+ httpClient.close();
+ }
+
+ private AuthenticationResponse parseOidcResponse(HttpServletRequest httpServletRequest, 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, AUTHENTICATION_NOT_ENABLED_MSG);
+ return null;
+ }
+
+ // ensure oidc is enabled
+ if (!oidcService.isOidcEnabled()) {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
+ return null;
+ }
+
+ final String oidcRequestIdentifier = WebUtils.getCookie(httpServletRequest, OIDC_REQUEST_IDENTIFIER).getValue();
+ if (oidcRequestIdentifier == null) {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle,"The request identifier was " +
+ "not found in the request. Unable to continue.");
+ return null;
+ }
+
+ final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse;
+ try {
+ oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
+ return oidcResponse;
+ } catch (final ParseException e) {
+ logger.error("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login/logout process.");
+
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle,"Unable to parse the redirect URI " +
+ "from the OpenId Connect Provider. Unable to continue login/logout process.");
+ return null;
+ }
+ }
+
+ private void checkOidcState(HttpServletResponse httpServletResponse, final String oidcRequestIdentifier, AuthenticationSuccessResponse successfulOidcResponse, boolean isLogin) throws Exception {
+ // confirm state
+ final State state = successfulOidcResponse.getState();
+ if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
+ logger.error("The state value returned by the OpenId Connect Provider does not match the stored " +
+ "state. Unable to continue login/logout process.");
+
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse, getForwardPageTitle(isLogin), "Purposed state does not match " +
+ "the stored state. Unable to continue login/logout process.");
+ return;
+ }
+ }
+
+ private String getForwardPageTitle(boolean isLogin) {
+ return isLogin ? ApplicationResource.LOGIN_ERROR_TITLE : ApplicationResource.LOGOUT_ERROR_TITLE;
+ }
+
+ private String getOidcCallback() {
+ return generateResourceUri("access", "oidc", "callback");
+ }
+
+ private String getOidcLogoutCallback() {
+ return generateResourceUri("access", "oidc", "logoutCallback");
+ }
+
+ private URI getRevokeEndpoint() {
+ return oidcService.getRevocationEndpoint();
+ }
+
+ private void removeOidcRequestCookie(final HttpServletResponse httpServletResponse) {
+ removeCookie(httpServletResponse, OIDC_REQUEST_IDENTIFIER);
+ }
+
+ public void setOidcService(OidcService oidcService) {
+ this.oidcService = oidcService;
+ }
+
+ public void setJwtService(JwtService jwtService) {
+ this.jwtService = jwtService;
+ }
+
+ public void setProperties(final NiFiProperties properties) {
+ this.properties = properties;
+ }
+
+ protected NiFiProperties getProperties() {
+ return properties;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java
new file mode 100644
index 0000000..8d2439d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/SAMLAccessResource.java
@@ -0,0 +1,540 @@
+/*
+ * 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.security.logout.LogoutRequest;
+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.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.saml.SAMLCredential;
+import org.springframework.web.util.WebUtils;
+
+import javax.servlet.http.Cookie;
+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.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 AccessResource {
+
+ private static final Logger logger = LoggerFactory.getLogger(SAMLAccessResource.class);
+ private static final String SAML_REQUEST_IDENTIFIER = "saml-request-identifier";
+ 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;
+
+ @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.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
+ 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 {
+
+ 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 String samlRequestIdentifier = WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
+ 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 = samlService.getUserIdentity(samlCredential);
+ 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
+ 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) 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.debug(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED);
+ return Response.status(Response.Status.CONFLICT).entity(SAMLService.SAML_SUPPORT_IS_NOT_CONFIGURED).build();
+ }
+
+ logger.info("Attempting to exchange SAML login request for a NiFi JWT...");
+
+ // ensure saml service provider is initialized
+ initializeSamlServiceProvider();
+
+ // ensure the request has the cookie with the request identifier
+ final String samlRequestIdentifier = WebUtils.getCookie(httpServletRequest, SAML_REQUEST_IDENTIFIER).getValue();
+ if (samlRequestIdentifier == null) {
+ 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();
+ }
+
+ // remove the saml request cookie
+ removeSamlRequestCookie(httpServletResponse);
+
+ // get the jwt
+ 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.");
+ }
+
+ // generate the response
+ logger.info("SAML login exchange complete");
+ 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 String logoutRequestIdentifier = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
+ if (StringUtils.isBlank(logoutRequestIdentifier)) {
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
+ return;
+ }
+
+ // ensure there is a logout request in progress for the given identifier
+ 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 (Exception e) {
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
+ return;
+ }
+ }
+
+ @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 String logoutRequestIdentifier = WebUtils.getCookie(httpServletRequest, LOGOUT_REQUEST_IDENTIFIER).getValue();
+ if (StringUtils.isBlank(logoutRequestIdentifier)) {
+ forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, LOGOUT_REQUEST_IDENTIFIER_NOT_FOUND);
+ return;
+ }
+
+ // ensure there is a logout request in progress for the given identifier
+ 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 LogoutRequest completedLogoutRequest = completeLogoutRequest(httpServletResponse);
+
+ // if a logout request was completed, then delete the stored SAMLCredential for that user
+ if (completedLogoutRequest != null) {
+ final String userIdentity = completedLogoutRequest.getMappedUserIdentity();
+
+ logger.info("Removing cached SAML information for " + userIdentity);
+ samlCredentialStore.delete(userIdentity);
+
+ logger.info("Removing cached SAML Groups for " + userIdentity);
+ idpUserGroupService.deleteUserGroups(userIdentity);
+ }
+
+ // redirect to logout landing page
+ httpServletResponse.sendRedirect(getNiFiLogoutCompleteUri());
+ }
+
+ private void initializeSamlServiceProvider() throws MetadataProviderException {
+ 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) {
+ removeCookie(httpServletResponse, 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, 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;
+ }
+
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index 49e373a..7f678b5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -575,12 +575,9 @@
<property name="flowController" ref="flowController" />
</bean>
<bean id="accessResource" class="org.apache.nifi.web.api.AccessResource" scope="singleton">
+ <property name="logoutRequestManager" ref="logoutRequestManager" />
<property name="loginIdentityProvider" ref="loginIdentityProvider"/>
- <property name="oidcService" ref="oidcService"/>
<property name="knoxService" ref="knoxService"/>
- <property name="samlService" ref="samlService" />
- <property name="samlStateManager" ref="samlStateManager"/>
- <property name="samlCredentialStore" ref="samlCredentialStore"/>
<property name="x509AuthenticationProvider" ref="x509AuthenticationProvider"/>
<property name="certificateExtractor" ref="certificateExtractor"/>
<property name="principalExtractor" ref="principalExtractor"/>
@@ -592,8 +589,19 @@
<property name="clusterCoordinator" ref="clusterCoordinator"/>
<property name="requestReplicator" ref="requestReplicator" />
<property name="flowController" ref="flowController" />
- <property name="idpUserGroupService" ref="idpUserGroupService" />
+ </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="jwtService" ref="jwtService"/>
+ <property name="oidcService" ref="oidcService"/>
+ <property name="properties" ref="nifiProperties"/>
</bean>
<bean id="accessPolicyResource" class="org.apache.nifi.web.api.AccessPolicyResource" scope="singleton">
<constructor-arg ref="serviceFacade"/>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OIDCEndpoints.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OIDCEndpoints.java
new file mode 100644
index 0000000..765b71e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OIDCEndpoints.java
@@ -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.oidc;
+
+public interface OIDCEndpoints {
+
+ String OIDC_ACCESS_ROOT = "/access/oidc";
+
+ String LOGIN_REQUEST_RELATIVE = "/request";
+ String LOGIN_REQUEST = OIDC_ACCESS_ROOT + LOGIN_REQUEST_RELATIVE;
+
+ String LOGIN_CALLBACK_RELATIVE = "/callback";
+ String LOGIN_CALLBACK = OIDC_ACCESS_ROOT + LOGIN_CALLBACK_RELATIVE;
+
+ String TOKEN_EXCHANGE_RELATIVE = "/exchange";
+ String TOKEN_EXCHANGE = OIDC_ACCESS_ROOT + TOKEN_EXCHANGE_RELATIVE;
+
+ String LOGOUT_REQUEST_RELATIVE = "/logout";
+ String LOGOUT_REQUEST = OIDC_ACCESS_ROOT + LOGOUT_REQUEST_RELATIVE;
+
+ String LOGOUT_CALLBACK_RELATIVE = "/logoutCallback";
+ String LOGOUT_CALLBACK = OIDC_ACCESS_ROOT + LOGOUT_CALLBACK_RELATIVE;
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/SAMLEndpoints.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/SAMLEndpoints.java
index 9ce860c..0d62dd5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/SAMLEndpoints.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/SAMLEndpoints.java
@@ -18,25 +18,27 @@ package org.apache.nifi.web.security.saml;
public interface SAMLEndpoints {
- String SERVICE_PROVIDER_METADATA_RELATIVE = "/saml/metadata";
- String SERVICE_PROVIDER_METADATA = "/access" + SERVICE_PROVIDER_METADATA_RELATIVE;
+ String SAML_ACCESS_ROOT = "/access/saml";
- String LOGIN_REQUEST_RELATIVE = "/saml/login/request";
- String LOGIN_REQUEST = "/access" + LOGIN_REQUEST_RELATIVE;
+ String SERVICE_PROVIDER_METADATA_RELATIVE = "/metadata";
+ String SERVICE_PROVIDER_METADATA = SAML_ACCESS_ROOT + SERVICE_PROVIDER_METADATA_RELATIVE;
- String LOGIN_CONSUMER_RELATIVE = "/saml/login/consumer";
- String LOGIN_CONSUMER = "/access" + LOGIN_CONSUMER_RELATIVE;
+ String LOGIN_REQUEST_RELATIVE = "login/request";
+ String LOGIN_REQUEST = SAML_ACCESS_ROOT + LOGIN_REQUEST_RELATIVE;
- String LOGIN_EXCHANGE_RELATIVE = "/saml/login/exchange";
- String LOGIN_EXCHANGE = "/access" + LOGIN_EXCHANGE_RELATIVE;
+ String LOGIN_CONSUMER_RELATIVE = "/login/consumer";
+ String LOGIN_CONSUMER = SAML_ACCESS_ROOT + LOGIN_CONSUMER_RELATIVE;
- String LOCAL_LOGOUT_RELATIVE = "/saml/local-logout";
- String LOCAL_LOGOUT = "/access" + LOCAL_LOGOUT_RELATIVE;
+ String LOGIN_EXCHANGE_RELATIVE = "/login/exchange";
+ String LOGIN_EXCHANGE = SAML_ACCESS_ROOT + LOGIN_EXCHANGE_RELATIVE;
- String SINGLE_LOGOUT_REQUEST_RELATIVE = "/saml/single-logout/request";
- String SINGLE_LOGOUT_REQUEST = "/access" + SINGLE_LOGOUT_REQUEST_RELATIVE;
+ String LOCAL_LOGOUT_RELATIVE = "/local-logout";
+ String LOCAL_LOGOUT = SAML_ACCESS_ROOT + LOCAL_LOGOUT_RELATIVE;
- String SINGLE_LOGOUT_CONSUMER_RELATIVE = "/saml/single-logout/consumer";
- String SINGLE_LOGOUT_CONSUMER = "/access" + SINGLE_LOGOUT_CONSUMER_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;
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java
index 6515abe..0df84ea 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/saml/impl/StandardSAMLStateManager.java
@@ -34,7 +34,7 @@ public class StandardSAMLStateManager implements SAMLStateManager {
private static Logger LOGGER = LoggerFactory.getLogger(StandardSAMLStateManager.class);
- private final JwtService jwtService;
+ private JwtService jwtService;
// identifier from cookie -> state value
private final Cache<CacheKey, String> stateLookupForPendingRequests;
@@ -140,4 +140,7 @@ public class StandardSAMLStateManager implements SAMLStateManager {
}
}
+ public void setJwtService(JwtService jwtService) {
+ this.jwtService = jwtService;
+ }
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
index dc205a2..0405dc5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
@@ -119,7 +119,7 @@
</bean>
<!-- logout -->
- <bean id="logoutRequestManager" class="org.apache.nifi.web.security.logout.LogoutRequestManager"/>
+ <bean id="logoutRequestManager" class="org.apache.nifi.web.security.logout.LogoutRequestManager" scope="singleton"/>
<!-- anonymous -->
<bean id="anonymousAuthenticationProvider" class="org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider">