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

jgresock 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 95bb23d403 NIFI-11781 Corrected OIDC Claim Identity Processing
95bb23d403 is described below

commit 95bb23d40317c521f003e9858f4229ca9eb36865
Author: exceptionfactory <exceptionfact...@apache.org>
AuthorDate: Tue Jul 11 12:43:10 2023 -0500

    NIFI-11781 Corrected OIDC Claim Identity Processing
    
    - Added StandardOidcUserService supporting fallback claim names
    - Updated StandardClientRegistrationProvider to use standard Subject claim
    - Updated OIDC Security Configuration to use customized OidcUserService for 
claim handling
    
    Signed-off-by: Joe Gresock <jgres...@gmail.com>
    This closes #7468.
---
 .../configuration/OidcSecurityConfiguration.java   |  17 ++-
 .../StandardClientRegistrationProvider.java        |   6 +-
 .../security/oidc/userinfo/StandardOidcUser.java   |  57 ++++++++
 .../oidc/userinfo/StandardOidcUserService.java     |  75 +++++++++++
 .../StandardClientRegistrationProviderTest.java    |   2 +-
 .../oidc/userinfo/StandardOidcUserServiceTest.java | 143 +++++++++++++++++++++
 6 files changed, 292 insertions(+), 8 deletions(-)

diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java
index 850028c3ca..fcc3c87f29 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java
@@ -50,6 +50,7 @@ import 
org.apache.nifi.web.security.oidc.registration.DisabledClientRegistration
 import 
org.apache.nifi.web.security.oidc.registration.StandardClientRegistrationProvider;
 import 
org.apache.nifi.web.security.oidc.revocation.StandardTokenRevocationResponseClient;
 import 
org.apache.nifi.web.security.oidc.revocation.TokenRevocationResponseClient;
+import org.apache.nifi.web.security.oidc.userinfo.StandardOidcUserService;
 import 
