NIFI-7332 Added method to log available claim names from the ID provider response when the OIDC Identifying User claim is not found. Revised log message to print available claims.

Added new StandardOidcIdentityProviderGroovyTest file.
Updated deprecated methods in StandardOidcIdentityProvider. Changed log output to print all available claim names from JWTClaimsSet. Added unit test.
Added comments in getAvailableClaims() method.
Fixed typos in NiFi Docs Admin Guide.
Added license to Groovy test.
Fixed a checkstyle error.
Refactor exchangeAuthorizationCode method.
Added unit tests.
Verified all unit tests added so far are passing.
Refactored code. Added unit tests.
Refactored OIDC provider to decouple constructor & network-dependent initialization.
Added unit tests.
Added unit tests.
Refactored OIDC provider to separately authorize the client. Added unit tests.
Added unit tests.

NIFI-7332 Refactored exchangeAuthorizationCode method to separately retrieve the NiFi JWT.

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #4344.
This commit is contained in:
mtien 2020-05-20 18:14:57 -07:00 committed by Kevin Doran
parent f040c6aadb
commit 43fb57e7bb
No known key found for this signature in database
GPG Key ID: C317DD14DC787438
5 changed files with 854 additions and 163 deletions

View File

@ -367,10 +367,9 @@ To enable authentication via OpenId Connect the following properties must be con
|`nifi.security.user.oidc.read.timeout` | Read timeout when communicating with the OpenId Connect Provider.
|`nifi.security.user.oidc.client.id` | The client id for NiFi after registration with the OpenId Connect Provider.
|`nifi.security.user.oidc.client.secret` | The client secret for NiFi after registration with the OpenId Connect Provider.
|`nifi.security.user.oidc.preferred.jwsalgorithm` | The preferred algorithm for for validating identity tokens. If this value is blank, it will default to `RS256` which is required to be supported
|`nifi.security.user.oidc.preferred.jwsalgorithm` | The preferred algorithm for validating identity tokens. If this value is blank, it will default to `RS256` which is required to be supported
|`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.
by the OpenId Connect Provider according to the specification. If this value is `HS256`, `HS384`, or `HS512`, NiFi will attempt to validate HMAC protected tokens using the specified client secret.
|`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 by the OpenId Connect Provider according to the specification. If this value is `HS256`, `HS384`, or `HS512`, NiFi will attempt to validate HMAC protected tokens using the specified client secret.
If this value is `none`, NiFi will attempt to validate unsecured/plain tokens. Other values for this algorithm will attempt to parse as an RSA or EC algorithm to be used in conjunction with the
JSON Web Key (JWK) provided through the jwks_uri in the metadata found at the discovery URL.
|==================================================================================================================================================

View File

