This is an automated email from the ASF dual-hosted git repository.
kdoran 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 43fb57e NIFI-7332 Added method to log available claim names from the
ID provider response when the OIDC Identifying User claim is not found. Revised
log message to print available claims. Added new
StandardOidcIdentityProviderGroovyTest file. Updated deprecated methods in
StandardOidcIdentityProvider. Changed log output to print all available claim
names from JWTClaimsSet. Added unit test. Added comments in
getAvailableClaims() method. Fixed typos in NiFi Docs Admin Guide. Added [...]
43fb57e is described below
commit 43fb57e7bb4b3a3a7714578816ab57ece63349c8
Author: mtien <[email protected]>
AuthorDate: Wed May 20 18:14:57 2020 -0700
NIFI-7332 Added method to log available claim names from the ID provider
response when the OIDC Identifying User claim is not found. Revised log message
to print available claims.
Added new StandardOidcIdentityProviderGroovyTest file.
Updated deprecated methods in StandardOidcIdentityProvider. Changed log
output to print all available claim names from JWTClaimsSet. Added unit test.
Added comments in getAvailableClaims() method.
Fixed typos in NiFi Docs Admin Guide.
Added license to Groovy test.
Fixed a checkstyle error.
Refactor exchangeAuthorizationCode method.
Added unit tests.
Verified all unit tests added so far are passing.
Refactored code. Added unit tests.
Refactored OIDC provider to decouple constructor & network-dependent
initialization.
Added unit tests.
Added unit tests.
Refactored OIDC provider to separately authorize the client. Added unit
tests.
Added unit tests.
NIFI-7332 Refactored exchangeAuthorizationCode method to separately
retrieve the NiFi JWT.
Signed-off-by: Nathan Gough <[email protected]>
This closes #4344.
---
.../src/main/asciidoc/administration-guide.adoc | 5 +-
.../web/security/oidc/OidcIdentityProvider.java | 6 +-
.../apache/nifi/web/security/oidc/OidcService.java | 17 +-
.../oidc/StandardOidcIdentityProvider.java | 391 ++++++++------
.../StandardOidcIdentityProviderGroovyTest.groovy | 584 +++++++++++++++++++++
5 files changed, 847 insertions(+), 156 deletions(-)
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index b2653d2..90daf3a 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -367,10 +367,9 @@ To enable authentication via OpenId Connect the following
properties must be con
|`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
+|`nifi.security.user.oidc.preferred.jwsalgorithm` | The preferred algorithm
for validating identity tokens. If this value is blank, it will default to
`RS256` which is required to be supported
|`nifi.security.user.oidc.additional.scopes` | Comma separated scopes that are
sent to OpenId Connect Provider in addition to `openid` and `email`.
-|`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the
user to be logged in; default is `email`. May need to be requested via the
`nifi.security.user.oidc.additional.scopes` before usage.
-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.
+|`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the
user to be logged in; default is `email`. May need to be requested via the
`nifi.security.user.oidc.additional.scopes` before usage 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.
|==================================================================================================================================================
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
index f51be91..cecd792 100644
---
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
@@ -20,7 +20,6 @@ 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;
@@ -29,6 +28,11 @@ public interface OidcIdentityProvider {
String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED = "OpenId Connect support
is not configured";
/**
+ * Initializes the provider.
+ */
+ void initializeProvider();
+
+ /**
* Returns whether OIDC support is enabled.
*
* @return whether OIDC support is enabled
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
index 4b0ec7c..b749085 100644
---
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
@@ -21,8 +21,6 @@ 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;
@@ -31,6 +29,7 @@ import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import org.apache.nifi.web.security.util.CacheKey;
import static
org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED;
@@ -66,6 +65,7 @@ public class OidcService {
throw new RuntimeException("The OidcIdentityProvider must be
specified.");
}
+ identityProvider.initializeProvider();
this.identityProvider = identityProvider;
this.stateLookupForPendingRequests =
CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
this.jwtLookupForCompletedRequests =
CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
@@ -198,7 +198,7 @@ public class OidcService {
}
final CacheKey oidcRequestIdentifierKey = new
CacheKey(oidcRequestIdentifier);
- final String nifiJwt =
identityProvider.exchangeAuthorizationCode(authorizationGrant);
+ final String nifiJwt = retrieveNifiJwt(authorizationGrant);
try {
// cache the jwt for later retrieval
@@ -214,6 +214,17 @@ public class OidcService {
}
/**
+ * Exchange the authorization code to retrieve a NiFi JWT.
+ *
+ * @param authorizationGrant authorization grant
+ * @return NiFi JWT
+ * @throws IOException exceptional case for communication error with the
OpenId Connect provider
+ */
+ public String retrieveNifiJwt(final AuthorizationGrant authorizationGrant)
throws IOException {
+ return identityProvider.exchangeAuthorizationCode(authorizationGrant);
+ }
+
+ /**
* 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.
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
index d7b7886..f7b54f1 100644
---
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
@@ -25,6 +25,7 @@ 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.Request;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
@@ -55,9 +56,12 @@ import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
import net.minidev.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.jwt.JwtService;
@@ -72,6 +76,7 @@ import org.slf4j.LoggerFactory;
public class StandardOidcIdentityProvider implements OidcIdentityProvider {
private static final Logger logger =
LoggerFactory.getLogger(StandardOidcIdentityProvider.class);
+ private final String EMAIL_CLAIM = "email";
private NiFiProperties properties;
private JwtService jwtService;
@@ -91,114 +96,146 @@ public class StandardOidcIdentityProvider implements
OidcIdentityProvider {
public StandardOidcIdentityProvider(final JwtService jwtService, final
NiFiProperties properties) {
this.properties = properties;
this.jwtService = jwtService;
+ }
+ /**
+ * Loads OIDC configuration values from {@link NiFiProperties}, connects
to external OIDC provider, and retrieves
+ * and validates provider metadata.
+ */
+ @Override
+ public void initializeProvider() {
// attempt to process the oidc configuration if configured
- if (properties.isOidcEnabled()) {
- if (properties.isLoginIdentityProviderEnabled() ||
properties.isKnoxSsoEnabled()) {
- throw new RuntimeException("OpenId Connect support cannot be
enabled if the Login Identity Provider or Apache Knox SSO is configured.");
- }
+ if (!properties.isOidcEnabled()) {
+ logger.warn("The OIDC provider is not configured or enabled");
+ return;
+ }
- // 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);
- }
+ validateOIDCConfiguration();
- // 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);
- }
+ 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);
+ }
- // 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);
+ validateOIDCProviderMetadata();
+ }
- // client secret
- final String rawClientSecret = properties.getOidcClientSecret();
- if (StringUtils.isBlank(rawClientSecret)) {
- throw new RuntimeException("Client secret is required when
configuring an OIDC Provider.");
- }
- clientSecret = new Secret(rawClientSecret);
+ /**
+ * Validates the retrieved OIDC provider metadata.
+ */
+ private void validateOIDCProviderMetadata() {
+ // ensure the authorization endpoint is present
+ if (oidcProviderMetadata.getAuthorizationEndpointURI() == null) {
+ throw new RuntimeException("OpenId Connect Provider metadata does
not contain an Authorization Endpoint.");
+ }
- 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 token endpoint is present
+ if (oidcProviderMetadata.getTokenEndpointURI() == null) {
+ throw new RuntimeException("OpenId Connect Provider metadata does
not contain a Token Endpoint.");
+ }
- // 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 oidc provider supports basic or post client auth
+ List<ClientAuthenticationMethod> clientAuthenticationMethods =
oidcProviderMetadata.getTokenEndpointAuthMethods();
+ logger.info("OpenId Connect: Available clientAuthenticationMethods {}
", clientAuthenticationMethods);
+ if (clientAuthenticationMethods == null ||
clientAuthenticationMethods.isEmpty()) {
+ clientAuthenticationMethods = new ArrayList<>();
+
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+
oidcProviderMetadata.setTokenEndpointAuthMethods(clientAuthenticationMethods);
+ logger.warn("OpenId Connect: ClientAuthenticationMethods is null,
Setting clientAuthenticationMethods as CLIENT_SECRET_BASIC");
+ } else if
(!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()));
+ }
- // ensure the token endpoint is present
- if (oidcProviderMetadata.getTokenEndpointURI() == null) {
- throw new RuntimeException("OpenId Connect Provider metadata
does not contain a Token Endpoint.");
- }
+ // 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.");
+ }
- // ensure the oidc provider supports basic or post client auth
- List<ClientAuthenticationMethod> clientAuthenticationMethods =
oidcProviderMetadata.getTokenEndpointAuthMethods();
- logger.info("OpenId Connect: Available clientAuthenticationMethods
{} ", clientAuthenticationMethods);
- if (clientAuthenticationMethods == null ||
clientAuthenticationMethods.isEmpty()) {
- clientAuthenticationMethods = new ArrayList<>();
-
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
-
oidcProviderMetadata.setTokenEndpointAuthMethods(clientAuthenticationMethods);
- logger.warn("OpenId Connect: ClientAuthenticationMethods is
null, Setting clientAuthenticationMethods as CLIENT_SECRET_BASIC");
- } else if
(!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()));
+ 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);
+ }
}
- // 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.");
+ 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);
+ }
+ }
+
+ /**
+ * Loads the initial configuration values relating to the OIDC provider
from the class {@link NiFiProperties} and populates the individual fields.
+ */
+ private void validateOIDCConfiguration() {
+ if (properties.isLoginIdentityProviderEnabled() ||
properties.isKnoxSsoEnabled()) {
+ throw new RuntimeException("OpenId Connect support cannot be
enabled if the Login Identity Provider or Apache Knox SSO is configured.");
+ }
- try {
- // get the preferred json web signature algorithm
- final String rawPreferredJwsAlgorithm =
properties.getOidcPreferredJwsAlgorithm();
+ // oidc connect timeout
+ final String rawConnectTimeout = properties.getOidcConnectTimeout();
+ try {
+ oidcConnectTimeout = (int)
FormatUtils.getPreciseTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
+ } catch (final Exception e) {
+ logger.warn("Failed to parse value of property '{}' as a valid
time period. Value was '{}'. Ignoring this value and using the default value of
'{}'",
+ NiFiProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT,
rawConnectTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
+ oidcConnectTimeout = (int)
FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT,
TimeUnit.MILLISECONDS);
+ }
- final JWSAlgorithm preferredJwsAlgorithm;
- if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
- preferredJwsAlgorithm = JWSAlgorithm.RS256;
- } else {
- if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
- preferredJwsAlgorithm = null;
- } else {
- preferredJwsAlgorithm =
JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
- }
- }
+ // oidc read timeout
+ final String rawReadTimeout = properties.getOidcReadTimeout();
+ try {
+ oidcReadTimeout = (int)
FormatUtils.getPreciseTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
+ } catch (final Exception e) {
+ logger.warn("Failed to parse value of property '{}' as a valid
time period. Value was '{}'. Ignoring this value and using the default value of
'{}'",
+ NiFiProperties.SECURITY_USER_OIDC_READ_TIMEOUT,
rawReadTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
+ oidcReadTimeout = (int)
FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT,
TimeUnit.MILLISECONDS);
+ }
- 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);
- }
+ // 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
configuring an OIDC Provider.");
+ }
+ clientSecret = new Secret(rawClientSecret);
}
+ /**
+ * Returns the retrieved OIDC provider metadata from the external provider.
+ *
+ * @param discoveryUri the remote OIDC provider endpoint for service
discovery
+ * @return the provider metadata
+ * @throws IOException if there is a problem connecting to the remote
endpoint
+ * @throws ParseException if there is a problem parsing the response
+ */
private OIDCProviderMetadata retrieveOidcProviderMetadata(final String
discoveryUri) throws IOException, ParseException {
final URL url = new URL(discoveryUri);
final HTTPRequest httpRequest = new
HTTPRequest(HTTPRequest.Method.GET, url);
@@ -243,7 +280,7 @@ public class StandardOidcIdentityProvider implements
OidcIdentityProvider {
throw new
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
- Scope scope = new Scope("openid", "email");
+ Scope scope = new Scope("openid", EMAIL_CLAIM);
for (String additionalScope : properties.getOidcAdditionalScopes()) {
// Scope automatically prevents duplicated entries
@@ -264,81 +301,137 @@ public class StandardOidcIdentityProvider implements
OidcIdentityProvider {
@Override
public String exchangeAuthorizationCode(final AuthorizationGrant
authorizationGrant) throws IOException {
+ // Check if OIDC is enabled
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);
+ // Build ClientAuthentication
+ final ClientAuthentication clientAuthentication =
createClientAuthentication();
+
+ try {
+ // Build the token request
+ final HTTPRequest tokenHttpRequest =
createTokenHTTPRequest(authorizationGrant, clientAuthentication);
+ return authorizeClient(tokenHttpRequest);
+
+ } catch (final ParseException | JOSEException | BadJOSEException |
java.text.ParseException e) {
+ throw new RuntimeException("Unable to parse the response from the
Token request: " + e.getMessage());
+ }
+ }
+
+ private String authorizeClient(HTTPRequest tokenHttpRequest) throws
ParseException, IOException, BadJOSEException, JOSEException,
java.text.ParseException {
+ // Get the token response
+ final TokenResponse response =
OIDCTokenResponseParser.parse(tokenHttpRequest.send());
+
+ // Handle success
+ if (response.indicatesSuccess()) {
+ return convertOIDCTokenToNiFiToken((OIDCTokenResponse) response);
} else {
- clientAuthentication = new ClientSecretBasic(clientId,
clientSecret);
+ // If the response was not successful
+ final TokenErrorResponse errorResponse = (TokenErrorResponse)
response;
+ throw new RuntimeException("An error occurred while invoking the
Token endpoint: " +
+ errorResponse.getErrorObject().getDescription());
}
+ }
- try {
- // build the token request
- final TokenRequest request = new
TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication,
authorizationGrant);
- final HTTPRequest tokenHttpRequest = request.toHTTPRequest();
- tokenHttpRequest.setConnectTimeout(oidcConnectTimeout);
- tokenHttpRequest.setReadTimeout(oidcReadTimeout);
+ private String convertOIDCTokenToNiFiToken(OIDCTokenResponse response)
throws BadJOSEException, JOSEException, java.text.ParseException, IOException {
+ final 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 configured claim to access the user's
identity; default is 'email'
+ String identityClaim = properties.getOidcClaimIdentifyingUser();
+ String identity = claimsSet.getStringClaim(identityClaim);
+
+ // If default identity not available, attempt secondary identity
extraction
+ if (StringUtils.isBlank(identity)) {
+ // Provide clear message to admin that desired claim is missing
and present available claims
+ List<String> availableClaims =
getAvailableClaims(oidcJwt.getJWTClaimsSet());
+ logger.warn("Failed to obtain the identity of the user with the
claim '{}'. The available claims on " +
+ "the OIDC response are: {}. Will attempt to obtain
the identity from secondary sources",
+ identityClaim, availableClaims);
+
+ // If the desired user claim was not "email" and "email" is
present, use that
+ if (!identityClaim.equalsIgnoreCase(EMAIL_CLAIM) &&
availableClaims.contains(EMAIL_CLAIM)) {
+ identity = claimsSet.getStringClaim(EMAIL_CLAIM);
+ logger.info("The 'email' claim was present. Using that claim
to avoid extra remote call");
+ } else {
+ identity = retrieveIdentityFromUserInfoEndpoint(oidcTokens);
+ logger.info("Retrieved identity from UserInfo endpoint");
+ }
+ }
- // get the token response
- final TokenResponse response =
OIDCTokenResponseParser.parse(tokenHttpRequest.send());
+ // extract expiration details from the claims set
+ final Calendar now = Calendar.getInstance();
+ final Date expiration = claimsSet.getExpirationTime();
+ final long expiresIn = expiration.getTime() - now.getTimeInMillis();
- if (response.indicatesSuccess()) {
- final OIDCTokenResponse oidcTokenResponse =
(OIDCTokenResponse) response;
- final OIDCTokens oidcTokens =
oidcTokenResponse.getOIDCTokens();
- final JWT oidcJwt = oidcTokens.getIDToken();
+ // convert into a nifi jwt for retrieval later
+ final LoginAuthenticationToken loginToken = new
LoginAuthenticationToken(identity, identity, expiresIn,
+ claimsSet.getIssuer().getValue());
+ return jwtService.generateSignedToken(loginToken);
+ }
- // validate the token - no nonce required for authorization
code flow
- final IDTokenClaimsSet claimsSet =
tokenValidator.validate(oidcJwt, null);
+ private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens)
throws IOException {
+ // explicitly try to get the identity from the UserInfo endpoint with
the configured claim
+ // extract the bearer access token
+ final BearerAccessToken bearerAccessToken =
oidcTokens.getBearerAccessToken();
+ if (bearerAccessToken == null) {
+ throw new IllegalStateException("No access token found in the ID
tokens");
+ }
- // attempt to extract the configured claim to access the
user's identity; default is 'email'
- String identity =
claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser());
- if (StringUtils.isBlank(identity)) {
- // explicitly try to get the identity from the UserInfo
endpoint with the configured claim
- logger.warn("Failed to obtain the identity of the user
with the claim '" +
- properties.getOidcClaimIdentifyingUser() + "'. The
claim is configured incorrectly. Will attempt to obtain the identity from the
UserInfo endpoint.");
-
- // 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
- identity = lookupIdentityInUserInfo(bearerAccessToken);
- }
+ // invoke the UserInfo endpoint
+ HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken);
+ return lookupIdentityInUserInfo(userInfoRequest);
+ }
- // extract expiration details from the claims set
- final Calendar now = Calendar.getInstance();
- final Date expiration = claimsSet.getExpirationTime();
- final long expiresIn = expiration.getTime() -
now.getTimeInMillis();
+ private HTTPRequest createTokenHTTPRequest(AuthorizationGrant
authorizationGrant, ClientAuthentication clientAuthentication) {
+ final TokenRequest request = new
TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication,
authorizationGrant);
+ return formHTTPRequest(request);
+ }
- // convert into a nifi jwt for retrieval later
- final LoginAuthenticationToken loginToken = new
LoginAuthenticationToken(identity, identity, 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 HTTPRequest createUserInfoRequest(BearerAccessToken
bearerAccessToken) {
+ final UserInfoRequest request = new
UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(),
bearerAccessToken);
+ return formHTTPRequest(request);
+ }
+
+ private HTTPRequest formHTTPRequest(Request request) {
+ final HTTPRequest httpRequest = request.toHTTPRequest();
+ httpRequest.setConnectTimeout(oidcConnectTimeout);
+ httpRequest.setReadTimeout(oidcReadTimeout);
+ return httpRequest;
+ }
+
+ private ClientAuthentication createClientAuthentication() {
+ final ClientAuthentication clientAuthentication;
+ List<ClientAuthenticationMethod> authMethods =
oidcProviderMetadata.getTokenEndpointAuthMethods();
+ if (authMethods != null &&
authMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
+ clientAuthentication = new ClientSecretPost(clientId,
clientSecret);
+ } else {
+ clientAuthentication = new ClientSecretBasic(clientId,
clientSecret);
}
+ return clientAuthentication;
}
- private String lookupIdentityInUserInfo(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);
+ private static List<String> getAvailableClaims(JWTClaimsSet claimSet) {
+ // Get the claims available in the ID token response
+ List<String> presentClaims = claimSet.getClaims().entrySet().stream()
+ // Check claim values are not empty
+ .filter(e -> StringUtils.isNotBlank(e.getValue().toString()))
+ // If not empty, put claim name in a map
+ .map(Map.Entry::getKey)
+ .sorted()
+ .collect(Collectors.toList());
+ return presentClaims;
+ }
+ private String lookupIdentityInUserInfo(final HTTPRequest
userInfoHttpRequest) throws IOException {
+ try {
// send the user request
- final UserInfoResponse response =
UserInfoResponse.parse(request.toHTTPRequest().send());
+ final UserInfoResponse response =
UserInfoResponse.parse(userInfoHttpRequest.send());
// interpret the details
if (response.indicatesSuccess()) {
@@ -362,10 +455,10 @@ public class StandardOidcIdentityProvider implements
OidcIdentityProvider {
}
} else {
final UserInfoErrorResponse errorResponse =
(UserInfoErrorResponse) response;
- throw new RuntimeException("An error occurred while invoking
the UserInfo endpoint: " + errorResponse.getErrorObject().getDescription());
+ throw new IdentityAccessException("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());
+ throw new IdentityAccessException("Unable to parse the response
from the UserInfo token request: " + e.getMessage());
}
}
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
new file mode 100644
index 0000000..2b1e2a2
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
@@ -0,0 +1,584 @@
+/*
+ * 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.jwt.JWT
+import com.nimbusds.jwt.JWTClaimsSet
+import com.nimbusds.jwt.PlainJWT
+import com.nimbusds.oauth2.sdk.AuthorizationCode
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant
+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.id.Issuer
+import com.nimbusds.oauth2.sdk.token.AccessToken
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken
+import com.nimbusds.oauth2.sdk.token.RefreshToken
+import com.nimbusds.openid.connect.sdk.Nonce
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse
+import com.nimbusds.openid.connect.sdk.SubjectType
+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 groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.SignatureAlgorithm
+import org.apache.nifi.admin.service.KeyService
+import org.apache.nifi.key.Key
+import org.apache.nifi.util.NiFiProperties
+import org.apache.nifi.util.StringUtils
+import org.apache.nifi.web.security.jwt.JwtService
+import org.apache.nifi.web.security.token.LoginAuthenticationToken
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+@RunWith(JUnit4.class)
+class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
+ private static final Logger logger =
LoggerFactory.getLogger(StandardOidcIdentityProviderGroovyTest.class)
+
+ private static final Key SIGNING_KEY = new Key(id: 1, identity:
"signingKey", key: "mock-signing-key-value")
+ private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
+ isOidcEnabled : false,
+ getOidcDiscoveryUrl : "https://localhost/oidc",
+ isLoginIdentityProviderEnabled: false,
+ isKnoxSsoEnabled : false,
+ getOidcConnectTimeout : 1000,
+ getOidcReadTimeout : 1000,
+ getOidcClientId : "expected_client_id",
+ getOidcClientSecret : "expected_client_secret",
+ getOidcClaimIdentifyingUser : "username"
+ ]
+
+ // Mock collaborators
+ private static NiFiProperties mockNiFiProperties
+ private static JwtService mockJwtService = [:] as JwtService
+
+ @BeforeClass
+ static void setUpOnce() throws Exception {
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ void setUp() throws Exception {
+ mockNiFiProperties = buildNiFiProperties()
+ }
+
+ @After
+ void teardown() throws Exception {
+ }
+
+ private static NiFiProperties buildNiFiProperties(Map<String, Object>
props = [:]) {
+ def combinedProps = DEFAULT_NIFI_PROPERTIES + props
+ def mockNFP = combinedProps.collectEntries { String k, def v ->
+ [k, { -> return v }]
+ }
+ mockNFP as NiFiProperties
+ }
+
+ private static JwtService buildJwtService() {
+ def mockJS = new JwtService([:] as KeyService) {
+ @Override
+ String generateSignedToken(LoginAuthenticationToken lat) {
+ signNiFiToken(lat)
+ }
+
+ }
+ mockJS
+ }
+
+ private static String signNiFiToken(LoginAuthenticationToken lat) {
+ String identity = "mockUser"
+ String USERNAME_CLAIM = "username"
+ String KEY_ID_CLAIM = "keyId"
+ Calendar expiration = Calendar.getInstance()
+ expiration.setTimeInMillis(System.currentTimeMillis() + 10_000)
+ String username = lat.getName()
+
+ return Jwts.builder().setSubject(identity)
+ .setIssuer(lat.getIssuer())
+ .setAudience(lat.getIssuer())
+ .claim(USERNAME_CLAIM, username)
+ .claim(KEY_ID_CLAIM, SIGNING_KEY.getId())
+ .setExpiration(expiration.getTime())
+ .setIssuedAt(Calendar.getInstance().getTime())
+ .signWith(SignatureAlgorithm.HS256,
SIGNING_KEY.key.getBytes("UTF-8")).compact()
+ }
+
+ @Test
+ void testShouldGetAvailableClaims() {
+ // Arrange
+ final Map<String, String> EXPECTED_CLAIMS = [
+ "iss" : "https://accounts.google.com",
+ "azp" :
"1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
+ "aud" :
"1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
+ "sub" : "10703475345439756345540",
+ "email" : "[email protected]",
+ "email_verified": "true",
+ "at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
+ "iat" : "1590022674",
+ "exp" : "1590026274",
+ "empty_claim" : ""
+ ]
+
+ final List<String> POPULATED_CLAIM_NAMES = EXPECTED_CLAIMS.findAll {
k, v -> StringUtils.isNotBlank(v) }.keySet().sort()
+
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(EXPECTED_CLAIMS)
+
+ // Act
+ def definedClaims =
StandardOidcIdentityProvider.getAvailableClaims(mockJWTClaimsSet)
+ logger.info("Defined claims: ${definedClaims}")
+
+ // Assert
+ assert definedClaims == POPULATED_CLAIM_NAMES
+ }
+
+ @Test
+ void testShouldCreateClientAuthenticationFromPost() {
+ // Arrange
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+
+ Issuer mockIssuer = new Issuer("https://localhost/oidc")
+ URI mockURI = new URI("https://localhost/oidc")
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+
+ soip.oidcProviderMetadata = metadata
+
+ // Set Authorization Method
+ soip.oidcProviderMetadata["tokenEndpointAuthMethods"] =
[ClientAuthenticationMethod.CLIENT_SECRET_POST]
+ final List<ClientAuthenticationMethod> mockAuthMethod =
soip.oidcProviderMetadata["tokenEndpointAuthMethods"]
+ logger.info("Provided Auth Method: ${mockAuthMethod}")
+
+ // Define expected values
+ final ClientID CLIENT_ID = new ClientID("expected_client_id")
+ final Secret CLIENT_SECRET = new Secret("expected_client_secret")
+
+ // Inject into OIP
+ soip.clientId = CLIENT_ID
+ soip.clientSecret = CLIENT_SECRET
+
+ final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new
ClientSecretPost(CLIENT_ID, CLIENT_SECRET)
+
+ // Act
+ def clientAuthentication = soip.createClientAuthentication()
+ logger.info("Client Auth properties:
${clientAuthentication.getProperties()}")
+
+ // Assert
+ assert clientAuthentication.getClientID() ==
EXPECTED_CLIENT_AUTHENTICATION.getClientID()
+ logger.info("Client secret: ${(clientAuthentication as
ClientSecretPost).clientSecret.value}")
+ assert ((ClientSecretPost) clientAuthentication).getClientSecret() ==
((ClientSecretPost) EXPECTED_CLIENT_AUTHENTICATION).getClientSecret()
+ }
+
+ @Test
+ void testShouldCreateClientAuthenticationFromBasic() {
+ // Arrange
+ // Mock collaborators
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+
+ Issuer mockIssuer = new Issuer("https://localhost/oidc")
+ URI mockURI = new URI("https://localhost/oidc")
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+ soip.oidcProviderMetadata = metadata
+
+ // Set Auth Method
+ soip.oidcProviderMetadata["tokenEndpointAuthMethods"] =
[ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
+ final List<ClientAuthenticationMethod> mockAuthMethod =
soip.oidcProviderMetadata["tokenEndpointAuthMethods"]
+ logger.info("Provided Auth Method: ${mockAuthMethod}")
+
+ // Define expected values
+ final ClientID CLIENT_ID = new ClientID("expected_client_id")
+ final Secret CLIENT_SECRET = new Secret("expected_client_secret")
+
+ // Inject into OIP
+ soip.clientId = CLIENT_ID
+ soip.clientSecret = CLIENT_SECRET
+
+ final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new
ClientSecretBasic(CLIENT_ID, CLIENT_SECRET)
+
+ // Act
+ def clientAuthentication = soip.createClientAuthentication()
+ logger.info("Client authentication properties:
${clientAuthentication.properties}")
+
+ // Assert
+ assert clientAuthentication.getClientID() ==
EXPECTED_CLIENT_AUTHENTICATION.getClientID()
+ assert clientAuthentication.getMethod() ==
EXPECTED_CLIENT_AUTHENTICATION.getMethod()
+ logger.info("Client secret: ${(clientAuthentication as
ClientSecretBasic).clientSecret.value}")
+ assert (clientAuthentication as ClientSecretBasic).getClientSecret()
== EXPECTED_CLIENT_AUTHENTICATION.clientSecret
+ }
+
+ @Test
+ void testShouldCreateTokenHTTPRequest() {
+ // Arrange
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+
+ // Mock AuthorizationGrant
+ Issuer mockIssuer = new Issuer("https://localhost/oidc")
+ URI mockURI = new URI("https://localhost/oidc")
+ AuthorizationCode mockCode = new AuthorizationCode("ABCDE")
+ def mockAuthGrant = new AuthorizationCodeGrant(mockCode, mockURI)
+
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+ soip.oidcProviderMetadata = metadata
+
+ // Set OIDC Provider metadata attributes
+ final ClientID CLIENT_ID = new ClientID("expected_client_id")
+ final Secret CLIENT_SECRET = new Secret("expected_client_secret")
+
+ // Inject into OIP
+ soip.clientId = CLIENT_ID
+ soip.clientSecret = CLIENT_SECRET
+ soip.oidcProviderMetadata["tokenEndpointAuthMethods"] =
[ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
+ soip.oidcProviderMetadata["tokenEndpointURI"] = new
URI("https://localhost/token")
+
+ // Mock ClientAuthentication
+ def clientAuthentication = soip.createClientAuthentication()
+
+ // Act
+ def httpRequest = soip.createTokenHTTPRequest(mockAuthGrant,
clientAuthentication)
+ logger.info("HTTP Request: ${httpRequest.dump()}")
+ logger.info("Query: ${URLDecoder.decode(httpRequest.query, "UTF-8")}")
+
+ // Assert
+ assert httpRequest.getMethod().name() == "POST"
+ assert httpRequest.query =~ "code=${mockCode.value}"
+ String encodedUri = URLEncoder.encode("https://localhost/oidc",
"UTF-8")
+ assert httpRequest.query =~
"redirect_uri=${encodedUri}&grant_type=authorization_code"
+ }
+
+ @Test
+ void testShouldLookupIdentityInUserInfo() {
+ // Arrange
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+
+ Issuer mockIssuer = new Issuer("https://localhost/oidc")
+ URI mockURI = new URI("https://localhost/oidc")
+
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+ soip.oidcProviderMetadata = metadata
+
+ final String EXPECTED_IDENTITY = "my_username"
+
+ def responseBody = [username: EXPECTED_IDENTITY, sub: "testSub"]
+ HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200,
"HTTP OK")
+
+ // Act
+ String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest)
+ logger.info("Identity: ${identity}")
+
+ // Assert
+ assert identity == EXPECTED_IDENTITY
+ }
+
+ @Test
+ void testLookupIdentityUserInfoShouldHandleMissingIdentity() {
+ // Arrange
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+
+ Issuer mockIssuer = new Issuer("https://localhost/oidc")
+ URI mockURI = new URI("https://localhost/oidc")
+
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+ soip.oidcProviderMetadata = metadata
+
+ def responseBody = [username: "", sub: "testSub"]
+ HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200,
"HTTP NO USER")
+
+ // Act
+ def msg = shouldFail(IllegalStateException) {
+ String identity =
soip.lookupIdentityInUserInfo(mockUserInfoRequest)
+ logger.info("Identity: ${identity}")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "Unable to extract identity from the UserInfo token
using the claim 'username'."
+ }
+
+ @Test
+ void testLookupIdentityUserInfoShouldHandle500() {
+ // Arrange
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+
+ Issuer mockIssuer = new Issuer("https://localhost/oidc")
+ URI mockURI = new URI("https://localhost/oidc")
+
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+ soip.oidcProviderMetadata = metadata
+
+ def errorBody = [error : "Failure to authenticate",
+ error_description: "The provided username and
password were not correct",
+ error_uri : "https://localhost/oidc/error"]
+ HTTPRequest mockUserInfoRequest = mockHttpRequest(errorBody, 500,
"HTTP ERROR")
+
+ // Act
+ def msg = shouldFail(RuntimeException) {
+ String identity =
soip.lookupIdentityInUserInfo(mockUserInfoRequest)
+ logger.info("Identity: ${identity}")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "An error occurred while invoking the UserInfo endpoint:
The provided username and password were not correct"
+ }
+
+ @Test
+ void testShouldConvertOIDCTokenToNiFiToken() {
+ // Arrange
+ StandardOidcIdentityProvider soip =
buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser":
"email"])
+
+ OIDCTokenResponse mockResponse = mockOIDCTokenResponse()
+ logger.info("OIDC Token Response: ${mockResponse.dump()}")
+
+ // Act
+ String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
+ logger.info("NiFi token: ${nifiToken}")
+
+ // Assert
+
+ // Split JWT into components and decode Base64 to JSON
+ def (String headerB64, String payloadB64, String signatureB64) =
nifiToken.tokenize("\\.")
+ logger.info("Header: ${headerB64} | Payload: ${payloadB64} |
Signature: ${signatureB64}")
+ String headerJson = new String(Base64.decoder.decode(headerB64),
"UTF-8")
+ String payloadJson = new String(Base64.decoder.decode(payloadB64),
"UTF-8")
+ // String signatureJson = new
String(Base64.decoder.decode(signatureB64), "UTF-8")
+
+ // Parse JSON into objects
+ def slurper = new JsonSlurper()
+ def header = slurper.parseText(headerJson)
+ logger.info("Header: ${header}")
+
+ assert header.alg == "HS256"
+
+ def payload = slurper.parseText(payloadJson)
+ logger.info("Payload: ${payload}")
+
+ assert payload.username == "[email protected]"
+ assert payload.keyId == 1
+ assert payload.exp <= System.currentTimeMillis() + 10_000
+ }
+
+ @Test
+ void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentity() {
+ // Arrange
+ StandardOidcIdentityProvider soip =
buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser":
"non-existent-claim"])
+
+ OIDCTokenResponse mockResponse = mockOIDCTokenResponse()
+ logger.info("OIDC Token Response: ${mockResponse.dump()}")
+
+ // Act
+ String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
+ logger.info("NiFi token: ${nifiToken}")
+
+ // Assert
+ // Split JWT into components and decode Base64 to JSON
+ def (String headerB64, String payloadB64, String signatureB64) =
nifiToken.tokenize("\\.")
+ logger.info("Header: ${headerB64} | Payload: ${payloadB64} |
Signature: ${signatureB64}")
+ String headerJson = new String(Base64.decoder.decode(headerB64),
"UTF-8")
+ String payloadJson = new String(Base64.decoder.decode(payloadB64),
"UTF-8")
+ // String signatureJson = new
String(Base64.decoder.decode(signatureB64), "UTF-8")
+
+ // Parse JSON into objects
+ def slurper = new JsonSlurper()
+ def header = slurper.parseText(headerJson)
+ logger.info("Header: ${header}")
+
+ assert header.alg == "HS256"
+
+ def payload = slurper.parseText(payloadJson)
+ logger.info("Payload: ${payload}")
+
+ assert payload.username == "[email protected]"
+ assert payload.keyId == 1
+ assert payload.exp <= System.currentTimeMillis() + 10_000
+ }
+
+ @Test
+ void
testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentityAndNoEmailClaim() {
+ // Arrange
+ StandardOidcIdentityProvider soip =
buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser":
"non-existent-claim"])
+
+ OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null])
+ logger.info("OIDC Token Response: ${mockResponse.dump()}")
+
+ // Act
+ def msg = shouldFail(ConnectException) {
+ String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
+ logger.info("NiFi token: ${nifiToken}")
+ }
+
+ // Assert
+ assert msg =~ "Connection refused"
+ }
+
+ @Test
+ void testShouldAuthorizeClient() {
+ // Arrange
+ // Build ID Provider with mock token endpoint URI to make a connection
+ StandardOidcIdentityProvider soip =
buildIdentityProviderWithMockTokenValidator([:])
+
+ // Mock the JWT
+ def jwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
+
+ def responseBody = [id_token: jwt, access_token: "some.access.token",
refresh_token: "some.refresh.token", token_type: "bearer"]
+ HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 200,
"HTTP OK")
+
+ // Act
+ def nifiToken = soip.authorizeClient(mockTokenRequest)
+ logger.info("NiFi Token: ${nifiToken.dump()}")
+
+ // Assert
+ assert nifiToken
+ }
+
+ @Test
+ void testAuthorizeClientShouldHandleError() {
+ // Arrange
+ // Build ID Provider with mock token endpoint URI to make a connection
+ StandardOidcIdentityProvider soip =
buildIdentityProviderWithMockTokenValidator([:])
+
+ // Mock the JWT
+ def jwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
+
+ def responseBody = [id_token: jwt, access_token: "some.access.token",
refresh_token: "some.refresh.token", token_type: "bearer"]
+ HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 500,
"HTTP SERVER ERROR")
+
+ // Act
+ def msg = shouldFail(RuntimeException) {
+ def nifiToken = soip.authorizeClient(mockTokenRequest)
+ logger.info("NiFi token: ${nifiToken}")
+ }
+
+ // Assert
+ assert msg =~ "An error occurred while invoking the Token endpoint:
null"
+ }
+
+
+ private StandardOidcIdentityProvider
buildIdentityProviderWithMockTokenValidator(Map<String, String>
additionalProperties = [:]) {
+ JwtService mockJS = buildJwtService()
+ NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
+ StandardOidcIdentityProvider soip = new
StandardOidcIdentityProvider(mockJS, mockNFP)
+
+ // Mock OIDC provider metadata
+ Issuer mockIssuer = new Issuer("mockIssuer")
+ URI mockURI = new URI("https://localhost/oidc")
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer,
[SubjectType.PUBLIC], mockURI)
+ soip.oidcProviderMetadata = metadata
+
+ // Set OIDC Provider metadata attributes
+ final ClientID CLIENT_ID = new ClientID("expected_client_id")
+ final Secret CLIENT_SECRET = new Secret("expected_client_secret")
+
+ // Inject into OIP
+ soip.clientId = CLIENT_ID
+ soip.clientSecret = CLIENT_SECRET
+ soip.oidcProviderMetadata["tokenEndpointAuthMethods"] =
[ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
+ soip.oidcProviderMetadata["tokenEndpointURI"] = new
URI("https://localhost/oidc/token")
+ soip.oidcProviderMetadata["userInfoEndpointURI"] = new
URI("https://localhost/oidc/userInfo")
+
+ // Mock token validator
+ IDTokenValidator mockTokenValidator = new IDTokenValidator(mockIssuer,
CLIENT_ID) {
+ @Override
+ IDTokenClaimsSet validate(JWT jwt, Nonce nonce) {
+ return new IDTokenClaimsSet(jwt.getJWTClaimsSet())
+ }
+ }
+ soip.tokenValidator = mockTokenValidator
+ soip
+ }
+
+ private OIDCTokenResponse mockOIDCTokenResponse(Map<String, Object>
additionalClaims = [:]) {
+ final Map<String, Object> claims = [
+ "iss" : "https://accounts.google.com",
+ "azp" :
"1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
+ "aud" :
"1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
+ "sub" : "10703475345439756345540",
+ "email" : "[email protected]",
+ "email_verified": "true",
+ "at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
+ "iat" : 1590022674,
+ "exp" : 1590026274
+ ] + additionalClaims
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Mock access tokens
+ AccessToken mockAccessToken = new BearerAccessToken()
+ RefreshToken mockRefreshToken = new RefreshToken()
+
+ // Create OIDC Tokens
+ OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken,
mockRefreshToken)
+
+ // Create OIDC Token Response
+ OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
+ mockResponse
+ }
+
+
+ /**
+ * Forms an {@link HTTPRequest} object which returns a static response
when {@code send( )} is called.
+ *
+ * @param body the JSON body in Map form
+ * @param statusCode the HTTP status code
+ * @param status the HTTP status message
+ * @param headers an optional map of HTTP response headers
+ * @param method the HTTP method to mock
+ * @param url the endpoint URL
+ * @return the static HTTP response
+ */
+ private static HTTPRequest mockHttpRequest(def body,
+ int statusCode = 200,
+ String status = "HTTP Response",
+ Map<String, String> headers =
[:],
+ HTTPRequest.Method method =
HTTPRequest.Method.GET,
+ URL url = new
URL("https://localhost/oidc")) {
+ new HTTPRequest(method, url) {
+ HTTPResponse send() {
+ HTTPResponse mockResponse = new HTTPResponse(statusCode)
+ mockResponse.setStatusMessage(status)
+ (["Content-Type": "application/json"] + headers).each { String
h, String v -> mockResponse.setHeader(h, v) }
+ def responseBody = body
+ mockResponse.setContent(JsonOutput.toJson(responseBody))
+ mockResponse
+ }
+ }
+ }
+
+ class MockOIDCProviderMetadata extends OIDCProviderMetadata {
+
+ MockOIDCProviderMetadata() {
+ super([:] as Issuer, [SubjectType.PUBLIC] as List<SubjectType>,
new URI("https://localhost"))
+ }
+ }
+}