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:
mtien 2020-09-14 18:43:43 -07:00 committed by Nathan Gough
parent c70f4ac530
commit bf962f6227
9 changed files with 1133 additions and 239 deletions

View File

@ -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>

View File

@ -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");
} }

View File

@ -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) {

View File

@ -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;
} }

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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"))
}
}
} }

View File

@ -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"));
} }
} }