@ -20,7 +20,6 @@ package org.apache.nifi.web.security.oidc;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
import java.io.IOException;
import java.net.URI;
@ -28,6 +27,11 @@ public interface OidcIdentityProvider {
String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED = "OpenId Connect support is not configured";
/**
* Initializes the provider.
*/
void initializeProvider();
/**
* Returns whether OIDC support is enabled.
*

View File

@ -21,8 +21,6 @@ import com.google.common.cache.CacheBuilder;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.State;
import org.apache.nifi.web.security.util.CacheKey;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
@ -31,6 +29,7 @@ import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.nifi.web.security.util.CacheKey;
import static org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED;
@ -66,6 +65,7 @@ public class OidcService {
throw new RuntimeException("The OidcIdentityProvider must be specified.");
}
identityProvider.initializeProvider();
this.identityProvider = identityProvider;
this.stateLookupForPendingRequests = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
this.jwtLookupForCompletedRequests = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build();
@ -198,7 +198,7 @@ public class OidcService {
}
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
final String nifiJwt = identityProvider.exchangeAuthorizationCode(authorizationGrant);
final String nifiJwt = retrieveNifiJwt(authorizationGrant);
try {
// cache the jwt for later retrieval
@ -213,6 +213,17 @@ public class OidcService {
}
}
/**
* Exchange the authorization code to retrieve a NiFi JWT.
*
* @param authorizationGrant authorization grant
* @return NiFi JWT
* @throws IOException exceptional case for communication error with the OpenId Connect provider
*/
public String retrieveNifiJwt(final AuthorizationGrant authorizationGrant) throws IOException {
return identityProvider.exchangeAuthorizationCode(authorizationGrant);
}
/**
* Returns the resulting JWT for the given request identifier. Will return null if the request
* identifier is not associated with a JWT or if the login sequence was not completed before

View File

@ -25,6 +25,7 @@ import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Request;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenErrorResponse;
import com.nimbusds.oauth2.sdk.TokenRequest;
@ -55,9 +56,12 @@ import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import net.minidev.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.authentication.exception.IdentityAccessException;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.jwt.JwtService;
@ -72,6 +76,7 @@ import org.slf4j.LoggerFactory;
public class StandardOidcIdentityProvider implements OidcIdentityProvider {
private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProvider.class);
private final String EMAIL_CLAIM = "email";
private NiFiProperties properties;
private JwtService jwtService;
@ -91,114 +96,146 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
public StandardOidcIdentityProvider(final JwtService jwtService, final NiFiProperties properties) {
this.properties = properties;
this.jwtService = jwtService;
}
/**
* Loads OIDC configuration values from {@link NiFiProperties}, connects to external OIDC provider, and retrieves
* and validates provider metadata.
*/
@Override
public void initializeProvider() {
// attempt to process the oidc configuration if configured
if (properties.isOidcEnabled()) {
if (properties.isLoginIdentityProviderEnabled() || properties.isKnoxSsoEnabled()) {
throw new RuntimeException("OpenId Connect support cannot be enabled if the Login Identity Provider or Apache Knox SSO is configured.");
}
if (!properties.isOidcEnabled()) {
logger.warn("The OIDC provider is not configured or enabled");
return;
}
// oidc connect timeout
final String rawConnectTimeout = properties.getOidcConnectTimeout();
try {
oidcConnectTimeout = (int) FormatUtils.getTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
} catch (final Exception e) {
logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
NiFiProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT, rawConnectTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
oidcConnectTimeout = (int) FormatUtils.getTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS);
}
validateOIDCConfiguration();
// oidc read timeout
final String rawReadTimeout = properties.getOidcReadTimeout();
try {
oidcReadTimeout = (int) FormatUtils.getTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
} catch (final Exception e) {
logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
NiFiProperties.SECURITY_USER_OIDC_READ_TIMEOUT, rawReadTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
oidcReadTimeout = (int) FormatUtils.getTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT, TimeUnit.MILLISECONDS);
}
try {
// retrieve the oidc provider metadata
oidcProviderMetadata = retrieveOidcProviderMetadata(properties.getOidcDiscoveryUrl());
} catch (IOException | ParseException e) {
throw new RuntimeException("Unable to retrieve OpenId Connect Provider metadata from: " + properties.getOidcDiscoveryUrl(), e);
}
// client id
final String rawClientId = properties.getOidcClientId();
if (StringUtils.isBlank(rawClientId)) {
throw new RuntimeException("Client ID is required when configuring an OIDC Provider.");
}
clientId = new ClientID(rawClientId);
validateOIDCProviderMetadata();
}
// client secret
final String rawClientSecret = properties.getOidcClientSecret();
if (StringUtils.isBlank(rawClientSecret)) {
throw new RuntimeException("Client secret is required when configuring an OIDC Provider.");
}
clientSecret = new Secret(rawClientSecret);
/**
* Validates the retrieved OIDC provider metadata.
*/
private void validateOIDCProviderMetadata() {
// ensure the authorization endpoint is present
if (oidcProviderMetadata.getAuthorizationEndpointURI() == null) {
throw new RuntimeException("OpenId Connect Provider metadata does not contain an Authorization Endpoint.");
}
try {
// retrieve the oidc provider metadata
oidcProviderMetadata = retrieveOidcProviderMetadata(properties.getOidcDiscoveryUrl());
} catch (IOException | ParseException e) {
throw new RuntimeException("Unable to retrieve OpenId Connect Provider metadata from: " + properties.getOidcDiscoveryUrl(), e);
}
// ensure the token endpoint is present
if (oidcProviderMetadata.getTokenEndpointURI() == null) {
throw new RuntimeException("OpenId Connect Provider metadata does not contain a Token Endpoint.");
}
// ensure the authorization endpoint is present
if (oidcProviderMetadata.getAuthorizationEndpointURI() == null) {
throw new RuntimeException("OpenId Connect Provider metadata does not contain an Authorization Endpoint.");
}
// ensure the oidc provider supports basic or post client auth
List<ClientAuthenticationMethod> clientAuthenticationMethods = oidcProviderMetadata.getTokenEndpointAuthMethods();
logger.info("OpenId Connect: Available clientAuthenticationMethods {} ", clientAuthenticationMethods);
if (clientAuthenticationMethods == null || clientAuthenticationMethods.isEmpty()) {
clientAuthenticationMethods = new ArrayList<>();
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
oidcProviderMetadata.setTokenEndpointAuthMethods(clientAuthenticationMethods);
logger.warn("OpenId Connect: ClientAuthenticationMethods is null, Setting clientAuthenticationMethods as CLIENT_SECRET_BASIC");
} else if (!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
&& !clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
throw new RuntimeException(String.format("OpenId Connect Provider does not support %s or %s",
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(),
ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()));
}
// ensure the token endpoint is present
if (oidcProviderMetadata.getTokenEndpointURI() == null) {
throw new RuntimeException("OpenId Connect Provider metadata does not contain a Token Endpoint.");
}
// extract the supported json web signature algorithms
final List<JWSAlgorithm> allowedAlgorithms = oidcProviderMetadata.getIDTokenJWSAlgs();
if (allowedAlgorithms == null || allowedAlgorithms.isEmpty()) {
throw new RuntimeException("The OpenId Connect Provider does not support any JWS algorithms.");
}
// ensure the oidc provider supports basic or post client auth
List<ClientAuthenticationMethod> clientAuthenticationMethods = oidcProviderMetadata.getTokenEndpointAuthMethods();
logger.info("OpenId Connect: Available clientAuthenticationMethods {} ", clientAuthenticationMethods);
if (clientAuthenticationMethods == null || clientAuthenticationMethods.isEmpty()) {
clientAuthenticationMethods = new ArrayList<>();
clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
oidcProviderMetadata.setTokenEndpointAuthMethods(clientAuthenticationMethods);
logger.warn("OpenId Connect: ClientAuthenticationMethods is null, Setting clientAuthenticationMethods as CLIENT_SECRET_BASIC");
} else if (!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
&& !clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
throw new RuntimeException(String.format("OpenId Connect Provider does not support %s or %s",
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(),
ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()));
}
try {
// get the preferred json web signature algorithm
final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
// extract the supported json web signature algorithms
final List<JWSAlgorithm> allowedAlgorithms = oidcProviderMetadata.getIDTokenJWSAlgs();
if (allowedAlgorithms == null || allowedAlgorithms.isEmpty()) {
throw new RuntimeException("The OpenId Connect Provider does not support any JWS algorithms.");
}
try {
// get the preferred json web signature algorithm
final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
final JWSAlgorithm preferredJwsAlgorithm;
if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
preferredJwsAlgorithm = JWSAlgorithm.RS256;
final JWSAlgorithm preferredJwsAlgorithm;
if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
preferredJwsAlgorithm = JWSAlgorithm.RS256;
} else {
if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
preferredJwsAlgorithm = null;
} else {
if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
preferredJwsAlgorithm = null;
} else {
preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
}
preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
}
if (preferredJwsAlgorithm == null) {
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId);
} else if (JWSAlgorithm.HS256.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS384.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS512.equals(preferredJwsAlgorithm)) {
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, clientSecret);
} else {
final ResourceRetriever retriever = new DefaultResourceRetriever(oidcConnectTimeout, oidcReadTimeout);
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, oidcProviderMetadata.getJWKSetURI().toURL(), retriever);
}
} catch (final Exception e) {
throw new RuntimeException("Unable to create the ID token validator for the configured OpenId Connect Provider: " + e.getMessage(), e);
}
if (preferredJwsAlgorithm == null) {
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId);
} else if (JWSAlgorithm.HS256.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS384.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS512.equals(preferredJwsAlgorithm)) {
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, clientSecret);
} else {
final ResourceRetriever retriever = new DefaultResourceRetriever(oidcConnectTimeout, oidcReadTimeout);
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, oidcProviderMetadata.getJWKSetURI().toURL(), retriever);
}
} catch (final Exception e) {
throw new RuntimeException("Unable to create the ID token validator for the configured OpenId Connect Provider: " + e.getMessage(), e);
}
}
/**
* Loads the initial configuration values relating to the OIDC provider from the class {@link NiFiProperties} and populates the individual fields.
*/
private void validateOIDCConfiguration() {
if (properties.isLoginIdentityProviderEnabled() || properties.isKnoxSsoEnabled()) {
throw new RuntimeException("OpenId Connect support cannot be enabled if the Login Identity Provider or Apache Knox SSO is configured.");
}
// oidc connect timeout
final String rawConnectTimeout = properties.getOidcConnectTimeout();
try {
oidcConnectTimeout = (int) FormatUtils.getPreciseTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS);
} catch (final Exception e) {
logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
NiFiProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT, rawConnectTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT);
oidcConnectTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS);
}
// oidc read timeout
final String rawReadTimeout = properties.getOidcReadTimeout();
try {
oidcReadTimeout = (int) FormatUtils.getPreciseTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS);
} catch (final Exception e) {
logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'",
NiFiProperties.SECURITY_USER_OIDC_READ_TIMEOUT, rawReadTimeout, NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT);
oidcReadTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT, TimeUnit.MILLISECONDS);
}
// client id
final String rawClientId = properties.getOidcClientId();
if (StringUtils.isBlank(rawClientId)) {
throw new RuntimeException("Client ID is required when configuring an OIDC Provider.");
}
clientId = new ClientID(rawClientId);
// client secret
final String rawClientSecret = properties.getOidcClientSecret();
if (StringUtils.isBlank(rawClientSecret)) {
throw new RuntimeException("Client secret is required when configuring an OIDC Provider.");
}
clientSecret = new Secret(rawClientSecret);
}
/**
* Returns the retrieved OIDC provider metadata from the external provider.
*
* @param discoveryUri the remote OIDC provider endpoint for service discovery
* @return the provider metadata
* @throws IOException if there is a problem connecting to the remote endpoint
* @throws ParseException if there is a problem parsing the response
*/
private OIDCProviderMetadata retrieveOidcProviderMetadata(final String discoveryUri) throws IOException, ParseException {
final URL url = new URL(discoveryUri);
final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.GET, url);
@ -243,7 +280,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
Scope scope = new Scope("openid", "email");
Scope scope = new Scope("openid", EMAIL_CLAIM);
for (String additionalScope : properties.getOidcAdditionalScopes()) {
// Scope automatically prevents duplicated entries
@ -264,81 +301,137 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
@Override
public String exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws IOException {
// Check if OIDC is enabled
if (!isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
final ClientAuthentication clientAuthentication;
if (oidcProviderMetadata.getTokenEndpointAuthMethods().contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
clientAuthentication = new ClientSecretPost(clientId, clientSecret);
} else {
clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
}
// Build ClientAuthentication
final ClientAuthentication clientAuthentication = createClientAuthentication();
try {
// build the token request
final TokenRequest request = new TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication, authorizationGrant);
final HTTPRequest tokenHttpRequest = request.toHTTPRequest();
tokenHttpRequest.setConnectTimeout(oidcConnectTimeout);
tokenHttpRequest.setReadTimeout(oidcReadTimeout);
// Build the token request
final HTTPRequest tokenHttpRequest = createTokenHTTPRequest(authorizationGrant, clientAuthentication);
return authorizeClient(tokenHttpRequest);
// get the token response
final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
if (response.indicatesSuccess()) {
final OIDCTokenResponse oidcTokenResponse = (OIDCTokenResponse) response;
final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens();
final JWT oidcJwt = oidcTokens.getIDToken();
// validate the token - no nonce required for authorization code flow
final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null);
// attempt to extract the configured claim to access the user's identity; default is 'email'
String identity = claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser());
if (StringUtils.isBlank(identity)) {
// explicitly try to get the identity from the UserInfo endpoint with the configured claim
logger.warn("Failed to obtain the identity of the user with the claim '" +
properties.getOidcClaimIdentifyingUser() + "'. The claim is configured incorrectly. Will attempt to obtain the identity from the UserInfo endpoint.");
// extract the bearer access token
final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken();
if (bearerAccessToken == null) {
throw new IllegalStateException("No access token found in the ID tokens");
}
// invoke the UserInfo endpoint
identity = lookupIdentityInUserInfo(bearerAccessToken);
}
// extract expiration details from the claims set
final Calendar now = Calendar.getInstance();
final Date expiration = claimsSet.getExpirationTime();
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
// convert into a nifi jwt for retrieval later
final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(identity, identity, expiresIn,
claimsSet.getIssuer().getValue());
return jwtService.generateSignedToken(loginToken);
} else {
final TokenErrorResponse errorResponse = (TokenErrorResponse) response;
throw new RuntimeException("An error occurred while invoking the Token endpoint: " +
errorResponse.getErrorObject().getDescription());
}
} catch (final ParseException | JOSEException | BadJOSEException e) {
} catch (final ParseException | JOSEException | BadJOSEException | java.text.ParseException e) {
throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage());
}
}
private String lookupIdentityInUserInfo(final BearerAccessToken bearerAccessToken) throws IOException {
try {
// build the user request
final UserInfoRequest request = new UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(), bearerAccessToken);
final HTTPRequest tokenHttpRequest = request.toHTTPRequest();
tokenHttpRequest.setConnectTimeout(oidcConnectTimeout);
tokenHttpRequest.setReadTimeout(oidcReadTimeout);
private String authorizeClient(HTTPRequest tokenHttpRequest) throws ParseException, IOException, BadJOSEException, JOSEException, java.text.ParseException {
// Get the token response
final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
// Handle success
if (response.indicatesSuccess()) {
return convertOIDCTokenToNiFiToken((OIDCTokenResponse) response);
} else {
// If the response was not successful
final TokenErrorResponse errorResponse = (TokenErrorResponse) response;
throw new RuntimeException("An error occurred while invoking the Token endpoint: " +
errorResponse.getErrorObject().getDescription());
}
}
private String convertOIDCTokenToNiFiToken(OIDCTokenResponse response) throws BadJOSEException, JOSEException, java.text.ParseException, IOException {
final OIDCTokenResponse oidcTokenResponse = response;
final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens();
final JWT oidcJwt = oidcTokens.getIDToken();
// validate the token - no nonce required for authorization code flow
final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null);
// attempt to extract the configured claim to access the user's identity; default is 'email'
String identityClaim = properties.getOidcClaimIdentifyingUser();
String identity = claimsSet.getStringClaim(identityClaim);
// 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
List<String> availableClaims = getAvailableClaims(oidcJwt.getJWTClaimsSet());
logger.warn("Failed to obtain the identity of the user with the claim '{}'. The available claims on " +
"the OIDC response are: {}. Will attempt to obtain the identity from secondary sources",
identityClaim, availableClaims);
// If the desired user claim was not "email" and "email" is present, use that
if (!identityClaim.equalsIgnoreCase(EMAIL_CLAIM) && availableClaims.contains(EMAIL_CLAIM)) {
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");
}
}
// extract expiration details from the claims set
final Calendar now = Calendar.getInstance();
final Date expiration = claimsSet.getExpirationTime();
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
// convert into a nifi jwt for retrieval later
final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(identity, identity, expiresIn,
claimsSet.getIssuer().getValue());
return jwtService.generateSignedToken(loginToken);
}
private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens) throws IOException {
// explicitly try to get the identity from the UserInfo endpoint with the configured claim
// extract the bearer access token
final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken();
if (bearerAccessToken == null) {
throw new IllegalStateException("No access token found in the ID tokens");
}
// invoke the UserInfo endpoint
HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken);
return lookupIdentityInUserInfo(userInfoRequest);
}
private HTTPRequest createTokenHTTPRequest(AuthorizationGrant authorizationGrant, ClientAuthentication clientAuthentication) {
final TokenRequest request = new TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication, authorizationGrant);
return formHTTPRequest(request);
}
private HTTPRequest createUserInfoRequest(BearerAccessToken bearerAccessToken) {
final UserInfoRequest request = new UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(), bearerAccessToken);
return formHTTPRequest(request);
}
private HTTPRequest formHTTPRequest(Request request) {
final HTTPRequest httpRequest = request.toHTTPRequest();
httpRequest.setConnectTimeout(oidcConnectTimeout);
httpRequest.setReadTimeout(oidcReadTimeout);
return httpRequest;
}
private ClientAuthentication createClientAuthentication() {
final ClientAuthentication clientAuthentication;
List<ClientAuthenticationMethod> authMethods = oidcProviderMetadata.getTokenEndpointAuthMethods();
if (authMethods != null && authMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
clientAuthentication = new ClientSecretPost(clientId, clientSecret);
} else {
clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
}
return clientAuthentication;
}
private static List<String> getAvailableClaims(JWTClaimsSet claimSet) {
// Get the claims available in the ID token response
List<String> presentClaims = claimSet.getClaims().entrySet().stream()
// Check claim values are not empty
.filter(e -> StringUtils.isNotBlank(e.getValue().toString()))
// If not empty, put claim name in a map
.map(Map.Entry::getKey)
.sorted()
.collect(Collectors.toList());
return presentClaims;
}
private String lookupIdentityInUserInfo(final HTTPRequest userInfoHttpRequest) throws IOException {
try {
// send the user request
final UserInfoResponse response = UserInfoResponse.parse(request.toHTTPRequest().send());
final UserInfoResponse response = UserInfoResponse.parse(userInfoHttpRequest.send());
// interpret the details
if (response.indicatesSuccess()) {
@ -362,10 +455,10 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
}
} else {
final UserInfoErrorResponse errorResponse = (UserInfoErrorResponse) response;
throw new RuntimeException("An error occurred while invoking the UserInfo endpoint: " + errorResponse.getErrorObject().getDescription());
throw new IdentityAccessException("An error occurred while invoking the UserInfo endpoint: " + errorResponse.getErrorObject().getDescription());
}
} catch (final ParseException | java.text.ParseException e) {
throw new RuntimeException("Unable to parse the response from the UserInfo token request: " + e.getMessage());
throw new IdentityAccessException("Unable to parse the response from the UserInfo token request: " + e.getMessage());
}
}
}

