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 #389: SCA passes even though iProov failed #390

Merged
merged 7 commits into from
Sep 26, 2022
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
@@ -0,0 +1,32 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2022 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.api.model.onboarding.request;

import lombok.Data;

/**
* Request class used when submitting presence check.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
@Data
public class PresenceCheckSubmitRequest {

private String processId;

}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public interface IdentityVerificationRepository extends CrudRepository<IdentityV
" OR (id.phase = com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.CLIENT_EVALUATION" +
" AND id.status = com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.ACCEPTED)" +
" OR (id.phase = com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK" +
" AND id.status = com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.IN_PROGRESS)"
" AND id.status = com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.VERIFICATION_PENDING)"
)
Stream<IdentityVerificationEntity> streamAllIdentityVerificationsToChangeState();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ public ObjectResponse<DocumentVerificationSdkInitResponse> initVerificationSdk(

/**
* Initialize presence check process.
*
* @param request Presence check initialization request.
* @param eciesContext ECIES context.
* @param apiAuthentication PowerAuth authentication.
Expand Down Expand Up @@ -408,6 +409,37 @@ public ResponseEntity<Response> initPresenceCheck(@EncryptedRequestBody ObjectRe
return createResponseEntity(stateMachine);
}

/**
* Submit presence check process.
*
* @param request Presence check initialization request.
* @param eciesContext ECIES context.
* @param apiAuthentication PowerAuth authentication.
* @return Presence check initialization response.
* @throws PowerAuthAuthenticationException Thrown when request authentication fails.
* @throws PowerAuthEncryptionException Thrown when request decryption fails.
* @throws IdentityVerificationException Thrown when identity verification is invalid.
*/
@PostMapping("presence-check/submit")
@PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE)
@PowerAuth(resourceId = "/api/identity/presence-check/submit", signatureType = PowerAuthSignatureTypes.POSSESSION)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kober32 A new endpoint.

