This is an automated email from the ASF dual-hosted git repository.

mcgilman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/master by this push:
     new 0650521  NIFI-6171 always send email scope for OIDC NIFI-6171 re-added 
lookupEmail() as fallback NIFI-6171 additional OIDC scopes via nifi.properties 
NIFI-6171 alternative user identification (instead of email)  via 
nifi.properties NIFI-6171 changed lookupEmail() so that any configured claim 
can be fetched fro the UserInfo endpoint
0650521 is described below

commit 0650521eb276bb4a68c271be9f49325fdcffaee0
Author: simonl <[email protected]>
AuthorDate: Tue Apr 2 12:16:01 2019 +0200

    NIFI-6171 always send email scope for OIDC
    NIFI-6171 re-added lookupEmail() as fallback
    NIFI-6171 additional OIDC scopes via nifi.properties
    NIFI-6171 alternative user identification (instead of email)  via 
nifi.properties
    NIFI-6171 changed lookupEmail() so that any configured claim can be fetched 
fro the UserInfo endpoint
    
    This closes #3398
    This closes #2346
---
 .../java/org/apache/nifi/util/NiFiProperties.java  | 26 +++++++
 .../org/apache/nifi/util/NiFiPropertiesTest.java   | 30 ++++++++
 .../src/main/asciidoc/administration-guide.adoc    |  2 +
 .../nifi-framework/nifi-resources/pom.xml          |  2 +
 .../src/main/resources/conf/nifi.properties        |  2 +
 .../oidc/StandardOidcIdentityProvider.java         | 55 +++++++--------
 .../oidc/StandardOidcIdentityProviderTest.java     | 80 ++++++++++++++++++++++
 7 files changed, 167 insertions(+), 30 deletions(-)

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 9132eae..31b1db5 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
@@ -158,6 +158,8 @@ public abstract class NiFiProperties {
     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";
+    public static final String SECURITY_USER_OIDC_ADDITIONAL_SCOPES = 
"nifi.security.user.oidc.additional.scopes";
+    public static final String SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER = 
"nifi.security.user.oidc.claim.identifying.user";
 
     // apache knox
     public static final String SECURITY_USER_KNOX_URL = 
"nifi.security.user.knox.url";
@@ -948,6 +950,30 @@ public abstract class NiFiProperties {
     }
 
     /**
+     * Returns additional scopes to be sent when requesting the access token 
from the IDP.
+     *
+     * @return List of additional scopes to be sent
+     */
+    public List<String> getOidcAdditionalScopes() {
+        String rawProperty = getProperty(SECURITY_USER_OIDC_ADDITIONAL_SCOPES, 
"");
+        if (rawProperty.isEmpty()) {
+            return new ArrayList<>();
+        }
+        List<String> additionalScopes = Arrays.asList(rawProperty.split(","));
+        return 
additionalScopes.stream().map(String::trim).collect(Collectors.toList());
+    }
+
+    /**
+     * Returns the claim to be used to identify a user.
+     * Claim must be requested by adding the scope for it.
+     * Default is 'email'.
+     * @return The claim to be used to identify the user.
+     */
+    public String getOidcClaimIdentifyingUser() {
+        return getProperty(SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER, 
"email").trim();
+    }
+
+    /**
      * Returns whether Knox SSO is enabled.
      *
      * @return whether Knox SSO is enabled
diff --git 
a/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java
 
b/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java
index 48b1b1a..1494f5c 100644
--- 
a/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java
+++ 
b/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java
@@ -17,6 +17,10 @@
 package org.apache.nifi.util;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.io.File;
 import java.net.URISyntaxException;
@@ -127,6 +131,32 @@ public class NiFiPropertiesTest {
         }
     }
 
+    @Test
+    public void testAdditionalOidcScopesAreTrimmed() {
+        final String scope = "abc";
+        final String scopeLeadingWhitespace = " def";
+        final String scopeTrailingWhitespace = "ghi ";
+        final String scopeLeadingTrailingWhitespace = " jkl ";
+
+        String additionalScopes = String.join(",", scope, 
scopeLeadingWhitespace,
+                scopeTrailingWhitespace, scopeLeadingTrailingWhitespace);
+
+        NiFiProperties properties = mock(NiFiProperties.class);
+        
when(properties.getProperty(NiFiProperties.SECURITY_USER_OIDC_ADDITIONAL_SCOPES,
 ""))
+                .thenReturn(additionalScopes);
+        when(properties.getOidcAdditionalScopes()).thenCallRealMethod();
+
+        List<String> scopes = properties.getOidcAdditionalScopes();
+
+        assertTrue(scopes.contains(scope));
+        assertFalse(scopes.contains(scopeLeadingWhitespace));
+        assertTrue(scopes.contains(scopeLeadingWhitespace.trim()));
+        assertFalse(scopes.contains(scopeTrailingWhitespace));
+        assertTrue(scopes.contains(scopeTrailingWhitespace.trim()));
+        assertFalse(scopes.contains(scopeLeadingTrailingWhitespace));
+        assertTrue(scopes.contains(scopeLeadingTrailingWhitespace.trim()));
+    }
+
     private NiFiProperties loadNiFiProperties(final String propsPath, final 
Map<String, String> additionalProperties){
         String realPath = null;
         try{
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc 
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 16d79f7..adcf58a 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -349,6 +349,8 @@ To enable authentication via OpenId Connect the following 
properties must be con
 |`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.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.
 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-resources/pom.xml 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
index 9301d9c..c9e3f2c 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
@@ -158,6 +158,8 @@
         <nifi.security.user.oidc.client.id />
         <nifi.security.user.oidc.client.secret />
         <nifi.security.user.oidc.preferred.jwsalgorithm />
+        <nifi.security.user.oidc.additional.scopes />
+        <nifi.security.user.oidc.claim.identifying.user />
 
         <!-- nifi.properties: apache knox -->
         <nifi.security.user.knox.url />
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 8e8814e..556d783 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
@@ -171,6 +171,8 @@ 
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}
+nifi.security.user.oidc.additional.scopes=${nifi.security.user.oidc.additional.scopes}
+nifi.security.user.oidc.claim.identifying.user=${nifi.security.user.oidc.claim.identifying.user}
 
 # Apache Knox SSO Properties #
 nifi.security.user.knox.url=${nifi.security.user.knox.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/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 73d6cdb..6760cef 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
@@ -38,7 +38,6 @@ 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;
@@ -67,8 +66,6 @@ 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.
@@ -153,17 +150,6 @@ public class StandardOidcIdentityProvider implements 
OidcIdentityProvider {
                 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
             List<ClientAuthenticationMethod> clientAuthenticationMethods = 
oidcProviderMetadata.getTokenEndpointAuthMethods();
             logger.info("OpenId Connect: Available clientAuthenticationMethods 
{} ", clientAuthenticationMethods);
@@ -258,12 +244,13 @@ public class StandardOidcIdentityProvider implements 
OidcIdentityProvider {
             throw new 
IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
         }
 
-        final Scope scope = new Scope("openid");
+        Scope scope = new Scope("openid", "email");
 
-        // 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");
+        for (String additionalScope : properties.getOidcAdditionalScopes()) {
+            // Scope automatically prevents duplicated entries
+            scope.add(additionalScope);
         }
+
         return scope;
     }
 
@@ -307,9 +294,14 @@ public class StandardOidcIdentityProvider implements 
OidcIdentityProvider {
                 // 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)) {
+                // 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("The identity of the user was tried to get 
with the claim '" +
+                            properties.getOidcClaimIdentifyingUser() + "'. The 
according additional scope is not " +
+                            "configured correctly. Trying to get it from the 
UserInfo endpoint.");
+
                     // extract the bearer access token
                     final BearerAccessToken bearerAccessToken = 
oidcTokens.getBearerAccessToken();
                     if (bearerAccessToken == null) {
@@ -317,7 +309,7 @@ public class StandardOidcIdentityProvider implements 
OidcIdentityProvider {
                     }
 
                     // invoke the UserInfo endpoint
-                    email = lookupEmail(bearerAccessToken);
+                    identity = lookupIdentityInUserInfo(bearerAccessToken);
                 }
 
                 // extract expiration details from the claims set
@@ -326,18 +318,20 @@ public class StandardOidcIdentityProvider implements 
OidcIdentityProvider {
                 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());
+                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());
+                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 {
+    private String lookupIdentityInUserInfo(final BearerAccessToken 
bearerAccessToken) throws IOException {
         try {
             // build the user request
             final UserInfoRequest request = new 
UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(), 
bearerAccessToken);
@@ -359,13 +353,14 @@ public class StandardOidcIdentityProvider implements 
OidcIdentityProvider {
                     claimsSet = 
successResponse.getUserInfoJWT().getJWTClaimsSet();
                 }
 
-                final String email = 
claimsSet.getStringClaim(EMAIL_CLAIM_NAME);
+                final String identity = 
claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser());
 
-                // ensure we were able to get the user email
-                if (StringUtils.isBlank(email)) {
-                    throw new IllegalStateException("Unable to extract email 
from the UserInfo token.");
+                // ensure we were able to get the user's identity
+                if (StringUtils.isBlank(identity)) {
+                    throw new IllegalStateException("Unable to extract 
identity from the UserInfo token using the claim '" +
+                            properties.getOidcClaimIdentifyingUser() + "'.");
                 } else {
-                    return email;
+                    return identity;
                 }
             } else {
                 final UserInfoErrorResponse errorResponse = 
(UserInfoErrorResponse) response;
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/StandardOidcIdentityProviderTest.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderTest.java
new file mode 100644
index 0000000..0bba94b
--- /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/StandardOidcIdentityProviderTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.Scope;
+import org.apache.nifi.util.NiFiProperties;
+import org.junit.Test;
+import org.mockito.internal.util.reflection.Whitebox;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class StandardOidcIdentityProviderTest {
+
+    @Test
+    public void testValidateScopes() {
+        final String additionalScope_profile = "profile";
+        final String additionalScope_abc = "abc";
+
+        final StandardOidcIdentityProvider provider = 
createOidcProviderWithAdditionalScopes(additionalScope_profile,
+            additionalScope_abc);
+        Scope scope = provider.getScope();
+
+        // two additional scopes are set, two (openid, email) are hard-coded
+        assertEquals(scope.toArray().length, 4);
+        assertTrue(scope.contains("openid"));
+        assertTrue(scope.contains("email"));
+        assertTrue(scope.contains(additionalScope_profile));
+        assertTrue(scope.contains(additionalScope_abc));
+    }
+
+    @Test
+    public void testNoDuplicatedScopes() {
+        final String additionalScopeDuplicate = "abc";
+
+        final StandardOidcIdentityProvider provider = 
createOidcProviderWithAdditionalScopes(additionalScopeDuplicate,
+                "def", additionalScopeDuplicate);
+        Scope scope = provider.getScope();
+
+        // three additional scopes are set but one is duplicated and mustn't 
be returned; note that there is
+        // another one inserted in between the duplicated; two (openid, email) 
are hard-coded
+        assertEquals(scope.toArray().length, 4);
+    }
+
+    private StandardOidcIdentityProvider 
createOidcProviderWithAdditionalScopes(String... additionalScopes) {
+        final StandardOidcIdentityProvider provider = 
mock(StandardOidcIdentityProvider.class);
+        NiFiProperties properties = 
createNiFiPropertiesMockWithAdditionalScopes(Arrays.asList(additionalScopes));
+        Whitebox.setInternalState(provider, "properties", properties);
+
+        when(provider.isOidcEnabled()).thenReturn(true);
+        when(provider.getScope()).thenCallRealMethod();
+
+        return provider;
+    }
+
+    private NiFiProperties 
createNiFiPropertiesMockWithAdditionalScopes(List<String> additionalScopes) {
+        NiFiProperties properties = mock(NiFiProperties.class);
+        
when(properties.getOidcAdditionalScopes()).thenReturn(additionalScopes);
+        return properties;
+    }
+}
\ No newline at end of file

Reply via email to