View File

@ -0,0 +1,584 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.security.oidc
import com.nimbusds.jwt.JWT
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.PlainJWT
import com.nimbusds.oauth2.sdk.AuthorizationCode
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost
import com.nimbusds.oauth2.sdk.auth.Secret
import com.nimbusds.oauth2.sdk.http.HTTPRequest
import com.nimbusds.oauth2.sdk.http.HTTPResponse
import com.nimbusds.oauth2.sdk.id.ClientID
import com.nimbusds.oauth2.sdk.id.Issuer
import com.nimbusds.oauth2.sdk.token.AccessToken
import com.nimbusds.oauth2.sdk.token.BearerAccessToken
import com.nimbusds.oauth2.sdk.token.RefreshToken
import com.nimbusds.openid.connect.sdk.Nonce
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse
import com.nimbusds.openid.connect.sdk.SubjectType
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
import com.nimbusds.openid.connect.sdk.token.OIDCTokens
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.apache.nifi.admin.service.KeyService
import org.apache.nifi.key.Key
import org.apache.nifi.util.NiFiProperties
import org.apache.nifi.util.StringUtils
import org.apache.nifi.web.security.jwt.JwtService
import org.apache.nifi.web.security.token.LoginAuthenticationToken
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@RunWith(JUnit4.class)
class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProviderGroovyTest.class)
private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
isOidcEnabled : false,
getOidcDiscoveryUrl : "https://localhost/oidc",
isLoginIdentityProviderEnabled: false,
isKnoxSsoEnabled : false,
getOidcConnectTimeout : 1000,
getOidcReadTimeout : 1000,
getOidcClientId : "expected_client_id",
getOidcClientSecret : "expected_client_secret",
getOidcClaimIdentifyingUser : "username"
]
// Mock collaborators
private static NiFiProperties mockNiFiProperties
private static JwtService mockJwtService = [:] as JwtService
@BeforeClass
static void setUpOnce() throws Exception {
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
void setUp() throws Exception {
mockNiFiProperties = buildNiFiProperties()
}
@After
void teardown() throws Exception {
}
private static NiFiProperties buildNiFiProperties(Map<String, Object> props = [:]) {
def combinedProps = DEFAULT_NIFI_PROPERTIES + props
def mockNFP = combinedProps.collectEntries { String k, def v ->
[k, { -> return v }]
}
mockNFP as NiFiProperties
}
private static JwtService buildJwtService() {
def mockJS = new JwtService([:] as KeyService) {
@Override
String generateSignedToken(LoginAuthenticationToken lat) {
signNiFiToken(lat)
}
}
mockJS
}
private static String signNiFiToken(LoginAuthenticationToken lat) {
String identity = "mockUser"
String USERNAME_CLAIM = "username"
String KEY_ID_CLAIM = "keyId"
Calendar expiration = Calendar.getInstance()
expiration.setTimeInMillis(System.currentTimeMillis() + 10_000)
String username = lat.getName()
return Jwts.builder().setSubject(identity)
.setIssuer(lat.getIssuer())
.setAudience(lat.getIssuer())
.claim(USERNAME_CLAIM, username)
.claim(KEY_ID_CLAIM, SIGNING_KEY.getId())
.setExpiration(expiration.getTime())
.setIssuedAt(Calendar.getInstance().getTime())
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY.key.getBytes("UTF-8")).compact()
}
@Test
void testShouldGetAvailableClaims() {
// Arrange
final Map<String, String> EXPECTED_CLAIMS = [
"iss" : "https://accounts.google.com",
"azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
"aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
"sub" : "10703475345439756345540",
"email" : "person@nifi.apache.org",
"email_verified": "true",
"at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
"iat" : "1590022674",
"exp" : "1590026274",
"empty_claim" : ""
]
final List<String> POPULATED_CLAIM_NAMES = EXPECTED_CLAIMS.findAll { k, v -> StringUtils.isNotBlank(v) }.keySet().sort()
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(EXPECTED_CLAIMS)
// Act
def definedClaims = StandardOidcIdentityProvider.getAvailableClaims(mockJWTClaimsSet)
logger.info("Defined claims: ${definedClaims}")
// Assert
assert definedClaims == POPULATED_CLAIM_NAMES
}
@Test
void testShouldCreateClientAuthenticationFromPost() {
// Arrange
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
Issuer mockIssuer = new Issuer("https://localhost/oidc")
URI mockURI = new URI("https://localhost/oidc")
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
// Set Authorization Method
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_POST]
final List<ClientAuthenticationMethod> mockAuthMethod = soip.oidcProviderMetadata["tokenEndpointAuthMethods"]
logger.info("Provided Auth Method: ${mockAuthMethod}")
// Define expected values
final ClientID CLIENT_ID = new ClientID("expected_client_id")
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
// Inject into OIP
soip.clientId = CLIENT_ID
soip.clientSecret = CLIENT_SECRET
final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new ClientSecretPost(CLIENT_ID, CLIENT_SECRET)
// Act
def clientAuthentication = soip.createClientAuthentication()
logger.info("Client Auth properties: ${clientAuthentication.getProperties()}")
// Assert
assert clientAuthentication.getClientID() == EXPECTED_CLIENT_AUTHENTICATION.getClientID()
logger.info("Client secret: ${(clientAuthentication as ClientSecretPost).clientSecret.value}")
assert ((ClientSecretPost) clientAuthentication).getClientSecret() == ((ClientSecretPost) EXPECTED_CLIENT_AUTHENTICATION).getClientSecret()
}
@Test
void testShouldCreateClientAuthenticationFromBasic() {
// Arrange
// Mock collaborators
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
Issuer mockIssuer = new Issuer("https://localhost/oidc")
URI mockURI = new URI("https://localhost/oidc")
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
// Set Auth Method
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
final List<ClientAuthenticationMethod> mockAuthMethod = soip.oidcProviderMetadata["tokenEndpointAuthMethods"]
logger.info("Provided Auth Method: ${mockAuthMethod}")
// Define expected values
final ClientID CLIENT_ID = new ClientID("expected_client_id")
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
// Inject into OIP
soip.clientId = CLIENT_ID
soip.clientSecret = CLIENT_SECRET
final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new ClientSecretBasic(CLIENT_ID, CLIENT_SECRET)
// Act
def clientAuthentication = soip.createClientAuthentication()
logger.info("Client authentication properties: ${clientAuthentication.properties}")
// Assert
assert clientAuthentication.getClientID() == EXPECTED_CLIENT_AUTHENTICATION.getClientID()
assert clientAuthentication.getMethod() == EXPECTED_CLIENT_AUTHENTICATION.getMethod()
logger.info("Client secret: ${(clientAuthentication as ClientSecretBasic).clientSecret.value}")
assert (clientAuthentication as ClientSecretBasic).getClientSecret() == EXPECTED_CLIENT_AUTHENTICATION.clientSecret
}
@Test
void testShouldCreateTokenHTTPRequest() {
// Arrange
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
// Mock AuthorizationGrant
Issuer mockIssuer = new Issuer("https://localhost/oidc")
URI mockURI = new URI("https://localhost/oidc")
AuthorizationCode mockCode = new AuthorizationCode("ABCDE")
def mockAuthGrant = new AuthorizationCodeGrant(mockCode, mockURI)
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
// Set OIDC Provider metadata attributes
final ClientID CLIENT_ID = new ClientID("expected_client_id")
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
// Inject into OIP
soip.clientId = CLIENT_ID
soip.clientSecret = CLIENT_SECRET
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
soip.oidcProviderMetadata["tokenEndpointURI"] = new URI("https://localhost/token")
// Mock ClientAuthentication
def clientAuthentication = soip.createClientAuthentication()
// Act
def httpRequest = soip.createTokenHTTPRequest(mockAuthGrant, clientAuthentication)
logger.info("HTTP Request: ${httpRequest.dump()}")
logger.info("Query: ${URLDecoder.decode(httpRequest.query, "UTF-8")}")
// Assert
assert httpRequest.getMethod().name() == "POST"
assert httpRequest.query =~ "code=${mockCode.value}"
String encodedUri = URLEncoder.encode("https://localhost/oidc", "UTF-8")
assert httpRequest.query =~ "redirect_uri=${encodedUri}&grant_type=authorization_code"
}
@Test
void testShouldLookupIdentityInUserInfo() {
// Arrange
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
Issuer mockIssuer = new Issuer("https://localhost/oidc")
URI mockURI = new URI("https://localhost/oidc")
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
final String EXPECTED_IDENTITY = "my_username"
def responseBody = [username: EXPECTED_IDENTITY, sub: "testSub"]
HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200, "HTTP OK")
// Act
String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest)
logger.info("Identity: ${identity}")
// Assert
assert identity == EXPECTED_IDENTITY
}
@Test
void testLookupIdentityUserInfoShouldHandleMissingIdentity() {
// Arrange
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
Issuer mockIssuer = new Issuer("https://localhost/oidc")
URI mockURI = new URI("https://localhost/oidc")
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
def responseBody = [username: "", sub: "testSub"]
HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200, "HTTP NO USER")
// Act
def msg = shouldFail(IllegalStateException) {
String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest)
logger.info("Identity: ${identity}")
}
logger.expected(msg)
// Assert
assert msg =~ "Unable to extract identity from the UserInfo token using the claim 'username'."
}
@Test
void testLookupIdentityUserInfoShouldHandle500() {
// Arrange
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
Issuer mockIssuer = new Issuer("https://localhost/oidc")
URI mockURI = new URI("https://localhost/oidc")
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
def errorBody = [error : "Failure to authenticate",
error_description: "The provided username and password were not correct",
error_uri : "https://localhost/oidc/error"]
HTTPRequest mockUserInfoRequest = mockHttpRequest(errorBody, 500, "HTTP ERROR")
// Act
def msg = shouldFail(RuntimeException) {
String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest)
logger.info("Identity: ${identity}")
}
logger.expected(msg)
// Assert
assert msg =~ "An error occurred while invoking the UserInfo endpoint: The provided username and password were not correct"
}
@Test
void testShouldConvertOIDCTokenToNiFiToken() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email"])
OIDCTokenResponse mockResponse = mockOIDCTokenResponse()
logger.info("OIDC Token Response: ${mockResponse.dump()}")
// Act
String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
logger.info("NiFi token: ${nifiToken}")
// Assert
// Split JWT into components and decode Base64 to JSON
def (String headerB64, String payloadB64, String signatureB64) = nifiToken.tokenize("\\.")
logger.info("Header: ${headerB64} | Payload: ${payloadB64} | Signature: ${signatureB64}")
String headerJson = new String(Base64.decoder.decode(headerB64), "UTF-8")
String payloadJson = new String(Base64.decoder.decode(payloadB64), "UTF-8")
// String signatureJson = new String(Base64.decoder.decode(signatureB64), "UTF-8")
// Parse JSON into objects
def slurper = new JsonSlurper()
def header = slurper.parseText(headerJson)
logger.info("Header: ${header}")
assert header.alg == "HS256"
def payload = slurper.parseText(payloadJson)
logger.info("Payload: ${payload}")
assert payload.username == "person@nifi.apache.org"
assert payload.keyId == 1
assert payload.exp <= System.currentTimeMillis() + 10_000
}
@Test
void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentity() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
OIDCTokenResponse mockResponse = mockOIDCTokenResponse()
logger.info("OIDC Token Response: ${mockResponse.dump()}")
// Act
String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
logger.info("NiFi token: ${nifiToken}")
// Assert
// Split JWT into components and decode Base64 to JSON
def (String headerB64, String payloadB64, String signatureB64) = nifiToken.tokenize("\\.")
logger.info("Header: ${headerB64} | Payload: ${payloadB64} | Signature: ${signatureB64}")
String headerJson = new String(Base64.decoder.decode(headerB64), "UTF-8")
String payloadJson = new String(Base64.decoder.decode(payloadB64), "UTF-8")
// String signatureJson = new String(Base64.decoder.decode(signatureB64), "UTF-8")
// Parse JSON into objects
def slurper = new JsonSlurper()
def header = slurper.parseText(headerJson)
logger.info("Header: ${header}")
assert header.alg == "HS256"
def payload = slurper.parseText(payloadJson)
logger.info("Payload: ${payload}")
assert payload.username == "person@nifi.apache.org"
assert payload.keyId == 1
assert payload.exp <= System.currentTimeMillis() + 10_000
}
@Test
void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentityAndNoEmailClaim() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null])
logger.info("OIDC Token Response: ${mockResponse.dump()}")
// Act
def msg = shouldFail(ConnectException) {
String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
logger.info("NiFi token: ${nifiToken}")
}
// Assert
assert msg =~ "Connection refused"
}
@Test
void testShouldAuthorizeClient() {
// Arrange
// Build ID Provider with mock token endpoint URI to make a connection
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
// Mock the JWT
def jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
def responseBody = [id_token: jwt, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"]
HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 200, "HTTP OK")
// Act
def nifiToken = soip.authorizeClient(mockTokenRequest)
logger.info("NiFi Token: ${nifiToken.dump()}")
// Assert
assert nifiToken
}
@Test
void testAuthorizeClientShouldHandleError() {
// Arrange
// Build ID Provider with mock token endpoint URI to make a connection
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
// Mock the JWT
def jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
def responseBody = [id_token: jwt, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"]
HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 500, "HTTP SERVER ERROR")
// Act
def msg = shouldFail(RuntimeException) {
def nifiToken = soip.authorizeClient(mockTokenRequest)
logger.info("NiFi token: ${nifiToken}")
}
// Assert
assert msg =~ "An error occurred while invoking the Token endpoint: null"
}
private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map<String, String> additionalProperties = [:]) {
JwtService mockJS = buildJwtService()
NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP)
// Mock OIDC provider metadata
Issuer mockIssuer = new Issuer("mockIssuer")
URI mockURI = new URI("https://localhost/oidc")
OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
soip.oidcProviderMetadata = metadata
// Set OIDC Provider metadata attributes
final ClientID CLIENT_ID = new ClientID("expected_client_id")
final Secret CLIENT_SECRET = new Secret("expected_client_secret")
// Inject into OIP
soip.clientId = CLIENT_ID
soip.clientSecret = CLIENT_SECRET
soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
soip.oidcProviderMetadata["tokenEndpointURI"] = new URI("https://localhost/oidc/token")
soip.oidcProviderMetadata["userInfoEndpointURI"] = new URI("https://localhost/oidc/userInfo")
// Mock token validator
IDTokenValidator mockTokenValidator = new IDTokenValidator(mockIssuer, CLIENT_ID) {
@Override
IDTokenClaimsSet validate(JWT jwt, Nonce nonce) {
return new IDTokenClaimsSet(jwt.getJWTClaimsSet())
}
}
soip.tokenValidator = mockTokenValidator
soip
}
private OIDCTokenResponse mockOIDCTokenResponse(Map<String, Object> additionalClaims = [:]) {
final Map<String, Object> claims = [
"iss" : "https://accounts.google.com",
"azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
"aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
"sub" : "10703475345439756345540",
"email" : "person@nifi.apache.org",
"email_verified": "true",
"at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
"iat" : 1590022674,
"exp" : 1590026274
] + additionalClaims
// Create Claims Set
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
// Create JWT
JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
// Mock access tokens
AccessToken mockAccessToken = new BearerAccessToken()
RefreshToken mockRefreshToken = new RefreshToken()
// Create OIDC Tokens
OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
// Create OIDC Token Response
OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
mockResponse
}
/**
* Forms an {@link HTTPRequest} object which returns a static response when {@code send( )} is called.
*
* @param body the JSON body in Map form
* @param statusCode the HTTP status code
* @param status the HTTP status message
* @param headers an optional map of HTTP response headers
* @param method the HTTP method to mock
* @param url the endpoint URL
* @return the static HTTP response
*/
private static HTTPRequest mockHttpRequest(def body,
int statusCode = 200,
String status = "HTTP Response",
Map<String, String> headers = [:],
HTTPRequest.Method method = HTTPRequest.Method.GET,
URL url = new URL("https://localhost/oidc")) {
new HTTPRequest(method, url) {
HTTPResponse send() {
HTTPResponse mockResponse = new HTTPResponse(statusCode)
mockResponse.setStatusMessage(status)
(["Content-Type": "application/json"] + headers).each { String h, String v -> mockResponse.setHeader(h, v) }
def responseBody = body
mockResponse.setContent(JsonOutput.toJson(responseBody))
mockResponse
}
}
}
class MockOIDCProviderMetadata extends OIDCProviderMetadata {
MockOIDCProviderMetadata() {
super([:] as Issuer, [SubjectType.PUBLIC] as List<SubjectType>, new URI("https://localhost"))
}
}
}