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 4 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 @@ -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<PresenceCheckInitRequest> request,
banterCZ marked this conversation as resolved.
Show resolved Hide resolved
@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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.wultra.app.onboardingserver.statemachine.action.clientevaluation.ClientEvaluationInitAction;
import com.wultra.app.onboardingserver.statemachine.action.otp.OtpVerificationResendAction;
import com.wultra.app.onboardingserver.statemachine.action.otp.OtpVerificationSendAction;
import com.wultra.app.onboardingserver.statemachine.action.presencecheck.MoveToPresenceCheckVerificationPendingAction;
import com.wultra.app.onboardingserver.statemachine.action.presencecheck.PresenceCheckInitAction;
import com.wultra.app.onboardingserver.statemachine.action.presencecheck.PresenceCheckNotInitializedAction;
import com.wultra.app.onboardingserver.statemachine.action.presencecheck.PresenceCheckVerificationAction;
Expand Down Expand Up @@ -81,6 +82,8 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<Onboar

private final PresenceCheckVerificationAction presenceCheckVerificationAction;

private final MoveToPresenceCheckVerificationPendingAction moveToPresenceCheckVerificationPendingAction;

private final MoveToDocumentUploadVerificationPendingAction moveToDocumentUploadVerificationPendingAction;

private final DocumentsVerificationPendingGuard documentsVerificationPendingGuard;
Expand Down Expand Up @@ -117,6 +120,7 @@ public StateMachineConfig(
final PresenceCheckInitAction presenceCheckInitAction,
final PresenceCheckNotInitializedAction presenceCheckNotInitializedAction,
final PresenceCheckVerificationAction presenceCheckVerificationAction,
final MoveToPresenceCheckVerificationPendingAction moveToPresenceCheckVerificationPendingAction,
final MoveToDocumentUploadVerificationPendingAction moveToDocumentUploadVerificationPendingAction,
final DocumentsVerificationPendingGuard documentsVerificationPendingGuard,
final VerificationDocumentStartAction verificationDocumentStartAction,
Expand All @@ -140,6 +144,7 @@ public StateMachineConfig(
this.presenceCheckNotInitializedAction = presenceCheckNotInitializedAction;
this.presenceCheckVerificationAction = presenceCheckVerificationAction;

this.moveToPresenceCheckVerificationPendingAction = moveToPresenceCheckVerificationPendingAction;
this.moveToDocumentUploadVerificationPendingAction = moveToDocumentUploadVerificationPendingAction;
this.documentsVerificationPendingGuard = documentsVerificationPendingGuard;
this.verificationDocumentStartAction = verificationDocumentStartAction;
Expand Down Expand Up @@ -329,14 +334,21 @@ private void configurePresenceCheckTransitions(StateMachineTransitionConfigurer<
.and()
.withExternal()
.source(OnboardingState.PRESENCE_CHECK_IN_PROGRESS)
.event(OnboardingEvent.EVENT_NEXT_STATE)
.event(OnboardingEvent.PRESENCE_CHECK_SUBMITTED)
.action(moveToPresenceCheckVerificationPendingAction)
.target(OnboardingState.PRESENCE_CHECK_VERIFICATION_PENDING)

.and()
.withExternal()
.source(OnboardingState.PRESENCE_CHECK_VERIFICATION_PENDING)
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 You may expect PRESENCE_CHECK / VERIFICATION_PENDING at the status endpoint.

.action(presenceCheckVerificationAction)
.event(OnboardingEvent.EVENT_NEXT_STATE)
.target(OnboardingState.CHOICE_PRESENCE_CHECK_PROCESSING)

.and()
.withChoice()
.source(OnboardingState.CHOICE_PRESENCE_CHECK_PROCESSING)
.first(OnboardingState.PRESENCE_CHECK_IN_PROGRESS, statusInProgressGuard)
.first(OnboardingState.PRESENCE_CHECK_VERIFICATION_PENDING, statusInProgressGuard)
.then(OnboardingState.OTP_VERIFICATION_PENDING,
context -> otpVerificationEnabledGuard.evaluate(context) && statusAcceptedGuard.evaluate(context),
otpVerificationSendAction
Expand Down
Loading