NIFI-7823 Added groups mapping from OIDC token claim

This closes #6454

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Malthe Borch 2022-09-28 11:16:50 +02:00 committed by exceptionfactory
parent d64574b565
commit 831a11d0b5
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
7 changed files with 83 additions and 7 deletions

View File

@ -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
*

View File

@ -21,6 +21,7 @@ package org.apache.nifi.idp;
*/
public enum IdpType {
OIDC,
SAML;
}

View File

@ -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<String> 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;
}

View File

@ -629,6 +629,7 @@
</bean>
<bean id="oidcResource" class="org.apache.nifi.web.api.OIDCAccessResource" scope="singleton">
<property name="oidcService" ref="oidcService"/>
<property name="idpUserGroupService" ref="idpUserGroupService" />
<property name="properties" ref="nifiProperties"/>
<property name="bearerTokenProvider" ref="bearerTokenProvider"/>
</bean>

View File

@ -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);
}

View File

@ -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<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
@ -474,8 +482,15 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
final Date expiration = claimsSet.getExpirationTime();
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
Set<SimpleGrantedAuthority> 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) {

View File

@ -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<? extends GrantedAuthority> authorities) {
super(authorities);
setAuthenticated(true);
this.identity = identity;
this.username = username;