mirror of https://github.com/apache/nifi.git
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:
parent
d64574b565
commit
831a11d0b5
|
@ -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
|
||||
*
|
||||
|
|
|
@ -21,6 +21,7 @@ package org.apache.nifi.idp;
|
|||
*/
|
||||
public enum IdpType {
|
||||
|
||||
OIDC,
|
||||
SAML;
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue