mirror of https://github.com/apache/nifi.git
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:
parent
72fadf9e51
commit
885c475f90
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue