-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Fix #911: Expose API to upload presence check data
- Loading branch information
Showing
5 changed files
with
297 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
...a/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* PowerAuth Enrollment Server | ||
* Copyright (C) 2023 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.provider.innovatrics; | ||
|
||
import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; | ||
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; | ||
import io.getlime.core.rest.model.base.response.Response; | ||
import io.getlime.security.powerauth.crypto.lib.enums.PowerAuthSignatureTypes; | ||
import io.getlime.security.powerauth.rest.api.spring.annotation.EncryptedRequestBody; | ||
import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuth; | ||
import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuthEncryption; | ||
import io.getlime.security.powerauth.rest.api.spring.authentication.PowerAuthApiAuthentication; | ||
import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionContext; | ||
import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionScope; | ||
import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthAuthenticationException; | ||
import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthEncryptionException; | ||
import io.getlime.security.powerauth.rest.api.spring.exception.authentication.PowerAuthTokenInvalidException; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import lombok.AllArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
|
||
/** | ||
* Controller publishing REST services for uploading Innovatrics liveness data. | ||
* | ||
* @author Lubos Racansky, [email protected] | ||
*/ | ||
@ConditionalOnExpression(""" | ||
'${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and ${enrollment-server-onboarding.onboarding-process.enabled} == true | ||
""") | ||
@RestController | ||
@RequestMapping(value = "api/identity") | ||
@AllArgsConstructor | ||
@Slf4j | ||
class InnovatricsLivenessController { | ||
|
||
private InnovatricsLivenessService innovatricsLivenessService; | ||
|
||
/** | ||
* Upload Innovatrics liveness data. | ||
* | ||
* @param requestData Binary request data | ||
* @param encryptionContext Encryption 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. | ||
* @throws RemoteCommunicationException Thrown when there is a problem with the remote communication. | ||
*/ | ||
@PostMapping("presence-check/upload") | ||
@PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) | ||
@PowerAuth(resourceId = "/api/identity/presence-check/upload", signatureType = PowerAuthSignatureTypes.POSSESSION) | ||
public Response upload( | ||
@EncryptedRequestBody byte[] requestData, | ||
@Parameter(hidden = true) EncryptionContext encryptionContext, | ||
@Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, RemoteCommunicationException { | ||
|
||
if (apiAuthentication == null) { | ||
throw new PowerAuthTokenInvalidException("Unable to verify device registration when uploading liveness"); | ||
} | ||
|
||
if (encryptionContext == null) { | ||
throw new PowerAuthEncryptionException("ECIES encryption failed when uploading liveness"); | ||
} | ||
|
||
if (requestData == null) { | ||
throw new PowerAuthEncryptionException("Invalid request received when uploading liveness"); | ||
} | ||
|
||
innovatricsLivenessService.upload(requestData, encryptionContext); | ||
return new Response(); | ||
} | ||
} |
134 changes: 134 additions & 0 deletions
134
...java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
/* | ||
* PowerAuth Enrollment Server | ||
* Copyright (C) 2023 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.provider.innovatrics; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.google.common.base.Strings; | ||
import com.wultra.app.enrollmentserver.model.integration.OwnerId; | ||
import com.wultra.app.enrollmentserver.model.integration.SessionInfo; | ||
import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository; | ||
import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; | ||
import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; | ||
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; | ||
import com.wultra.app.onboardingserver.common.service.AuditService; | ||
import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateCustomerLivenessRecordResponse; | ||
import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateSelfieResponse; | ||
import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionContext; | ||
import lombok.AllArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
/** | ||
* Service providing Innovatrics business features beyond {@link InnovatricsPresenceCheckProvider}. | ||
* | ||
* @author Lubos Racansky, [email protected] | ||
*/ | ||
@Service | ||
@Transactional(readOnly = true) | ||
@Slf4j | ||
@AllArgsConstructor | ||
@ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "innovatrics") | ||
class InnovatricsLivenessService { | ||
|
||
private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId"; | ||
|
||
private final InnovatricsApiService innovatricsApiService; | ||
|
||
private final IdentityVerificationRepository identityVerificationRepository; | ||
|
||
private AuditService auditService; | ||
|
||
public void upload(final byte[] requestData, final EncryptionContext encryptionContext) throws IdentityVerificationException, RemoteCommunicationException { | ||
final String activationId = encryptionContext.getActivationId(); | ||
final IdentityVerificationEntity identityVerification = identityVerificationRepository.findFirstByActivationIdOrderByTimestampCreatedDesc(activationId).orElseThrow(() -> | ||
new IdentityVerificationException("No identity verification entity found for Activation ID: " + activationId)); | ||
|
||
final OwnerId ownerId = extractOwnerId(identityVerification); | ||
final String customerId = fetchCustomerId(ownerId, identityVerification); | ||
|
||
createLiveness(customerId, ownerId); | ||
final CreateCustomerLivenessRecordResponse livenessRecordResponse = createLivenessRecord(requestData, customerId, ownerId); | ||
createSelfie(livenessRecordResponse, customerId, ownerId); | ||
|
||
auditService.auditPresenceCheckProvider(identityVerification, "Uploaded presence check data for user: {}", ownerId.getUserId()); | ||
logger.info("Liveness record successfully uploaded, {}", ownerId); | ||
} | ||
|
||
private void createSelfie(final CreateCustomerLivenessRecordResponse livenessRecordResponse, final String customerId, final OwnerId ownerId) throws IdentityVerificationException, RemoteCommunicationException { | ||
final String livenessSelfieLink = fetchSelfieLink(livenessRecordResponse); | ||
final CreateSelfieResponse createSelfieResponse = innovatricsApiService.createSelfie(customerId, livenessSelfieLink, ownerId); | ||
if (createSelfieResponse.getErrorCode() != null) { | ||
logger.warn("Customer selfie error: {}, {}", createSelfieResponse.getErrorCode(), ownerId); | ||
} | ||
if (createSelfieResponse.getWarnings() != null) { | ||
for (CreateSelfieResponse.WarningsEnum warning : createSelfieResponse.getWarnings()) { | ||
logger.warn("Customer selfie warning: {}, {}", warning.getValue(), ownerId); | ||
} | ||
} | ||
logger.debug("Selfie created, {}", ownerId); | ||
} | ||
|
||
private CreateCustomerLivenessRecordResponse createLivenessRecord(final byte[] requestData, final String customerId, final OwnerId ownerId) throws RemoteCommunicationException, IdentityVerificationException { | ||
final CreateCustomerLivenessRecordResponse livenessRecordResponse = innovatricsApiService.createLivenessRecord(customerId, requestData, ownerId); | ||
if (livenessRecordResponse.getErrorCode() != null) { | ||
throw new IdentityVerificationException("Unable to create liveness record: " + livenessRecordResponse.getErrorCode()); | ||
} | ||
logger.debug("Liveness record created, {}", ownerId); | ||
return livenessRecordResponse; | ||
} | ||
|
||
private void createLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { | ||
innovatricsApiService.createLiveness(customerId, ownerId); | ||
logger.debug("Liveness created, {}", ownerId); | ||
} | ||
|
||
private static String fetchSelfieLink(final CreateCustomerLivenessRecordResponse livenessRecordResponse) throws IdentityVerificationException { | ||
if (livenessRecordResponse.getLinks() == null) { | ||
throw new IdentityVerificationException("Unable to get selfie link"); | ||
} | ||
return livenessRecordResponse.getLinks().getSelfie(); | ||
} | ||
|
||
private static OwnerId extractOwnerId(final IdentityVerificationEntity identityVerification) { | ||
final OwnerId ownerId = new OwnerId(); | ||
ownerId.setActivationId(identityVerification.getActivationId()); | ||
ownerId.setUserId(identityVerification.getUserId()); | ||
return ownerId; | ||
} | ||
|
||
private static String fetchCustomerId(final OwnerId id, final IdentityVerificationEntity identityVerification) throws IdentityVerificationException { | ||
final String sessionInfoString = StringUtils.defaultIfEmpty(identityVerification.getSessionInfo(), "{}"); | ||
final SessionInfo sessionInfo; | ||
try { | ||
sessionInfo = new ObjectMapper().readValue(sessionInfoString, SessionInfo.class); | ||
} catch (JsonProcessingException e) { | ||
throw new IdentityVerificationException("Unable to deserialize session info", e); | ||
} | ||
|
||
// TODO (racansky, 2023-11-28) discuss the format with Jan Pesek, extract to common logic | ||
final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID); | ||
if (Strings.isNullOrEmpty(customerId)) { | ||
throw new IdentityVerificationException("Missing a customer ID value for calling Innovatrics, " + id); | ||
} | ||
return customerId; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters