NIFI-4210:
- Introducing support for OpenId Connect.
- Updating REST API and UI to support the authorization code flow.
- Adding/fixing documentation.
- Implementing time constant equality checks where appropriate.
- Corrected error handling during startup and throughout the OIDC login 
sequence.
- Redacting the token values from the user log.
- Defaulting to RS256 when not preferred algorithm is specified.
- Marking the OIDC endpoints as non-guaranteed in to allow for minor 
adjustments if/when additional SSO techniques are introduced.

This closes #2047.

Signed-off-by: Andy LoPresto <alopre...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/nifi/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/528b8263
Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/528b8263
Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/528b8263

Branch: refs/heads/master
Commit: 528b82634f528468970eaa655c0085b1c9592b71
Parents: 505e930
Author: Matt Gilman <matt.c.gil...@gmail.com>
Authored: Tue Aug 1 10:46:45 2017 -0400
Committer: Andy LoPresto <alopre...@apache.org>
Committed: Thu Aug 10 11:15:35 2017 -0700

----------------------------------------------------------------------
 .../org/apache/nifi/util/NiFiProperties.java    |  85 ++++-
 .../src/main/asciidoc/administration-guide.adoc |  32 +-
 .../src/main/resources/META-INF/LICENSE         |  28 ++
 .../nifi-framework/nifi-resources/pom.xml       |   8 +
 .../src/main/resources/conf/nifi.properties     |   8 +
 .../org/apache/nifi/web/server/JettyServer.java |   4 +-
 .../nifi-web/nifi-web-api/pom.xml               |   5 +
 .../web/NiFiWebApiSecurityConfiguration.java    |   2 +-
 .../org/apache/nifi/web/api/AccessResource.java | 231 ++++++++++++
 .../nifi/web/api/ApplicationResource.java       |   4 +
 .../src/main/resources/nifi-web-api-context.xml |   1 +
 .../nifi-web/nifi-web-security/pom.xml          |   4 +
 .../jwt/JwtAuthenticationRequestToken.java      |   2 +-
 .../web/security/oidc/OidcIdentityProvider.java |  68 ++++
 .../nifi/web/security/oidc/OidcService.java     | 246 +++++++++++++
 .../oidc/StandardOidcIdentityProvider.java      | 364 +++++++++++++++++++
 .../otp/OtpAuthenticationRequestToken.java      |   2 +-
 .../resources/nifi-web-security-context.xml     |   9 +
 .../nifi/web/security/oidc/OidcServiceTest.java | 154 ++++++++
 .../org/apache/nifi/web/filter/LoginFilter.java |  55 +++
 .../main/webapp/WEB-INF/pages/message-page.jsp  |  21 +-
 .../nifi-web-ui/src/main/webapp/WEB-INF/web.xml |  34 +-
 .../src/main/webapp/js/nf/canvas/nf-canvas.js   |  29 +-
 pom.xml                                         |   7 +-
 24 files changed, 1373 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
----------------------------------------------------------------------
diff --git 
a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
 
