Skip to content

Commit

Permalink
Fix #494: Check that only one verification ID is present for accepted…
Browse files Browse the repository at this point in the history
… documents (#501)
  • Loading branch information
banterCZ authored Nov 8, 2022
1 parent 1a01c16 commit 215b3cf
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;

import java.util.function.Consumer;
import java.util.Set;

import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.CLIENT_EVALUATION;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.ACCEPTED;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.IN_PROGRESS;
import static java.util.stream.Collectors.toSet;

/**
* Service for client evaluation features.
Expand All @@ -51,14 +49,14 @@
@Slf4j
public class ClientEvaluationService {

private static final String ERROR_VERIFICATION_ID = "unableToGetDocumentVerificationId";

private final OnboardingProvider onboardingProvider;

private final IdentityVerificationConfig config;

private final IdentityVerificationService identityVerificationService;

private final TransactionTemplate transactionTemplate;

private final AuditService auditService;

/**
Expand All @@ -67,20 +65,17 @@ public class ClientEvaluationService {
* @param onboardingProvider Onboarding provider.
* @param config Identity verification config.
* @param identityVerificationService Identity verification repository.
* @param transactionTemplate Transaction template.
* @param auditService Audit service.
*/
@Autowired
public ClientEvaluationService(
final OnboardingProvider onboardingProvider,
final IdentityVerificationConfig config,
final IdentityVerificationService identityVerificationService,
final TransactionTemplate transactionTemplate,
final AuditService auditService) {
this.onboardingProvider = onboardingProvider;
this.config = config;
this.identityVerificationService = identityVerificationService;
this.transactionTemplate = transactionTemplate;
this.auditService = auditService;
}

Expand All @@ -103,33 +98,50 @@ public void initClientEvaluation(final OwnerId ownerId, final IdentityVerificati
public void processClientEvaluation(final IdentityVerificationEntity identityVerification, final OwnerId ownerId) {
logger.debug("Client evaluation started for {}", identityVerification);

final String verificationId;
try {
verificationId = getVerificationId(identityVerification);
} catch (Exception e) {
processVerificationIdError(identityVerification, ownerId, e);
return;
}

final EvaluateClientRequest request = EvaluateClientRequest.builder()
.processId(identityVerification.getProcessId())
.userId(identityVerification.getUserId())
.identityVerificationId(identityVerification.getId())
.verificationId(getVerificationId(identityVerification))
.verificationId(verificationId)
.build();

final int maxFailedAttempts = config.getClientEvaluationMaxFailedAttempts();
for (int i = 0; i < maxFailedAttempts; i++) {
final int attempt = i + 1;
try {
final EvaluateClientResponse response = onboardingProvider.evaluateClient(request);
processEvaluationSuccess(identityVerification, ownerId, response);
break;
} catch (Throwable t) {
processEvaluationError(identityVerification, ownerId, t);
logger.debug("Client evaluation finished for {}, attempt: {}", identityVerification, attempt);
return;
} catch (Exception e) {
logger.warn("Client evaluation failed for {}, attempt: {}, {}, {}", identityVerification, attempt, ownerId, e.getMessage());
logger.debug("Client evaluation failed for {} - attempt: {}, {}", identityVerification, attempt, ownerId, e);
}
}
logger.debug("Client evaluation finished for {}", identityVerification);
processTooManyEvaluationError(identityVerification, ownerId);
}

private static String getVerificationId(final IdentityVerificationEntity identityVerification) {
return identityVerification.getDocumentVerifications().stream()
final Set<String> verificationIds = identityVerification.getDocumentVerifications().stream()
.filter(DocumentVerificationEntity::isUsedForVerification)
.filter(it -> it.getStatus() == DocumentStatus.ACCEPTED)
.findAny()
.map(DocumentVerificationEntity::getVerificationId)
.orElseThrow(() -> new IllegalStateException("No accepted document verification for " + identityVerification));
.collect(toSet());

if (verificationIds.size() == 1) {
return verificationIds.iterator().next();
} else {
throw new IllegalStateException(
String.format("Expected just one document verificationId for %s but got %s", identityVerification, verificationIds));
}
}

private void processEvaluationSuccess(final IdentityVerificationEntity identityVerification, final OwnerId ownerId, final EvaluateClientResponse response) {
Expand All @@ -145,9 +157,7 @@ private void processEvaluationSuccess(final IdentityVerificationEntity identityV
final IdentityVerificationPhase phase = identityVerification.getPhase();
if (response.isAccepted()) {
logger.info("Client evaluation accepted for {}", identityVerification);
saveInExistingTransaction(status ->
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, ACCEPTED, ownerId)
);
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, ACCEPTED, ownerId);
} else {
logger.info("Client evaluation rejected for {}", identityVerification);
identityVerification.getDocumentVerifications()
Expand All @@ -156,24 +166,26 @@ private void processEvaluationSuccess(final IdentityVerificationEntity identityV
auditService.audit(document, "Document rejected because of client evaluation for user: {}", identityVerification.getUserId());
});
identityVerification.setTimestampFailed(ownerId.getTimestamp());
saveInExistingTransaction(status ->
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, IdentityVerificationStatus.REJECTED, ownerId));
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, IdentityVerificationStatus.REJECTED, ownerId);
}
}

private void processEvaluationError(final IdentityVerificationEntity identityVerification, final OwnerId ownerId, final Throwable t) {
logger.warn("Client evaluation failed for {} - {}", identityVerification, t.getMessage());
logger.debug("Client evaluation failed for {}", identityVerification, t);
private void processTooManyEvaluationError(final IdentityVerificationEntity identityVerification, final OwnerId ownerId) {
logger.warn("Client evaluation too many attempts for {} - {}", identityVerification, ownerId);
identityVerification.setErrorDetail(IdentityVerificationEntity.ERROR_MAX_FAILED_ATTEMPTS_CLIENT_EVALUATION);
identityVerification.setErrorOrigin(ErrorOrigin.PROCESS_LIMIT_CHECK);
identityVerification.setTimestampFailed(ownerId.getTimestamp());
final IdentityVerificationPhase phase = identityVerification.getPhase();
saveInExistingTransaction(status ->
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, IdentityVerificationStatus.FAILED, ownerId));
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, IdentityVerificationStatus.FAILED, ownerId);
}

private void saveInExistingTransaction(final Consumer<TransactionStatus> consumer) {
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
transactionTemplate.executeWithoutResult(consumer);
private void processVerificationIdError(final IdentityVerificationEntity identityVerification, final OwnerId ownerId, final Exception e) {
logger.warn("Client evaluation failed to get verificationId for {}, {} - {}", identityVerification, ownerId, e.getMessage());
logger.debug("Client evaluation failed to get verificationId for {}, {}", identityVerification, ownerId, e);
identityVerification.setErrorDetail(ERROR_VERIFICATION_ID);
identityVerification.setErrorOrigin(ErrorOrigin.CLIENT_EVALUATION);
identityVerification.setTimestampFailed(ownerId.getTimestamp());
final IdentityVerificationPhase phase = identityVerification.getPhase();
identityVerificationService.moveToPhaseAndStatus(identityVerification, phase, IdentityVerificationStatus.FAILED, ownerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@

import com.wultra.app.onboardingserver.common.annotation.PublicApi;
import com.wultra.app.onboardingserver.provider.OnboardingProvider;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import lombok.*;

/**
* Request object for {@link OnboardingProvider#evaluateClient(EvaluateClientRequest)}.
Expand All @@ -34,6 +31,7 @@
@Getter
@ToString
@PublicApi
@EqualsAndHashCode
public final class EvaluateClientRequest {

@NonNull
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* 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.onboardingserver.impl.service;

import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus;
import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin;
import com.wultra.app.enrollmentserver.model.integration.OwnerId;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
import com.wultra.app.onboardingserver.common.service.AuditService;
import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig;
import com.wultra.app.onboardingserver.errorhandling.OnboardingProviderException;
import com.wultra.app.onboardingserver.provider.OnboardingProvider;
import com.wultra.app.onboardingserver.provider.model.request.EvaluateClientRequest;
import com.wultra.app.onboardingserver.provider.model.response.EvaluateClientResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Set;
import java.util.UUID;

import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.CLIENT_EVALUATION;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.ACCEPTED;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.FAILED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
* Test for {@link ClientEvaluationService}.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
@ExtendWith(MockitoExtension.class)
class ClientEvaluationServiceTest {

@Mock
private AuditService auditService;

@Mock
private OnboardingProvider onboardingProvider;

@Mock
private IdentityVerificationService identityVerificationService;

@Mock
private IdentityVerificationConfig identityVerificationConfig;

@InjectMocks
private ClientEvaluationService tested;

@Test
void testProcessClientEvaluation_successful() throws Exception {
when(identityVerificationConfig.getClientEvaluationMaxFailedAttempts())
.thenReturn(1);

final EvaluateClientRequest evaluateClientRequest = EvaluateClientRequest.builder()
.processId("p1")
.userId("u1")
.identityVerificationId("i1")
.verificationId("v1")
.build();
final EvaluateClientResponse evaluateClientResponse = EvaluateClientResponse.builder()
.accepted(true)
.build();
when(onboardingProvider.evaluateClient(evaluateClientRequest))
.thenReturn(evaluateClientResponse);

final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity();
identityVerification.setId("i1");
identityVerification.setProcessId("p1");
identityVerification.setUserId("u1");
identityVerification.setPhase(CLIENT_EVALUATION);
identityVerification.setDocumentVerifications(Set.of(
createDocumentVerification("d1", DocumentStatus.ACCEPTED, "v1"),
createDocumentVerification("d2", DocumentStatus.ACCEPTED, "v1"),
createDocumentVerification("d3", DocumentStatus.DISPOSED, "v2")));

final OwnerId ownerId = new OwnerId();

tested.processClientEvaluation(identityVerification, ownerId);

verify(identityVerificationService).moveToPhaseAndStatus(identityVerification, CLIENT_EVALUATION, ACCEPTED, ownerId);
}

@Test
void testProcessClientEvaluation_invalidVerificationId() {
final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity();
identityVerification.setId("i1");
identityVerification.setProcessId("p1");
identityVerification.setUserId("u1");
identityVerification.setPhase(CLIENT_EVALUATION);
identityVerification.setDocumentVerifications(Set.of(
createDocumentVerification("d1", DocumentStatus.ACCEPTED, "v1"),
createDocumentVerification("d2", DocumentStatus.ACCEPTED, "v2")));

final OwnerId ownerId = new OwnerId();

tested.processClientEvaluation(identityVerification, ownerId);

verify(identityVerificationService).moveToPhaseAndStatus(identityVerification, CLIENT_EVALUATION, FAILED, ownerId);

assertEquals("unableToGetDocumentVerificationId", identityVerification.getErrorDetail());
assertEquals(ErrorOrigin.CLIENT_EVALUATION, identityVerification.getErrorOrigin());
}

@Test
void testProcessClientEvaluation_tooManyAttempts() throws Exception {
when(identityVerificationConfig.getClientEvaluationMaxFailedAttempts())
.thenReturn(1);

final EvaluateClientRequest evaluateClientRequest = EvaluateClientRequest.builder()
.processId("p1")
.userId("u1")
.identityVerificationId("i1")
.verificationId("v1")
.build();
final EvaluateClientResponse evaluateClientResponse = EvaluateClientResponse.builder()
.accepted(true)
.build();
when(onboardingProvider.evaluateClient(evaluateClientRequest))
.thenThrow(new OnboardingProviderException());

final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity();
identityVerification.setId("i1");
identityVerification.setProcessId("p1");
identityVerification.setUserId("u1");
identityVerification.setPhase(CLIENT_EVALUATION);
identityVerification.setDocumentVerifications(Set.of(
createDocumentVerification("d1", DocumentStatus.ACCEPTED, "v1")));

final OwnerId ownerId = new OwnerId();

tested.processClientEvaluation(identityVerification, ownerId);

verify(identityVerificationService).moveToPhaseAndStatus(identityVerification, CLIENT_EVALUATION, FAILED, ownerId);

assertEquals("maxFailedAttemptsClientEvaluation", identityVerification.getErrorDetail());
assertEquals(ErrorOrigin.PROCESS_LIMIT_CHECK, identityVerification.getErrorOrigin());
}

private static DocumentVerificationEntity createDocumentVerification(final String id, final DocumentStatus status, final String verificationId) {
final DocumentVerificationEntity documentVerification = new DocumentVerificationEntity();
documentVerification.setId(id);
documentVerification.setFilename(UUID.randomUUID().toString());
documentVerification.setStatus(status);
documentVerification.setVerificationId(verificationId);
documentVerification.setUsedForVerification(true);
return documentVerification;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
*/
package com.wultra.app.onboardingserver.statemachine;

import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus;
import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase;
import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus;
import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus;
import com.wultra.app.onboardingserver.EnrollmentServerTestApplication;
import com.wultra.app.onboardingserver.common.database.OnboardingProcessRepository;
import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity;
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity;
import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig;
import com.wultra.app.onboardingserver.impl.service.IdentityVerificationOtpService;
Expand All @@ -39,6 +41,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.Set;

import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.CLIENT_EVALUATION;
import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.IN_PROGRESS;
Expand Down Expand Up @@ -140,8 +143,15 @@ private void testDocumentVerificationStatus(IdentityVerificationStatus identityS
}

private IdentityVerificationEntity createIdentityVerificationLocal(IdentityVerificationPhase phase, IdentityVerificationStatus status) {
IdentityVerificationEntity idVerification =
super.createIdentityVerification(phase, status);
final DocumentVerificationEntity documentVerification = new DocumentVerificationEntity();
documentVerification.setUsedForVerification(true);
documentVerification.setStatus(DocumentStatus.ACCEPTED);
documentVerification.setVerificationId("verificationId-1");

final IdentityVerificationEntity idVerification = super.createIdentityVerification(phase, status);
idVerification.setDocumentVerifications(Set.of(documentVerification));
documentVerification.setIdentityVerification(idVerification);

when(onboardingProcessRepository.findByActivationIdAndStatusWithLock(idVerification.getActivationId(), OnboardingStatus.VERIFICATION_IN_PROGRESS))
.thenReturn(Optional.of(createOnboardingProcessEntity()));
return idVerification;
Expand Down

0 comments on commit 215b3cf

Please sign in to comment.