public ResponseEntity<Response> submitPresenceCheck(@EncryptedRequestBody ObjectRequest<PresenceCheckSubmitRequest> request,
@Parameter(hidden = true) EciesEncryptionContext eciesContext,
@Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication)
throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException {

final String operationDescription = "submitting presence check";
checkApiAuthentication(apiAuthentication, operationDescription);
checkEciesContext(eciesContext, operationDescription);
checkRequestObject(request, operationDescription);

final OwnerId ownerId = PowerAuthUtil.getOwnerId(apiAuthentication);
final String processId = request.getRequestObject().getProcessId();

StateMachine<OnboardingState, OnboardingEvent> stateMachine = stateMachineService.processStateMachineEvent(ownerId, processId, OnboardingEvent.PRESENCE_CHECK_SUBMITTED);
return createResponseEntity(stateMachine);
banterCZ marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Resend OTP code to the user.
* @param request Presence check initialization request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,27 @@
import com.wultra.app.enrollmentserver.api.model.onboarding.response.OtpVerifyResponse;
import com.wultra.app.enrollmentserver.model.enumeration.*;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository;
import com.wultra.app.onboardingserver.common.database.OnboardingOtpRepository;
import com.wultra.app.onboardingserver.common.database.OnboardingProcessRepository;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
import com.wultra.app.onboardingserver.common.database.entity.OnboardingOtpEntity;
import com.wultra.app.onboardingserver.common.database.entity.OnboardingProcessEntity;
import com.wultra.app.onboardingserver.common.enumeration.OnboardingProcessError;
import com.wultra.app.onboardingserver.common.errorhandling.OnboardingProcessException;
import com.wultra.app.onboardingserver.common.service.OnboardingProcessLimitService;
import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig;
import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
import com.wultra.app.onboardingserver.errorhandling.OnboardingOtpDeliveryException;
import com.wultra.app.onboardingserver.errorhandling.OnboardingProviderException;
import com.wultra.app.onboardingserver.provider.OnboardingProvider;
import com.wultra.app.onboardingserver.provider.model.request.SendOtpCodeRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.Optional;

import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK;

Expand Down Expand Up @@ -182,13 +180,12 @@ private OtpVerifyResponse verifyPresenceCheck(final OnboardingProcessEntity proc
final String rejectReason = idVerification.getRejectReason();
final RejectOrigin rejectOrigin = idVerification.getRejectOrigin();

if (errorOrigin == ErrorOrigin.PRESENCE_CHECK && StringUtils.isNotBlank(errorDetail)
|| rejectOrigin == RejectOrigin.PRESENCE_CHECK && StringUtils.isNotBlank(rejectReason)) {
if (errorOrigin == ErrorOrigin.PRESENCE_CHECK || rejectOrigin == RejectOrigin.PRESENCE_CHECK) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether this is a permanent or temporary change. When we called iProov validate before verification, the reject reason was null so SCA passed.

logger.info("SCA failed, identity verification ID: {} of process ID: {} contains errorDetail: {}, rejectReason: {} from previous step",
idVerification.getId(), processId, errorDetail, rejectReason);
return moveToPhasePresenceCheck(process, response, idVerification);
} else {
logger.debug("PRESENCE_CHECK without error or reject reason, process ID: {}", idVerification.getProcessId());
logger.debug("PRESENCE_CHECK without error or reject origin, process ID: {}", idVerification.getProcessId());
}
return response;
}
Expand Down Expand Up @@ -244,19 +241,10 @@ private void markVerificationOtpAsFailed(String processId) throws OnboardingProc
* @throws OnboardingOtpDeliveryException Thrown when OTP code could not be sent.
*/
private void sendOtpCode(String processId, boolean isResend) throws OnboardingProcessException, OnboardingOtpDeliveryException {
final Optional<OnboardingProcessEntity> processOptional = onboardingProcessRepository.findById(processId);
if (processOptional.isEmpty()) {
logger.warn("Onboarding process not found: {}", processId);
throw new OnboardingProcessException();
}
final OnboardingProcessEntity process = processOptional.get();
// Create an OTP code
final String otpCode;
if (isResend) {
otpCode = otpService.createOtpCodeForResend(process, OtpType.USER_VERIFICATION);
} else {
otpCode = otpService.createOtpCode(process, OtpType.USER_VERIFICATION);
}
final OnboardingProcessEntity process = onboardingProcessRepository.findById(processId).orElseThrow(() ->
new OnboardingProcessException("Onboarding process not found: " + processId));

final String otpCode = createOtpCode(isResend, process);
// Send the OTP code
try {
final SendOtpCodeRequest request = SendOtpCodeRequest.builder()
Expand All @@ -274,4 +262,11 @@ private void sendOtpCode(String processId, boolean isResend) throws OnboardingPr
}
}

private String createOtpCode(final boolean isResend, final OnboardingProcessEntity process) throws OnboardingOtpDeliveryException, OnboardingProcessException {
if (isResend) {
return otpService.createOtpCodeForResend(process, OtpType.USER_VERIFICATION);
} else {
return otpService.createOtpCode(process, OtpType.USER_VERIFICATION);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,27 @@ public IdentityVerificationEntity updateIdentityVerification(IdentityVerificatio
return identityVerificationRepository.save(identityVerification);
}

/**
* Move the given identity verification to the given phase and status.
*
* @param identityVerification Identity verification identity.
* @param phase Target phase.
* @param status Target status.
* @param ownerId Owner identification.
*/
@Transactional
public void moveToPhaseAndStatus(final IdentityVerificationEntity identityVerification,
final IdentityVerificationPhase phase,
final IdentityVerificationStatus status,
final OwnerId ownerId) {

identityVerification.setPhase(phase);
identityVerification.setStatus(status);
identityVerification.setTimestampLastUpdated(ownerId.getTimestamp());
identityVerificationRepository.save(identityVerification);
logger.info("Switched to {}/{}; process ID: {}", phase, status, identityVerification.getProcessId());
}

/**
* Submit identity-related documents for verification.
* @param request Document submit request.
Expand Down Expand Up @@ -255,11 +276,7 @@ public void startVerification(OwnerId ownerId, IdentityVerificationEntity identi

DocumentsVerificationResult result = documentVerificationProvider.verifyDocuments(ownerId, uploadIds);

identityVerification.setPhase(IdentityVerificationPhase.DOCUMENT_VERIFICATION);
identityVerification.setStatus(IdentityVerificationStatus.IN_PROGRESS);
identityVerification.setTimestampLastUpdated(ownerId.getTimestamp());

logger.info("Switched to DOCUMENT_VERIFICATION/IN_PROGRESS; process ID: {}", identityVerification.getProcessId());
moveToPhaseAndStatus(identityVerification, IdentityVerificationPhase.DOCUMENT_VERIFICATION, IdentityVerificationStatus.IN_PROGRESS, ownerId);

docVerifications.forEach(docVerification -> {
docVerification.setStatus(DocumentStatus.VERIFICATION_IN_PROGRESS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@ public SessionInfo init(OwnerId ownerId, String processId)
public void checkPresenceVerification(OwnerId ownerId,
IdentityVerificationEntity idVerification,
SessionInfo sessionInfo) throws PresenceCheckException {
PresenceCheckResult result = presenceCheckProvider.getResult(ownerId, sessionInfo);
final PresenceCheckResult result = presenceCheckProvider.getResult(ownerId, sessionInfo);

if (!PresenceCheckStatus.ACCEPTED.equals(result.getStatus())) {
logger.info("Not accepted presence check, {}", ownerId);
if (result.getStatus() != PresenceCheckStatus.ACCEPTED) {
logger.info("Not accepted presence check, status: {}, process ID: {}", result.getStatus(), idVerification.getProcessId());
evaluatePresenceCheckResult(ownerId, idVerification, result);
return;
}
Expand Down Expand Up @@ -290,8 +290,8 @@ private void evaluatePresenceCheckResult(OwnerId ownerId,
idVerification.setStatus(IdentityVerificationStatus.REJECTED);
idVerification.setTimestampLastUpdated(ownerId.getTimestamp());
idVerification.setTimestampFinished(ownerId.getTimestamp());
logger.info("Presence check rejected, process ID: {}, rejectReason: '{}'", idVerification.getProcessId(), result.getRejectReason());
logger.info("Switched to {}/REJECTED; process ID: {}", idVerification.getPhase(), idVerification.getProcessId());
logger.info("Presence check rejected, {}, rejectReason: '{}'", ownerId, result.getRejectReason());
break;
default:
throw new IllegalStateException("Unexpected presence check result status: " + result.getStatus());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,16 @@ public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) throws
try {
responseEntity = iProovRestApiService.validateVerification(id, token);
} catch (RestClientException e) {
logger.warn("Failed REST call to validate a verification in iProov, statusCode={}, responseBody='{}', {}",
logger.warn("Failed REST call to validate a verification in iProov, statusCode={}, responseBody='{}', {}, {}",
e.getStatusCode(), e.getResponse(), id, e.getMessage());
logger.debug("Failed REST call to validate a verification in iProov, statusCode={}, responseBody='{}', {}",
e.getStatusCode(), e.getResponse(), id, e);
final PresenceCheckResult result = new PresenceCheckResult();
if (HttpStatus.BAD_REQUEST.equals(e.getStatusCode())) {
final ClientErrorResponse clientErrorResponse = parseResponse(e.getResponse(), ClientErrorResponse.class);
if (ClientErrorResponse.ErrorEnum.INVALID_TOKEN.equals(clientErrorResponse.getError())) {
// TODO same response when validating the verification using same token repeatedly
logger.warn("Reusing iProov token, {}", id);
result.setStatus(PresenceCheckStatus.IN_PROGRESS);
} else {
result.setStatus(PresenceCheckStatus.FAILED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult;
import com.wultra.app.enrollmentserver.model.integration.SessionInfo;
import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException;
import com.wultra.app.onboardingserver.presencecheck.mock.MockConst;
import com.wultra.app.onboardingserver.provider.PresenceCheckProvider;
import org.slf4j.Logger;
Expand Down Expand Up @@ -53,12 +52,12 @@ public WultraMockPresenceCheckProvider() {
}

@Override
public void initPresenceCheck(OwnerId id, Image photo) throws PresenceCheckException {
public void initPresenceCheck(OwnerId id, Image photo) {
logger.info("Mock - initialized presence check with a photo, {}", id);
}

@Override
public SessionInfo startPresenceCheck(OwnerId id) throws PresenceCheckException {
public SessionInfo startPresenceCheck(OwnerId id) {
String token = UUID.randomUUID().toString();

SessionInfo sessionInfo = new SessionInfo();
Expand All @@ -70,7 +69,7 @@ public SessionInfo startPresenceCheck(OwnerId id) throws PresenceCheckException
}

@Override
public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) throws PresenceCheckException {
public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) {
String selfiePhotoPath = "/images/specimen_photo.jpg";
Image photo = new Image();
try (InputStream is = WultraMockPresenceCheckProvider.class.getResourceAsStream(selfiePhotoPath)) {
Expand All @@ -95,7 +94,7 @@ public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) throws
}

@Override
public void cleanupIdentityData(OwnerId id) throws PresenceCheckException {
public void cleanupIdentityData(OwnerId id) {
logger.info("Mock - cleaned up identity data, {}", id);
}

Expand Down
Loading