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 2 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 @@ -18,6 +18,7 @@

package com.wultra.app.enrollmentserver.controller.api;

import com.wultra.app.enrollmentserver.errorhandling.InternalServiceException;
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenAuthException;
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenConfigurationException;
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenException;
Expand All @@ -26,7 +27,9 @@
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 {

public static final String APPLICATION_NOT_FOUND = "ERR0015";
public static final String INVALID_REQUEST = "ERR0024";
public static final String OPERATION_NOT_FOUND = "ERR0034";
public static final String OPERATION_INVALID_STATE = "ERR0036";
public static final String OPERATION_APPROVE_FAILURE = "ERR0037";
public 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, InternalServiceException {
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 InternalServiceException("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, InternalServiceException {
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 InternalServiceException("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, InternalServiceException {
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 InternalServiceException("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, InternalServiceException {
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 InternalServiceException("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, InternalServiceException {
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 InternalServiceException("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, InternalServiceException {
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 InternalServiceException("Unable to call upstream service.");
}
}
}
}

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

/**
* Exception handler for issues related to internal services.
* @param e Exception.
* @return Response with error details.
*/
@ExceptionHandler(InternalServiceException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public @ResponseBody ErrorResponse handleUpstreamServiceException(InternalServiceException e) {
logger.error("Error occurred when calling an internal API: {}", e.getMessage());
return new ErrorResponse("ERROR_INTERNAL_API", e.getMessage());
}

/**
* Exception handler for invalid request exception.
* @param ex Exception.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 raised when call to an upstream service fails with unexpected error.
*
* @author Jan Pesek, jan.pesek@wultra.com
*/
public class InternalServiceException extends Exception {

@Serial
private static final long serialVersionUID = 3539063915259282763L;

/**
* Constructor with a specified message.
* @param message Error message.
*/
public InternalServiceException(String message) {
super(message);
}

/**
* Constructor with a specified message and cause.
* @param message Message.
* @param cause Cause.
*/
public InternalServiceException(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