diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
index 51b665f843..6803f0e34f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
@@ -422,5 +422,9 @@
1.3
test
+
+ org.apache.httpcomponents
+ httpclient
+
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
index b8d35e934a..7aee52b021 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.web;
+import java.util.Arrays;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
@@ -48,8 +49,6 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-import java.util.Arrays;
-
/**
* NiFi Web Api Spring security. Applies the various NiFiAuthenticationFilter servlet filters which will extract authentication
* credentials from API requests.
@@ -92,7 +91,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
webSecurity
.ignoring()
.antMatchers("/access", "/access/config", "/access/token", "/access/kerberos",
- "/access/oidc/exchange", "/access/oidc/callback", "/access/oidc/request",
+ "/access/oidc/exchange", "/access/oidc/callback", "/access/oidc/logoutCallback", "/access/oidc/request",
"/access/knox/callback", "/access/knox/request");
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index 9a3eec241f..32e1b226f1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -20,6 +20,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
@@ -29,10 +30,15 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
+import java.io.IOException;
import java.net.URI;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@@ -49,6 +55,14 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.apache.commons.lang3.StringUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+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.AdministrationException;
import org.apache.nifi.authentication.AuthenticationResponse;
import org.apache.nifi.authentication.LoginCredentials;
@@ -104,6 +118,14 @@ public class AccessResource extends ApplicationResource {
private static final String OIDC_REQUEST_IDENTIFIER = "oidc-request-identifier";
private static final String OIDC_ERROR_TITLE = "Unable to continue login sequence";
+ private static final String OIDC_ID_TOKEN_AUTHN_ERROR = "Unable to exchange authorization for ID token: ";
+ private static final String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG = "OpenId Connect support is not configured";
+ private static final String REVOKE_ACCESS_TOKEN_LOGOUT = "oidc_access_token_logout";
+ private static final String ID_TOKEN_LOGOUT = "oidc_id_token_logout";
+ private static final String STANDARD_LOGOUT = "oidc_standard_logout";
+ private static final Pattern REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT = Pattern.compile("(\\.google\\.com)");
+ private static final Pattern ID_TOKEN_LOGOUT_FORMAT = Pattern.compile("(\\.okta)");
+ private static final int msTimeout = 30_000;
private static final String AUTHENTICATION_NOT_ENABLED_MSG = "User authentication/authorization is only supported when running over HTTPS.";
@@ -166,34 +188,15 @@ public class AccessResource extends ApplicationResource {
// ensure oidc is enabled
if (!oidcService.isOidcEnabled()) {
- forwardToMessagePage(httpServletRequest, httpServletResponse, "OpenId Connect is not configured.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
return;
}
- final String oidcRequestIdentifier = UUID.randomUUID().toString();
-
- // generate a cookie to associate this login sequence
- final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
- cookie.setPath("/");
- cookie.setHttpOnly(true);
- cookie.setMaxAge(60);
- cookie.setSecure(true);
- httpServletResponse.addCookie(cookie);
-
- // get the state for this request
- final State state = oidcService.createState(oidcRequestIdentifier);
-
- // build the authorization uri
- final URI authorizationUri = UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
- .queryParam("client_id", oidcService.getClientId())
- .queryParam("response_type", "code")
- .queryParam("scope", oidcService.getScope().toString())
- .queryParam("state", state.getValue())
- .queryParam("redirect_uri", getOidcCallback())
- .build();
+ // generate the authorization uri
+ URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcCallback());
// generate the response
- httpServletResponse.sendRedirect(authorizationUri.toString());
+ httpServletResponse.sendRedirect(authorizationURI.toString());
}
@GET
@@ -207,19 +210,20 @@ public class AccessResource extends ApplicationResource {
public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
- forwardToMessagePage(httpServletRequest, httpServletResponse, "User authentication/authorization is only supported when running over HTTPS.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG);
return;
}
// ensure oidc is enabled
if (!oidcService.isOidcEnabled()) {
- forwardToMessagePage(httpServletRequest, httpServletResponse, "OpenId Connect is not configured.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
return;
}
final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
if (oidcRequestIdentifier == null) {
- forwardToMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was not found in the request. Unable to continue.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was " +
+ "not found in the request. Unable to continue.");
return;
}
@@ -233,7 +237,8 @@ public class AccessResource extends ApplicationResource {
removeOidcRequestCookie(httpServletResponse);
// forward to the error page
- forwardToMessagePage(httpServletRequest, httpServletResponse, "Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login process.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "Unable to parse the redirect URI " +
+ "from the OpenId Connect Provider. Unable to continue login process.");
return;
}
@@ -243,13 +248,15 @@ public class AccessResource extends ApplicationResource {
// confirm state
final State state = successfulOidcResponse.getState();
if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
- logger.error("The state value returned by the OpenId Connect Provider does not match the stored state. Unable to continue login process.");
+ logger.error("The state value returned by the OpenId Connect Provider does not match the stored state. " +
+ "Unable to continue login process.");
// remove the oidc request cookie
removeOidcRequestCookie(httpServletResponse);
// forward to the error page
- forwardToMessagePage(httpServletRequest, httpServletResponse, "Purposed state does not match the stored state. Unable to continue login process.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "Purposed state does not match " +
+ "the stored state. Unable to continue login process.");
return;
}
@@ -257,15 +264,24 @@ public class AccessResource extends ApplicationResource {
// exchange authorization code for id token
final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcCallback()));
- oidcService.exchangeAuthorizationCode(oidcRequestIdentifier, authorizationGrant);
+
+ // get the oidc token
+ LoginAuthenticationToken oidcToken = oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(authorizationGrant);
+
+ // exchange the oidc token for the NiFi token
+ String nifiJwt = jwtService.generateSignedToken(oidcToken);
+
+ // store the NiFi token
+ oidcService.storeJwt(oidcRequestIdentifier, nifiJwt);
+
} catch (final Exception e) {
- logger.error("Unable to exchange authorization for ID token: " + e.getMessage(), e);
+ logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
// remove the oidc request cookie
removeOidcRequestCookie(httpServletResponse);
// forward to the error page
- forwardToMessagePage(httpServletRequest, httpServletResponse, "Unable to exchange authorization for ID token: " + e.getMessage());
+ forwardToMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
return;
}
@@ -277,7 +293,8 @@ public class AccessResource extends ApplicationResource {
// report the unsuccessful login
final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
- forwardToMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt: " + errorOidcResponse.getErrorObject().getDescription());
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt: "
+ + errorOidcResponse.getErrorObject().getDescription());
}
}
@@ -290,7 +307,7 @@ public class AccessResource extends ApplicationResource {
response = String.class,
notes = NON_GUARANTEED_ENDPOINT
)
- public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
+ public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
@@ -298,7 +315,7 @@ public class AccessResource extends ApplicationResource {
// ensure oidc is enabled
if (!oidcService.isOidcEnabled()) {
- throw new IllegalStateException("OpenId Connect is not configured.");
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
}
final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
@@ -329,24 +346,198 @@ public class AccessResource extends ApplicationResource {
)
public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
if (!httpServletRequest.isSecure()) {
- throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS.");
+ throw new IllegalStateException(AUTHENTICATION_NOT_ENABLED_MSG);
}
if (!oidcService.isOidcEnabled()) {
- throw new IllegalStateException("OpenId Connect is not configured.");
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
}
- URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
- String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
+ // Get the oidc discovery url
+ String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
- if (endSessionEndpoint == null) {
- // handle the case, where the OpenID Provider does not have an end session endpoint
- httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ // Determine the logout method
+ String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
+
+ switch (logoutMethod) {
+ case REVOKE_ACCESS_TOKEN_LOGOUT:
+ case ID_TOKEN_LOGOUT:
+ // Make a request to the IdP
+ URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcLogoutCallback());
+ httpServletResponse.sendRedirect(authorizationURI.toString());
+ break;
+ case STANDARD_LOGOUT:
+ default:
+ // Get the OIDC end session endpoint
+ URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
+ String postLogoutRedirectUri = generateResourceUri( "..", "nifi", "logout-complete");
+
+ if (endSessionEndpoint == null) {
+ httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ } else {
+ URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
+ .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
+ .build();
+ httpServletResponse.sendRedirect(logoutUri.toString());
+ }
+ break;
+ }
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.WILDCARD)
+ @Path("oidc/logoutCallback")
+ @ApiOperation(
+ value = "Redirect/callback URI for processing the result of the OpenId Connect logout sequence.",
+ notes = NON_GUARANTEED_ENDPOINT
+ )
+ public void oidcLogoutCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
+ // only consider user specific access over https
+ if (!httpServletRequest.isSecure()) {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG);
+ return;
+ }
+
+ // ensure oidc is enabled
+ if (!oidcService.isOidcEnabled()) {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
+ return;
+ }
+
+ final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
+ if (oidcRequestIdentifier == null) {
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "The login request identifier was " +
+ "not found in the request. Unable to continue.");
+ return;
+ }
+
+ final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse;
+ try {
+ oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
+ } catch (final ParseException e) {
+ logger.error("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue " +
+ "logout process: " + e.getMessage(), e);
+
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "Unable to parse the redirect URI " +
+ "from the OpenId Connect Provider. Unable to continue logout process.");
+ return;
+ }
+
+ if (oidcResponse.indicatesSuccess()) {
+ final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
+
+ // confirm state
+ final State state = successfulOidcResponse.getState();
+ if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
+ logger.error("The state value returned by the OpenId Connect Provider does not match the stored " +
+ "state. Unable to continue login process.");
+
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "Purposed state does not match " +
+ "the stored state. Unable to continue login process.");
+ return;
+ }
+
+ // Get the oidc discovery url
+ String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
+
+ // Determine which logout method to use
+ String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
+
+ // Get the authorization code and grant
+ final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
+ final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcLogoutCallback()));
+
+ switch (logoutMethod) {
+ case REVOKE_ACCESS_TOKEN_LOGOUT:
+ // Use the Revocation endpoint + access token
+ final String accessToken;
+ try {
+ // Return the access token
+ accessToken = oidcService.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
+ } catch (final Exception e) {
+ logger.error("Unable to exchange authorization for the Access token: " + e.getMessage(), e);
+
+ // Remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // Forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
+ return;
+ }
+
+ // Build the revoke URI and send the POST request
+ URI revokeEndpoint = getRevokeEndpoint();
+
+ if (revokeEndpoint != null) {
+ try {
+ // Logout with the revoke endpoint
+ revokeEndpointRequest(httpServletResponse, accessToken, revokeEndpoint);
+
+ } catch (final IOException e) {
+ logger.error("There was an error logging out of the OpenId Connect Provider: "
+ + e.getMessage(), e);
+
+ // Remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // Forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse,
+ "There was an error logging out of the OpenId Connect Provider: "
+ + e.getMessage());
+ }
+ }
+ break;
+ case ID_TOKEN_LOGOUT:
+ // Use the end session endpoint + ID Token
+ final String idToken;
+ try {
+ // Return the ID Token
+ idToken = oidcService.exchangeAuthorizationCodeForIdToken(authorizationGrant);
+ } catch (final Exception e) {
+ logger.error(OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage(), e);
+
+ // Remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // Forward to the error page
+ forwardToMessagePage(httpServletRequest, httpServletResponse, OIDC_ID_TOKEN_AUTHN_ERROR + e.getMessage());
+ return;
+ }
+
+ // Get the OIDC end session endpoint
+ URI endSessionEndpoint = oidcService.getEndSessionEndpoint();
+ String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
+
+ if (endSessionEndpoint == null) {
+ logger.debug("Unable to log out of the OpenId Connect Provider. The end session endpoint is: null." +
+ " Redirecting to the logout page.");
+ httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ } else {
+ URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
+ .queryParam("id_token_hint", idToken)
+ .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
+ .build();
+ httpServletResponse.sendRedirect(logoutUri.toString());
+ }
+ break;
+ }
} else {
- URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
- .queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
- .build();
- httpServletResponse.sendRedirect(logoutUri.toString());
+ // remove the oidc request cookie
+ removeOidcRequestCookie(httpServletResponse);
+
+ // report the unsuccessful logout
+ final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
+ forwardToMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful logout attempt: "
+ + errorOidcResponse.getErrorObject().getDescription());
}
}
@@ -394,7 +585,7 @@ public class AccessResource extends ApplicationResource {
public void knoxCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
- forwardToMessagePage(httpServletRequest, httpServletResponse, "User authentication/authorization is only supported when running over HTTPS.");
+ forwardToMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG);
return;
}
@@ -768,7 +959,7 @@ public class AccessResource extends ApplicationResource {
)
public Response logOut(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
if (!httpServletRequest.isSecure()) {
- throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS.");
+ throw new IllegalStateException(AUTHENTICATION_NOT_ENABLED_MSG);
}
String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
@@ -828,6 +1019,14 @@ public class AccessResource extends ApplicationResource {
return generateResourceUri("access", "oidc", "callback");
}
+ private String getOidcLogoutCallback() {
+ return generateResourceUri("access", "oidc", "logoutCallback");
+ }
+
+ private URI getRevokeEndpoint() {
+ return oidcService.getRevocationEndpoint();
+ }
+
private String getNiFiUri() {
final String nifiApiUrl = generateResourceUri();
final String baseUrl = StringUtils.substringBeforeLast(nifiApiUrl, "/nifi-api");
@@ -851,6 +1050,98 @@ public class AccessResource extends ApplicationResource {
uiContext.getRequestDispatcher("/WEB-INF/pages/message-page.jsp").forward(httpServletRequest, httpServletResponse);
}
+ private String determineLogoutMethod(String oidcDiscoveryUrl) {
+ Matcher accessTokenMatcher = REVOKE_ACCESS_TOKEN_LOGOUT_FORMAT.matcher(oidcDiscoveryUrl);
+ Matcher idTokenMatcher = ID_TOKEN_LOGOUT_FORMAT.matcher(oidcDiscoveryUrl);
+
+ if (accessTokenMatcher.find()) {
+ return REVOKE_ACCESS_TOKEN_LOGOUT;
+ } else if (idTokenMatcher.find()) {
+ return ID_TOKEN_LOGOUT;
+ } else {
+ return STANDARD_LOGOUT;
+ }
+ }
+
+ /**
+ * Generates the request Authorization URI for the OpenID Connect Provider. Returns an authorization
+ * URI using the provided callback URI.
+ *
+ * @param httpServletResponse the servlet response
+ * @param callback the OIDC callback URI
+ * @return the authorization URI
+ */
+ private URI oidcRequestAuthorizationCode(@Context HttpServletResponse httpServletResponse, String callback) {
+
+ final String oidcRequestIdentifier = UUID.randomUUID().toString();
+
+ // generate a cookie to associate this login sequence
+ final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier);
+ cookie.setPath("/");
+ cookie.setHttpOnly(true);
+ cookie.setMaxAge(60);
+ cookie.setSecure(true);
+ httpServletResponse.addCookie(cookie);
+
+ // get the state for this request
+ final State state = oidcService.createState(oidcRequestIdentifier);
+
+ // build the authorization uri
+ final URI authorizationUri = UriBuilder.fromUri(oidcService.getAuthorizationEndpoint())
+ .queryParam("client_id", oidcService.getClientId())
+ .queryParam("response_type", "code")
+ .queryParam("scope", oidcService.getScope().toString())
+ .queryParam("state", state.getValue())
+ .queryParam("redirect_uri", callback)
+ .build();
+
+ // return Authorization URI
+ return authorizationUri;
+ }
+
+ /**
+ * Sends a POST request to the revoke endpoint to log out of the ID Provider.
+ *
+ * @param httpServletResponse the servlet response
+ * @param accessToken the OpenID Connect Provider access token
+ * @param revokeEndpoint the name of the cookie
+ * @throws IOException exceptional case for communication error with the OpenId Connect Provider
+ */
+
+ private void revokeEndpointRequest(@Context HttpServletResponse httpServletResponse, String accessToken, URI revokeEndpoint) throws IOException {
+
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(msTimeout)
+ .setConnectionRequestTimeout(msTimeout)
+ .setSocketTimeout(msTimeout)
+ .build();
+
+ CloseableHttpClient httpClient = HttpClientBuilder
+ .create()
+ .setDefaultRequestConfig(config)
+ .build();
+ HttpPost httpPost = new HttpPost(revokeEndpoint);
+
+ List params = new ArrayList<>();
+ // Append a query param with the access token
+ params.add(new BasicNameValuePair("token", accessToken));
+ httpPost.setEntity(new UrlEncodedFormEntity(params));
+
+ try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+ httpClient.close();
+
+ if (response.getStatusLine().getStatusCode() == HTTPResponse.SC_OK) {
+ // Redirect to logout page
+ logger.debug("You are logged out of the OpenId Connect Provider.");
+ String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
+ httpServletResponse.sendRedirect(postLogoutRedirectUri);
+ } else {
+ logger.error("There was an error logging out of the OpenId Connect Provider. " +
+ "Response status: " + response.getStatusLine().getStatusCode());
+ }
+ }
+ }
+
// setters
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
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/OidcIdentityProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcIdentityProvider.java
index cecd792d7b..8be9b6d687 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcIdentityProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcIdentityProvider.java
@@ -22,6 +22,7 @@ import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
import java.io.IOException;
import java.net.URI;
+import org.apache.nifi.web.security.token.LoginAuthenticationToken;
public interface OidcIdentityProvider {
@@ -60,6 +61,13 @@ public interface OidcIdentityProvider {
*/
URI getEndSessionEndpoint();
+ /**
+ * Returns the URI for the revocation endpoint.
+ *
+ * @return uri for the revocation endpoint
+ */
+ URI getRevocationEndpoint();
+
/**
* Returns the scopes supported by the OIDC provider.
*
@@ -68,12 +76,30 @@ public interface OidcIdentityProvider {
Scope getScope();
/**
- * Exchanges the supplied authorization grant for an ID token. Extracts the identity from the ID
- * token and converts it into NiFi JWT.
+ * Exchanges the supplied authorization grant for a Login ID Token. Extracts the identity from the ID
+ * token.
*
* @param authorizationGrant authorization grant for invoking the Token Endpoint
- * @return a NiFi JWT
+ * @return a Login Authentication Token
* @throws IOException if there was an exceptional error while communicating with the OIDC provider
*/
- String exchangeAuthorizationCode(AuthorizationGrant authorizationGrant) throws IOException;
+ LoginAuthenticationToken exchangeAuthorizationCodeforLoginAuthenticationToken(AuthorizationGrant authorizationGrant) throws IOException;
+
+ /**
+ * Exchanges the supplied authorization grant for an Access Token.
+ *
+ * @param authorizationGrant authorization grant for invoking the Token Endpoint
+ * @return an Access Token String
+ * @throws Exception if there was an exceptional error while communicating with the OIDC provider
+ */
+ String exchangeAuthorizationCodeForAccessToken(AuthorizationGrant authorizationGrant) throws Exception;
+
+ /**
+ * Exchanges the supplied authorization grant for an ID Token.
+ *
+ * @param authorizationGrant authorization grant for invoking the Token Endpoint
+ * @return an ID Token String
+ * @throws IOException if there was an exceptional error while communicating with the OIDC provider
+ */
+ String exchangeAuthorizationCodeForIdToken(final AuthorizationGrant authorizationGrant) throws IOException;
}
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/OidcService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcService.java
index b749085556..5474c43b01 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/OidcService.java
@@ -29,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.token.LoginAuthenticationToken;
import org.apache.nifi.web.security.util.CacheKey;
import static org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED;
@@ -98,6 +99,15 @@ public class OidcService {
return identityProvider.getEndSessionEndpoint();
}
+ /**
+ * Returns the OpenId Connect revocation endpoint.
+ *
+ * @return the revocation endpoint
+ */
+ public URI getRevocationEndpoint() {
+ return identityProvider.getRevocationEndpoint();
+ }
+
/**
* Returns the OpenId Connect scope.
*
@@ -186,25 +196,66 @@ public class OidcService {
}
/**
- * Exchanges the specified authorization grant for an ID token for the given request identifier.
+ * Exchanges the specified authorization grant for an ID token.
*
- * @param oidcRequestIdentifier request identifier
* @param authorizationGrant authorization grant
+ * @return a Login Authentication Token
* @throws IOException exceptional case for communication error with the OpenId Connect provider
*/
- public void exchangeAuthorizationCode(final String oidcRequestIdentifier, final AuthorizationGrant authorizationGrant) throws IOException {
+ public LoginAuthenticationToken exchangeAuthorizationCodeForLoginAuthenticationToken(final AuthorizationGrant authorizationGrant) throws IOException {
if (!isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
- final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
- final String nifiJwt = retrieveNifiJwt(authorizationGrant);
+ // Retrieve Login Authentication Token
+ return identityProvider.exchangeAuthorizationCodeforLoginAuthenticationToken(authorizationGrant);
+ }
+ /**
+ * Exchanges the specified authorization grant for an access token.
+ *
+ * @param authorizationGrant authorization grant
+ * @return an Access Token string
+ * @throws IOException exceptional case for communication error with the OpenId Connect provider
+ */
+ public String exchangeAuthorizationCodeForAccessToken(final AuthorizationGrant authorizationGrant) throws Exception {
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ // Retrieve access token
+ return identityProvider.exchangeAuthorizationCodeForAccessToken(authorizationGrant);
+ }
+
+ /**
+ * Exchanges the specified authorization grant for an ID Token.
+ *
+ * @param authorizationGrant authorization grant
+ * @return an ID Token string
+ * @throws IOException exceptional case for communication error with the OpenId Connect provider
+ */
+ public String exchangeAuthorizationCodeForIdToken(final AuthorizationGrant authorizationGrant) throws IOException {
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ // Retrieve ID token
+ return identityProvider.exchangeAuthorizationCodeForIdToken(authorizationGrant);
+ }
+
+ /**
+ * Stores the NiFi Jwt.
+ *
+ * @param oidcRequestIdentifier request identifier
+ * @param jwt NiFi JWT
+ */
+ public void storeJwt(final String oidcRequestIdentifier, final String jwt) {
+ final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
try {
- // cache the jwt for later retrieval
+ // Cache the jwt for later retrieval
synchronized (jwtLookupForCompletedRequests) {
- final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> nifiJwt);
- if (!timeConstantEqualityCheck(nifiJwt, cachedJwt)) {
+ final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> jwt);
+ if (!timeConstantEqualityCheck(jwt, cachedJwt)) {
throw new IllegalStateException("An existing login request is already in progress.");
}
}
@@ -213,17 +264,6 @@ 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
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 f7b54f109e..a26926e2d6 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
@@ -38,6 +38,7 @@ 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.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
@@ -45,10 +46,13 @@ import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.AccessTokenHash;
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.AccessTokenValidator;
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
+import com.nimbusds.openid.connect.sdk.validators.InvalidHashException;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
@@ -159,18 +163,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
try {
// get the preferred json web signature algorithm
- final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
-
- final JWSAlgorithm preferredJwsAlgorithm;
- if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
- preferredJwsAlgorithm = JWSAlgorithm.RS256;
- } else {
- if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
- preferredJwsAlgorithm = null;
- } else {
- preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
- }
- }
+ final JWSAlgorithm preferredJwsAlgorithm = extractJwsAlgorithm();
if (preferredJwsAlgorithm == null) {
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId);
@@ -185,6 +178,23 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
}
}
+ private JWSAlgorithm extractJwsAlgorithm() {
+
+ final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
+
+ final JWSAlgorithm preferredJwsAlgorithm;
+ if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
+ preferredJwsAlgorithm = JWSAlgorithm.RS256;
+ } else {
+ if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
+ preferredJwsAlgorithm = null;
+ } else {
+ preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
+ }
+ }
+ return preferredJwsAlgorithm;
+ }
+
/**
* Loads the initial configuration values relating to the OIDC provider from the class {@link NiFiProperties} and populates the individual fields.
*/
@@ -262,7 +272,6 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
if (!isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
-
return oidcProviderMetadata.getAuthorizationEndpointURI();
}
@@ -274,6 +283,14 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
return oidcProviderMetadata.getEndSessionEndpointURI();
}
+ @Override
+ public URI getRevocationEndpoint() {
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+ return oidcProviderMetadata.getRevocationEndpointURI();
+ }
+
@Override
public Scope getScope() {
if (!isOidcEnabled()) {
@@ -295,37 +312,101 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
if (!isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
-
return clientId;
}
@Override
- public String exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws IOException {
+ public LoginAuthenticationToken exchangeAuthorizationCodeforLoginAuthenticationToken(final AuthorizationGrant authorizationGrant) throws IOException {
// Check if OIDC is enabled
if (!isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
}
- // Build ClientAuthentication
- final ClientAuthentication clientAuthentication = createClientAuthentication();
-
try {
- // Build the token request
- final HTTPRequest tokenHttpRequest = createTokenHTTPRequest(authorizationGrant, clientAuthentication);
- return authorizeClient(tokenHttpRequest);
+ // Authenticate and authorize the client request
+ final TokenResponse response = authorizeClient(authorizationGrant);
- } catch (final ParseException | JOSEException | BadJOSEException | java.text.ParseException e) {
- throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage());
+ // Convert response to Login Authentication Token
+ // We only want to do this for login
+ return convertOIDCTokenToLoginAuthenticationToken((OIDCTokenResponse) response);
+
+ } catch (final RuntimeException | ParseException | JOSEException | BadJOSEException | java.text.ParseException e) {
+ throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage(), e);
}
}
- private String authorizeClient(HTTPRequest tokenHttpRequest) throws ParseException, IOException, BadJOSEException, JOSEException, java.text.ParseException {
+ @Override
+ public String exchangeAuthorizationCodeForAccessToken(final AuthorizationGrant authorizationGrant) throws Exception {
+ // Check if OIDC is enabled
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ try {
+ // Authenticate and authorize the client request
+ final TokenResponse response = authorizeClient(authorizationGrant);
+ return getAccessTokenString((OIDCTokenResponse) response);
+
+ } catch (final RuntimeException | ParseException | IOException | java.text.ParseException | InvalidHashException e) {
+ throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String exchangeAuthorizationCodeForIdToken(final AuthorizationGrant authorizationGrant) {
+ // Check if OIDC is enabled
+ if (!isOidcEnabled()) {
+ throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
+ }
+
+ try {
+ // Authenticate and authorize the client request
+ final TokenResponse response = authorizeClient(authorizationGrant);
+ return getIdTokenString((OIDCTokenResponse) response);
+
+ } catch (final RuntimeException | JOSEException | BadJOSEException | ParseException | IOException e) {
+ throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage(), e);
+ }
+ }
+
+ private String getAccessTokenString(final OIDCTokenResponse response) throws Exception {
+ final OIDCTokens oidcTokens = getOidcTokens(response);
+
+ // Validate the Access Token
+ validateAccessToken(oidcTokens);
+
+ // Return the Access Token String
+ return oidcTokens.getAccessToken().getValue();
+ }
+
+ private String getIdTokenString(OIDCTokenResponse response) throws BadJOSEException, JOSEException {
+ final OIDCTokens oidcTokens = getOidcTokens(response);
+
+ // Validate the Token - no nonce required for authorization code flow
+ validateIdToken(oidcTokens.getIDToken());
+
+ // Return the ID Token string
+ return oidcTokens.getIDTokenString();
+ }
+
+ private TokenResponse authorizeClient(AuthorizationGrant authorizationGrant) throws ParseException, IOException {
+ // Build ClientAuthentication
+ final ClientAuthentication clientAuthentication = createClientAuthentication();
+
+ // Build the token request
+ final HTTPRequest tokenHttpRequest = createTokenHTTPRequest(authorizationGrant, clientAuthentication);
+
+ // Send the request and parse for success
+ return authorizeClientRequest(tokenHttpRequest);
+ }
+
+ private TokenResponse authorizeClientRequest(HTTPRequest tokenHttpRequest) throws ParseException, IOException {
// Get the token response
final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
// Handle success
if (response.indicatesSuccess()) {
- return convertOIDCTokenToNiFiToken((OIDCTokenResponse) response);
+ return response;
} else {
// If the response was not successful
final TokenErrorResponse errorResponse = (TokenErrorResponse) response;
@@ -334,15 +415,14 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
}
}
- private String convertOIDCTokenToNiFiToken(OIDCTokenResponse response) throws BadJOSEException, JOSEException, java.text.ParseException, IOException {
- final OIDCTokenResponse oidcTokenResponse = response;
- final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens();
+ private LoginAuthenticationToken convertOIDCTokenToLoginAuthenticationToken(OIDCTokenResponse response) throws BadJOSEException, JOSEException, java.text.ParseException, IOException {
+ final OIDCTokens oidcTokens = getOidcTokens(response);
final JWT oidcJwt = oidcTokens.getIDToken();
- // validate the token - no nonce required for authorization code flow
- final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null);
+ // Validate the token
+ final IDTokenClaimsSet claimsSet = validateIdToken(oidcJwt);
- // attempt to extract the configured claim to access the user's identity; default is 'email'
+ // Attempt to extract the configured claim to access the user's identity; default is 'email'
String identityClaim = properties.getOidcClaimIdentifyingUser();
String identity = claimsSet.getStringClaim(identityClaim);
@@ -364,26 +444,30 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
}
}
- // extract expiration details from the claims set
+ // 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);
+ // Convert into a NiFi JWT for retrieval later
+ final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(
+ identity, identity, expiresIn, claimsSet.getIssuer().getValue());
+ return loginToken;
+ }
+
+ private OIDCTokens getOidcTokens(OIDCTokenResponse response) {
+ return response.getOIDCTokens();
}
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
+ // 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
+ // Invoke the UserInfo endpoint
HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken);
return lookupIdentityInUserInfo(userInfoRequest);
}
@@ -428,6 +512,38 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
return presentClaims;
}
+ private void validateAccessToken(OIDCTokens oidcTokens) throws Exception {
+ // Get the Access Token to validate
+ final AccessToken accessToken = oidcTokens.getAccessToken();
+
+ // Get the preferredJwsAlgorithm for validation
+ final JWSAlgorithm jwsAlgorithm = extractJwsAlgorithm();
+
+ // Get the accessTokenHash for validation
+ final String atHashString = oidcTokens
+ .getIDToken()
+ .getJWTClaimsSet()
+ .getStringClaim("at_hash");
+
+ // Compute the Access Token hash
+ final AccessTokenHash atHash = new AccessTokenHash(atHashString);
+
+ try {
+ // Validate the Token
+ AccessTokenValidator.validate(accessToken, jwsAlgorithm, atHash);
+ } catch (InvalidHashException e) {
+ throw new Exception("Unable to validate the Access Token: " + e.getMessage());
+ }
+ }
+
+ private IDTokenClaimsSet validateIdToken(JWT oidcJwt) throws BadJOSEException, JOSEException {
+ try {
+ return tokenValidator.validate(oidcJwt, null);
+ } catch (BadJOSEException e) {
+ throw new BadJOSEException("Unable to validate the ID Token: " + e.getMessage());
+ }
+ }
+
private String lookupIdentityInUserInfo(final HTTPRequest userInfoHttpRequest) throws IOException {
try {
// send the user request
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy
new file mode 100644
index 0000000000..5480ad4837
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy
@@ -0,0 +1,214 @@
+/*
+ * 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.oauth2.sdk.auth.ClientAuthenticationMethod
+import com.nimbusds.oauth2.sdk.id.Issuer
+import com.nimbusds.openid.connect.sdk.SubjectType
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
+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.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
+
+import java.util.concurrent.TimeUnit
+
+@RunWith(JUnit4.class)
+class OidcServiceGroovyTest extends GroovyTestCase {
+ private static final Logger logger = LoggerFactory.getLogger(OidcServiceGroovyTest.class)
+
+ private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
+ private static final Map DEFAULT_NIFI_PROPERTIES = [
+ isOidcEnabled : true,
+ getOidcDiscoveryUrl : "https://localhost/oidc",
+ isLoginIdentityProviderEnabled: false,
+ isKnoxSsoEnabled : false,
+ getOidcConnectTimeout : "1000",
+ getOidcReadTimeout : "1000",
+ getOidcClientId : "expected_client_id",
+ getOidcClientSecret : "expected_client_secret",
+ getOidcClaimIdentifyingUser : "username",
+ getOidcPreferredJwsAlgorithm : ""
+ ]
+
+ // Mock collaborators
+ private static NiFiProperties mockNiFiProperties
+ private static JwtService mockJwtService = [:] as JwtService
+ private static StandardOidcIdentityProvider soip
+
+ private static final String MOCK_REQUEST_IDENTIFIER = "mock-request-identifier"
+ private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
+ ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3Rlci" +
+ "IsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3Vua" +
+ "XRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzd" +
+ "CIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9" +
+ ".b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
+
+ @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()
+ soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiProperties)
+ }
+
+ @After
+ void teardown() throws Exception {
+ }
+
+ private static NiFiProperties buildNiFiProperties(Map 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 testShouldStoreJwt() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockInitializedProvider([:])
+
+ OidcService service = new OidcService(soip)
+
+ // Expected JWT
+ logger.info("EXPECTED_JWT: ${MOCK_JWT}")
+
+ // Act
+ service.storeJwt(MOCK_REQUEST_IDENTIFIER, MOCK_JWT)
+
+ // Assert
+ final String cachedJwt = service.getJwt(MOCK_REQUEST_IDENTIFIER)
+ logger.info("Cached JWT: ${cachedJwt}")
+
+ assert cachedJwt == MOCK_JWT
+ }
+
+ @Test
+ void testShouldGetJwt() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockInitializedProvider([:])
+
+ OidcService service = new OidcService(soip)
+
+ // Expected JWT
+ logger.info("EXPECTED_JWT: ${MOCK_JWT}")
+
+ // store the jwt
+ service.storeJwt(MOCK_REQUEST_IDENTIFIER, MOCK_JWT)
+
+ // Act
+ final String retrievedJwt = service.getJwt(MOCK_REQUEST_IDENTIFIER)
+ logger.info("Retrieved JWT: ${retrievedJwt}")
+
+ // Assert
+ assert retrievedJwt == MOCK_JWT
+ }
+
+ @Test
+ void testGetJwtShouldReturnNullWithExpiredDuration() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockInitializedProvider([:])
+
+ final int DURATION = 500
+ final TimeUnit EXPIRATION_UNITS = TimeUnit.MILLISECONDS
+ OidcService service = new OidcService(soip, DURATION, EXPIRATION_UNITS)
+
+ // Expected JWT
+ logger.info("EXPECTED_JWT: ${MOCK_JWT}")
+
+ // Store the jwt
+ service.storeJwt(MOCK_REQUEST_IDENTIFIER, MOCK_JWT)
+
+ // Put thread to sleep
+ long millis = 1000
+ Thread.sleep(millis)
+ logger.info("Thread will sleep for: ${millis} ms")
+
+ // Act
+ final String retrievedJwt = service.getJwt(MOCK_REQUEST_IDENTIFIER)
+ logger.info("Retrieved JWT: ${retrievedJwt}")
+
+ // Assert
+ assert retrievedJwt == null
+ }
+
+ private static StandardOidcIdentityProvider buildIdentityProviderWithMockInitializedProvider(Map additionalProperties = [:]) {
+ JwtService mockJS = buildJwtService()
+ NiFiProperties mockNFP = buildNiFiProperties(additionalProperties)
+
+ // Mock OIDC provider metadata
+ Issuer mockIssuer = new Issuer("mockIssuer")
+ URI mockURI = new URI("https://localhost/oidc")
+ OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI)
+
+ StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP) {
+ @Override
+ void initializeProvider() {
+ soip.oidcProviderMetadata = metadata
+ soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC]
+ soip.oidcProviderMetadata["userInfoEndpointURI"] = new URI("https://localhost/oidc/token")
+ }
+ }
+ soip
+ }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
index 0c343edf0c..43afb9bcd6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.web.security.oidc
+import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jwt.JWT
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.PlainJWT
@@ -36,14 +37,16 @@ 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.AccessTokenHash
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 net.minidev.json.JSONObject
+import org.apache.commons.codec.binary.Base64
import org.apache.nifi.admin.service.KeyService
import org.apache.nifi.key.Key
import org.apache.nifi.util.NiFiProperties
@@ -65,7 +68,8 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
private static final Map DEFAULT_NIFI_PROPERTIES = [
- isOidcEnabled : false,
+// isOidcEnabled : false,
+ isOidcEnabled : true,
getOidcDiscoveryUrl : "https://localhost/oidc",
isLoginIdentityProviderEnabled: false,
isKnoxSsoEnabled : false,
@@ -73,13 +77,19 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
getOidcReadTimeout : 1000,
getOidcClientId : "expected_client_id",
getOidcClientSecret : "expected_client_secret",
- getOidcClaimIdentifyingUser : "username"
+ getOidcClaimIdentifyingUser : "username",
+ getOidcPreferredJwsAlgorithm : ""
]
// Mock collaborators
private static NiFiProperties mockNiFiProperties
private static JwtService mockJwtService = [:] as JwtService
+ private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ" +
+ "SI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZp" +
+ "X3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY19" +
+ "0ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
+
@BeforeClass
static void setUpOnce() throws Exception {
logger.metaClass.methodMissing = { String name, args ->
@@ -110,7 +120,6 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
String generateSignedToken(LoginAuthenticationToken lat) {
signNiFiToken(lat)
}
-
}
mockJS
}
@@ -137,9 +146,9 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
void testShouldGetAvailableClaims() {
// Arrange
final Map EXPECTED_CLAIMS = [
- "iss" : "https://accounts.google.com",
- "azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
- "aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
+ "iss" : "https://accounts.issuer.com",
+ "azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
+ "aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
"sub" : "10703475345439756345540",
"email" : "person@nifi.apache.org",
"email_verified": "true",
@@ -346,11 +355,12 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
logger.expected(msg)
// Assert
- assert msg =~ "An error occurred while invoking the UserInfo endpoint: The provided username and password were not correct"
+ assert msg =~ "An error occurred while invoking the UserInfo endpoint: The provided username and password " +
+ "were not correct"
}
@Test
- void testShouldConvertOIDCTokenToNiFiToken() {
+ void testShouldConvertOIDCTokenToLoginAuthenticationToken() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email"])
@@ -358,35 +368,24 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
logger.info("OIDC Token Response: ${mockResponse.dump()}")
// Act
- String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
- logger.info("NiFi token: ${nifiToken}")
+ final String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
+ logger.info("Login Authentication token: ${loginToken}")
// Assert
+ // Split ID Token into components
+ def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
+ logger.info("Token contents: ${contents} | Expiration: ${expiration}")
- // 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")
+ assert contents =~ "LoginAuthenticationToken for person@nifi\\.apache\\.org issued by https://accounts\\.issuer\\.com expiring at"
- // 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
+ // Assert expiration
+ final String[] expList = expiration.split("\\s")
+ final Long expLong = Long.parseLong(expList[0])
+ assert expLong <= System.currentTimeMillis() + 10_000
}
@Test
- void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentity() {
+ void testConvertOIDCTokenToLoginAuthenticationTokenShouldHandleBlankIdentity() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
@@ -394,34 +393,26 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
logger.info("OIDC Token Response: ${mockResponse.dump()}")
// Act
- String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
- logger.info("NiFi token: ${nifiToken}")
+ String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
+ logger.info("Login Authentication token: ${loginToken}")
// 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")
+ // Split ID Token into components
+ def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
+ logger.info("Token contents: ${contents} | Expiration: ${expiration}")
- // Parse JSON into objects
- def slurper = new JsonSlurper()
- def header = slurper.parseText(headerJson)
- logger.info("Header: ${header}")
+ assert contents =~ "LoginAuthenticationToken for person@nifi\\.apache\\.org issued by https://accounts\\.issuer\\.com expiring at"
- assert header.alg == "HS256"
+ // Get the expiration
+ final ArrayList expires = expiration.split("[\\D*]")
+ final long exp = Long.parseLong(expires[0])
+ logger.info("exp: ${exp} ms")
- 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
+ assert exp <= System.currentTimeMillis() + 10_000
}
@Test
- void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentityAndNoEmailClaim() {
+ void testConvertOIDCTokenToLoginAuthNTokenShouldHandleBlankIdentityAndNoEmailClaim() {
// Arrange
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
@@ -429,57 +420,270 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
logger.info("OIDC Token Response: ${mockResponse.dump()}")
// Act
- def msg = shouldFail {
- String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
- logger.info("NiFi token: ${nifiToken}")
+ def msg = shouldFail(ConnectException) {
+ String loginAuthenticationToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
+ logger.info("Login authentication token: ${loginAuthenticationToken}")
}
+ logger.expected(msg)
// Assert
assert msg =~ "Connection refused|Remote host terminated the handshake"
}
@Test
- void testShouldAuthorizeClient() {
+ void testShouldAuthorizeClientRequest() {
// 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"]
+ def responseBody = [id_token: MOCK_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()}")
+ def tokenResponse = soip.authorizeClientRequest(mockTokenRequest)
+ logger.info("Token Response: ${tokenResponse.dump()}")
// Assert
- assert nifiToken
+ assert tokenResponse
}
@Test
- void testAuthorizeClientShouldHandleError() {
+ void testAuthorizeClientRequestShouldHandleError() {
// 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"]
+ def responseBody = [id_token: MOCK_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)
+ def nifiToken = soip.authorizeClientRequest(mockTokenRequest)
logger.info("NiFi token: ${nifiToken}")
}
+ logger.expected(msg)
// Assert
assert msg =~ "An error occurred while invoking the Token endpoint: null"
}
+ @Test
+ void testShouldGetAccessTokenString() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
+
+ // Mock access tokens
+ AccessToken mockAccessToken = new BearerAccessToken()
+ RefreshToken mockRefreshToken = new RefreshToken()
+
+ // Compute the access token hash
+ final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256
+ AccessTokenHash EXPECTED_HASH = AccessTokenHash.compute(mockAccessToken, jwsAlgorithm)
+ logger.info("Expected at_hash: ${EXPECTED_HASH}")
+
+ // Create mock claims with at_hash
+ Map mockClaims = (["at_hash": EXPECTED_HASH.toString()])
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(mockClaims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Create OIDC Tokens
+ OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
+
+ // Create OIDC Token Response
+ OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
+
+ // Act
+ String accessTokenString = soip.getAccessTokenString(mockResponse)
+ logger.info("Access token: ${accessTokenString}")
+
+ // Assert
+ assert accessTokenString
+ }
+
+ @Test
+ void testShouldValidateAccessToken() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
+
+ // Mock access tokens
+ AccessToken mockAccessToken = new BearerAccessToken()
+ logger.info("mock access token: ${mockAccessToken.toString()}")
+ RefreshToken mockRefreshToken = new RefreshToken()
+
+ // Compute the access token hash
+ final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256
+ AccessTokenHash EXPECTED_HASH = AccessTokenHash.compute(mockAccessToken, jwsAlgorithm)
+ logger.info("Expected at_hash: ${EXPECTED_HASH}")
+
+ // Create mock claim
+ final Map claims = mockClaims(["at_hash":EXPECTED_HASH.toString()])
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Create OIDC Tokens
+ OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
+ logger.info("mock id tokens: ${mockOidcTokens.getIDToken().properties}")
+
+ // Act
+ String accessTokenString = soip.validateAccessToken(mockOidcTokens)
+ logger.info("Access Token: ${accessTokenString}")
+
+ // Assert
+ assert accessTokenString == null
+ }
+
+ @Test
+ void testValidateAccessTokenShouldHandleMismatchedATHash() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
+
+ // Mock access tokens
+ AccessToken mockAccessToken = new BearerAccessToken()
+ RefreshToken mockRefreshToken = new RefreshToken()
+
+ // Compute the access token hash
+ final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256
+ AccessTokenHash expectedHash = AccessTokenHash.compute(mockAccessToken, jwsAlgorithm)
+ logger.info("Expected at_hash: ${expectedHash}")
+
+ // Create mock claim with incorrect 'at_hash'
+ final Map claims = mockClaims()
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Create OIDC Tokens
+ OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
+
+ // Act
+ def msg = shouldFail(Exception) {
+ soip.validateAccessToken(mockOidcTokens)
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "Unable to validate the Access Token: Access token hash \\(at_hash\\) mismatch"
+ }
+
+ @Test
+ void testShouldGetIdTokenString() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
+
+ // Mock access tokens
+ AccessToken mockAccessToken = new BearerAccessToken()
+ RefreshToken mockRefreshToken = new RefreshToken()
+
+ // Create mock claim
+ final Map claims = mockClaims()
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Create OIDC Tokens
+ OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
+
+ final String EXPECTED_ID_TOKEN = mockOidcTokens.getIDTokenString()
+ logger.info("EXPECTED_ID_TOKEN: ${EXPECTED_ID_TOKEN}")
+
+ // Create OIDC Token Response
+ OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
+
+ // Act
+ final String idTokenString = soip.getIdTokenString(mockResponse)
+ logger.info("ID Token: ${idTokenString}")
+
+ // Assert
+ assert idTokenString
+ assert idTokenString == EXPECTED_ID_TOKEN
+
+ // Assert that 'email:person@nifi.apache.org' is present
+ def (String header, String payload) = idTokenString.split("\\.")
+ final byte[] idTokenBytes = Base64.decodeBase64(payload)
+ final String payloadString = new String(idTokenBytes, "UTF-8")
+ logger.info(payloadString)
+
+ assert payloadString =~ "\"email\":\"person@nifi\\.apache\\.org\""
+ }
+
+ @Test
+ void testShouldValidateIdToken() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
+
+ // Create mock claim
+ final Map claims = mockClaims()
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Act
+ final IDTokenClaimsSet claimsSet = soip.validateIdToken(mockJwt)
+ final String claimsSetString = claimsSet.toJSONObject().toString()
+ logger.info("ID Token Claims Set: ${claimsSetString}")
+
+ // Assert
+ assert claimsSet
+ assert claimsSetString =~ "\"email\":\"person@nifi\\.apache\\.org\""
+ }
+
+ @Test
+ void testShouldGetOidcTokens() {
+ // Arrange
+ StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator()
+
+ // Mock access tokens
+ AccessToken mockAccessToken = new BearerAccessToken()
+ RefreshToken mockRefreshToken = new RefreshToken()
+
+ // Create mock claim
+ final Map claims = mockClaims()
+
+ // Create Claims Set
+ JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
+
+ // Create JWT
+ JWT mockJwt = new PlainJWT(mockJWTClaimsSet)
+
+ // Create OIDC Tokens
+ OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken)
+
+ final String EXPECTED_ID_TOKEN = mockOidcTokens.getIDTokenString()
+ logger.info("EXPECTED_ID_TOKEN: ${EXPECTED_ID_TOKEN}")
+
+ // Create OIDC Token Response
+ OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens)
+
+ // Act
+ final OIDCTokens oidcTokens = soip.getOidcTokens(mockResponse)
+ logger.info("OIDC Tokens: ${oidcTokens.toJSONObject()}")
+
+ // Assert
+ assert oidcTokens
+
+ // Assert ID Tokens match
+ final JSONObject oidcJson = oidcTokens.toJSONObject()
+ final String idToken = oidcJson["id_token"]
+ logger.info("ID Token String: ${idToken}")
+ assert idToken == EXPECTED_ID_TOKEN
+ }
private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map additionalProperties = [:]) {
JwtService mockJS = buildJwtService()
@@ -515,17 +719,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
}
private OIDCTokenResponse mockOIDCTokenResponse(Map additionalClaims = [:]) {
- final Map 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
+ Map claims = mockClaims(additionalClaims)
// Create Claims Set
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
@@ -545,6 +739,20 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
mockResponse
}
+ private static Map mockClaims(Map additionalClaims = [:]) {
+ final Map claims = [
+ "iss" : "https://accounts.issuer.com",
+ "azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
+ "aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
+ "sub" : "10703475345439756345540",
+ "email" : "person@nifi.apache.org",
+ "email_verified": "true",
+ "at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A",
+ "iat" : 1590022674,
+ "exp" : 1590026274
+ ] + additionalClaims
+ claims
+ }
/**
* Forms an {@link HTTPRequest} object which returns a static response when {@code send( )} is called.
@@ -574,11 +782,4 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
}
}
}
-
- class MockOIDCProviderMetadata extends OIDCProviderMetadata {
-
- MockOIDCProviderMetadata() {
- super([:] as Issuer, [SubjectType.PUBLIC] as List, new URI("https://localhost"))
- }
- }
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
index 543b003c2b..15aedb9fdf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/OidcServiceTest.java
@@ -18,16 +18,15 @@ package org.apache.nifi.web.security.oidc;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.id.State;
-import org.junit.Test;
-
+import java.io.IOException;
import java.net.URI;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import org.junit.Test;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -39,32 +38,32 @@ public class OidcServiceTest {
public static final String TEST_STATE = "test-state";
@Test(expected = IllegalStateException.class)
- public void testOidcNotEnabledCreateState() throws Exception {
+ public void testOidcNotEnabledCreateState() {
final OidcService service = getServiceWithNoOidcSupport();
service.createState(TEST_REQUEST_IDENTIFIER);
}
@Test(expected = IllegalStateException.class)
- public void testCreateStateMultipleInvocations() throws Exception {
+ public void testCreateStateMultipleInvocations() {
final OidcService service = getServiceWithOidcSupport();
service.createState(TEST_REQUEST_IDENTIFIER);
service.createState(TEST_REQUEST_IDENTIFIER);
}
@Test(expected = IllegalStateException.class)
- public void testOidcNotEnabledValidateState() throws Exception {
+ public void testOidcNotEnabledValidateState() {
final OidcService service = getServiceWithNoOidcSupport();
service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE));
}
@Test
- public void testOidcUnknownState() throws Exception {
+ public void testOidcUnknownState() {
final OidcService service = getServiceWithOidcSupport();
assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE)));
}
@Test
- public void testValidateState() throws Exception {
+ public void testValidateState() {
final OidcService service = getServiceWithOidcSupport();
final State state = service.createState(TEST_REQUEST_IDENTIFIER);
assertTrue(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
@@ -81,41 +80,46 @@ public class OidcServiceTest {
}
@Test(expected = IllegalStateException.class)
- public void testOidcNotEnabledExchangeCode() throws Exception {
- final OidcService service = getServiceWithNoOidcSupport();
- service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant());
- }
-
- @Test(expected = IllegalStateException.class)
- public void testExchangeCodeMultipleInvocation() throws Exception {
+ public void testStoreJwtMultipleInvocation() {
final OidcService service = getServiceWithOidcSupport();
- service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant());
- service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant());
+
+ final String TEST_JWT1 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0" +
+ "lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hd" +
+ "XRoyZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw";
+
+ final String TEST_JWT2 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5ODc2NTQzMjEwIiwibmFtZSI6Ik5pRm" +
+ "kgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF" +
+ "9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmk" +
+ "uYXBhY2hlLm9yZyJ9.nlYhplDLXeGAwW62rJ_ZnEaG7nxEB4TbaJNK-_pC4WQ";
+
+ service.storeJwt(TEST_REQUEST_IDENTIFIER, TEST_JWT1);
+ service.storeJwt(TEST_REQUEST_IDENTIFIER, TEST_JWT2);
}
@Test(expected = IllegalStateException.class)
- public void testOidcNotEnabledGetJwt() throws Exception {
+ public void testOidcNotEnabledExchangeCodeForLoginAuthenticationToken() throws Exception {
+ final OidcService service = getServiceWithNoOidcSupport();
+ service.exchangeAuthorizationCodeForLoginAuthenticationToken(getAuthorizationGrant());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOidcNotEnabledExchangeCodeForAccessToken() throws Exception {
+ final OidcService service = getServiceWithNoOidcSupport();
+ service.exchangeAuthorizationCodeForAccessToken(getAuthorizationGrant());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOidcNotEnabledExchangeCodeForIdToken() throws IOException {
+ final OidcService service = getServiceWithNoOidcSupport();
+ service.exchangeAuthorizationCodeForIdToken(getAuthorizationGrant());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOidcNotEnabledGetJwt() {
final OidcService service = getServiceWithNoOidcSupport();
service.getJwt(TEST_REQUEST_IDENTIFIER);
}
- @Test
- public void testGetJwt() throws Exception {
- final OidcService service = getServiceWithOidcSupport();
- service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant());
- assertNotNull(service.getJwt(TEST_REQUEST_IDENTIFIER));
- }
-
- @Test
- public void testGetJwtExpiration() throws Exception {
- final OidcService service = getServiceWithOidcSupportAndCustomExpiration(1, TimeUnit.SECONDS);
- service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant());
-
- Thread.sleep(3 * 1000);
-
- assertNull(service.getJwt(TEST_REQUEST_IDENTIFIER));
- }
-
private OidcService getServiceWithNoOidcSupport() {
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
when(provider.isOidcEnabled()).thenReturn(false);
@@ -126,10 +130,9 @@ public class OidcServiceTest {
return service;
}
- private OidcService getServiceWithOidcSupport() throws Exception {
+ private OidcService getServiceWithOidcSupport() {
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
when(provider.isOidcEnabled()).thenReturn(true);
- when(provider.exchangeAuthorizationCode(any())).then(invocation -> UUID.randomUUID().toString());
final OidcService service = new OidcService(provider);
assertTrue(service.isOidcEnabled());
@@ -140,7 +143,7 @@ public class OidcServiceTest {
private OidcService getServiceWithOidcSupportAndCustomExpiration(final int duration, final TimeUnit units) throws Exception {
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
when(provider.isOidcEnabled()).thenReturn(true);
- when(provider.exchangeAuthorizationCode(any())).then(invocation -> UUID.randomUUID().toString());
+ when(provider.exchangeAuthorizationCodeforLoginAuthenticationToken(any())).then(invocation -> UUID.randomUUID().toString());
final OidcService service = new OidcService(provider, duration, units);
assertTrue(service.isOidcEnabled());
@@ -148,7 +151,7 @@ public class OidcServiceTest {
return service;
}
- private AuthorizationCodeGrant getAuthorizationCodeGrant() {
+ private AuthorizationGrant getAuthorizationGrant() {
return new AuthorizationCodeGrant(new AuthorizationCode("code"), URI.create("http://localhost:8080/nifi"));
}
}
\ No newline at end of file