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