NIFI-9699 - Updated oidcCallback method to handle error cases. Added some unit tests.

This closes #5824

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Nathan Gough 2022-03-01 13:47:10 -05:00 committed by exceptionfactory
parent 72fadf9e51
commit 885c475f90
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
2 changed files with 204 additions and 59 deletions

View File

@ -22,7 +22,6 @@ import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
@ -82,8 +81,11 @@ import java.util.regex.Pattern;
public class OIDCAccessResource extends ApplicationResource {
private static final Logger logger = LoggerFactory.getLogger(OIDCAccessResource.class);
private static final String OIDC_AUTHENTICATION_FAILED = "OIDC authentication attempt failed: ";
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 OIDC_IS_NOT_CONFIGURED_MESSAGE = "OIDC is not configured.";
private static final String OIDC_REQUEST_IDENTIFIER_NOT_FOUND = "The request identifier was not found in the request.";
private static final String OIDC_FAILED_TO_PARSE_REDIRECT_URI = "Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login/logout process.";
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";
@ -108,16 +110,12 @@ public class OIDCAccessResource extends ApplicationResource {
notes = NON_GUARANTEED_ENDPOINT
)
public void oidcRequest(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
return;
}
// ensure oidc is enabled
if (!oidcService.isOidcEnabled()) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
return;
try {
validateOidcConfiguration();
} catch (AuthenticationNotSupportedException e) {
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, e.getMessage());
throw e;
}
// generate the authorization uri
@ -136,10 +134,11 @@ public class OIDCAccessResource extends ApplicationResource {
notes = NON_GUARANTEED_ENDPOINT
)
public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, LOGGING_IN);
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
if (requestIdentifier.isPresent() && oidcResponse != null && oidcResponse.indicatesSuccess()) {
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, LOGGING_IN);
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
if (requestIdentifier.isPresent() && oidcResponse.indicatesSuccess()) {
final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
final String oidcRequestIdentifier = requestIdentifier.get();
@ -176,9 +175,7 @@ public class OIDCAccessResource extends ApplicationResource {
removeOidcRequestCookie(httpServletResponse);
// report the unsuccessful login
final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt: "
+ errorOidcResponse.getErrorObject().getDescription());
forwardToLoginMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful login attempt.");
}
}
@ -192,18 +189,15 @@ public class OIDCAccessResource extends ApplicationResource {
notes = NON_GUARANTEED_ENDPOINT
)
public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
try {
validateOidcConfiguration();
} catch (final AuthenticationNotSupportedException e) {
logger.debug(OIDC_AUTHENTICATION_FAILED, e.getMessage());
return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build();
}
// ensure oidc is enabled
if (!oidcService.isOidcEnabled()) {
logger.debug(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
return Response.status(Response.Status.CONFLICT).entity(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG).build();
}
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
if (!requestIdentifier.isPresent()) {
final String message = "The login request identifier was not found in the request. Unable to continue.";
logger.warn(message);
@ -232,12 +226,12 @@ public class OIDCAccessResource extends ApplicationResource {
notes = NON_GUARANTEED_ENDPOINT
)
public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
if (!httpServletRequest.isSecure()) {
throw new IllegalStateException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
}
if (!oidcService.isOidcEnabled()) {
throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
try {
validateOidcConfiguration();
} catch (final AuthenticationNotSupportedException e) {
logger.debug(OIDC_AUTHENTICATION_FAILED, e.getMessage());
throw e;
}
final String mappedUserIdentity = NiFiUserUtils.getNiFiUserIdentity();
@ -285,7 +279,7 @@ public class OIDCAccessResource extends ApplicationResource {
)
public void oidcLogoutCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception {
final AuthenticationResponse oidcResponse = parseOidcResponse(httpServletRequest, httpServletResponse, !LOGGING_IN);
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
if (requestIdentifier.isPresent() && oidcResponse != null && oidcResponse.indicatesSuccess()) {
final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse;
@ -383,9 +377,7 @@ public class OIDCAccessResource extends ApplicationResource {
removeOidcRequestCookie(httpServletResponse);
// report the unsuccessful logout
final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse;
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful logout attempt: "
+ errorOidcResponse.getErrorObject().getDescription());
forwardToLogoutMessagePage(httpServletRequest, httpServletResponse, "Unsuccessful logout attempt.");
}
}
@ -473,46 +465,41 @@ public class OIDCAccessResource extends ApplicationResource {
return builder.build();
}
private AuthenticationResponse parseOidcResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, boolean isLogin) throws Exception {
protected AuthenticationResponse parseOidcResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, boolean isLogin) throws Exception {
final String pageTitle = getForwardPageTitle(isLogin);
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
return null;
try {
validateOidcConfiguration();
} catch (final AuthenticationNotSupportedException e) {
logger.debug(OIDC_AUTHENTICATION_FAILED, e.getMessage());
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, e.getMessage());
throw e;
}
// ensure oidc is enabled
if (!oidcService.isOidcEnabled()) {
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED_MSG);
return null;
}
final Optional<String> requestIdentifier = getOidcRequestIdentifier();
final Optional<String> requestIdentifier = getOidcRequestIdentifier(httpServletRequest);
if (!requestIdentifier.isPresent()) {
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle,"The request identifier was " +
"not found in the request. Unable to continue.");
return null;
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, OIDC_REQUEST_IDENTIFIER_NOT_FOUND);
throw new IllegalStateException(OIDC_REQUEST_IDENTIFIER_NOT_FOUND);
}
final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse;
try {
oidcResponse = AuthenticationResponseParser.parse(getRequestUri());
return oidcResponse;
} catch (final ParseException e) {
logger.error("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login/logout process.");
logger.error(OIDC_FAILED_TO_PARSE_REDIRECT_URI);
// remove the oidc request cookie
removeOidcRequestCookie(httpServletResponse);
// forward to the error page
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle,"Unable to parse the redirect URI " +
"from the OpenId Connect Provider. Unable to continue login/logout process.");
return null;
forwardToMessagePage(httpServletRequest, httpServletResponse, pageTitle, OIDC_FAILED_TO_PARSE_REDIRECT_URI);
throw e;
}
}
private void checkOidcState(HttpServletResponse httpServletResponse, final String oidcRequestIdentifier, AuthenticationSuccessResponse successfulOidcResponse, boolean isLogin) throws Exception {
protected void checkOidcState(HttpServletResponse httpServletResponse, final String oidcRequestIdentifier, AuthenticationSuccessResponse successfulOidcResponse, boolean isLogin) throws Exception {
// confirm state
final State state = successfulOidcResponse.getState();
if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) {
@ -534,11 +521,23 @@ public class OIDCAccessResource extends ApplicationResource {
}
}
private void validateOidcConfiguration() throws AuthenticationNotSupportedException {
// only consider user specific access over https
if (!httpServletRequest.isSecure()) {
throw new AuthenticationNotSupportedException(AccessResource.AUTHENTICATION_NOT_ENABLED_MSG);
}
// ensure OIDC is actually configured/enabled
if (!oidcService.isOidcEnabled()) {
throw new AuthenticationNotSupportedException(OIDC_IS_NOT_CONFIGURED_MESSAGE);
}
}
private String getForwardPageTitle(boolean isLogin) {
return isLogin ? ApplicationResource.LOGIN_ERROR_TITLE : ApplicationResource.LOGOUT_ERROR_TITLE;
}
private String getOidcCallback() {
protected String getOidcCallback() {
return generateResourceUri("access", "oidc", "callback");
}
@ -554,8 +553,8 @@ public class OIDCAccessResource extends ApplicationResource {
applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
}
private Optional<String> getOidcRequestIdentifier() {
return applicationCookieService.getCookieValue(httpServletRequest, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
private Optional<String> getOidcRequestIdentifier(final HttpServletRequest request) {
return applicationCookieService.getCookieValue(request, ApplicationCookieName.OIDC_REQUEST_IDENTIFIER);
}
public void setOidcService(OidcService oidcService) {

View File

@ -0,0 +1,146 @@
/*
* 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.api;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
import org.apache.nifi.web.security.jwt.provider.BearerTokenProvider;
import org.apache.nifi.web.security.jwt.provider.StandardBearerTokenProvider;
import org.apache.nifi.web.security.oidc.OidcService;
import org.apache.nifi.web.security.token.LoginAuthenticationToken;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.mock.web.MockHttpServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import static org.apache.nifi.web.api.cookie.ApplicationCookieName.OIDC_REQUEST_IDENTIFIER;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
public class OIDCAccessResourceTest {
final static String REQUEST_IDENTIFIER = "an-identifier";
final static String OIDC_LOGIN_FAILURE_MESSAGE = "Unsuccessful login attempt.";
@Test
public void testOidcCallbackSuccess() throws Exception {
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
Cookie[] cookies = { new Cookie(OIDC_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER) };
Mockito.when(mockRequest.getCookies()).thenReturn(cookies);
OidcService oidcService = Mockito.mock(OidcService.class);
MockOIDCAccessResource accessResource = new MockOIDCAccessResource(oidcService, true);
accessResource.oidcCallback(mockRequest, mockResponse);
Mockito.verify(oidcService).storeJwt(any(String.class), any(String.class));
}
@Test
public void testOidcCallbackFailure() throws Exception {
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
Cookie[] cookies = { new Cookie(OIDC_REQUEST_IDENTIFIER.getCookieName(), REQUEST_IDENTIFIER) };
Mockito.when(mockRequest.getCookies()).thenReturn(cookies);
OidcService oidcService = Mockito.mock(OidcService.class);
MockOIDCAccessResource accessResource = new MockOIDCCallbackFailure(oidcService, false);
accessResource.oidcCallback(mockRequest, mockResponse);
}
public class MockOIDCCallbackFailure extends MockOIDCAccessResource {
public MockOIDCCallbackFailure(OidcService oidcService, Boolean requestShouldSucceed) throws IOException {
super(oidcService, requestShouldSucceed);
}
@Override
protected void forwardToLoginMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception {
assertEquals(OIDC_LOGIN_FAILURE_MESSAGE, message);
}
}
public class MockOIDCAccessResource extends OIDCAccessResource {
final static String BEARER_TOKEN = "bearer_token";
final static String AUTHORIZATION_CODE = "authorization_code";
final static String CALLBACK_URL = "https://nifi.apache.org/nifi-api/access/oidc/callback";
final static String RESOURCE_URI = "resource_uri";
private Boolean requestShouldSucceed;
public MockOIDCAccessResource(final OidcService oidcService, final Boolean requestShouldSucceed) throws IOException {
this.requestShouldSucceed = requestShouldSucceed;
final BearerTokenProvider bearerTokenProvider = Mockito.mock(StandardBearerTokenProvider.class);
Mockito.when(bearerTokenProvider.getBearerToken(any(LoginAuthenticationToken.class))).thenReturn(BEARER_TOKEN);
setOidcService(oidcService);
setBearerTokenProvider(bearerTokenProvider);
final LoginAuthenticationToken token = Mockito.mock(LoginAuthenticationToken.class);
Mockito.when(oidcService.exchangeAuthorizationCodeForLoginAuthenticationToken(any(AuthorizationGrant.class))).thenReturn(token);
}
@Override
protected AuthenticationResponse parseOidcResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, boolean isLogin) {
if (requestShouldSucceed) {
return getSuccessResponse();
} else {
return getErrorResponse();
}
}
@Override
protected void checkOidcState(HttpServletResponse httpServletResponse, final String oidcRequestIdentifier, AuthenticationSuccessResponse successfulOidcResponse, boolean isLogin)
throws Exception {
// do nothing
}
@Override
protected String getOidcCallback() {
return CALLBACK_URL;
}
@Override
protected String generateResourceUri(final String... path) {
return RESOURCE_URI;
}
@Override
protected URI getCookieResourceUri() {
return URI.create(RESOURCE_URI);
}
private AuthenticationResponse getSuccessResponse() {
AuthenticationSuccessResponse successResponse = Mockito.mock(AuthenticationSuccessResponse.class);
Mockito.when(successResponse.indicatesSuccess()).thenReturn(true);
Mockito.when(successResponse.getAuthorizationCode()).thenReturn(new AuthorizationCode(AUTHORIZATION_CODE));
return successResponse;
}
private AuthenticationResponse getErrorResponse() {
AuthenticationErrorResponse errorResponse = Mockito.mock(AuthenticationErrorResponse.class);
Mockito.when(errorResponse.indicatesSuccess()).thenReturn(false);
Mockito.when(errorResponse.getErrorObject()).thenReturn(new ErrorObject("HTTP 500", "OIDC server error"));
return errorResponse;
}
}
}