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