Skip to content

Commit

Permalink
NIFI-7924 Add fallback claims for identifying user to OIDC provider
Browse files Browse the repository at this point in the history
This closes apache#4630

Signed-off-by: Joey Frazee <jfrazee@apache.org>
  • Loading branch information
sjyang18 authored and driesva committed Mar 19, 2021
1 parent 1af9e5d commit 01c9a86
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 3 deletions.
Expand Up @@ -169,6 +169,7 @@ public abstract class NiFiProperties {
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 SECURITY_USER_OIDC_FALLBACK_CLAIMS_IDENTIFYING_USER = "nifi.security.user.oidc.fallback.claims.identifying.user";

// apache knox
public static final String SECURITY_USER_KNOX_URL = "nifi.security.user.knox.url";
Expand Down Expand Up @@ -1011,6 +1012,21 @@ public String getOidcClaimIdentifyingUser() {
return getProperty(SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER, "email").trim();
}

/**
* Returns the list of fallback claims to be used to identify a user when the configured claim is empty for a user
*
* @return The list of fallback claims to be used to identify the user
*/
public List<String> getOidcFallbackClaimsIdentifyingUser() {
String rawProperty = getProperty(SECURITY_USER_OIDC_FALLBACK_CLAIMS_IDENTIFYING_USER, "").trim();
if (StringUtils.isBlank(rawProperty)) {
return Collections.emptyList();
} else {
List<String> fallbackClaims = Arrays.asList(rawProperty.split(","));
return fallbackClaims.stream().map(String::trim).filter(s->!s.isEmpty()).collect(Collectors.toList());
}
}

public boolean shouldSendServerVersion() {
return Boolean.parseBoolean(getProperty(WEB_SHOULD_SEND_SERVER_VERSION, DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION));
}
Expand Down
1 change: 1 addition & 0 deletions nifi-docs/src/main/asciidoc/administration-guide.adoc
Expand Up @@ -373,6 +373,7 @@ 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.security.user.oidc.additional.scopes` | Comma separated scopes that are sent to OpenId Connect Provider in addition to `openid` and `email`.
|`nifi.security.user.oidc.claim.identifying.user` | Claim that identifies the user to be logged in; default is `email`. May need to be requested via the `nifi.security.user.oidc.additional.scopes` before usage.
|`nifi.security.user.oidc.fallback.claims.identifying.user` | Comma separated possible fallback claims used to identify the user in case `nifi.security.user.oidc.claim.identifying.user` claim is not present for the login user.
|==================================================================================================================================================

[[saml]]
Expand Down
Expand Up @@ -164,6 +164,7 @@
<nifi.security.user.oidc.preferred.jwsalgorithm />
<nifi.security.user.oidc.additional.scopes />
<nifi.security.user.oidc.claim.identifying.user />
<nifi.security.user.oidc.fallback.claims.identifying.user />

<!-- nifi.properties: apache knox -->
<nifi.security.user.knox.url />
Expand Down
Expand Up @@ -178,6 +178,7 @@ nifi.security.user.oidc.client.secret=${nifi.security.user.oidc.client.secret}
nifi.security.user.oidc.preferred.jwsalgorithm=${nifi.security.user.oidc.preferred.jwsalgorithm}
nifi.security.user.oidc.additional.scopes=${nifi.security.user.oidc.additional.scopes}
nifi.security.user.oidc.claim.identifying.user=${nifi.security.user.oidc.claim.identifying.user}
nifi.security.user.oidc.fallback.claims.identifying.user=${nifi.security.user.oidc.fallback.claims.identifying.user}

# Apache Knox SSO Properties #
nifi.security.user.knox.url=${nifi.security.user.knox.url}
Expand Down
Expand Up @@ -439,8 +439,16 @@ private LoginAuthenticationToken convertOIDCTokenToLoginAuthenticationToken(OIDC
identity = claimsSet.getStringClaim(EMAIL_CLAIM);
logger.info("The 'email' claim was present. Using that claim to avoid extra remote call");
} else {
identity = retrieveIdentityFromUserInfoEndpoint(oidcTokens);
logger.info("Retrieved identity from UserInfo endpoint");
final List<String> fallbackClaims = properties.getOidcFallbackClaimsIdentifyingUser();
for (String fallbackClaim : fallbackClaims) {
if (availableClaims.contains(fallbackClaim)) {
identity = claimsSet.getStringClaim(fallbackClaim);
break;
}
}
if (StringUtils.isBlank(identity)) {
identity = retrieveIdentityFromUserInfoEndpoint(oidcTokens);
}
}
}

Expand Down
Expand Up @@ -411,10 +411,28 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
assert exp <= System.currentTimeMillis() + 10_000
}

@Test
void testConvertOIDCTokenToLoginAuthenticationTokenShouldHandleNoEmailClaimHasFallbackClaims() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email", "getOidcFallbackClaimsIdentifyingUser": ["upn"] ])
String expectedUpn = "xxx@aaddomain";

OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null, "upn": expectedUpn])
logger.info("OIDC Token Response with no email and upn: ${mockResponse.dump()}")

String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
logger.info("NiFi token create with upn: ${loginToken}")
// Assert
// Split JWT into components and decode Base64 to JSON
def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
logger.info("Token contents: ${contents} | Expiration: ${expiration}")
assert contents =~ "LoginAuthenticationToken for ${expectedUpn} issued by https://accounts\\.issuer\\.com expiring at"
}

@Test
void testConvertOIDCTokenToLoginAuthNTokenShouldHandleBlankIdentityAndNoEmailClaim() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim", "getOidcFallbackClaimsIdentifyingUser": [] ])

OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null])
logger.info("OIDC Token Response: ${mockResponse.dump()}")
Expand Down

0 comments on commit 01c9a86

Please sign in to comment.