Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: on RootProvider always rely on the Error page if unexpected erro… #2553

Merged
merged 1 commit into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.gravitee.am.common.web.UriBuilder;
import io.gravitee.am.gateway.handler.common.vertx.utils.RequestUtils;
import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
import io.gravitee.am.gateway.handler.root.RootProvider;
import io.gravitee.am.gateway.handler.root.resources.handler.error.AbstractErrorHandler;
import io.gravitee.am.service.AuthenticationFlowContextService;
import io.vertx.core.Handler;
import io.vertx.reactivex.core.MultiMap;
Expand All @@ -36,22 +38,21 @@
* @author GraviteeSource Team
*/

public class MFAChallengeFailureHandler implements Handler<RoutingContext> {
public class MFAChallengeFailureHandler extends AbstractErrorHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(MFAChallengeFailureHandler.class);
private static final String ERROR_CODE_VALUE = "send_challenge_failed";

private final AuthenticationFlowContextService authenticationFlowContextService;

public MFAChallengeFailureHandler(AuthenticationFlowContextService authenticationFlowContextService) {
super(RootProvider.PATH_ERROR);
this.authenticationFlowContextService = authenticationFlowContextService;
}

@Override
public void handle(RoutingContext routingContext) {
if (routingContext.failed()) {
Throwable throwable = routingContext.failure();
handleException(routingContext, throwable == null ? "MFA Challenge failed for unexpected reason" : throwable.getMessage());
}
public void doHandle(RoutingContext routingContext) {
Throwable throwable = routingContext.failure();
handleException(routingContext, throwable == null ? "MFA Challenge failed for unexpected reason" : throwable.getMessage());
}

private void handleException(RoutingContext context, String errorDescription) {
Expand All @@ -70,7 +71,7 @@ private MultiMap updateQueryParams(RoutingContext context, String errorDescripti

if (context.request().getParam(Parameters.LOGIN_HINT) != null) {
// encode login_hint parameter (to not replace '+' sign by a space ' ')
queryParams.set(Parameters.LOGIN_HINT, UriBuilder.encodeURIComponent(context.request().getParam(ConstantKeys.USERNAME_PARAM_KEY)));
queryParams.set(Parameters.LOGIN_HINT, UriBuilder.encodeURIComponent(context.request().getParam(Parameters.LOGIN_HINT)));
}

return queryParams;
Expand All @@ -87,11 +88,4 @@ private void logoutUser(RoutingContext context) {
context.session().destroy();
}
}

private void doRedirect(HttpServerResponse response, String url) {
response
.putHeader(HttpHeaders.LOCATION, url)
.setStatusCode(302)
.end();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* Licensed 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 io.gravitee.am.gateway.handler.root.resources.handler.error;

import io.gravitee.am.common.exception.oauth2.OAuth2Exception;
import io.gravitee.am.common.oauth2.Parameters;
import io.gravitee.am.common.utils.ConstantKeys;
import io.gravitee.am.gateway.handler.common.vertx.utils.RequestUtils;
import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
import io.gravitee.am.gateway.policy.PolicyChainException;
import io.gravitee.am.model.oidc.Client;
import io.gravitee.am.service.exception.AbstractManagementException;
import io.gravitee.common.http.HttpHeaders;
import io.vertx.core.Handler;
import io.vertx.ext.auth.webauthn.impl.attestation.AttestationException;
import io.vertx.ext.web.handler.HttpException;
import io.vertx.reactivex.core.MultiMap;
import io.vertx.reactivex.core.http.HttpServerRequest;
import io.vertx.reactivex.core.http.HttpServerResponse;
import io.vertx.reactivex.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.gravitee.am.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY;
import static io.gravitee.am.common.utils.ConstantKeys.TOKEN_CONTEXT_KEY;
import static io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest.CONTEXT_PATH;
import static java.util.Objects.isNull;

/**
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
* @author GraviteeSource Team
*/
public abstract class AbstractErrorHandler implements Handler<RoutingContext> {

protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected final String errorPage;

public AbstractErrorHandler(String errorPage) {
this.errorPage = errorPage;
}

@Override
public final void handle(RoutingContext routingContext) {
if (routingContext.failed()) {
try {
doHandle(routingContext);
} catch (Exception e) {
logger.error("Unable to handle error response", e);
String errorPageURL = routingContext.get(CONTEXT_PATH) + errorPage;

final HttpServerRequest request = routingContext.request();
// prepare query parameters
MultiMap parameters = RequestUtils.getCleanedQueryParams(routingContext.request());
// get client if exists
Client client = routingContext.get(CLIENT_CONTEXT_KEY);
if (client != null) {
parameters.set(Parameters.CLIENT_ID, client.getClientId());
} else if (request.getParam(Parameters.CLIENT_ID) != null) {
parameters.set(Parameters.CLIENT_ID, (request.getParam(Parameters.CLIENT_ID)));
}

// append error information
parameters.set(ConstantKeys.ERROR_PARAM_KEY, "server_error");
parameters.set(ConstantKeys.ERROR_DESCRIPTION_PARAM_KEY, "Unexpected error occurred");

// redirect
String proxiedErrorPage = UriBuilderRequest.resolveProxyRequest(request, errorPageURL, parameters, true);
doRedirect(routingContext.response(), proxiedErrorPage);
}
}
}

protected abstract void doHandle(RoutingContext routingContext);

protected void doRedirect(HttpServerResponse response, String url) {
response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,11 @@
import io.gravitee.am.gateway.policy.PolicyChainException;
import io.gravitee.am.model.oidc.Client;
import io.gravitee.am.service.exception.AbstractManagementException;
import io.gravitee.common.http.HttpHeaders;
import io.gravitee.common.http.HttpStatusCode;
import io.vertx.core.Handler;
import io.vertx.ext.auth.webauthn.impl.attestation.AttestationException;
import io.vertx.ext.web.handler.HttpException;
import io.vertx.reactivex.core.MultiMap;
import io.vertx.reactivex.core.http.HttpServerRequest;
import io.vertx.reactivex.core.http.HttpServerResponse;
import io.vertx.reactivex.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.gravitee.am.common.utils.ConstantKeys.CLIENT_CONTEXT_KEY;
import static io.gravitee.am.common.utils.ConstantKeys.TOKEN_CONTEXT_KEY;
Expand All @@ -44,17 +38,14 @@
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
* @author GraviteeSource Team
*/
public class ErrorHandler implements Handler<RoutingContext> {

private static final Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
private final String errorPage;
public class ErrorHandler extends AbstractErrorHandler {

public ErrorHandler(String errorPage) {
this.errorPage = errorPage;
super(errorPage);
}

@Override
public void handle(RoutingContext routingContext) {
protected void doHandle(RoutingContext routingContext) {
if (routingContext.failed()) {
Throwable throwable = routingContext.failure();
// management exception (resource not found, server error, ...)
Expand All @@ -75,17 +66,7 @@ public void handle(RoutingContext routingContext) {
handleException(routingContext, "technical_error", "Invalid WebAuthn attestation, make sure your device is compliant with the platform requirements");
} else {
logger.error("An exception occurs while handling incoming request", throwable);
if (routingContext.statusCode() != -1) {
routingContext
.response()
.setStatusCode(routingContext.statusCode())
.end();
} else {
routingContext
.response()
.setStatusCode(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
.end();
}
handleException(routingContext, "unexpected_error", "An unexpected error occurs");
}
}
}
Expand Down Expand Up @@ -125,7 +106,4 @@ private void handleException(RoutingContext routingContext, String errorCode, St
}
}

private void doRedirect(HttpServerResponse response, String url) {
response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,12 @@
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.*;

import static io.gravitee.am.common.utils.ConstantKeys.PARAM_CONTEXT_KEY;
import static io.gravitee.am.service.utils.ResponseTypeUtils.isHybridFlow;
import static io.gravitee.am.service.utils.ResponseTypeUtils.isImplicitFlow;
import static java.util.Objects.nonNull;

/**
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
Expand Down Expand Up @@ -168,7 +166,7 @@ private void handleException(RoutingContext context, String errorCode, String er
if (errorDescription != null) {
queryParams.set(ConstantKeys.ERROR_DESCRIPTION_PARAM_KEY, errorDescription);
}
if (context.request().getParam(Parameters.LOGIN_HINT) != null) {
if (nonNull(context.request().getParam(Parameters.LOGIN_HINT)) && nonNull(context.request().getParam(ConstantKeys.USERNAME_PARAM_KEY))) {
// encode login_hint parameter (to not replace '+' sign by a space ' ')
queryParams.set(Parameters.LOGIN_HINT, UriBuilder.encodeURIComponent(context.request().getParam(ConstantKeys.USERNAME_PARAM_KEY)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import io.gravitee.am.common.utils.ConstantKeys;
import io.gravitee.am.gateway.handler.common.vertx.utils.RequestUtils;
import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest;
import io.gravitee.am.gateway.handler.root.RootProvider;
import io.gravitee.am.gateway.handler.root.resources.handler.error.AbstractErrorHandler;
import io.gravitee.am.service.exception.EmailFormatInvalidException;
import io.gravitee.am.service.exception.InvalidUserException;
import io.gravitee.common.http.HttpHeaders;
Expand All @@ -34,27 +36,27 @@
* @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
* @author GraviteeSource Team
*/
public class RegisterFailureHandler implements Handler<RoutingContext> {
public class RegisterFailureHandler extends AbstractErrorHandler {

private static final Logger logger = LoggerFactory.getLogger(RegisterFailureHandler.class);
public RegisterFailureHandler() {
super(RootProvider.PATH_ERROR);
}

@Override
public void handle(RoutingContext context) {
if (context.failed()) {
// prepare response
final MultiMap queryParams = RequestUtils.getCleanedQueryParams(context.request());
// if failure, return to the register page with an error
Throwable cause = context.failure();
if (cause instanceof InvalidUserException) {
queryParams.set(ConstantKeys.WARNING_PARAM_KEY, "invalid_user_information");
} else if (cause instanceof EmailFormatInvalidException) {
queryParams.set(ConstantKeys.WARNING_PARAM_KEY, "invalid_email");
} else {
logger.error("An error occurs while ending user registration", cause);
queryParams.set(ConstantKeys.ERROR_PARAM_KEY, "registration_failed");
}
redirectToPage(context, queryParams, cause);
protected void doHandle(RoutingContext context) {
// prepare response
final MultiMap queryParams = RequestUtils.getCleanedQueryParams(context.request());
// if failure, return to the register page with an error
Throwable cause = context.failure();
if (cause instanceof InvalidUserException) {
queryParams.set(ConstantKeys.WARNING_PARAM_KEY, "invalid_user_information");
} else if (cause instanceof EmailFormatInvalidException) {
queryParams.set(ConstantKeys.WARNING_PARAM_KEY, "invalid_email");
} else {
logger.error("An error occurs while ending user registration", cause);
queryParams.set(ConstantKeys.ERROR_PARAM_KEY, "registration_failed");
}
redirectToPage(context, queryParams, cause);
}

private void redirectToPage(RoutingContext context, MultiMap queryParams, Throwable... exceptions) {
Expand All @@ -72,11 +74,4 @@ private void redirectToPage(RoutingContext context, MultiMap queryParams, Throwa
.end();
}
}

private void doRedirect(HttpServerResponse response, String url) {
response
.putHeader(HttpHeaders.LOCATION, url)
.setStatusCode(302)
.end();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.gravitee.am.model.factor.EnrolledFactor;
import io.gravitee.am.model.factor.EnrolledFactorSecurity;
import io.gravitee.am.model.oidc.Client;
import io.gravitee.am.service.AuthenticationFlowContextService;
import io.gravitee.am.service.CredentialService;
import io.gravitee.am.service.DeviceService;
import io.gravitee.am.service.FactorService;
Expand Down Expand Up @@ -85,6 +86,9 @@ public class MFAChallengeEndpointTest extends RxWebTestBase {
@Mock
private FactorService factorService;

@Mock
private AuthenticationFlowContextService authenticationFlowContextService;

private LocalSessionStore localSessionStore;

@Override
Expand All @@ -95,8 +99,7 @@ public void setUp() throws Exception {
localSessionStore = LocalSessionStore.create(vertx);
router.route("/mfa/challenge")
.handler(SessionHandler.create(localSessionStore))
.handler(BodyHandler.create())
.failureHandler(rc -> rc.response().setStatusCode(500).end());
.handler(BodyHandler.create());
}

@Test
Expand Down Expand Up @@ -151,4 +154,45 @@ public void shouldVerify_fido2Factor() throws Exception {
},
HttpStatusCode.FOUND_302, "Found", null);
}

@Test
public void shouldRejectVerification() throws Exception {
when(factorManager.getFactor("factorId")).thenThrow(new NullPointerException());

router.route(HttpMethod.POST, "/mfa/challenge")
.handler(ctx -> {
User user = new User();
user.setId("userId");
EnrolledFactor enrolledFactor = new EnrolledFactor();
EnrolledFactorSecurity enrolledFactorSecurity = new EnrolledFactorSecurity();
enrolledFactorSecurity.setType(FactorSecurityType.WEBAUTHN_CREDENTIAL);
enrolledFactor.setFactorId("factorId");
enrolledFactor.setSecurity(enrolledFactorSecurity);
user.setFactors(Collections.singletonList(enrolledFactor));
Client client = new Client();
client.setFactors(Collections.singleton("factorId"));
ctx.setUser(io.vertx.reactivex.ext.auth.User.newInstance(new io.gravitee.am.gateway.handler.common.vertx.web.auth.user.User(user)));
ctx.put(ConstantKeys.CLIENT_CONTEXT_KEY, client);
ctx.next();
})
.handler(new MFAChallengeEndpoint(factorManager, userService, engine, deviceService, applicationContext, domain, credentialService, factorService))
.failureHandler(new MFAChallengeFailureHandler(authenticationFlowContextService));

testRequest(
HttpMethod.POST,
"/mfa/challenge",
req -> {
Buffer buffer = Buffer.buffer();
buffer.appendString("code={\"id\":\"credentialId\"}&factorId=factorId");
req.headers().set("content-length", String.valueOf(buffer.length()));
req.headers().set("content-type", "application/x-www-form-urlencoded");
req.write(buffer);
},
resp -> {
String location = resp.headers().get("location");
assertNotNull(location);
assertTrue(location.contains("/error"));
},
HttpStatusCode.FOUND_302, "Found", null);
}
}