b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index 2d14165..fd26a97 100644
--- 
a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ 
b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -148,6 +148,14 @@ public abstract class NiFiProperties {
     public static final String SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX = 
"nifi.security.identity.mapping.pattern.";
     public static final String SECURITY_IDENTITY_MAPPING_VALUE_PREFIX = 
"nifi.security.identity.mapping.value.";
 
+    // oidc
+    public static final String SECURITY_USER_OIDC_DISCOVERY_URL = 
"nifi.security.user.oidc.discovery.url";
+    public static final String SECURITY_USER_OIDC_CONNECT_TIMEOUT = 
"nifi.security.user.oidc.connect.timeout";
+    public static final String SECURITY_USER_OIDC_READ_TIMEOUT = 
"nifi.security.user.oidc.read.timeout";
+    public static final String SECURITY_USER_OIDC_CLIENT_ID = 
"nifi.security.user.oidc.client.id";
+    public static final String SECURITY_USER_OIDC_CLIENT_SECRET = 
"nifi.security.user.oidc.client.secret";
+    public static final String SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM = 
"nifi.security.user.oidc.preferred.jwsalgorithm";
+
     // web properties
     public static final String WEB_WAR_DIR = "nifi.web.war.directory";
     public static final String WEB_HTTP_PORT = "nifi.web.http.port";
@@ -244,6 +252,8 @@ public abstract class NiFiProperties {
     public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_ENABLED = 
"true";
     public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_TIME = 
"30 days";
     public static final String DEFAULT_FLOW_CONFIGURATION_ARCHIVE_MAX_STORAGE 
= "500 MB";
+    public static final String DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT = "5 
secs";
+    public static final String DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT = "5 
secs";
 
     // cluster common defaults
     public static final String DEFAULT_CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL = 
"5 sec";
@@ -804,17 +814,90 @@ public abstract class NiFiProperties {
     }
 
     /**
+     * Returns true if the login identity provider has been configured.
+     *
+     * @return true if the login identity provider has been configured
+     */
+    public boolean isLoginIdentityProviderEnabled() {
+        return 
!StringUtils.isBlank(getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER));
+    }
+
+    /**
+     * Returns whether an OpenId Connect (OIDC) URL is set.
+     *
+     * @return whether an OpenId Connection URL is set
+     */
+    public boolean isOidcEnabled() {
+        return !StringUtils.isBlank(getOidcDiscoveryUrl());
+    }
+
+    /**
+     * Returns the OpenId Connect (OIDC) URL. Null otherwise.
+     *
+     * @return OIDC discovery url
+     */
+    public String getOidcDiscoveryUrl() {
+        return getProperty(SECURITY_USER_OIDC_DISCOVERY_URL);
+    }
+
+    /**
+     * Returns the OpenId Connect connect timeout. Non null.
+     *
+     * @return OIDC connect timeout
+     */
+    public String getOidcConnectTimeout() {
+        return getProperty(SECURITY_USER_OIDC_CONNECT_TIMEOUT, 
DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
+    }
+
+    /**
+     * Returns the OpenId Connect read timeout. Non null.
+     *
+     * @return OIDC read timeout
+     */
+    public String getOidcReadTimeout() {
+        return getProperty(SECURITY_USER_OIDC_READ_TIMEOUT, 
DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
+    }
+
+    /**
+     * Returns the OpenId Connect client id.
+     *
+     * @return OIDC client id
+     */
+    public String getOidcClientId() {
+        return getProperty(SECURITY_USER_OIDC_CLIENT_ID);
+    }
+
+    /**
+     * Returns the OpenId Connect client secret.
+     *
+     * @return OIDC client secret
+     */
+    public String getOidcClientSecret() {
+        return getProperty(SECURITY_USER_OIDC_CLIENT_SECRET);
+    }
+
+    /**
+     * Returns the preferred json web signature algorithm. May be null/blank.
+     *
+     * @return OIDC preferred json web signature algorithm
+     */
+    public String getOidcPreferredJwsAlgorithm() {
+        return getProperty(SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM);
+    }
+
+    /**
      * Returns true if client certificates are required for REST API. 
Determined
      * if the following conditions are all true:
      * <p>
      * - login identity provider is not populated
      * - Kerberos service support is not enabled
+     * - openid connect is not enabled
      *
      * @return true if client certificates are required for access to the REST
      * API
      */
     public boolean isClientAuthRequiredForRestApi() {
-        return 
StringUtils.isBlank(getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER))
 && !isKerberosSpnegoSupportEnabled();
+        return !isLoginIdentityProviderEnabled() && 
!isKerberosSpnegoSupportEnabled() && !isOidcEnabled();
     }
 
     public InetSocketAddress getNodeApiAddress() {

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-docs/src/main/asciidoc/administration-guide.adoc
----------------------------------------------------------------------
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc 
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 81350a6..f922d92 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -282,15 +282,21 @@ For a client certificate that can be easily imported into 
the browser, specify:
 User Authentication
 -------------------
 
-NiFi supports user authentication via client certificates or via 
username/password. Username/password authentication is performed by a 'Login 
Identity
-Provider'. The Login Identity Provider is a pluggable mechanism for 
authenticating users via their username/password. Which Login Identity Provider
-to use is configured in two properties in the _nifi.properties_ file.
+NiFi supports user authentication via client certificates, via 
username/password, or using OpenId Connect (http://openid.net/connect).
+
+Username/password authentication is performed by a 'Login Identity Provider'. 
The Login Identity Provider is a pluggable mechanism for
+authenticating users via their username/password. Which Login Identity 
Provider to use is configured in two properties in the _nifi.properties_ file.
 
 The `nifi.login.identity.provider.configuration.file` property specifies the 
configuration file for Login Identity Providers.
 The `nifi.security.user.login.identity.provider` property indicates which of 
the configured Login Identity Provider should be
 used. If this property is not configured, NiFi will not support 
username/password authentication and will require client
 certificates for authenticating users over HTTPS. By default, this property is 
not configured meaning that username/password must be explicitly enabled.
 
+During OpenId Connect authentication, NiFi will redirect users to login with 
the Provider before returning to NiFi. NiFi will then
+call the Provider to obtain the user identity.
+
+NOTE: NiFi cannot be configured for both username/password and OpenId Connect 
authentication at the same time.
+
 A secured instance of NiFi cannot be accessed anonymously unless configured to 
use an LDAP or Kerberos Login Identity Provider, which in turn must be 
configured to explicitly allow anonymous access. Anonymous access is not 
currently possible by the default FileAuthorizer (see 
<<authorizer-configuration>>), but is a future effort 
(https://issues.apache.org/jira/browse/NIFI-2730[NIFI-2730]).
 
 NOTE: NiFi does not perform user authentication over HTTP. Using HTTP, all 
users will be granted all roles.
@@ -397,6 +403,26 @@ 
nifi.security.user.login.identity.provider=kerberos-provider
 
 See also <<kerberos_service>> to allow single sign-on access via client 
Kerberos tickets.
 
+[[openid_connect]]
+OpenId Connect
+~~~~~~~~~~~~~~
+
+To enable authentication via OpenId Connect the following properties must be 
configured in nifi.properties.
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`nifi.security.user.oidc.discovery.url` | The discovery URL for the desired 
OpenId Connect Provider 
(http://openid.net/specs/openid-connect-discovery-1_0.html).
+|`nifi.security.user.oidc.connect.timeout` | Connect timeout when 
communicating with the OpenId Connect Provider.
+|`nifi.security.user.oidc.read.timeout` | Read timeout when communicating with 
the OpenId Connect Provider.
+|`nifi.security.user.oidc.client.id` | The client id for NiFi after 
registration with the OpenId Connect Provider.
+|`nifi.security.user.oidc.client.secret` | The client secret for NiFi after 
registration with the OpenId Connect Provider.
+|`nifi.security.user.oidc.preferred.jwsalgorithm` | The preferred algorithm 
for for validating identity tokens. If this value is blank, it will default to 
'RS256' which is required to be supported
+by the OpenId Connect Provider according to the specification. If this value 
is 'HS256', 'HS384', or 'HS512', NiFi will attempt to validate HMAC protected 
tokens using the specified client secret.
+If this value is 'none', NiFi will attempt to validate unsecured/plain tokens. 
Other values for this algorithm will attempt to parse as an RSA or EC algorithm 
to be used in conjunction with the
+JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the 
discovery URL.
+|==================================================================================================================================================
+
 [[multi-tenant-authorization]]
 Multi-Tenant Authorization
 --------------------------

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
index 175ec8f..78eb4c1 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/LICENSE
@@ -743,5 +743,33 @@ This product bundles 'jsonlint' which is available under 
an MIT license.
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     THE SOFTWARE.
 
+This product bundles 'asm' which is available under a 3-Clause BSD style 
license.
+For details see http://asm.ow2.org/asmdex-license.html
 
+    Copyright (c) 2012 France Télécom
+    All rights reserved.
+
+    Redistribution and use in source and binary forms, with or without
+    modification, are permitted provided that the following conditions
+    are met:
+    1. Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+    3. Neither the name of the copyright holders nor the names of its
+       contributors may be used to endorse or promote products derived from
+       this software without specific prior written permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+    THE POSSIBILITY OF SUCH DAMAGE.
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
index b1c4b9d..fc8b572 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
@@ -147,6 +147,14 @@
         <nifi.security.ocsp.responder.url />
         <nifi.security.ocsp.responder.certificate />
 
+        <!-- nifi.properties: openid connect -->
+        <nifi.security.user.oidc.discovery.url />
+        <nifi.security.user.oidc.connect.timeout>5 
secs</nifi.security.user.oidc.connect.timeout>
+        <nifi.security.user.oidc.read.timeout>5 
secs</nifi.security.user.oidc.read.timeout>
+        <nifi.security.user.oidc.client.id />
+        <nifi.security.user.oidc.client.secret />
+        <nifi.security.user.oidc.preferred.jwsalgorithm />
+
         <!-- nifi.properties: cluster common properties (cluster manager and 
nodes must have same values) -->
         <nifi.cluster.protocol.heartbeat.interval>5 
sec</nifi.cluster.protocol.heartbeat.interval>
         
<nifi.cluster.protocol.is.secure>false</nifi.cluster.protocol.is.secure>

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
index 6e77f04..54c6e5d 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
@@ -156,6 +156,14 @@ 
nifi.security.user.login.identity.provider=${nifi.security.user.login.identity.p
 nifi.security.ocsp.responder.url=${nifi.security.ocsp.responder.url}
 
nifi.security.ocsp.responder.certificate=${nifi.security.ocsp.responder.certificate}
 
+# OpenId Connect Properties #
+nifi.security.user.oidc.discovery.url=${nifi.security.user.oidc.discovery.url}
+nifi.security.user.oidc.connect.timeout=${nifi.security.user.oidc.connect.timeout}
+nifi.security.user.oidc.read.timeout=${nifi.security.user.oidc.read.timeout}
+nifi.security.user.oidc.client.id=${nifi.security.user.oidc.client.id}
+nifi.security.user.oidc.client.secret=${nifi.security.user.oidc.client.secret}
+nifi.security.user.oidc.preferred.jwsalgorithm=${nifi.security.user.oidc.preferred.jwsalgorithm}
+
 # Identity Mapping Properties #
 # These properties allow normalizing user identities such that identities 
coming from different identity providers
 # (certificates, LDAP, Kerberos) can be treated the same internally in NiFi. 
The following example demonstrates normalizing

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
index 4fa01a5..cab3922 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
@@ -289,7 +289,9 @@ public class JettyServer implements NiFiServer {
         }
 
         // load the web ui app
-        handlers.addHandler(loadWar(webUiWar, "/nifi", frameworkClassLoader));
+        final WebAppContext webUiContext = loadWar(webUiWar, "/nifi", 
frameworkClassLoader);
+        webUiContext.getInitParams().put("oidc-supported", 
String.valueOf(props.isOidcEnabled()));
+        handlers.addHandler(webUiContext);
 
         // load the web api app
         webApiContext = loadWar(webApiWar, "/nifi-api", frameworkClassLoader);

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
index 928a068..ded51d0 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
@@ -326,6 +326,11 @@
             <artifactId>cglib-nodep</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>oauth2-oidc-sdk</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
         <!-- testing dependencies -->
         <dependency>

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
----------------------------------------------------------------------
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 5a9de3e..26f2443 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
@@ -78,7 +78,7 @@ public class NiFiWebApiSecurityConfiguration extends 
WebSecurityConfigurerAdapte
         // the /access/download-token and /access/ui-extension-token endpoints
         webSecurity
                 .ignoring()
-                    .antMatchers("/access", "/access/config", "/access/token", 
"/access/kerberos");
+                    .antMatchers("/access", "/access/config", "/access/token", 
"/access/kerberos", "/access/oidc/**");
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
----------------------------------------------------------------------
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 9342c8f..5c0360e 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,6 +16,14 @@
  */
 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.id.State;
+import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
+import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
+import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
 import com.wordnik.swagger.annotations.ApiResponse;
@@ -45,6 +53,7 @@ import 
org.apache.nifi.web.security.jwt.JwtAuthenticationProvider;
 import org.apache.nifi.web.security.jwt.JwtAuthenticationRequestToken;
 import org.apache.nifi.web.security.jwt.JwtService;
 import org.apache.nifi.web.security.kerberos.KerberosService;
+import org.apache.nifi.web.security.oidc.OidcService;
 import org.apache.nifi.web.security.otp.OtpService;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
@@ -59,7 +68,10 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
@@ -69,8 +81,10 @@ 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.net.URI;
 import java.security.cert.X509Certificate;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -85,6 +99,9 @@ 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_ERROR_TITLE = "Unable to continue login 
sequence";
+
     private X509CertificateExtractor certificateExtractor;
     private X509AuthenticationProvider x509AuthenticationProvider;
     private X509PrincipalExtractor principalExtractor;
@@ -93,6 +110,7 @@ public class AccessResource extends ApplicationResource {
     private JwtAuthenticationProvider jwtAuthenticationProvider;
     private JwtService jwtService;
     private OtpService otpService;
+    private OidcService oidcService;
 
     private KerberosService kerberosService;
 
@@ -125,6 +143,176 @@ public class AccessResource extends ApplicationResource {
         return generateOkResponse(entity).build();
     }
 
+    @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()) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
"User authentication/authorization is only supported when running over HTTPS.");
+            return;
+        }
+
+        // ensure oidc is enabled
+        if (!oidcService.isOidcEnabled()) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
"OpenId Connect is not configured.");
+            return;
+        }
+
+        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", getOidcCallback())
+                .build();
+
+        // 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()) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
"User authentication/authorization is only supported when running over HTTPS.");
+            return;
+        }
+
+        // ensure oidc is enabled
+        if (!oidcService.isOidcEnabled()) {
+            forwardToMessagePage(httpServletRequest, httpServletResponse, 
"OpenId Connect is not configured.");
+            return;
+        }
+
+        final String oidcRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+        if (oidcRequestIdentifier == null) {
+            forwardToMessagePage(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
+            forwardToMessagePage(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
+                forwardToMessagePage(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()));
+                oidcService.exchangeAuthorizationCode(oidcRequestIdentifier, 
authorizationGrant);
+            } catch (final Exception e) {
+                logger.error("Unable to exchange authorization for ID token: " 
+ e.getMessage(), e);
+
+                // remove the oidc request cookie
+                removeOidcRequestCookie(httpServletResponse);
+
+                // forward to the error page
+                forwardToMessagePage(httpServletRequest, httpServletResponse, 
"Unable to exchange authorization for ID token: " + e.getMessage());
+                return;
+            }
+
+            // redirect to the name page
+            httpServletResponse.sendRedirect("../../../nifi");
+        } else {
+            // remove the oidc request cookie
+            removeOidcRequestCookie(httpServletResponse);
+
+            // report the unsuccessful login
+            final AuthenticationErrorResponse errorOidcResponse = 
(AuthenticationErrorResponse) oidcResponse;
+            forwardToMessagePage(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) throws 
Exception {
+        // only consider user specific access over https
+        if (!httpServletRequest.isSecure()) {
+            throw new IllegalStateException("User authentication/authorization 
is only supported when running over HTTPS.");
+        }
+
+        // ensure oidc is enabled
+        if (!oidcService.isOidcEnabled()) {
+            throw new IllegalStateException("OpenId Connect is not 
configured.");
+        }
+
+        final String oidcRequestIdentifier = 
getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+        if (oidcRequestIdentifier == null) {
+            throw new IllegalArgumentException("The login request identifier 
was not found in the request. Unable to continue.");
+        }
+
+        // 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.");
+        }
+
+        // generate the response
+        return generateOkResponse(jwt).build();
+    }
+
     /**
      * Gets the status the client's access.
      *
@@ -470,6 +658,46 @@ public class AccessResource extends ApplicationResource {
         return proposedTokenExpiration;
     }
 
+    /**
+     * Gets the value of a cookie matching the specified name. If no cookie 
with that name exists, null is returned.
+     *
+     * @param cookies the cookies
+     * @param name the name of the cookie
+     * @return the value of the corresponding cookie, or null if the cookie 
does not exist
+     */
+    private String getCookieValue(final Cookie[] cookies, final String name) {
+        if (cookies != null) {
+            for (final Cookie cookie : cookies) {
+                if (name.equals(cookie.getName())) {
+                    return cookie.getValue();
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private String getOidcCallback() {
+        return generateResourceUri("access", "oidc", "callback");
+    }
+
+    private void removeOidcRequestCookie(final HttpServletResponse 
httpServletResponse) {
+        final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, null);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(0);
+        cookie.setSecure(true);
+        httpServletResponse.addCookie(cookie);
+    }
+
+    private void forwardToMessagePage(final HttpServletRequest 
httpServletRequest, final HttpServletResponse httpServletResponse, final String 
message) throws Exception {
+        httpServletRequest.setAttribute("title", OIDC_ERROR_TITLE);
+        httpServletRequest.setAttribute("messages", message);
+
+        final ServletContext uiContext = 
httpServletRequest.getServletContext().getContext("/nifi");
+        
uiContext.getRequestDispatcher("/WEB-INF/pages/message-page.jsp").forward(httpServletRequest,
 httpServletResponse);
+    }
+
     // setters
 
     public void setLoginIdentityProvider(LoginIdentityProvider 
loginIdentityProvider) {
@@ -504,4 +732,7 @@ public class AccessResource extends ApplicationResource {
         this.otpService = otpService;
     }
 
+    public void setOidcService(OidcService oidcService) {
+        this.oidcService = oidcService;
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
----------------------------------------------------------------------
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 4ca5032..3d78741 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
@@ -295,6 +295,10 @@ public abstract class ApplicationResource {
         return uriInfo.getAbsolutePath();
     }
 
+    protected URI getRequestUri() {
+        return uriInfo.getRequestUri();
+    }
+
     protected MultivaluedMap<String, String> getRequestParameters() {
         final MultivaluedMap<String, String> entity = new MultivaluedMapImpl();
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
----------------------------------------------------------------------
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 ca8a7e1..2311b07 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
@@ -382,6 +382,7 @@
     </bean>
     <bean id="accessResource" class="org.apache.nifi.web.api.AccessResource" 
scope="singleton">
         <property name="loginIdentityProvider" ref="loginIdentityProvider"/>
+        <property name="oidcService" ref="oidcService"/>
         <property name="x509AuthenticationProvider" 
ref="x509AuthenticationProvider"/>
         <property name="certificateExtractor" ref="certificateExtractor"/>
         <property name="principalExtractor" ref="principalExtractor"/>

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
index d251b7c..59f88c7 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml
@@ -136,5 +136,9 @@
             <artifactId>spring-security-kerberos-core</artifactId>
             <version>1.0.1.RELEASE</version>
         </dependency>
+        <dependency>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>oauth2-oidc-sdk</artifactId>
+        </dependency>
     </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java
index 3cef3a0..5125bc5 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/jwt/JwtAuthenticationRequestToken.java
@@ -53,7 +53,7 @@ public class JwtAuthenticationRequestToken extends 
NiFiAuthenticationRequestToke
 
     @Override
     public String toString() {
-        return getName();
+        return "<JWT token>";
     }
 
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcIdentityProvider.java
----------------------------------------------------------------------
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/OidcIdentityProvider.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcIdentityProvider.java
new file mode 100644
index 0000000..4cc5ba1
--- /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/OidcIdentityProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import java.io.IOException;
+import java.net.URI;
+
+public interface OidcIdentityProvider {
+
+    String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED = "OpenId Connect support 
is not configured";
+
+    /**
+     * Returns whether OIDC support is enabled.
+     *
+     * @return whether OIDC support is enabled
+     */
+    boolean isOidcEnabled();
+
+    /**
+     * Returns the configured client id.
+     *
+     * @return the client id
+     */
+    ClientID getClientId();
+
+    /**
+     * Returns the URI for the authorization endpoint.
+     *
+     * @return uri for the authorization endpoint
+     */
+    URI getAuthorizationEndpoint();
+
+    /**
+     * Returns the scopes supported by the OIDC provider.
+     *
+     * @return support scopes
+     */
+    Scope getScope();
+
+    /**
+     * Exchanges the supplied authorization grant for an ID token. Extracts 
the identity from the ID
+     * token and converts it into NiFi JWT.
+     *
+     * @param authorizationGrant authorization grant for invoking the Token 
Endpoint
+     * @return a NiFi JWT
+     * @throws IOException if there was an exceptional error while 
communicating with the OIDC provider
+     */
+    String exchangeAuthorizationCode(AuthorizationGrant authorizationGrant) 
throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcService.java
----------------------------------------------------------------------
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/OidcService.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcService.java
new file mode 100644
index 0000000..427fbdf
--- /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/OidcService.java
@@ -0,0 +1,246 @@
+/*
+ * 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;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.State;
+import org.apache.nifi.web.security.util.CacheKey;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static 
org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED;
+
+/**
+ * OidcService is a service for managing the OpenId Connect Authorization flow.
+ */
+public class OidcService {
+
+    private OidcIdentityProvider identityProvider;
+    private Cache<CacheKey, State> stateLookupForPendingRequests; // 
identifier from cookie -> state value
+    private Cache<CacheKey, String> jwtLookupForCompletedRequests; // 
identifier from cookie -> jwt or identity (and generate jwt on retrieval)
+
+    /**
+     * Creates a new OtpService with an expiration of 1 minute.
+     *
+     * @param identityProvider          The identity provider
+     */
+    public OidcService(final OidcIdentityProvider identityProvider) {
+        this(identityProvider, 60, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Creates a new OtpService.
+     *
+     * @param identityProvider          The identity provider
+     * @param duration                  The expiration duration
+     * @param units                     The expiration units
+     * @throws NullPointerException     If units is null
+     * @throws IllegalArgumentException If duration is negative
+     */
+    public OidcService(final OidcIdentityProvider identityProvider, final int 
duration, final TimeUnit units) {
+        if (identityProvider == null) {
+            throw new RuntimeException("The OidcIdentityProvider must be 
specified.");
+        }
+
+        this.identityProvider = identityProvider;
+        this.stateLookupForPendingRequests = 
CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
+        this.jwtLookupForCompletedRequests = 
CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
+    }
+
+    /**
+     * Returns whether OpenId Connect is enabled.
+     *
+     * @return whether OpenId Connect is enabled
+     */
+    public boolean isOidcEnabled() {
+        return identityProvider.isOidcEnabled();
+    }
+
+    /**
+     * Returns the OpenId Connect authorization endpoint.
+     *
+     * @return the authorization endpoint
+     */
+    public URI getAuthorizationEndpoint() {
+        return identityProvider.getAuthorizationEndpoint();
+    }
+
+    /**
+     * Returns the OpenId Connect scope.
+     *
+     * @return scope
+     */
+    public Scope getScope() {
+        return identityProvider.getScope();
+    }
+
+    /**
+     * Returns the OpenId Connect client id.
+     *
+     * @return client id
+     */
+    public String getClientId() {
+        return identityProvider.getClientId().getValue();
+    }
+
+    /**
+     * Initiates an OpenId Connection authorization code flow using the 
specified request identifier to maintain state.
+     *
+     * @param oidcRequestIdentifier request identifier
+     * @return state
+     */
+    public State createState(final String oidcRequestIdentifier) {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        final CacheKey oidcRequestIdentifierKey = new 
CacheKey(oidcRequestIdentifier);
+        final State state = new State(generateStateValue());
+
+        try {
+            synchronized (stateLookupForPendingRequests) {
+                final State cachedState = 
stateLookupForPendingRequests.get(oidcRequestIdentifierKey, () -> state);
+                if (!timeConstantEqualityCheck(state.getValue(), 
cachedState.getValue())) {
+                    throw new IllegalStateException("An existing login request 
is already in progress.");
+                }
+            }
+        } catch (ExecutionException e) {
+            throw new IllegalStateException("Unable to store the login request 
state.");
+        }
+
+        return state;
+    }
+
+    /**
+     * Generates a value to use as State in the OpenId Connect login sequence. 
128 bits is considered cryptographically strong
+     * with current hardware/software, but a Base32 digit needs 5 bits to be 
fully encoded, so 128 is rounded up to 130. Base32
+     * is chosen because it encodes data with a single case and without 
including confusing or URI-incompatible characters,
+     * unlike Base64, but is approximately 20% more compact than 
Base16/hexadecimal
+     *
+     * @return the state value
+     */
+    private String generateStateValue() {
+        return new BigInteger(130, new SecureRandom()).toString(32);
+    }
+
+    /**
+     * Validates the proposed state with the given request identifier. Will 
return false if the
+     * state does not match or if entry for this request identifier has 
expired.
+     *
+     * @param oidcRequestIdentifier request identifier
+     * @param proposedState proposed state
+     * @return whether the state is valid or not
+     */
+    public boolean isStateValid(final String oidcRequestIdentifier, final 
State proposedState) {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        if (proposedState == null) {
+            throw new IllegalArgumentException("Proposed state must be 
specified.");
+        }
+
+        final CacheKey oidcRequestIdentifierKey = new 
CacheKey(oidcRequestIdentifier);
+
+        synchronized (stateLookupForPendingRequests) {
+            final State state = 
stateLookupForPendingRequests.getIfPresent(oidcRequestIdentifierKey);
+            if (state != null) {
+                
stateLookupForPendingRequests.invalidate(oidcRequestIdentifierKey);
+            }
+
+            return state != null && 
timeConstantEqualityCheck(state.getValue(), proposedState.getValue());
+        }
+    }
+
+    /**
+     * Exchanges the specified authorization grant for an ID token for the 
given request identifier.
+     *
+     * @param oidcRequestIdentifier request identifier
+     * @param authorizationGrant authorization grant
+     * @throws IOException exceptional case for communication error with the 
OpenId Connect provider
+     */
+    public void exchangeAuthorizationCode(final String oidcRequestIdentifier, 
final AuthorizationGrant authorizationGrant) throws IOException {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        final CacheKey oidcRequestIdentifierKey = new 
CacheKey(oidcRequestIdentifier);
+        final String nifiJwt = 
identityProvider.exchangeAuthorizationCode(authorizationGrant);
+
+        try {
+            // cache the jwt for later retrieval
+            synchronized (jwtLookupForCompletedRequests) {
+                final String cachedJwt = 
jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> nifiJwt);
+                if (!timeConstantEqualityCheck(nifiJwt, cachedJwt)) {
+                    throw new IllegalStateException("An existing login request 
is already in progress.");
+                }
+            }
+        } catch (final ExecutionException e) {
+            throw new IllegalStateException("Unable to store the login 
authentication token.");
+        }
+    }
+
+    /**
+     * Returns the resulting JWT for the given request identifier. Will return 
null if the request
+     * identifier is not associated with a JWT or if the login sequence was 
not completed before
+     * this request identifier expired.
+     *
+     * @param oidcRequestIdentifier request identifier
+     * @return jwt token
+     */
+    public String getJwt(final String oidcRequestIdentifier) {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        final CacheKey oidcRequestIdentifierKey = new 
CacheKey(oidcRequestIdentifier);
+
+        synchronized (jwtLookupForCompletedRequests) {
+            final String jwt = 
jwtLookupForCompletedRequests.getIfPresent(oidcRequestIdentifierKey);
+            if (jwt != null) {
+                
jwtLookupForCompletedRequests.invalidate(oidcRequestIdentifierKey);
+            }
+
+            return jwt;
+        }
+    }
+
+    /**
+     * Implements a time constant equality check. If either value is null, 
false is returned.
+     *
+     * @param value1 value1
+     * @param value2 value2
+     * @return if value1 equals value2
+     */
+    private boolean timeConstantEqualityCheck(final String value1, final 
String value2) {
+        if (value1 == null || value2 == null) {
+            return false;
+        }
+
+        return MessageDigest.isEqual(value1.getBytes(StandardCharsets.UTF_8), 
value2.getBytes(StandardCharsets.UTF_8));
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java
----------------------------------------------------------------------
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/StandardOidcIdentityProvider.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProvider.java
new file mode 100644
index 0000000..6f4b6b9
--- /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/StandardOidcIdentityProvider.java
@@ -0,0 +1,364 @@
+/*
+ * 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;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.util.DefaultResourceRetriever;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
+import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
+import net.minidev.json.JSONObject;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.security.jwt.JwtService;
+import org.apache.nifi.web.security.token.LoginAuthenticationToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static com.nimbusds.openid.connect.sdk.claims.UserInfo.EMAIL_CLAIM_NAME;
+
+/**
+ * OidcProvider for managing the OpenId Connect Authorization flow.
+ */
+public class StandardOidcIdentityProvider implements OidcIdentityProvider {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(StandardOidcIdentityProvider.class);
+
+    private NiFiProperties properties;
+    private JwtService jwtService;
+    private OIDCProviderMetadata oidcProviderMetadata;
+    private int oidcConnectTimeout;
+    private int oidcReadTimeout;
+    private IDTokenValidator tokenValidator;
+    private ClientID clientId;
+    private Secret clientSecret;
+
+    /**
+     * Creates a new StandardOidcIdentityProvider.
+     *
+     * @param jwtService jwt service
+     * @param properties properties
+     */
+    public StandardOidcIdentityProvider(final JwtService jwtService, final 
NiFiProperties properties) {
+        this.properties = properties;
+        this.jwtService = jwtService;
+
+        // attempt to process the oidc configuration if configured
+        if (properties.isOidcEnabled()) {
+            if (properties.isLoginIdentityProviderEnabled()) {
+                throw new RuntimeException("OpenId Connect support cannot be 
enabled if the Login Identity Provider is configured.");
+            }
+
+            // oidc connect timeout
+            final String rawConnectTimeout = 
properties.getOidcConnectTimeout();
+            try {
+                oidcConnectTimeout = (int) 
FormatUtils.getTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
+            } catch (final Exception e) {
+                logger.warn("Failed to parse value of property '{}' as a valid 
time period. Value was '{}'. Ignoring this value and using the default value of 
'{}'",
+                        NiFiProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT, 
rawConnectTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
+                oidcConnectTimeout = (int) 
FormatUtils.getTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT,
 TimeUnit.MILLISECONDS);
+            }
+
+            // oidc read timeout
+            final String rawReadTimeout = properties.getOidcReadTimeout();
+            try {
+                oidcReadTimeout = (int) 
FormatUtils.getTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
+            } catch (final Exception e) {
+                logger.warn("Failed to parse value of property '{}' as a valid 
time period. Value was '{}'. Ignoring this value and using the default value of 
'{}'",
+                        NiFiProperties.SECURITY_USER_OIDC_READ_TIMEOUT, 
rawReadTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
+                oidcReadTimeout = (int) 
FormatUtils.getTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT,
 TimeUnit.MILLISECONDS);
+            }
+
+            // client id
+            final String rawClientId = properties.getOidcClientId();
+            if (StringUtils.isBlank(rawClientId)) {
+                throw new RuntimeException("Client ID is required when 
configuring an OIDC Provider.");
+            }
+            clientId = new ClientID(rawClientId);
+
+            // client secret
+            final String rawClientSecret = properties.getOidcClientSecret();
+            if (StringUtils.isBlank(rawClientSecret)) {
+                throw new RuntimeException("Client secret is required when 
configured an OIDC Provider.");
+            }
+            clientSecret = new Secret(rawClientSecret);
+
+            try {
+                // retrieve the oidc provider metadata
+                oidcProviderMetadata = 
retrieveOidcProviderMetadata(properties.getOidcDiscoveryUrl());
+            } catch (IOException | ParseException e) {
+                throw new RuntimeException("Unable to retrieve OpenId Connect 
Provider metadata from: " + properties.getOidcDiscoveryUrl(), e);
+            }
+
+            // ensure the authorization endpoint is present
+            if (oidcProviderMetadata.getAuthorizationEndpointURI() == null) {
+                throw new RuntimeException("OpenId Connect Provider metadata 
does not contain an Authorization Endpoint.");
+            }
+
+            // ensure the token endpoint is present
+            if (oidcProviderMetadata.getTokenEndpointURI() == null) {
+                throw new RuntimeException("OpenId Connect Provider metadata 
does not contain a Token Endpoint.");
+            }
+
+            // ensure the required scopes are present
+            if (oidcProviderMetadata.getScopes() == null) {
+                if 
(!oidcProviderMetadata.getScopes().contains(OIDCScopeValue.OPENID)) {
+                    throw new RuntimeException("OpenId Connect Provider does 
not support the required scope: " + OIDCScopeValue.OPENID.getValue());
+                }
+
+                if 
(!oidcProviderMetadata.getScopes().contains(OIDCScopeValue.EMAIL) && 
oidcProviderMetadata.getUserInfoEndpointURI() == null) {
+                    throw new RuntimeException(String.format("OpenId Connect 
Provider does not support '%s' scope and does not provide a UserInfo 
Endpoint.", OIDCScopeValue.EMAIL.getValue()));
+                }
+            }
+
+            // ensure the oidc provider supports basic or post client auth
+            final List<ClientAuthenticationMethod> clientAuthenticationMethods 
= oidcProviderMetadata.getTokenEndpointAuthMethods();
+            if (clientAuthenticationMethods == null
+                    || 
(!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+                    && 
!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)))
 {
+
+                throw new RuntimeException(String.format("OpenId Connect 
Provider does not support %s or %s",
+                        
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(),
+                        
ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()));
+            }
+
+            // extract the supported json web signature algorithms
+            final List<JWSAlgorithm> allowedAlgorithms = 
oidcProviderMetadata.getIDTokenJWSAlgs();
+            if (allowedAlgorithms == null || allowedAlgorithms.isEmpty()) {
+                throw new RuntimeException("The OpenId Connect Provider does 
not support any JWS algorithms.");
+            }
+
+            try {
+                // get the preferred json web signature algorithm
+                final String rawPreferredJwsAlgorithm = 
properties.getOidcPreferredJwsAlgorithm();
+
+                final JWSAlgorithm preferredJwsAlgorithm;
+                if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
+                    preferredJwsAlgorithm = JWSAlgorithm.RS256;
+                } else {
+                    if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
+                        preferredJwsAlgorithm = null;
+                    } else {
+                        preferredJwsAlgorithm = 
JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
+                    }
+                }
+
+                if (preferredJwsAlgorithm == null) {
+                    tokenValidator = new 
IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId);
+                } else if (JWSAlgorithm.HS256.equals(preferredJwsAlgorithm) || 
JWSAlgorithm.HS384.equals(preferredJwsAlgorithm) || 
JWSAlgorithm.HS512.equals(preferredJwsAlgorithm)) {
+                    tokenValidator = new 
IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, 
preferredJwsAlgorithm, clientSecret);
+                } else {
+                    final ResourceRetriever retriever = new 
DefaultResourceRetriever(oidcConnectTimeout, oidcReadTimeout);
+                    tokenValidator = new 
IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, 
preferredJwsAlgorithm, oidcProviderMetadata.getJWKSetURI().toURL(), retriever);
+                }
+            } catch (final Exception e) {
+                throw new RuntimeException("Unable to create the ID token 
validator for the configured OpenId Connect Provider: " + e.getMessage(), e);
+            }
+        }
+    }
+
+    private OIDCProviderMetadata retrieveOidcProviderMetadata(final String 
discoveryUri) throws IOException, ParseException {
+        final URL url = new URL(discoveryUri);
+        final HTTPRequest httpRequest = new 
HTTPRequest(HTTPRequest.Method.GET, url);
+        httpRequest.setConnectTimeout(oidcConnectTimeout);
+        httpRequest.setReadTimeout(oidcReadTimeout);
+
+        final HTTPResponse httpResponse = httpRequest.send();
+
+        if (httpResponse.getStatusCode() != 200) {
+            throw new IOException("Unable to download OpenId Connect Provider 
metadata from " + url + ": Status code " + httpResponse.getStatusCode());
+        }
+
+        final JSONObject jsonObject = httpResponse.getContentAsJSONObject();
+        return OIDCProviderMetadata.parse(jsonObject);
+    }
+
+    @Override
+    public boolean isOidcEnabled() {
+        return properties.isOidcEnabled();
+    }
+
+    @Override
+    public URI getAuthorizationEndpoint() {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        return oidcProviderMetadata.getAuthorizationEndpointURI();
+    }
+
+    @Override
+    public Scope getScope() {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        final Scope scope = new Scope("openid");
+
+        // if this provider supports email scope, include it to prevent a 
subsequent request to the user endpoint
+        if (oidcProviderMetadata.getScopes() != null && 
oidcProviderMetadata.getScopes().contains(OIDCScopeValue.EMAIL)) {
+            scope.add("email");
+        }
+        return scope;
+    }
+
+    @Override
+    public ClientID getClientId() {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        return clientId;
+    }
+
+    @Override
+    public String exchangeAuthorizationCode(final AuthorizationGrant 
authorizationGrant) throws IOException {
+        if (!isOidcEnabled()) {
+            throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+        }
+
+        final ClientAuthentication clientAuthentication;
+        if 
(oidcProviderMetadata.getTokenEndpointAuthMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_POST))
 {
+            clientAuthentication = new ClientSecretPost(clientId, 
clientSecret);
+        } else {
+            clientAuthentication = new ClientSecretBasic(clientId, 
clientSecret);
+        }
+
+        try {
+            // build the token request
+            final TokenRequest request = new 
TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication, 
authorizationGrant, getScope());
+            final HTTPRequest tokenHttpRequest = request.toHTTPRequest();
+            tokenHttpRequest.setConnectTimeout(oidcConnectTimeout);
+            tokenHttpRequest.setReadTimeout(oidcReadTimeout);
+
+            // get the token response
+            final TokenResponse response = 
OIDCTokenResponseParser.parse(tokenHttpRequest.send());
+
+            if (response.indicatesSuccess()) {
+                final OIDCTokenResponse oidcTokenResponse = 
(OIDCTokenResponse) response;
+                final OIDCTokens oidcTokens = 
oidcTokenResponse.getOIDCTokens();
+                final JWT oidcJwt = oidcTokens.getIDToken();
+
+                // validate the token - no nonce required for authorization 
code flow
+                final IDTokenClaimsSet claimsSet = 
tokenValidator.validate(oidcJwt, null);
+
+                // attempt to extract the email from the id token if possible
+                String email = claimsSet.getStringClaim(EMAIL_CLAIM_NAME);
+                if (StringUtils.isBlank(email)) {
+                    // extract the bearer access token
+                    final BearerAccessToken bearerAccessToken = 
oidcTokens.getBearerAccessToken();
+                    if (bearerAccessToken == null) {
+                        throw new IllegalStateException("No access token found 
in the ID tokens");
+                    }
+
+                    // invoke the UserInfo endpoint
+                    email = lookupEmail(bearerAccessToken);
+                }
+
+                // extract expiration details from the claims set
+                final Calendar now = Calendar.getInstance();
+                final Date expiration = claimsSet.getExpirationTime();
+                final long expiresIn = expiration.getTime() - 
now.getTimeInMillis();
+
+                // convert into a nifi jwt for retrieval later
+                final LoginAuthenticationToken loginToken = new 
LoginAuthenticationToken(email, email, expiresIn, 
claimsSet.getIssuer().getValue());
+                return jwtService.generateSignedToken(loginToken);
+            } else {
+                final TokenErrorResponse errorResponse = (TokenErrorResponse) 
response;
+                throw new RuntimeException("An error occurred while invoking 
the Token endpoint: " + errorResponse.getErrorObject().getDescription());
+            }
+        } catch (final ParseException | JOSEException | BadJOSEException e) {
+            throw new RuntimeException("Unable to parse the response from the 
Token request: " + e.getMessage());
+        }
+    }
+
+    private String lookupEmail(final BearerAccessToken bearerAccessToken) 
throws IOException {
+        try {
+            // build the user request
+            final UserInfoRequest request = new 
UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(), 
bearerAccessToken);
+            final HTTPRequest tokenHttpRequest = request.toHTTPRequest();
+            tokenHttpRequest.setConnectTimeout(oidcConnectTimeout);
+            tokenHttpRequest.setReadTimeout(oidcReadTimeout);
+
+            // send the user request
+            final UserInfoResponse response = 
UserInfoResponse.parse(request.toHTTPRequest().send());
+
+            // interpret the details
+            if (response.indicatesSuccess()) {
+                final UserInfoSuccessResponse successResponse = 
(UserInfoSuccessResponse) response;
+
+                final JWTClaimsSet claimsSet;
+                if (successResponse.getUserInfo() != null) {
+                    claimsSet = successResponse.getUserInfo().toJWTClaimsSet();
+                } else {
+                    claimsSet = 
successResponse.getUserInfoJWT().getJWTClaimsSet();
+                }
+
+                final String email = 
claimsSet.getStringClaim(EMAIL_CLAIM_NAME);
+
+                // ensure we were able to get the user email
+                if (StringUtils.isBlank(email)) {
+                    throw new IllegalStateException("Unable to extract email 
from the UserInfo token.");
+                } else {
+                    return email;
+                }
+            } else {
+                final UserInfoErrorResponse errorResponse = 
(UserInfoErrorResponse) response;
+                throw new RuntimeException("An error occurred while invoking 
the UserInfo endpoint: " + errorResponse.getErrorObject().getDescription());
+            }
+        } catch (final ParseException | java.text.ParseException e) {
+            throw new RuntimeException("Unable to parse the response from the 
UserInfo token request: " + e.getMessage());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationRequestToken.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationRequestToken.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationRequestToken.java
index af7006a..bad7c94 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationRequestToken.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/otp/OtpAuthenticationRequestToken.java
@@ -60,7 +60,7 @@ public class OtpAuthenticationRequestToken extends 
NiFiAuthenticationRequestToke
 
     @Override
     public String toString() {
-        return getName();
+        return "<OTP token>";
     }
 
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/resources/nifi-web-security-context.xml
----------------------------------------------------------------------
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 369b33c..6b0c0c1 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
@@ -78,4 +78,13 @@
         <property name="properties" ref="nifiProperties"/>
     </bean>
 
+    <!-- oidc -->
+    <bean id="oidcProvider" 
class="org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider">
+        <constructor-arg ref="jwtService" index="0"/>
+        <constructor-arg ref="nifiProperties" index="1"/>
+    </bean>
+    <bean id="oidcService" 
class="org.apache.nifi.web.security.oidc.OidcService">
+        <constructor-arg ref="oidcProvider"/>
+    </bean>
+
 </beans>

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
new file mode 100644
index 0000000..6c13576
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.id.State;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OidcServiceTest {
+
+    public static final String TEST_REQUEST_IDENTIFIER = 
"test-request-identifier";
+    public static final String TEST_STATE = "test-state";
+
+    @Test(expected = IllegalStateException.class)
+    public void testOidcNotEnabledCreateState() throws Exception {
+        final OidcService service = getServiceWithNoOidcSupport();
+        service.createState(TEST_REQUEST_IDENTIFIER);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCreateStateMultipleInvocations() throws Exception {
+        final OidcService service = getServiceWithOidcSupport();
+        service.createState(TEST_REQUEST_IDENTIFIER);
+        service.createState(TEST_REQUEST_IDENTIFIER);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOidcNotEnabledValidateState() throws Exception {
+        final OidcService service = getServiceWithNoOidcSupport();
+        service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE));
+    }
+
+    @Test
+    public void testOidcUnknownState() throws Exception {
+        final OidcService service = getServiceWithOidcSupport();
+        assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, new 
State(TEST_STATE)));
+    }
+
+    @Test
+    public void testValidateState() throws Exception {
+        final OidcService service = getServiceWithOidcSupport();
+        final State state = service.createState(TEST_REQUEST_IDENTIFIER);
+        assertTrue(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
+    }
+
+    @Test
+    public void testValidateStateExpiration() throws Exception {
+        final OidcService service = 
getServiceWithOidcSupportAndCustomExpiration(1, TimeUnit.SECONDS);
+        final State state = service.createState(TEST_REQUEST_IDENTIFIER);
+
+        Thread.sleep(3 * 1000);
+
+        assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOidcNotEnabledExchangeCode() throws Exception {
+        final OidcService service = getServiceWithNoOidcSupport();
+        service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, 
getAuthorizationCodeGrant());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testExchangeCodeMultipleInvocation() throws Exception {
+        final OidcService service = getServiceWithOidcSupport();
+        service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, 
getAuthorizationCodeGrant());
+        service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, 
getAuthorizationCodeGrant());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOidcNotEnabledGetJwt() throws Exception {
+        final OidcService service = getServiceWithNoOidcSupport();
+        service.getJwt(TEST_REQUEST_IDENTIFIER);
+    }
+
+    @Test
+    public void testGetJwt() throws Exception {
+        final OidcService service = getServiceWithOidcSupport();
+        service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, 
getAuthorizationCodeGrant());
+        assertNotNull(service.getJwt(TEST_REQUEST_IDENTIFIER));
+    }
+
+    @Test
+    public void testGetJwtExpiration() throws Exception {
+        final OidcService service = 
getServiceWithOidcSupportAndCustomExpiration(1, TimeUnit.SECONDS);
+        service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, 
getAuthorizationCodeGrant());
+
+        Thread.sleep(3 * 1000);
+
+        assertNull(service.getJwt(TEST_REQUEST_IDENTIFIER));
+    }
+
+    private OidcService getServiceWithNoOidcSupport() {
+        final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
+        when(provider.isOidcEnabled()).thenReturn(false);
+
+        final OidcService service = new OidcService(provider);
+        assertFalse(service.isOidcEnabled());
+
+        return service;
+    }
+
+    private OidcService getServiceWithOidcSupport() throws Exception {
+        final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
+        when(provider.isOidcEnabled()).thenReturn(true);
+        when(provider.exchangeAuthorizationCode(any())).then(invocation -> 
UUID.randomUUID().toString());
+
+        final OidcService service = new OidcService(provider);
+        assertTrue(service.isOidcEnabled());
+
+        return service;
+    }
+
+    private OidcService getServiceWithOidcSupportAndCustomExpiration(final int 
duration, final TimeUnit units) throws Exception {
+        final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
+        when(provider.isOidcEnabled()).thenReturn(true);
+        when(provider.exchangeAuthorizationCode(any())).then(invocation -> 
UUID.randomUUID().toString());
+
+        final OidcService service = new OidcService(provider, duration, units);
+        assertTrue(service.isOidcEnabled());
+
+        return service;
+    }
+
+    private AuthorizationCodeGrant getAuthorizationCodeGrant() {
+        return new AuthorizationCodeGrant(new AuthorizationCode("code"), 
URI.create("http://localhost:8080/nifi";));
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/528b8263/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/java/org/apache/nifi/web/filter/LoginFilter.java
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/java/org/apache/nifi/web/filter/LoginFilter.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/java/org/apache/nifi/web/filter/LoginFilter.java
new file mode 100644
index 0000000..4f70ed8
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/java/org/apache/nifi/web/filter/LoginFilter.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.filter;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+
+/**
+ * Filter for determining appropriate login location.
+ */
+public class LoginFilter implements Filter {
+
+    private ServletContext servletContext;
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        servletContext = filterConfig.getServletContext();
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain filterChain) throws IOException, ServletException {
+        final boolean supportsOidc = 
Boolean.parseBoolean(servletContext.getInitParameter("oidc-supported"));
+
+        if (supportsOidc) {
+            final ServletContext apiContext = 
servletContext.getContext("/nifi-api");
+            
apiContext.getRequestDispatcher("/access/oidc/request").forward(request, 
response);
+        } else {
+            filterChain.doFilter(request, response);
+        }
+    }
+
+    @Override
+    public void destroy() {
+    }
+}

Reply via email to