You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2025/01/24 03:06:30 UTC
(nifi) branch main updated: NIFI-13016 Add groups claim mapping from OIDC token for Registry (#9566)
This is an automated email from the ASF dual-hosted git repository.
exceptionfactory 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 0c34095245 NIFI-13016 Add groups claim mapping from OIDC token for Registry (#9566)
0c34095245 is described below
commit 0c3409524598173fb74bc79c1601e30cb9cd8778
Author: Matt Patrick <73...@users.noreply.github.com>
AuthorDate: Thu Jan 23 22:06:24 2025 -0500
NIFI-13016 Add groups claim mapping from OIDC token for Registry (#9566)
Signed-off-by: David Handermann <ex...@apache.org>
---
nifi-registry/nifi-registry-assembly/pom.xml | 1 +
.../src/main/asciidoc/administration-guide.adoc | 3 ++
.../authorization/StandardManagedAuthorizer.java | 39 ++++++++++++++++++----
.../properties/NiFiRegistryProperties.java | 11 ++++++
.../main/resources/conf/nifi-registry.properties | 1 +
.../authentication/AuthenticationResponse.java | 21 ++++++++++++
.../IdentityAuthenticationProvider.java | 9 ++++-
.../authentication/jwt/JwtIdentityProvider.java | 14 +++++---
.../security/authentication/jwt/JwtService.java | 34 +++++++++++++++----
.../oidc/StandardOidcIdentityProvider.java | 6 +++-
10 files changed, 121 insertions(+), 18 deletions(-)
diff --git a/nifi-registry/nifi-registry-assembly/pom.xml b/nifi-registry/nifi-registry-assembly/pom.xml
index 7aa4033a05..3b02b89b39 100644
--- a/nifi-registry/nifi-registry-assembly/pom.xml
+++ b/nifi-registry/nifi-registry-assembly/pom.xml
@@ -215,6 +215,7 @@
<nifi.registry.security.user.oidc.client.id />
<nifi.registry.security.user.oidc.client.secret />
<nifi.registry.security.user.oidc.preferred.jwsalgorithm />
+ <nifi.registry.security.user.oidc.claim.groups>groups</nifi.registry.security.user.oidc.claim.groups>
<!-- nifi.registry.properties: revision management properties -->
<nifi.registry.revisions.enabled>false</nifi.registry.revisions.enabled>
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc
index deced57fc7..5cb8be13c3 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc
@@ -282,6 +282,9 @@ If this value is `none`, NiFi will attempt to validate unsecured/plain tokens. O
JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the discovery URL
|`nifi.registry.security.user.oidc.additional.scopes` | Comma separated scopes that are sent to OpenID Connect Provider in addition to `openid` and `email`.
|`nifi.registry.security.user.oidc.claim.identifying.user` | Claim that identifies the authenticated user. The default value is `email`. Claim names may need to be requested using the `nifi.registry.security.user.oidc.additional.scopes` property
+|`nifi.registry.security.user.oidc.claim.groups` | Name of the ID token claim that contains an array of group names of which the
+user is a member. Application groups must be supplied from a User Group Provider with matching names in order for the
+authorization process to use ID token claim groups. The default value is `groups`.
|==================================================================================================================================================
[[authorization]]
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java
index 974a411316..39b592b8d7 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java
@@ -37,7 +37,10 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.Set;
+import java.util.stream.Collectors;
public class StandardManagedAuthorizer implements ManagedAuthorizer {
@@ -95,19 +98,29 @@ public class StandardManagedAuthorizer implements ManagedAuthorizer {
final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(request.getIdentity());
- final User user = userAndGroups.getUser();
- if (user == null) {
- return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity()));
- }
+ // combine groups from incoming request with groups from UserAndGroups because the request may contain groups from
+ // an external identity provider and the membership may not be maintained within any of the UserGroupProviders
+ final Set<Group> userGroups = new HashSet<>();
+ userGroups.addAll(userAndGroups.getGroups() == null ? Collections.emptySet() : userAndGroups.getGroups());
+ userGroups.addAll(getGroups(request.getGroups()));
- final Set<Group> userGroups = userAndGroups.getGroups();
- if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) {
+ if (containsUser(userAndGroups.getUser(), policy) || containsGroup(userGroups, policy)) {
return AuthorizationResult.approved();
}
return AuthorizationResult.denied(request.getExplanationSupplier().get());
}
+ private Set<Group> getGroups(final Set<String> groupNames) {
+ if (groupNames == null || groupNames.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ return userGroupProvider.getGroups().stream()
+ .filter(group -> groupNames.contains(group.getName()))
+ .collect(Collectors.toSet());
+ }
+
/**
* Determines if the policy contains one of the user's groups.
*
@@ -129,6 +142,20 @@ public class StandardManagedAuthorizer implements ManagedAuthorizer {
return false;
}
+ /**
+ * Determines if the policy contains the user's identifier.
+ *
+ * @param user the user
+ * @param policy the policy
+ * @return true if the user is non-null and the user's identifies is contained in the policy's users
+ */
+ private boolean containsUser(final User user, final AccessPolicy policy) {
+ if (user == null || policy.getUsers().isEmpty()) {
+ return false;
+ }
+ return policy.getUsers().contains(user.getIdentifier());
+ }
+
@Override
public String getFingerprint() throws AuthorizationAccessException {
XMLStreamWriter writer = null;
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
index 83a3109087..fe024e4cf2 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -117,6 +117,7 @@ public class NiFiRegistryProperties extends ApplicationProperties {
public static final String SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM = "nifi.registry.security.user.oidc.preferred.jwsalgorithm";
public static final String SECURITY_USER_OIDC_ADDITIONAL_SCOPES = "nifi.registry.security.user.oidc.additional.scopes";
public static final String SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER = "nifi.registry.security.user.oidc.claim.identifying.user";
+ public static final String SECURITY_USER_OIDC_CLAIM_GROUPS = "nifi.registry.security.user.oidc.claim.groups";
// Revision Management Properties
public static final String REVISIONS_ENABLED = "nifi.registry.revisions.enabled";
@@ -481,6 +482,16 @@ public class NiFiRegistryProperties extends ApplicationProperties {
public String getOidcClaimIdentifyingUser() {
return getProperty(SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER, "email").trim();
}
+ /**
+ * Returns the claim to be used to extract user groups from the OIDC payload.
+ * Claim must be requested by adding the scope for it.
+ * Default is 'groups'.
+ *
+ * @return The claim to be used to extract user groups.
+ */
+ public String getOidcClaimGroups() {
+ return getProperty(SECURITY_USER_OIDC_CLAIM_GROUPS, "groups").trim();
+ }
/**
* Returns the network interface list to use for HTTPS
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
index e674f3c82c..fbd5aa1de2 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
+++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
@@ -106,6 +106,7 @@ nifi.registry.security.user.oidc.read.timeout=${nifi.registry.security.user.oidc
nifi.registry.security.user.oidc.client.id=${nifi.registry.security.user.oidc.client.id}
nifi.registry.security.user.oidc.client.secret=${nifi.registry.security.user.oidc.client.secret}
nifi.registry.security.user.oidc.preferred.jwsalgorithm=${nifi.registry.security.user.oidc.preferred.jwsalgorithm}
+nifi.registry.security.user.oidc.claim.groups=${nifi.registry.security.user.oidc.claim.groups}
# revision management #
# This feature should remain disabled until a future NiFi release that supports the revision API changes
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java
index b8eb721838..7edfdc0a97 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java
@@ -17,6 +17,8 @@
package org.apache.nifi.registry.security.authentication;
import java.io.Serializable;
+import java.util.Collections;
+import java.util.Set;
/**
* Authentication response for a user login attempt.
@@ -27,6 +29,7 @@ public class AuthenticationResponse implements Serializable {
private final String username;
private final long expiration;
private final String issuer;
+ private final Set<String> groups;
/**
* Creates an authentication response. The username and how long the authentication is valid in milliseconds
@@ -37,10 +40,24 @@ public class AuthenticationResponse implements Serializable {
* @param issuer The issuer of the token
*/
public AuthenticationResponse(final String identity, final String username, final long expiration, final String issuer) {
+ this(identity, username, expiration, issuer, Collections.emptySet());
+ }
+
+ /**
+ * Creates an authentication response. The username and how long the authentication is valid in milliseconds
+ *
+ * @param identity The user identity
+ * @param username The username
+ * @param expiration The expiration in milliseconds
+ * @param issuer The issuer of the token
+ * @param groups The user groups
+ */
+ public AuthenticationResponse(final String identity, final String username, final long expiration, final String issuer, final Set<String> groups) {
this.identity = identity;
this.username = username;
this.expiration = expiration;
this.issuer = issuer;
+ this.groups = groups;
}
public String getIdentity() {
@@ -64,6 +81,10 @@ public class AuthenticationResponse implements Serializable {
return expiration;
}
+ public Set<String> getGroups() {
+ return groups;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java
index c89a189961..d697e4bc97 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java
@@ -36,6 +36,7 @@ import org.springframework.security.core.AuthenticationException;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class IdentityAuthenticationProvider implements AuthenticationProvider {
@@ -94,7 +95,7 @@ public class IdentityAuthenticationProvider implements AuthenticationProvider {
return new AuthenticationSuccessToken(new NiFiUserDetails(
new StandardNiFiUser.Builder()
.identity(mappedIdentity)
- .groups(getUserGroups(mappedIdentity))
+ .groups(getUserGroups(mappedIdentity, response))
.clientAddress(requestToken.getClientAddress())
.build()));
}
@@ -112,6 +113,12 @@ public class IdentityAuthenticationProvider implements AuthenticationProvider {
return getUserGroups(authorizer, identity);
}
+ protected Set<String> getUserGroups(final String identity, AuthenticationResponse response) {
+ return Stream
+ .concat(getUserGroups(authorizer, identity).stream(), response.getGroups().stream())
+ .collect(Collectors.toSet());
+ }
+
private static Set<String> getUserGroups(final Authorizer authorizer, final String userIdentity) {
if (authorizer instanceof ManagedAuthorizer) {
final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java
index e6d2dc2a21..ef9ead3e3a 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java
@@ -16,6 +16,8 @@
*/
package org.apache.nifi.registry.web.security.authentication.jwt;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
@@ -34,6 +36,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
@@ -61,16 +64,19 @@ public class JwtIdentityProvider extends BearerAuthIdentityProvider implements I
}
final Object credentials = authenticationRequest.getCredentials();
- String jwtAuthToken = credentials != null && credentials instanceof String ? (String) credentials : null;
-
if (credentials == null) {
logger.info("JWT not found in authenticationRequest credentials, returning null.");
return null;
}
try {
- final String jwtPrincipal = jwtService.getUserIdentityFromToken(jwtAuthToken);
- return new AuthenticationResponse(jwtPrincipal, jwtPrincipal, expiration, issuer);
+ String jwtAuthToken = credentials.toString();
+ final Jws<Claims> jws = jwtService.parseAndValidateToken(jwtAuthToken);
+
+ final String jwtPrincipal = jwtService.getUserIdentityFromToken(jws);
+ final Set<String> groups = jwtService.getUserGroupsFromToken(jws);
+
+ return new AuthenticationResponse(jwtPrincipal, jwtPrincipal, expiration, issuer, groups);
} catch (JwtException e) {
throw new InvalidAuthenticationException(e.getMessage(), e);
}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
index a48c2ba679..030e82bb40 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
@@ -37,7 +37,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
@Service
@@ -48,6 +54,7 @@ public class JwtService {
private static final MacAlgorithm SIGNATURE_ALGORITHM = Jwts.SIG.HS256;
private static final String KEY_ID_CLAIM = "kid";
private static final String USERNAME_CLAIM = "preferred_username";
+ private static final String GROUPS_CLAIM = "groups";
private final KeyService keyService;
@@ -56,7 +63,7 @@ public class JwtService {
this.keyService = keyService;
}
- public String getUserIdentityFromToken(final String base64EncodedToken) throws JwtException {
+ public Jws<Claims> parseAndValidateToken(final String base64EncodedToken) throws JwtException {
// The library representations of the JWT should be kept internal to this service.
try {
final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
@@ -74,14 +81,24 @@ public class JwtService {
if (StringUtils.isEmpty(jws.getPayload().getIssuer())) {
throw new JwtException("No issuer available in token");
}
- return jws.getPayload().getSubject();
+
+ return jws;
} catch (JwtException e) {
- final String errorMessage = "There was an error validating the JWT";
- logger.error(errorMessage, e);
- throw e;
+ throw new JwtException("There was an error validating the JWT", e);
}
}
+ public String getUserIdentityFromToken(final Jws<Claims> jws) throws JwtException {
+ return jws.getPayload().getSubject();
+ }
+
+ public Set<String> getUserGroupsFromToken(final Jws<Claims> jws) throws JwtException {
+ @SuppressWarnings("unchecked")
+ final List<String> groupsString = jws.getPayload().get(GROUPS_CLAIM, ArrayList.class);
+
+ return new HashSet<>(groupsString != null ? groupsString : Collections.emptyList());
+ }
+
private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
try {
return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
@@ -125,11 +142,15 @@ public class JwtService {
authenticationResponse.getUsername(),
authenticationResponse.getIssuer(),
authenticationResponse.getIssuer(),
- authenticationResponse.getExpiration());
+ authenticationResponse.getExpiration(),
+ null);
}
public String generateSignedToken(String identity, String preferredUsername, String issuer, String audience, long expirationMillis) throws JwtException {
+ return this.generateSignedToken(identity, preferredUsername, issuer, audience, expirationMillis, null);
+ }
+ public String generateSignedToken(String identity, String preferredUsername, String issuer, String audience, long expirationMillis, Collection<String> groups) throws JwtException {
if (identity == null || StringUtils.isEmpty(identity)) {
String errorMessage = "Cannot generate a JWT for a token with an empty identity";
errorMessage = issuer != null ? errorMessage + " issued by " + issuer + "." : ".";
@@ -155,6 +176,7 @@ public class JwtService {
.audience().add(audience).and()
.claim(USERNAME_CLAIM, preferredUsername)
.claim(KEY_ID_CLAIM, key.getId())
+ .claim(GROUPS_CLAIM, groups != null ? groups : Collections.EMPTY_LIST)
.issuedAt(now.getTime())
.expiration(expiration.getTime())
.signWith(Keys.hmacShaKeyFor(keyBytes), SIGNATURE_ALGORITHM).compact();
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java
index 8adeb00ae5..ec7972e84e 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java
@@ -401,6 +401,10 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
String identityClaim = properties.getOidcClaimIdentifyingUser();
String identity = claimsSet.getStringClaim(identityClaim);
+ // Attempt to extract groups from the configured claim; default is 'groups'
+ final String groupsClaim = properties.getOidcClaimGroups();
+ final List<String> groups = claimsSet.getStringListClaim(groupsClaim);
+
// 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
@@ -425,7 +429,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
final String issuer = claimsSet.getIssuer().getValue();
// convert into a nifi jwt for retrieval later
- return jwtService.generateSignedToken(identity, identity, issuer, issuer, expiresIn);
+ return jwtService.generateSignedToken(identity, identity, issuer, issuer, expiresIn, groups);
}
private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens) throws IOException {