You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by jg...@apache.org on 2023/07/15 15:04:57 UTC
[nifi] branch support/nifi-1.x updated: NIFI-11781 Corrected OIDC Claim Identity Processing
This is an automated email from the ASF dual-hosted git repository.
jgresock pushed a commit to branch support/nifi-1.x
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/support/nifi-1.x by this push:
new 1b0b37e6e4 NIFI-11781 Corrected OIDC Claim Identity Processing
1b0b37e6e4 is described below
commit 1b0b37e6e4ad450e4cd9b79ec37f6682c3d58e11
Author: exceptionfactory <ex...@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 <jg...@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);
+ }
+}