From 831a11d0b5b118002e9969778d3b164401761a7d Mon Sep 17 00:00:00 2001 From: Malthe Borch Date: Wed, 28 Sep 2022 11:16:50 +0200 Subject: [PATCH] NIFI-7823 Added groups mapping from OIDC token claim This closes #6454 Signed-off-by: David Handermann --- .../org/apache/nifi/util/NiFiProperties.java | 12 +++++++++ .../java/org/apache/nifi/idp/IdpType.java | 1 + .../nifi/web/api/OIDCAccessResource.java | 19 +++++++++++++ .../main/resources/nifi-web-api-context.xml | 1 + .../nifi/web/api/OIDCAccessResourceTest.java | 13 +++++++++ .../oidc/StandardOidcIdentityProvider.java | 17 +++++++++++- .../token/LoginAuthenticationToken.java | 27 ++++++++++++++----- 7 files changed, 83 insertions(+), 7 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 ec6385fe6c..73c3ae20c0 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 @@ -193,6 +193,7 @@ public class NiFiProperties extends ApplicationProperties { 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"; + public static final String NIFI_SECURITY_USER_OIDC_CLAIM_GROUPS = "nifi.security.user.oidc.claim.groups"; public static final String SECURITY_USER_OIDC_FALLBACK_CLAIMS_IDENTIFYING_USER = "nifi.security.user.oidc.fallback.claims.identifying.user"; // apache knox @@ -1145,6 +1146,17 @@ public class NiFiProperties extends ApplicationProperties { 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(NIFI_SECURITY_USER_OIDC_CLAIM_GROUPS, "groups").trim(); + } + /** * Returns the list of fallback claims to be used to identify a user when the configured claim is empty for a user * diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/idp/IdpType.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/idp/IdpType.java index 2ce9b34c66..cc3c5ff0fa 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/idp/IdpType.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-administration/src/main/java/org/apache/nifi/idp/IdpType.java @@ -21,6 +21,7 @@ package org.apache.nifi.idp; */ public enum IdpType { + OIDC, SAML; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java index d13a3eebb0..ac5f4227fb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/OIDCAccessResource.java @@ -35,8 +35,10 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; +import org.apache.nifi.admin.service.IdpUserGroupService; import org.apache.nifi.authentication.exception.AuthenticationNotSupportedException; import org.apache.nifi.authorization.user.NiFiUserUtils; +import org.apache.nifi.idp.IdpType; import org.apache.nifi.security.util.SslContextFactory; import org.apache.nifi.security.util.StandardTlsConfiguration; import org.apache.nifi.security.util.TlsConfiguration; @@ -66,11 +68,14 @@ import javax.ws.rs.core.UriBuilder; import java.io.IOException; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; @Path(OIDCEndpoints.OIDC_ACCESS_ROOT) @@ -94,6 +99,7 @@ public class OIDCAccessResource extends ApplicationResource { private static final boolean LOGGING_IN = true; private OidcService oidcService; + private IdpUserGroupService idpUserGroupService; private BearerTokenProvider bearerTokenProvider; public OIDCAccessResource() { @@ -156,6 +162,12 @@ public class OIDCAccessResource extends ApplicationResource { // store the NiFi token oidcService.storeJwt(oidcRequestIdentifier, bearerToken); + + Set groups = oidcToken.getAuthorities().stream().map(authority -> authority.getAuthority()).collect(Collectors.collectingAndThen( + Collectors.toSet(), + Collections::unmodifiableSet + )); + idpUserGroupService.replaceUserGroups(oidcToken.getName(), IdpType.OIDC, groups); } catch (final Exception e) { logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e); @@ -236,6 +248,9 @@ public class OIDCAccessResource extends ApplicationResource { applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER); logger.debug("Invalidated JWT for user [{}]", mappedUserIdentity); + idpUserGroupService.deleteUserGroups(mappedUserIdentity); + logger.debug("Deleted user groups for user [{}]", mappedUserIdentity); + // Get the oidc discovery url String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl(); @@ -558,6 +573,10 @@ public class OIDCAccessResource extends ApplicationResource { this.oidcService = oidcService; } + public void setIdpUserGroupService(IdpUserGroupService idpUserGroupService) { + this.idpUserGroupService = idpUserGroupService; + } + public void setBearerTokenProvider(final BearerTokenProvider bearerTokenProvider) { this.bearerTokenProvider = bearerTokenProvider; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml index a59ad30d63..6c38f0c029 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml @@ -629,6 +629,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/OIDCAccessResourceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/OIDCAccessResourceTest.java index 710def6c2e..8bd9cabce3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/OIDCAccessResourceTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/OIDCAccessResourceTest.java @@ -22,6 +22,7 @@ import com.nimbusds.oauth2.sdk.ErrorObject; import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; import com.nimbusds.openid.connect.sdk.AuthenticationResponse; import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse; +import org.apache.nifi.admin.service.IdpUserGroupService; import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider; import org.apache.nifi.web.security.jwt.provider.StandardBearerTokenProvider; import org.apache.nifi.web.security.oidc.OidcService; @@ -29,13 +30,17 @@ import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.junit.Test; import org.mockito.Mockito; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.apache.nifi.idp.IdpType.OIDC; import static org.apache.nifi.web.security.cookie.ApplicationCookieName.OIDC_REQUEST_IDENTIFIER; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; @@ -43,6 +48,7 @@ import static org.mockito.ArgumentMatchers.any; public class OIDCAccessResourceTest { final static String REQUEST_IDENTIFIER = "an-identifier"; + final static String OIDC_LOGIN_FAILURE_MESSAGE = "Unsuccessful login attempt."; @Test @@ -53,8 +59,11 @@ public class OIDCAccessResourceTest { Mockito.when(mockRequest.getCookies()).thenReturn(cookies); OidcService oidcService = Mockito.mock(OidcService.class); MockOIDCAccessResource accessResource = new MockOIDCAccessResource(oidcService, true); + IdpUserGroupService idpUserGroupService = Mockito.mock(IdpUserGroupService.class); + accessResource.setIdpUserGroupService(idpUserGroupService); accessResource.oidcCallback(mockRequest, mockResponse); Mockito.verify(oidcService).storeJwt(any(String.class), any(String.class)); + Mockito.verify(idpUserGroupService).replaceUserGroups(MockOIDCAccessResource.IDENTITY, OIDC, Stream.of(MockOIDCAccessResource.ROLE).collect(Collectors.toSet())); } @Test @@ -83,6 +92,8 @@ public class OIDCAccessResourceTest { public class MockOIDCAccessResource extends OIDCAccessResource { final static String BEARER_TOKEN = "bearer_token"; + final static String IDENTITY = "identity"; + final static String ROLE = "role"; final static String AUTHORIZATION_CODE = "authorization_code"; final static String CALLBACK_URL = "https://nifi.apache.org/nifi-api/access/oidc/callback"; final static String RESOURCE_URI = "resource_uri"; @@ -95,6 +106,8 @@ public class OIDCAccessResourceTest { setOidcService(oidcService); setBearerTokenProvider(bearerTokenProvider); final LoginAuthenticationToken token = Mockito.mock(LoginAuthenticationToken.class); + Mockito.when(token.getName()).thenReturn(IDENTITY); + Mockito.when(token.getAuthorities()).thenReturn(Stream.of(new SimpleGrantedAuthority(ROLE)).collect(Collectors.toSet())); Mockito.when(oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(any(AuthorizationGrant.class))).thenReturn(token); } 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 a2b4ba2be5..82837ba75e 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 @@ -66,6 +66,7 @@ import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.security.token.LoginAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import javax.net.ssl.SSLContext; import java.io.IOException; @@ -73,9 +74,11 @@ import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -89,6 +92,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider { private final String EMAIL_CLAIM = "email"; private final NiFiProperties properties; + private OIDCProviderMetadata oidcProviderMetadata; private int oidcConnectTimeout; private int oidcReadTimeout; @@ -443,6 +447,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' + String groupsClaim = properties.getOidcClaimGroups(); + List 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 @@ -474,8 +482,15 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider { final Date expiration = claimsSet.getExpirationTime(); final long expiresIn = expiration.getTime() - now.getTimeInMillis(); + Set authorities = groups != null ? groups.stream().map( + group -> new SimpleGrantedAuthority(group)).collect( + Collectors.collectingAndThen( + Collectors.toSet(), + Collections::unmodifiableSet + )) : null; + return new LoginAuthenticationToken( - identity, identity, expiresIn, claimsSet.getIssuer().getValue()); + identity, identity, expiresIn, claimsSet.getIssuer().getValue(), authorities); } private OIDCTokens getOidcTokens(OIDCTokenResponse response) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java index c0e895c58e..33f4c3bd6d 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/token/LoginAuthenticationToken.java @@ -18,8 +18,10 @@ package org.apache.nifi.web.security.token; import org.apache.nifi.security.util.CertificateUtils; import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; import java.time.Instant; +import java.util.Collection; /** * This is an Authentication Token for logging in. Once a user is authenticated, they can be issued an ID token. @@ -39,19 +41,32 @@ public class LoginAuthenticationToken extends AbstractAuthenticationToken { * @param issuer The IdentityProvider implementation that generated this token */ public LoginAuthenticationToken(final String identity, final long expiration, final String issuer) { - this(identity, null, expiration, issuer); + this(identity, null, expiration, issuer, null); } /** * Creates a representation of the authentication token for a user. * - * @param identity The unique identifier for this user (cannot be null or empty) - * @param username The preferred username for this user - * @param expiration The relative time to expiration in milliseconds - * @param issuer The IdentityProvider implementation that generated this token + * @param identity The unique identifier for this user (cannot be null or empty) + * @param username The preferred username for this user + * @param expiration The relative time to expiration in milliseconds + * @param issuer The IdentityProvider implementation that generated this token */ public LoginAuthenticationToken(final String identity, final String username, final long expiration, final String issuer) { - super(null); + this(identity, username, expiration, issuer, null); + } + + /** + * Creates a representation of the authentication token for a user. + * + * @param identity The unique identifier for this user (cannot be null or empty) + * @param username The preferred username for this user + * @param expiration The relative time to expiration in milliseconds + * @param issuer The IdentityProvider implementation that generated this token + * @param authorities The authorities that have been granted this token. + */ + public LoginAuthenticationToken(final String identity, final String username, final long expiration, final String issuer, Collection authorities) { + super(authorities); setAuthenticated(true); this.identity = identity; this.username = username;