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 #913: Improve error handling in MobileTokenController #992

Merged
merged 4 commits into from
Feb 20, 2024
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,12 +21,15 @@
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenAuthException;
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenConfigurationException;
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenException;
import com.wultra.app.enrollmentserver.errorhandling.RemoteCommunicationException;
import com.wultra.app.enrollmentserver.impl.service.MobileTokenService;
import com.wultra.app.enrollmentserver.impl.service.OperationApproveParameterObject;
import com.wultra.core.http.common.request.RequestContext;
import com.wultra.core.http.common.request.RequestContextConverter;
import com.wultra.security.powerauth.client.model.error.PowerAuthClientException;
import com.wultra.security.powerauth.client.model.error.PowerAuthError;
import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation;
import com.wultra.security.powerauth.lib.mtoken.model.enumeration.ErrorCode;
import com.wultra.security.powerauth.lib.mtoken.model.request.OperationApproveRequest;
import com.wultra.security.powerauth.lib.mtoken.model.request.OperationDetailRequest;
import com.wultra.security.powerauth.lib.mtoken.model.request.OperationRejectRequest;
Expand Down Expand Up @@ -68,6 +71,13 @@
@RequestMapping("api/auth/token/app")
public class MobileTokenController {

private static final String APPLICATION_NOT_FOUND = "ERR0015";
private static final String INVALID_REQUEST = "ERR0024";
private static final String OPERATION_NOT_FOUND = "ERR0034";
private static final String OPERATION_INVALID_STATE = "ERR0036";
private static final String OPERATION_APPROVE_FAILURE = "ERR0037";
private static final String OPERATION_REJECT_FAILURE = "ERR0038";

private static final Logger logger = LoggerFactory.getLogger(MobileTokenController.class);

// Disallowed flags contain onboarding flags used before onboarding process is finished
Expand Down Expand Up @@ -101,7 +111,7 @@ public MobileTokenController(MobileTokenService mobileTokenService) {
PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE,
PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE_BIOMETRY
})
public ObjectResponse<OperationListResponse> operationList(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException {
public ObjectResponse<OperationListResponse> operationList(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException {
try {
if (auth != null) {
final String userId = auth.getUserId();
Expand All @@ -115,8 +125,24 @@ public ObjectResponse<OperationListResponse> operationList(@Parameter(hidden = t
throw new MobileTokenAuthException();
}
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new MobileTokenAuthException();
final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING");
switch (errorCode) {
case APPLICATION_NOT_FOUND -> {
logger.info("Application ID: {} not found: {}", auth.getApplicationId(), e.getMessage());
logger.debug("Application ID: {} not found.", auth.getApplicationId(), e);
throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier.");
}
case INVALID_REQUEST -> {
logger.info("Request validation error: {}", e.getMessage());
logger.debug("Request validation error.", e);
throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage()));
}
default -> {
logger.warn("Calling PowerAuth service failed: {}", e.getMessage());
logger.debug("Calling PowerAuth service failed.", e);
throw new RemoteCommunicationException("Unable to call upstream service.");
}
}
}
}

Expand All @@ -138,7 +164,7 @@ public ObjectResponse<OperationListResponse> operationList(@Parameter(hidden = t
})
public ObjectResponse<Operation> getOperationDetail(@RequestBody ObjectRequest<OperationDetailRequest> request,
@Parameter(hidden = true) PowerAuthApiAuthentication auth,
@Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException {
@Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException {
try {
if (auth != null) {
final String operationId = request.getRequestObject().getId();
Expand All @@ -151,8 +177,24 @@ public ObjectResponse<Operation> getOperationDetail(@RequestBody ObjectRequest<O
throw new MobileTokenAuthException();
}
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new MobileTokenAuthException();
final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING");
switch (errorCode) {
case OPERATION_NOT_FOUND -> {
logger.info("Operation ID: {} not found: {}", request.getRequestObject().getId(), e.getMessage());
logger.debug("Operation ID: {} not found.", request.getRequestObject().getId(), e);
throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "No operation was found with the provided identifier.");
}
case INVALID_REQUEST -> {
logger.info("Request validation error: {}", e.getMessage());
logger.debug("Request validation error.", e);
throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage()));
}
default -> {
logger.warn("Calling PowerAuth service failed: {}", e.getMessage());
logger.debug("Calling PowerAuth service failed.", e);
throw new RemoteCommunicationException("Unable to call upstream service.");
}
}
}
}

Expand All @@ -174,7 +216,7 @@ public ObjectResponse<Operation> getOperationDetail(@RequestBody ObjectRequest<O
})
public ObjectResponse<Operation> claimOperation(@RequestBody ObjectRequest<OperationDetailRequest> request,
@Parameter(hidden = true) PowerAuthApiAuthentication auth,
@Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException {
@Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException {
try {
if (auth != null) {
final String operationId = request.getRequestObject().getId();
Expand All @@ -187,8 +229,24 @@ public ObjectResponse<Operation> claimOperation(@RequestBody ObjectRequest<Opera
throw new MobileTokenAuthException();
}
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new MobileTokenAuthException();
final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING");
switch (errorCode) {
case OPERATION_NOT_FOUND -> {
logger.info("Operation ID: {} not found: {}", request.getRequestObject().getId(), e.getMessage());
logger.debug("Operation ID: {} not found.", request.getRequestObject().getId(), e);
throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "No operation was found with the provided identifier.");
}
case INVALID_REQUEST -> {
logger.info("Request validation error: {}", e.getMessage());
logger.debug("Request validation error.", e);
throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage()));
}
default -> {
logger.warn("Calling PowerAuth service failed: {}", e.getMessage());
logger.debug("Calling PowerAuth service failed.", e);
throw new RemoteCommunicationException("Unable to call upstream service.");
}
}
}
}

Expand All @@ -206,7 +264,7 @@ public ObjectResponse<Operation> claimOperation(@RequestBody ObjectRequest<Opera
PowerAuthSignatureTypes.POSSESSION_BIOMETRY,
PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE
})
public ObjectResponse<OperationListResponse> operationListAll(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException {
public ObjectResponse<OperationListResponse> operationListAll(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException {
try {
if (auth != null) {
final String userId = auth.getUserId();
Expand All @@ -219,8 +277,24 @@ public ObjectResponse<OperationListResponse> operationListAll(@Parameter(hidden
throw new MobileTokenAuthException();
}
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new MobileTokenAuthException();
final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING");
switch (errorCode) {
case APPLICATION_NOT_FOUND -> {
logger.info("Application ID: {} not found: {}", auth.getApplicationId(), e.getMessage());
logger.debug("Application ID: {} not found.", auth.getApplicationId(), e);
throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier.");
}
case INVALID_REQUEST -> {
logger.info("Request validation error: {}", e.getMessage());
logger.debug("Request validation error.", e);
throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage()));
}
default -> {
logger.warn("Calling PowerAuth service failed: {}", e.getMessage());
logger.debug("Calling PowerAuth service failed.", e);
throw new RemoteCommunicationException("Unable to call upstream service.");
}
}
}
}

Expand All @@ -242,7 +316,7 @@ public ObjectResponse<OperationListResponse> operationListAll(@Parameter(hidden
public Response operationApprove(
@RequestBody ObjectRequest<OperationApproveRequest> request,
@Parameter(hidden = true) PowerAuthApiAuthentication auth,
HttpServletRequest servletRequest) throws MobileTokenException {
HttpServletRequest servletRequest) throws MobileTokenException, RemoteCommunicationException {
try {

final OperationApproveRequest requestObject = request.getRequestObject();
Expand Down Expand Up @@ -290,8 +364,30 @@ public Response operationApprove(
throw new MobileTokenAuthException();
}
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new MobileTokenAuthException();
final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING");
switch (errorCode) {
case APPLICATION_NOT_FOUND -> {
final String applicationId = auth != null ? auth.getApplicationId() : null;
logger.info("Application ID: {} not found: {}", applicationId, e.getMessage());
logger.debug("Application ID: {} not found.", applicationId, e);
throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier.");
}
case OPERATION_NOT_FOUND, OPERATION_APPROVE_FAILURE, OPERATION_INVALID_STATE -> {
logger.info("Operation ID: {} not found or is in unexpected state: {}", request.getRequestObject().getId(), e.getMessage());
logger.debug("Operation ID: {} not found or is in unexpected state.", request.getRequestObject().getId(), e);
throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "Operation not found or is in an unexpected state.");
}
case INVALID_REQUEST -> {
logger.info("Request validation error: {}", e.getMessage());
logger.debug("Request validation error.", e);
throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage()));
}
default -> {
logger.warn("Calling PowerAuth service failed: {}", e.getMessage());
logger.debug("Calling PowerAuth service failed.", e);
throw new RemoteCommunicationException("Unable to call upstream service.");
}
}
}
}

Expand Down Expand Up @@ -320,7 +416,7 @@ private static String fetchProximityCheckOtp(OperationApproveRequest requestObje
public Response operationReject(
@RequestBody ObjectRequest<OperationRejectRequest> request,
@Parameter(hidden = true) PowerAuthApiAuthentication auth,
HttpServletRequest servletRequest) throws MobileTokenException {
HttpServletRequest servletRequest) throws MobileTokenException, RemoteCommunicationException {
try {

final OperationRejectRequest requestObject = request.getRequestObject();
Expand All @@ -342,8 +438,29 @@ public Response operationReject(
throw new MobileTokenAuthException();
}
} catch (PowerAuthClientException e) {
logger.error("Unable to call upstream service.", e);
throw new MobileTokenAuthException();
final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING");
switch (errorCode) {
case APPLICATION_NOT_FOUND -> {
logger.info("Application ID: {} not found: {}", auth.getApplicationId(), e.getMessage());
logger.debug("Application ID: {} not found.", auth.getApplicationId(), e);
throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier: %s".formatted(auth.getApplicationId()));
}
case OPERATION_NOT_FOUND, OPERATION_REJECT_FAILURE -> {
logger.info("Operation ID: {} not found or is in unexpected state: {}", request.getRequestObject().getId(), e.getMessage());
logger.debug("Operation ID: {} not found or is in unexpected state.", request.getRequestObject().getId(), e);
throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "Operation not found or is in an unexpected state");
}
case INVALID_REQUEST -> {
logger.info("Request validation error: {}", e.getMessage());
logger.debug("Request validation error.", e);
throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage()));
}
default -> {
logger.warn("Calling PowerAuth service failed: {}", e.getMessage());
logger.debug("Calling PowerAuth service failed.", e);
throw new RemoteCommunicationException("Unable to call upstream service.");
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ public class DefaultExceptionHandler {
return new ErrorResponse("ERROR_GENERIC", "Unknown error occurred while processing request.");
}

/**
* Handling of remote communication exception.
*
* @param ex Exception.
* @return Response with error details.
*/
@ExceptionHandler(RemoteCommunicationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public @ResponseBody ErrorResponse handleRemoteExceptionException(RemoteCommunicationException ex) {
logger.warn("Communication with remote system failed", ex);
return new ErrorResponse("REMOTE_COMMUNICATION_ERROR", "Communication with remote system failed.");
}

/**
* Exception handler for invalid request exception.
* @param ex Exception.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2024 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.enrollmentserver.errorhandling;

import java.io.Serial;

/**
* Exception thrown in case of an error during communication with remote system.
*
* @author Jan Pesek, jan.pesek@wultra.com
*/
public class RemoteCommunicationException extends Exception {

@Serial
private static final long serialVersionUID = -2565764734609472778L;

public RemoteCommunicationException(String message) {
super(message);
}

public RemoteCommunicationException(String message, Throwable cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ public class ErrorCode {
*/
public static final String INVALID_ACTIVATION = "INVALID_ACTIVATION";

/**
* Error code for situation when an invalid application identifier is
* attempted for operation manipulation.
*/
public static final String INVALID_APPLICATION = "INVALID_APPLICATION";

/**
* Error code for situation when an invalid operation identifier is
* attempted for operation manipulation.
*/
public static final String INVALID_OPERATION = "INVALID_OPERATION";

/**
* Error code for situation when signature verification fails.
*/
Expand Down
Loading