org.apache.nifi.web.security.oidc.web.authentication.OidcAuthenticationSuccessHandler;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.caffeine.CaffeineCache;
@@ -296,7 +297,10 @@ public class OidcSecurityConfiguration {
      */
     @Bean
     public OidcUserService oidcUserService() {
-        final OidcUserService oidcUserService = new OidcUserService();
+        final StandardOidcUserService oidcUserService = new 
StandardOidcUserService(
+                getUserClaimNames(),
+                IdentityMappingUtil.getIdentityMappings(properties)
+        );
         final DefaultOAuth2UserService userService = new 
DefaultOAuth2UserService();
         userService.setRestOperations(oidcRestOperations());
         oidcUserService.setOauth2UserService(userService);
@@ -468,9 +472,7 @@ public class OidcSecurityConfiguration {
     }
 
     private OidcAuthenticationSuccessHandler getAuthenticationSuccessHandler() 
{
-        final List<String> userClaimNames = new ArrayList<>();
-        userClaimNames.add(properties.getOidcClaimIdentifyingUser());
-        
userClaimNames.addAll(properties.getOidcFallbackClaimsIdentifyingUser());
+        final List<String> userClaimNames = getUserClaimNames();
 
         return new OidcAuthenticationSuccessHandler(
                 bearerTokenProvider,
@@ -480,4 +482,11 @@ public class OidcSecurityConfiguration {
                 properties.getOidcClaimGroups()
         );
     }
+
+    private List<String> getUserClaimNames() {
+        final List<String> userClaimNames = new ArrayList<>();
+        userClaimNames.add(properties.getOidcClaimIdentifyingUser());
+        
userClaimNames.addAll(properties.getOidcFallbackClaimsIdentifyingUser());
+        return userClaimNames;
+    }
 }
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/registration/StandardClientRegistrationProvider.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/registration/StandardClientRegistrationProvider.java
index f2368c3eeb..c24b106490 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/registration/StandardClientRegistrationProvider.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/registration/StandardClientRegistrationProvider.java
@@ -29,6 +29,7 @@ import 
org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
 import 
org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
 import org.springframework.security.oauth2.core.oidc.OidcScopes;
 import org.springframework.web.client.RestOperations;
 
@@ -85,8 +86,6 @@ public class StandardClientRegistrationProvider implements 
ClientRegistrationPro
         final List<String> additionalScopes = 
properties.getOidcAdditionalScopes();
         scope.addAll(additionalScopes);
 
-        final String userNameAttributeName = 
properties.getOidcClaimIdentifyingUser();
-
         return 
ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
                 .clientId(clientId)
                 .clientSecret(clientSecret)
@@ -99,7 +98,8 @@ public class StandardClientRegistrationProvider implements 
ClientRegistrationPro
                 .providerConfigurationMetadata(configurationMetadata)
                 .redirectUri(REGISTRATION_REDIRECT_URI)
                 .scope(scope)
-                .userNameAttributeName(userNameAttributeName)
+                // OpenID Connect 1.0 requires the sub claim and other 
components handle application username mapping
+                .userNameAttributeName(IdTokenClaimNames.SUB)
                 .clientAuthenticationMethod(clientAuthenticationMethod)
                 
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                 .build();
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/userinfo/StandardOidcUser.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/userinfo/StandardOidcUser.java
new file mode 100644
index 0000000000..3ce4fea527
--- /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/userinfo/StandardOidcUser.java
@@ -0,0 +1,57 @@
+/*
+ * 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.userinfo;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Standard extension of Spring Security OIDC User supporting customized name 
configuration
+ */
+class StandardOidcUser extends DefaultOidcUser {
+    private final String name;
+
+    /**
+     * Standard OIDC User constructor with required parameters and customized 
name value for identification
+     *
+     * @param authorities Granted Authorities
+     * @param idToken OIDC ID Token
+     * @param userInfo OIDC User Information
+     * @param nameAttributeKey Claim name that parent class uses to determine 
username for identification
+     * @param name Customized name identifying the user
+     */
+    public StandardOidcUser(
+            final Collection<? extends GrantedAuthority> authorities,
+            final OidcIdToken idToken,
+            final OidcUserInfo userInfo,
+            final String nameAttributeKey,
+            final String name
+    ) {
+        super(authorities, idToken, userInfo, nameAttributeKey);
+        this.name = Objects.requireNonNull(name, "Name required");
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+}
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/userinfo/StandardOidcUserService.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/userinfo/StandardOidcUserService.java
new file mode 100644
index 0000000000..96dc4ce107
--- /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/userinfo/StandardOidcUserService.java
@@ -0,0 +1,75 @@
+/*
+ * 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.userinfo;
+
+import org.apache.nifi.authorization.util.IdentityMapping;
+import org.apache.nifi.authorization.util.IdentityMappingUtil;
+import org.apache.nifi.web.security.oidc.OidcConfigurationException;
+import 
org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import 
org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Standard extension of Spring Security OIDC User Service supporting 
customized identity claim mapping
+ */
+public class StandardOidcUserService extends OidcUserService {
+    private final List<String> userClaimNames;
+
+    private final List<IdentityMapping> userIdentityMappings;
+
+    /**
+     * Standard OIDC User Service constructor with arguments derived from 
application properties for mapping usernames
+     *
+     * @param userClaimNames Ordered list of Token Claim names from which to 
determine the user identity
+     * @param userIdentityMappings List of Identity Mapping rules for optional 
transformation of a user identity
+     */
+    public StandardOidcUserService(final List<String> userClaimNames, final 
List<IdentityMapping> userIdentityMappings) {
+        this.userClaimNames = Objects.requireNonNull(userClaimNames, "User 
Claim Names required");
+        this.userIdentityMappings = 
Objects.requireNonNull(userIdentityMappings, "User Identity Mappings required");
+    }
+
+    /**
+     * Load User with user identity based on first available Token Claim found
+     *
+     * @param userRequest OIDC User Request information
+     * @return Standard OIDC User
+     * @throws OAuth2AuthenticationException Thrown on failures loading user 
information from Identity Provider
+     */
+    @Override
+    public OidcUser loadUser(final OidcUserRequest userRequest) throws 
OAuth2AuthenticationException {
+        final OidcUser oidcUser = super.loadUser(userRequest);
+        final String userClaimName = getUserClaimName(oidcUser);
+        final String claim = oidcUser.getClaimAsString(userClaimName);
+        final String name = IdentityMappingUtil.mapIdentity(claim, 
userIdentityMappings);
+        return new StandardOidcUser(oidcUser.getAuthorities(), 
oidcUser.getIdToken(), oidcUser.getUserInfo(), userClaimName, name);
+    }
+
+    private String getUserClaimName(final OidcUser oidcUser) {
+        final Optional<String> userClaimNameFound = userClaimNames.stream()
+                .filter(oidcUser::hasClaim)
+                .findFirst();
+        return userClaimNameFound.orElseThrow(() -> {
+            final String message = String.format("User Claim Name not found in 
configured Token Claims %s", userClaimNames);
+            return new OidcConfigurationException(message);
+        });
+    }
+}
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/registration/StandardClientRegistrationProviderTest.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/registration/StandardClientRegistrationProviderTest.java
index 164cd7c58c..c478dfa094 100644
--- 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/registration/StandardClientRegistrationProviderTest.java
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/registration/StandardClientRegistrationProviderTest.java
@@ -63,7 +63,7 @@ class StandardClientRegistrationProviderTest {
 
     private static final String CLIENT_SECRET = "client-secret";
 
-    private static final String USER_NAME_ATTRIBUTE_NAME = "email";
+    private static final String USER_NAME_ATTRIBUTE_NAME = "sub";
 
     private static final Set<String> EXPECTED_SCOPES = new 
LinkedHashSet<>(Arrays.asList(OidcScopes.OPENID, OidcScopes.EMAIL, 
OidcScopes.PROFILE));
 
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/userinfo/StandardOidcUserServiceTest.java
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/userinfo/StandardOidcUserServiceTest.java
new file mode 100644
index 0000000000..51d02c3cc4
--- /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/userinfo/StandardOidcUserServiceTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.userinfo;
+
+import org.apache.nifi.authorization.util.IdentityMapping;
+import org.apache.nifi.web.security.jwt.provider.SupportedClaim;
+import org.apache.nifi.web.security.oidc.OidcConfigurationException;
+import org.apache.nifi.web.security.oidc.client.web.OidcRegistrationProperty;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.core.Authentication;
+import 
org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import 
org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StandardOidcUserServiceTest {
+    private static final String REDIRECT_URI = 
"https://localhost:8443/nifi-api/callback";;
+
+    private static final String AUTHORIZATION_URI = 
"http://localhost/authorize";;
+
+    private static final String TOKEN_URI = "http://localhost/token";;
+
+    private static final String CLIENT_ID = "client-id";
+
+    private static final String ACCESS_TOKEN = "access";
+
+    private static final String ID_TOKEN = "id";
+
+    private static final String USER_NAME_CLAIM = "email";
+
+    private static final String FALLBACK_CLAIM = "preferred_username";
+
+    private static final String MISSING_CLAIM = "missing";
+
+    private static final String SUBJECT = String.class.getSimpleName();
+
+    private static final String IDENTITY = 
Authentication.class.getSimpleName();
+
+    private static final String FIRST_GROUP = "$1";
+
+    private static final Pattern MATCH_PATTERN = Pattern.compile("(.*)");
+
+    private static final IdentityMapping UPPER_IDENTITY_MAPPING = new 
IdentityMapping(
+            IdentityMapping.Transform.UPPER.toString(),
+            MATCH_PATTERN,
+            FIRST_GROUP,
+            IdentityMapping.Transform.UPPER
+    );
+
+    private StandardOidcUserService service;
+
+    @BeforeEach
+    void setService() {
+        service = new StandardOidcUserService(
+                Arrays.asList(USER_NAME_CLAIM, FALLBACK_CLAIM),
+                Collections.singletonList(UPPER_IDENTITY_MAPPING)
+        );
+    }
+
+    @Test
+    void testLoadUser() {
+        final OidcUserRequest userRequest = getUserRequest(USER_NAME_CLAIM);
+        final OidcUser oidcUser = service.loadUser(userRequest);
+
+        assertNotNull(oidcUser);
+        assertEquals(IDENTITY.toUpperCase(), oidcUser.getName());
+    }
+
+    @Test
+    void testLoadUserFallbackClaim() {
+        final OidcUserRequest userRequest = getUserRequest(FALLBACK_CLAIM);
+        final OidcUser oidcUser = service.loadUser(userRequest);
+
+        assertNotNull(oidcUser);
+        assertEquals(IDENTITY.toUpperCase(), oidcUser.getName());
+    }
+
+    @Test
+    void testLoadUserClaimNotFound() {
+        final OidcUserRequest userRequest = getUserRequest(MISSING_CLAIM);
+
+        assertThrows(OidcConfigurationException.class, () -> 
service.loadUser(userRequest));
+    }
+
+    OidcUserRequest getUserRequest(final String userNameClaim) {
+        final ClientRegistration clientRegistration = 
getClientRegistrationBuilder().build();
+
+        final Instant issuedAt = Instant.now();
+        final Instant expiresAt = Instant.MAX;
+        final OAuth2AccessToken accessToken = new 
OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN, issuedAt, 
expiresAt);
+
+        final Map<String, Object> claims = getClaims(userNameClaim);
+
+        final OidcIdToken idToken = new OidcIdToken(ID_TOKEN, issuedAt, 
expiresAt, claims);
+        return new OidcUserRequest(clientRegistration, accessToken, idToken);
+    }
+
+    Map<String, Object> getClaims(final String userNameClaim) {
+        final Map<String, Object> claims = new LinkedHashMap<>();
+        claims.put(SupportedClaim.SUBJECT.getClaim(), SUBJECT);
+        claims.put(SupportedClaim.ISSUED_AT.getClaim(), Instant.now());
+        claims.put(SupportedClaim.EXPIRATION.getClaim(), Instant.MAX);
+        claims.put(userNameClaim, IDENTITY);
+        return claims;
+    }
+
+    ClientRegistration.Builder getClientRegistrationBuilder() {
+        return 
ClientRegistration.withRegistrationId(OidcRegistrationProperty.REGISTRATION_ID.getProperty())
+                
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+                .clientId(CLIENT_ID)
+                .redirectUri(REDIRECT_URI)
+                .authorizationUri(AUTHORIZATION_URI)
+                .tokenUri(TOKEN_URI);
+    }
+}

Reply via email to