mirror of https://github.com/apache/nifi.git
NIFI-7584 Added OIDC logout mechanism.
Added method to validate the OIDC Access Token for the revoke endpoint. Created a new callback URI of oidc/logoutCallback to handle certain OIDC logout cases. Changed method to exchange the Authorization Code for a Login Authentication Token. Added a new method to exchange the AuthN Code for an Access Token. Changed method to convert OIDC Token to a Login AuthN Token instead of a NiFi JWT. Created new OidcServiceGroovyTest class. NIFI-7584-rebase Added test. NIFI-7584 Fixed a checkstyle issue. NIFI-7584 Removed a dependency not in use. NIFI-7584 Made revisions based on PR review. Refactored revoke endpoint POST request to a private method. Removed unnecessary dependencies. Fixed Regex Pattern to search for literal dot character. Fixed logging the Exception message. Fixed caught Exception. Changed timeout value to a static variable. Changed repeating error messages to a static string. Reduced sleep duration in unit test. Refactored cookie generation to private method. NIFI-7584 Fixed the snapshot version. Signed-off-by: Nathan Gough <thenatog@gmail.com> This closes #4593.
This commit is contained in:
parent
c70f4ac530
commit
bf962f6227
|
@ -422,5 +422,9 @@
|
||||||
<version>1.3</version>
|
<version>1.3</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.nifi.web;
|
package org.apache.nifi.web;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import org.apache.nifi.util.NiFiProperties;
|
import org.apache.nifi.util.NiFiProperties;
|
||||||
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
|
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
|
||||||
import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
|
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.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
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
|
* NiFi Web Api Spring security. Applies the various NiFiAuthenticationFilter servlet filters which will extract authentication
|
||||||
* credentials from API requests.
|
* credentials from API requests.
|
||||||
|
@ -92,7 +91,7 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
|
||||||
webSecurity
|
webSecurity
|
||||||
.ignoring()
|
.ignoring()
|
||||||
.antMatchers("/access", "/access/config", "/access/token", "/access/kerberos",
|
.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");
|
"/access/knox/callback", "/access/knox/request");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||||
import com.nimbusds.oauth2.sdk.ParseException;
|
import com.nimbusds.oauth2.sdk.ParseException;
|
||||||
|
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||||
import com.nimbusds.oauth2.sdk.id.State;
|
import com.nimbusds.oauth2.sdk.id.State;
|
||||||
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
|
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
|
||||||
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
|
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.ApiOperation;
|
||||||
import io.swagger.annotations.ApiResponse;
|
import io.swagger.annotations.ApiResponse;
|
||||||
import io.swagger.annotations.ApiResponses;
|
import io.swagger.annotations.ApiResponses;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
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.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.admin.service.AdministrationException;
|
||||||
import org.apache.nifi.authentication.AuthenticationResponse;
|
import org.apache.nifi.authentication.AuthenticationResponse;
|
||||||
import org.apache.nifi.authentication.LoginCredentials;
|
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_REQUEST_IDENTIFIER = "oidc-request-identifier";
|
||||||
private static final String OIDC_ERROR_TITLE = "Unable to continue login sequence";
|
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.";
|
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
|
// ensure oidc is enabled
|
||||||
if (!oidcService.isOidcEnabled()) {
|
if (!oidcService.isOidcEnabled()) {
|
||||||
forwardToMessagePage(httpServletRequest, httpServletResponse, "OpenId Connect is not configured.");
|
forwardToMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String oidcRequestIdentifier = UUID.randomUUID().toString();
|
// generate the authorization uri
|
||||||
|
URI authorizationURI = oidcRequestAuthorizationCode(httpServletResponse, getOidcCallback());
|
||||||
// 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 response
|
// generate the response
|
||||||
httpServletResponse.sendRedirect(authorizationUri.toString());
|
httpServletResponse.sendRedirect(authorizationURI.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -207,19 +210,20 @@ public class AccessResource extends ApplicationResource {
|
||||||
public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||||
// only consider user specific access over https
|
// only consider user specific access over https
|
||||||
if (!httpServletRequest.isSecure()) {
|
if (!httpServletRequest.isSecure()) {
|
||||||
forwardToMessagePage(httpServletRequest, httpServletResponse, "User authentication/authorization is only supported when running over HTTPS.");
|
forwardToMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure oidc is enabled
|
// ensure oidc is enabled
|
||||||
if (!oidcService.isOidcEnabled()) {
|
if (!oidcService.isOidcEnabled()) {
|
||||||
forwardToMessagePage(httpServletRequest, httpServletResponse, "OpenId Connect is not configured.");
|
forwardToMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
|
final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER);
|
||||||
if (oidcRequestIdentifier == null) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +237,8 @@ public class AccessResource extends ApplicationResource {
|
||||||
removeOidcRequestCookie(httpServletResponse);
|
removeOidcRequestCookie(httpServletResponse);
|
||||||
|
|
||||||
// forward to the error page
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,13 +248,15 @@ public class AccessResource extends ApplicationResource {
|
||||||
// confirm state
|
// confirm state
|
||||||
final State state = successfulOidcResponse.getState();
|
final State state = successfulOidcResponse.getState();
|
||||||
if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
|
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
|
// remove the oidc request cookie
|
||||||
removeOidcRequestCookie(httpServletResponse);
|
removeOidcRequestCookie(httpServletResponse);
|
||||||
|
|
||||||
// forward to the error page
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,15 +264,24 @@ public class AccessResource extends ApplicationResource {
|
||||||
// exchange authorization code for id token
|
// exchange authorization code for id token
|
||||||
final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
|
final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode();
|
||||||
final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcCallback()));
|
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) {
|
} 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
|
// remove the oidc request cookie
|
||||||
removeOidcRequestCookie(httpServletResponse);
|
removeOidcRequestCookie(httpServletResponse);
|
||||||
|
|
||||||
// forward to the error page
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +293,8 @@ public class AccessResource extends ApplicationResource {
|
||||||
|
|
||||||
// report the unsuccessful login
|
// report the unsuccessful login
|
||||||
final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
|
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,
|
response = String.class,
|
||||||
notes = NON_GUARANTEED_ENDPOINT
|
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
|
// only consider user specific access over https
|
||||||
if (!httpServletRequest.isSecure()) {
|
if (!httpServletRequest.isSecure()) {
|
||||||
throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
|
throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
|
||||||
|
@ -298,7 +315,7 @@ public class AccessResource extends ApplicationResource {
|
||||||
|
|
||||||
// ensure oidc is enabled
|
// ensure oidc is enabled
|
||||||
if (!oidcService.isOidcEnabled()) {
|
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);
|
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 {
|
public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||||
if (!httpServletRequest.isSecure()) {
|
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()) {
|
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();
|
// Get the oidc discovery url
|
||||||
String postLogoutRedirectUri = generateResourceUri("..", "nifi", "logout-complete");
|
String oidcDiscoveryUrl = properties.getOidcDiscoveryUrl();
|
||||||
|
|
||||||
if (endSessionEndpoint == null) {
|
// Determine the logout method
|
||||||
// handle the case, where the OpenID Provider does not have an end session endpoint
|
String logoutMethod = determineLogoutMethod(oidcDiscoveryUrl);
|
||||||
httpServletResponse.sendRedirect(postLogoutRedirectUri);
|
|
||||||
|
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 {
|
} else {
|
||||||
URI logoutUri = UriBuilder.fromUri(endSessionEndpoint)
|
// remove the oidc request cookie
|
||||||
.queryParam("post_logout_redirect_uri", postLogoutRedirectUri)
|
removeOidcRequestCookie(httpServletResponse);
|
||||||
.build();
|
|
||||||
httpServletResponse.sendRedirect(logoutUri.toString());
|
// 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 {
|
public void knoxCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
|
||||||
// only consider user specific access over https
|
// only consider user specific access over https
|
||||||
if (!httpServletRequest.isSecure()) {
|
if (!httpServletRequest.isSecure()) {
|
||||||
forwardToMessagePage(httpServletRequest, httpServletResponse, "User authentication/authorization is only supported when running over HTTPS.");
|
forwardToMessagePage(httpServletRequest, httpServletResponse, AUTHENTICATION_NOT_ENABLED_MSG);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,7 +959,7 @@ public class AccessResource extends ApplicationResource {
|
||||||
)
|
)
|
||||||
public Response logOut(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
|
public Response logOut(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
|
||||||
if (!httpServletRequest.isSecure()) {
|
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();
|
String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
|
||||||
|
@ -828,6 +1019,14 @@ public class AccessResource extends ApplicationResource {
|
||||||
return generateResourceUri("access", "oidc", "callback");
|
return generateResourceUri("access", "oidc", "callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getOidcLogoutCallback() {
|
||||||
|
return generateResourceUri("access", "oidc", "logoutCallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI getRevokeEndpoint() {
|
||||||
|
return oidcService.getRevocationEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
private String getNiFiUri() {
|
private String getNiFiUri() {
|
||||||
final String nifiApiUrl = generateResourceUri();
|
final String nifiApiUrl = generateResourceUri();
|
||||||
final String baseUrl = StringUtils.substringBeforeLast(nifiApiUrl, "/nifi-api");
|
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);
|
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<NameValuePair> 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
|
// setters
|
||||||
|
|
||||||
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
|
public void setLoginIdentityProvider(LoginIdentityProvider loginIdentityProvider) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.nimbusds.oauth2.sdk.Scope;
|
||||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||||
|
|
||||||
public interface OidcIdentityProvider {
|
public interface OidcIdentityProvider {
|
||||||
|
|
||||||
|
@ -60,6 +61,13 @@ public interface OidcIdentityProvider {
|
||||||
*/
|
*/
|
||||||
URI getEndSessionEndpoint();
|
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.
|
* Returns the scopes supported by the OIDC provider.
|
||||||
*
|
*
|
||||||
|
@ -68,12 +76,30 @@ public interface OidcIdentityProvider {
|
||||||
Scope getScope();
|
Scope getScope();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchanges the supplied authorization grant for an ID token. Extracts the identity from the ID
|
* Exchanges the supplied authorization grant for a Login ID Token. Extracts the identity from the ID
|
||||||
* token and converts it into NiFi JWT.
|
* token.
|
||||||
*
|
*
|
||||||
* @param authorizationGrant authorization grant for invoking the Token Endpoint
|
* @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
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.security.MessageDigest;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
|
||||||
import org.apache.nifi.web.security.util.CacheKey;
|
import org.apache.nifi.web.security.util.CacheKey;
|
||||||
|
|
||||||
import static org.apache.nifi.web.security.oidc.StandardOidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED;
|
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();
|
return identityProvider.getEndSessionEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the OpenId Connect revocation endpoint.
|
||||||
|
*
|
||||||
|
* @return the revocation endpoint
|
||||||
|
*/
|
||||||
|
public URI getRevocationEndpoint() {
|
||||||
|
return identityProvider.getRevocationEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the OpenId Connect scope.
|
* 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
|
* @param authorizationGrant authorization grant
|
||||||
|
* @return a Login Authentication Token
|
||||||
* @throws IOException exceptional case for communication error with the OpenId Connect provider
|
* @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()) {
|
if (!isOidcEnabled()) {
|
||||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
|
|
||||||
final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier);
|
// Retrieve Login Authentication Token
|
||||||
final String nifiJwt = retrieveNifiJwt(authorizationGrant);
|
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 {
|
try {
|
||||||
// cache the jwt for later retrieval
|
// Cache the jwt for later retrieval
|
||||||
synchronized (jwtLookupForCompletedRequests) {
|
synchronized (jwtLookupForCompletedRequests) {
|
||||||
final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> nifiJwt);
|
final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> jwt);
|
||||||
if (!timeConstantEqualityCheck(nifiJwt, cachedJwt)) {
|
if (!timeConstantEqualityCheck(jwt, cachedJwt)) {
|
||||||
throw new IllegalStateException("An existing login request is already in progress.");
|
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
|
* 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
|
* identifier is not associated with a JWT or if the login sequence was not completed before
|
||||||
|
|
|
@ -38,6 +38,7 @@ import com.nimbusds.oauth2.sdk.auth.Secret;
|
||||||
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
||||||
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||||
|
import com.nimbusds.oauth2.sdk.token.AccessToken;
|
||||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
|
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
|
||||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
|
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.UserInfoRequest;
|
||||||
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
|
import com.nimbusds.openid.connect.sdk.UserInfoResponse;
|
||||||
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
|
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.claims.IDTokenClaimsSet;
|
||||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
|
||||||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
|
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.IDTokenValidator;
|
||||||
|
import com.nimbusds.openid.connect.sdk.validators.InvalidHashException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
@ -159,18 +163,7 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get the preferred json web signature algorithm
|
// get the preferred json web signature algorithm
|
||||||
final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm();
|
final JWSAlgorithm preferredJwsAlgorithm = extractJwsAlgorithm();
|
||||||
|
|
||||||
final JWSAlgorithm preferredJwsAlgorithm;
|
|
||||||
if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) {
|
|
||||||
preferredJwsAlgorithm = JWSAlgorithm.RS256;
|
|
||||||
} else {
|
|
||||||
if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) {
|
|
||||||
preferredJwsAlgorithm = null;
|
|
||||||
} else {
|
|
||||||
preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferredJwsAlgorithm == null) {
|
if (preferredJwsAlgorithm == null) {
|
||||||
tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId);
|
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.
|
* 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()) {
|
if (!isOidcEnabled()) {
|
||||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return oidcProviderMetadata.getAuthorizationEndpointURI();
|
return oidcProviderMetadata.getAuthorizationEndpointURI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +283,14 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
||||||
return oidcProviderMetadata.getEndSessionEndpointURI();
|
return oidcProviderMetadata.getEndSessionEndpointURI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getRevocationEndpoint() {
|
||||||
|
if (!isOidcEnabled()) {
|
||||||
|
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||||
|
}
|
||||||
|
return oidcProviderMetadata.getRevocationEndpointURI();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Scope getScope() {
|
public Scope getScope() {
|
||||||
if (!isOidcEnabled()) {
|
if (!isOidcEnabled()) {
|
||||||
|
@ -295,37 +312,101 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
||||||
if (!isOidcEnabled()) {
|
if (!isOidcEnabled()) {
|
||||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws IOException {
|
public LoginAuthenticationToken exchangeAuthorizationCodeforLoginAuthenticationToken(final AuthorizationGrant authorizationGrant) throws IOException {
|
||||||
// Check if OIDC is enabled
|
// Check if OIDC is enabled
|
||||||
if (!isOidcEnabled()) {
|
if (!isOidcEnabled()) {
|
||||||
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build ClientAuthentication
|
|
||||||
final ClientAuthentication clientAuthentication = createClientAuthentication();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build the token request
|
// Authenticate and authorize the client request
|
||||||
final HTTPRequest tokenHttpRequest = createTokenHTTPRequest(authorizationGrant, clientAuthentication);
|
final TokenResponse response = authorizeClient(authorizationGrant);
|
||||||
return authorizeClient(tokenHttpRequest);
|
|
||||||
|
|
||||||
} catch (final ParseException | JOSEException | BadJOSEException | java.text.ParseException e) {
|
// Convert response to Login Authentication Token
|
||||||
throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage());
|
// 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
|
// Get the token response
|
||||||
final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
|
final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send());
|
||||||
|
|
||||||
// Handle success
|
// Handle success
|
||||||
if (response.indicatesSuccess()) {
|
if (response.indicatesSuccess()) {
|
||||||
return convertOIDCTokenToNiFiToken((OIDCTokenResponse) response);
|
return response;
|
||||||
} else {
|
} else {
|
||||||
// If the response was not successful
|
// If the response was not successful
|
||||||
final TokenErrorResponse errorResponse = (TokenErrorResponse) response;
|
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 {
|
private LoginAuthenticationToken convertOIDCTokenToLoginAuthenticationToken(OIDCTokenResponse response) throws BadJOSEException, JOSEException, java.text.ParseException, IOException {
|
||||||
final OIDCTokenResponse oidcTokenResponse = response;
|
final OIDCTokens oidcTokens = getOidcTokens(response);
|
||||||
final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens();
|
|
||||||
final JWT oidcJwt = oidcTokens.getIDToken();
|
final JWT oidcJwt = oidcTokens.getIDToken();
|
||||||
|
|
||||||
// validate the token - no nonce required for authorization code flow
|
// Validate the token
|
||||||
final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null);
|
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 identityClaim = properties.getOidcClaimIdentifyingUser();
|
||||||
String identity = claimsSet.getStringClaim(identityClaim);
|
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 Calendar now = Calendar.getInstance();
|
||||||
final Date expiration = claimsSet.getExpirationTime();
|
final Date expiration = claimsSet.getExpirationTime();
|
||||||
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
|
final long expiresIn = expiration.getTime() - now.getTimeInMillis();
|
||||||
|
|
||||||
// convert into a nifi jwt for retrieval later
|
// Convert into a NiFi JWT for retrieval later
|
||||||
final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(identity, identity, expiresIn,
|
final LoginAuthenticationToken loginToken = new LoginAuthenticationToken(
|
||||||
claimsSet.getIssuer().getValue());
|
identity, identity, expiresIn, claimsSet.getIssuer().getValue());
|
||||||
return jwtService.generateSignedToken(loginToken);
|
return loginToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OIDCTokens getOidcTokens(OIDCTokenResponse response) {
|
||||||
|
return response.getOIDCTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens) throws IOException {
|
private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens) throws IOException {
|
||||||
// explicitly try to get the identity from the UserInfo endpoint with the configured claim
|
// Explicitly try to get the identity from the UserInfo endpoint with the configured claim
|
||||||
// extract the bearer access token
|
// Extract the bearer access token
|
||||||
final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken();
|
final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken();
|
||||||
if (bearerAccessToken == null) {
|
if (bearerAccessToken == null) {
|
||||||
throw new IllegalStateException("No access token found in the ID tokens");
|
throw new IllegalStateException("No access token found in the ID tokens");
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoke the UserInfo endpoint
|
// Invoke the UserInfo endpoint
|
||||||
HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken);
|
HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken);
|
||||||
return lookupIdentityInUserInfo(userInfoRequest);
|
return lookupIdentityInUserInfo(userInfoRequest);
|
||||||
}
|
}
|
||||||
|
@ -428,6 +512,38 @@ public class StandardOidcIdentityProvider implements OidcIdentityProvider {
|
||||||
return presentClaims;
|
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 {
|
private String lookupIdentityInUserInfo(final HTTPRequest userInfoHttpRequest) throws IOException {
|
||||||
try {
|
try {
|
||||||
// send the user request
|
// send the user request
|
||||||
|
|
|
@ -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<String, Object> 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<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 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<String, String> 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.nifi.web.security.oidc
|
package org.apache.nifi.web.security.oidc
|
||||||
|
|
||||||
|
import com.nimbusds.jose.JWSAlgorithm
|
||||||
import com.nimbusds.jwt.JWT
|
import com.nimbusds.jwt.JWT
|
||||||
import com.nimbusds.jwt.JWTClaimsSet
|
import com.nimbusds.jwt.JWTClaimsSet
|
||||||
import com.nimbusds.jwt.PlainJWT
|
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.Nonce
|
||||||
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse
|
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse
|
||||||
import com.nimbusds.openid.connect.sdk.SubjectType
|
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.claims.IDTokenClaimsSet
|
||||||
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata
|
||||||
import com.nimbusds.openid.connect.sdk.token.OIDCTokens
|
import com.nimbusds.openid.connect.sdk.token.OIDCTokens
|
||||||
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
|
import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator
|
||||||
import groovy.json.JsonOutput
|
import groovy.json.JsonOutput
|
||||||
import groovy.json.JsonSlurper
|
|
||||||
import io.jsonwebtoken.Jwts
|
import io.jsonwebtoken.Jwts
|
||||||
import io.jsonwebtoken.SignatureAlgorithm
|
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.admin.service.KeyService
|
||||||
import org.apache.nifi.key.Key
|
import org.apache.nifi.key.Key
|
||||||
import org.apache.nifi.util.NiFiProperties
|
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 Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
|
||||||
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
|
private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
|
||||||
isOidcEnabled : false,
|
// isOidcEnabled : false,
|
||||||
|
isOidcEnabled : true,
|
||||||
getOidcDiscoveryUrl : "https://localhost/oidc",
|
getOidcDiscoveryUrl : "https://localhost/oidc",
|
||||||
isLoginIdentityProviderEnabled: false,
|
isLoginIdentityProviderEnabled: false,
|
||||||
isKnoxSsoEnabled : false,
|
isKnoxSsoEnabled : false,
|
||||||
|
@ -73,13 +77,19 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
getOidcReadTimeout : 1000,
|
getOidcReadTimeout : 1000,
|
||||||
getOidcClientId : "expected_client_id",
|
getOidcClientId : "expected_client_id",
|
||||||
getOidcClientSecret : "expected_client_secret",
|
getOidcClientSecret : "expected_client_secret",
|
||||||
getOidcClaimIdentifyingUser : "username"
|
getOidcClaimIdentifyingUser : "username",
|
||||||
|
getOidcPreferredJwsAlgorithm : ""
|
||||||
]
|
]
|
||||||
|
|
||||||
// Mock collaborators
|
// Mock collaborators
|
||||||
private static NiFiProperties mockNiFiProperties
|
private static NiFiProperties mockNiFiProperties
|
||||||
private static JwtService mockJwtService = [:] as JwtService
|
private static JwtService mockJwtService = [:] as JwtService
|
||||||
|
|
||||||
|
private static final String MOCK_JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZ" +
|
||||||
|
"SI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZp" +
|
||||||
|
"X3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY19" +
|
||||||
|
"0ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw"
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
static void setUpOnce() throws Exception {
|
static void setUpOnce() throws Exception {
|
||||||
logger.metaClass.methodMissing = { String name, args ->
|
logger.metaClass.methodMissing = { String name, args ->
|
||||||
|
@ -110,7 +120,6 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
String generateSignedToken(LoginAuthenticationToken lat) {
|
String generateSignedToken(LoginAuthenticationToken lat) {
|
||||||
signNiFiToken(lat)
|
signNiFiToken(lat)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
mockJS
|
mockJS
|
||||||
}
|
}
|
||||||
|
@ -137,9 +146,9 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
void testShouldGetAvailableClaims() {
|
void testShouldGetAvailableClaims() {
|
||||||
// Arrange
|
// Arrange
|
||||||
final Map<String, String> EXPECTED_CLAIMS = [
|
final Map<String, String> EXPECTED_CLAIMS = [
|
||||||
"iss" : "https://accounts.google.com",
|
"iss" : "https://accounts.issuer.com",
|
||||||
"azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
|
"azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
|
||||||
"aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com",
|
"aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.usercontent.com",
|
||||||
"sub" : "10703475345439756345540",
|
"sub" : "10703475345439756345540",
|
||||||
"email" : "person@nifi.apache.org",
|
"email" : "person@nifi.apache.org",
|
||||||
"email_verified": "true",
|
"email_verified": "true",
|
||||||
|
@ -346,11 +355,12 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
logger.expected(msg)
|
logger.expected(msg)
|
||||||
|
|
||||||
// Assert
|
// 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
|
@Test
|
||||||
void testShouldConvertOIDCTokenToNiFiToken() {
|
void testShouldConvertOIDCTokenToLoginAuthenticationToken() {
|
||||||
// Arrange
|
// Arrange
|
||||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email"])
|
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email"])
|
||||||
|
|
||||||
|
@ -358,35 +368,24 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
|
final String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
|
||||||
logger.info("NiFi token: ${nifiToken}")
|
logger.info("Login Authentication token: ${loginToken}")
|
||||||
|
|
||||||
// Assert
|
// 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
|
assert contents =~ "LoginAuthenticationToken for person@nifi\\.apache\\.org issued by https://accounts\\.issuer\\.com expiring at"
|
||||||
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
|
// Assert expiration
|
||||||
def slurper = new JsonSlurper()
|
final String[] expList = expiration.split("\\s")
|
||||||
def header = slurper.parseText(headerJson)
|
final Long expLong = Long.parseLong(expList[0])
|
||||||
logger.info("Header: ${header}")
|
assert expLong <= System.currentTimeMillis() + 10_000
|
||||||
|
|
||||||
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
|
@Test
|
||||||
void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentity() {
|
void testConvertOIDCTokenToLoginAuthenticationTokenShouldHandleBlankIdentity() {
|
||||||
// Arrange
|
// Arrange
|
||||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
|
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
|
||||||
|
|
||||||
|
@ -394,34 +393,26 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
|
String loginToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
|
||||||
logger.info("NiFi token: ${nifiToken}")
|
logger.info("Login Authentication token: ${loginToken}")
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// Split JWT into components and decode Base64 to JSON
|
// Split ID Token into components
|
||||||
def (String headerB64, String payloadB64, String signatureB64) = nifiToken.tokenize("\\.")
|
def (String contents, String expiration) = loginToken.tokenize("\\[\\]")
|
||||||
logger.info("Header: ${headerB64} | Payload: ${payloadB64} | Signature: ${signatureB64}")
|
logger.info("Token contents: ${contents} | Expiration: ${expiration}")
|
||||||
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
|
assert contents =~ "LoginAuthenticationToken for person@nifi\\.apache\\.org issued by https://accounts\\.issuer\\.com expiring at"
|
||||||
def slurper = new JsonSlurper()
|
|
||||||
def header = slurper.parseText(headerJson)
|
|
||||||
logger.info("Header: ${header}")
|
|
||||||
|
|
||||||
assert header.alg == "HS256"
|
// Get the expiration
|
||||||
|
final ArrayList<String> expires = expiration.split("[\\D*]")
|
||||||
|
final long exp = Long.parseLong(expires[0])
|
||||||
|
logger.info("exp: ${exp} ms")
|
||||||
|
|
||||||
def payload = slurper.parseText(payloadJson)
|
assert exp <= System.currentTimeMillis() + 10_000
|
||||||
logger.info("Payload: ${payload}")
|
|
||||||
|
|
||||||
assert payload.username == "person@nifi.apache.org"
|
|
||||||
assert payload.keyId == 1
|
|
||||||
assert payload.exp <= System.currentTimeMillis() + 10_000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentityAndNoEmailClaim() {
|
void testConvertOIDCTokenToLoginAuthNTokenShouldHandleBlankIdentityAndNoEmailClaim() {
|
||||||
// Arrange
|
// Arrange
|
||||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
|
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"])
|
||||||
|
|
||||||
|
@ -429,57 +420,270 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
logger.info("OIDC Token Response: ${mockResponse.dump()}")
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
def msg = shouldFail {
|
def msg = shouldFail(ConnectException) {
|
||||||
String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse)
|
String loginAuthenticationToken = soip.convertOIDCTokenToLoginAuthenticationToken(mockResponse)
|
||||||
logger.info("NiFi token: ${nifiToken}")
|
logger.info("Login authentication token: ${loginAuthenticationToken}")
|
||||||
}
|
}
|
||||||
|
logger.expected(msg)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert msg =~ "Connection refused|Remote host terminated the handshake"
|
assert msg =~ "Connection refused|Remote host terminated the handshake"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testShouldAuthorizeClient() {
|
void testShouldAuthorizeClientRequest() {
|
||||||
// Arrange
|
// Arrange
|
||||||
// Build ID Provider with mock token endpoint URI to make a connection
|
// Build ID Provider with mock token endpoint URI to make a connection
|
||||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
|
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
|
||||||
|
|
||||||
// Mock the JWT
|
def responseBody = [id_token: MOCK_JWT, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"]
|
||||||
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")
|
HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 200, "HTTP OK")
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
def nifiToken = soip.authorizeClient(mockTokenRequest)
|
def tokenResponse = soip.authorizeClientRequest(mockTokenRequest)
|
||||||
logger.info("NiFi Token: ${nifiToken.dump()}")
|
logger.info("Token Response: ${tokenResponse.dump()}")
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert nifiToken
|
assert tokenResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAuthorizeClientShouldHandleError() {
|
void testAuthorizeClientRequestShouldHandleError() {
|
||||||
// Arrange
|
// Arrange
|
||||||
// Build ID Provider with mock token endpoint URI to make a connection
|
// Build ID Provider with mock token endpoint URI to make a connection
|
||||||
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
|
StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:])
|
||||||
|
|
||||||
// Mock the JWT
|
def responseBody = [id_token: MOCK_JWT, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"]
|
||||||
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")
|
HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 500, "HTTP SERVER ERROR")
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
def msg = shouldFail(RuntimeException) {
|
def msg = shouldFail(RuntimeException) {
|
||||||
def nifiToken = soip.authorizeClient(mockTokenRequest)
|
def nifiToken = soip.authorizeClientRequest(mockTokenRequest)
|
||||||
logger.info("NiFi token: ${nifiToken}")
|
logger.info("NiFi token: ${nifiToken}")
|
||||||
}
|
}
|
||||||
|
logger.expected(msg)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert msg =~ "An error occurred while invoking the Token endpoint: null"
|
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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, String> additionalProperties = [:]) {
|
private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map<String, String> additionalProperties = [:]) {
|
||||||
JwtService mockJS = buildJwtService()
|
JwtService mockJS = buildJwtService()
|
||||||
|
@ -515,17 +719,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private OIDCTokenResponse mockOIDCTokenResponse(Map<String, Object> additionalClaims = [:]) {
|
private OIDCTokenResponse mockOIDCTokenResponse(Map<String, Object> additionalClaims = [:]) {
|
||||||
final Map<String, Object> claims = [
|
Map<String, Object> claims = mockClaims(additionalClaims)
|
||||||
"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
|
// Create Claims Set
|
||||||
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims)
|
||||||
|
@ -545,6 +739,20 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
|
||||||
mockResponse
|
mockResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> mockClaims(Map<String, Object> additionalClaims = [:]) {
|
||||||
|
final Map<String, Object> 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.
|
* 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<SubjectType>, new URI("https://localhost"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,15 @@ package org.apache.nifi.web.security.oidc;
|
||||||
|
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
import com.nimbusds.oauth2.sdk.AuthorizationCode;
|
||||||
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
|
||||||
|
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
|
||||||
import com.nimbusds.oauth2.sdk.id.State;
|
import com.nimbusds.oauth2.sdk.id.State;
|
||||||
import org.junit.Test;
|
import java.io.IOException;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
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.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
@ -39,32 +38,32 @@ public class OidcServiceTest {
|
||||||
public static final String TEST_STATE = "test-state";
|
public static final String TEST_STATE = "test-state";
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testOidcNotEnabledCreateState() throws Exception {
|
public void testOidcNotEnabledCreateState() {
|
||||||
final OidcService service = getServiceWithNoOidcSupport();
|
final OidcService service = getServiceWithNoOidcSupport();
|
||||||
service.createState(TEST_REQUEST_IDENTIFIER);
|
service.createState(TEST_REQUEST_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testCreateStateMultipleInvocations() throws Exception {
|
public void testCreateStateMultipleInvocations() {
|
||||||
final OidcService service = getServiceWithOidcSupport();
|
final OidcService service = getServiceWithOidcSupport();
|
||||||
service.createState(TEST_REQUEST_IDENTIFIER);
|
service.createState(TEST_REQUEST_IDENTIFIER);
|
||||||
service.createState(TEST_REQUEST_IDENTIFIER);
|
service.createState(TEST_REQUEST_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testOidcNotEnabledValidateState() throws Exception {
|
public void testOidcNotEnabledValidateState() {
|
||||||
final OidcService service = getServiceWithNoOidcSupport();
|
final OidcService service = getServiceWithNoOidcSupport();
|
||||||
service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE));
|
service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOidcUnknownState() throws Exception {
|
public void testOidcUnknownState() {
|
||||||
final OidcService service = getServiceWithOidcSupport();
|
final OidcService service = getServiceWithOidcSupport();
|
||||||
assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE)));
|
assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateState() throws Exception {
|
public void testValidateState() {
|
||||||
final OidcService service = getServiceWithOidcSupport();
|
final OidcService service = getServiceWithOidcSupport();
|
||||||
final State state = service.createState(TEST_REQUEST_IDENTIFIER);
|
final State state = service.createState(TEST_REQUEST_IDENTIFIER);
|
||||||
assertTrue(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
|
assertTrue(service.isStateValid(TEST_REQUEST_IDENTIFIER, state));
|
||||||
|
@ -81,41 +80,46 @@ public class OidcServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
@Test(expected = IllegalStateException.class)
|
||||||
public void testOidcNotEnabledExchangeCode() throws Exception {
|
public void testStoreJwtMultipleInvocation() {
|
||||||
final OidcService service = getServiceWithNoOidcSupport();
|
|
||||||
service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = IllegalStateException.class)
|
|
||||||
public void testExchangeCodeMultipleInvocation() throws Exception {
|
|
||||||
final OidcService service = getServiceWithOidcSupport();
|
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)
|
@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();
|
final OidcService service = getServiceWithNoOidcSupport();
|
||||||
service.getJwt(TEST_REQUEST_IDENTIFIER);
|
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() {
|
private OidcService getServiceWithNoOidcSupport() {
|
||||||
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
||||||
when(provider.isOidcEnabled()).thenReturn(false);
|
when(provider.isOidcEnabled()).thenReturn(false);
|
||||||
|
@ -126,10 +130,9 @@ public class OidcServiceTest {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
private OidcService getServiceWithOidcSupport() throws Exception {
|
private OidcService getServiceWithOidcSupport() {
|
||||||
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
||||||
when(provider.isOidcEnabled()).thenReturn(true);
|
when(provider.isOidcEnabled()).thenReturn(true);
|
||||||
when(provider.exchangeAuthorizationCode(any())).then(invocation -> UUID.randomUUID().toString());
|
|
||||||
|
|
||||||
final OidcService service = new OidcService(provider);
|
final OidcService service = new OidcService(provider);
|
||||||
assertTrue(service.isOidcEnabled());
|
assertTrue(service.isOidcEnabled());
|
||||||
|
@ -140,7 +143,7 @@ public class OidcServiceTest {
|
||||||
private OidcService getServiceWithOidcSupportAndCustomExpiration(final int duration, final TimeUnit units) throws Exception {
|
private OidcService getServiceWithOidcSupportAndCustomExpiration(final int duration, final TimeUnit units) throws Exception {
|
||||||
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
final OidcIdentityProvider provider = mock(OidcIdentityProvider.class);
|
||||||
when(provider.isOidcEnabled()).thenReturn(true);
|
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);
|
final OidcService service = new OidcService(provider, duration, units);
|
||||||
assertTrue(service.isOidcEnabled());
|
assertTrue(service.isOidcEnabled());
|
||||||
|
@ -148,7 +151,7 @@ public class OidcServiceTest {
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationCodeGrant getAuthorizationCodeGrant() {
|
private AuthorizationGrant getAuthorizationGrant() {
|
||||||
return new AuthorizationCodeGrant(new AuthorizationCode("code"), URI.create("http://localhost:8080/nifi"));
|
return new AuthorizationCodeGrant(new AuthorizationCode("code"), URI.create("http://localhost:8080/nifi"));
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue