diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d21fc919..e160a01a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,13 +1,9 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# name: "CodeQL" on: + workflow_dispatch: + branches: + - 'develop' push: branches: [ develop, main ] pull_request: @@ -17,63 +13,12 @@ on: - cron: '0 2 * * 4' jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'java' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: 'temurin' - cache: maven - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{matrix.language}}" + codeql-analysis: + uses: wultra/wultra-infrastructure/.github/workflows/codeql-analysis.yml@develop + secrets: inherit + with: + languages: "['java']" + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support \ No newline at end of file diff --git a/docs/Database-Structure.md b/docs/Database-Structure.md new file mode 100644 index 00000000..f4037859 --- /dev/null +++ b/docs/Database-Structure.md @@ -0,0 +1,42 @@ +# Database Structure + + + +You can download DDL scripts for supported databases: + +- [PostgreSQL - Create Database Schema](./sql/postgresql/enrollment/create-schema.sql) +- [Oracle - Create Database Schema](./sql/oracle/enrollment/create-schema.sql) + + +## Auditing + +The DDL files contain an `audit_log` table definition. The table differs slightly per database. + +Only one `audit_log` table is required per PowerAuth stack in case the same schema is used for all deployed applications. + +For more information about auditing library, see the [Wultra auditing library documentation](https://github.com/wultra/lime-java-core#wultra-auditing-library). + + +## Table Documentation + +This chapter explains individual tables and their columns. The column types are used from PostgreSQL dialect, other databases use types that are equivalent (mapping is usually straight-forward). + + +### Operation Template Table + +Stores operation templates to be shown by the mobile application. +For more information, see [Operation Extensions](Operation-Extensions.md) and [Customizing Operation Form Data](Operation-Form-Data.md). + +#### Schema + +| Name | Type | Info | Note | +|---------------|----------------|------------------------|------------------------------------------------------------------------------------------| +| `id` | `BIGINT` | `NOT NULL PRIMARY KEY` | Autogenerated record identifier. | +| `placeholder` | `VARCHAR(255)` | `NOT NULL` | Operation type at PowerAuth server. | +| `language` | `VARCHAR(8)` | `NOT NULL` | Language of the template. | +| `title` | `VARCHAR(255)` | `NOT NULL` | Title of the operation. | +| `message` | `TEXT` | `NOT NULL` | Message for the user related to the operation. | +| `attributes` | `TEXT` | | Structured custom form data attributes as JSON. | +| `ui` | `TEXT` | | JSON configuration which may affect behavior or visual aspect of the mobile application. | + + diff --git a/docs/Home.md b/docs/Home.md index c16d9beb..0b605886 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -6,6 +6,7 @@ PowerAuth Enrollment Server is an easy to deploy backend service used for bootst - [Deploying Enrollment Server](./Deploying-Enrollment-Server.md) - [Deploying Enrollment Server on JBoss/Wildfly](./Deploying-Wildfly.md) +- [Database Structure](./Database-Structure.md) - [Migration Instructions](./Migration-Instructions.md) - [Configuration Properties](./Configuration-Properties.md) - [Documentation for Onboarding Server](./onboarding/Home.md) diff --git a/docs/PowerAuth-Enrollment-Server-1.5.0.md b/docs/PowerAuth-Enrollment-Server-1.5.0.md index f8a7ef54..1b28a05f 100644 --- a/docs/PowerAuth-Enrollment-Server-1.5.0.md +++ b/docs/PowerAuth-Enrollment-Server-1.5.0.md @@ -24,6 +24,19 @@ Make sure that you use dialect without version. Since version `1.5.0`, MySQL database is not supported anymore. +### Oracle + +#### Operation Template + +In the `1.5.0` version, the `ES_OPERATION_TEMPLATE` table in the **Oracle** database had a change in the data type of the `MESSAGE` and `ATTRIBUTES` columns. They have been altered from `BLOB` to `CLOB`. + +You need to execute the following commands to alter the columns: + +```sql +ALTER TABLE ES_OPERATION_TEMPLATE MODIFY (MESSAGE CLOB); +ALTER TABLE ES_OPERATION_TEMPLATE MODIFY (ATTRIBUTES CLOB); +``` + ## API Extensions Since version `1.5.0`, the API supports new cell types in operation responses. These are: diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index f1cb671e..4a723510 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -2,6 +2,7 @@ - [Deploying Enrollment Server](./Deploying-Enrollment-Server.md) - [Deploying Enrollment Server on JBoss/Wildfly](./Deploying-Wildfly.md) +- [Database Structure](./Database-Structure.md) - [Migration Instructions](./Migration-Instructions.md) - [Configuration Properties](./Configuration-Properties.md) - [Documentation for Onboarding Server](./onboarding/Home.md) diff --git a/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/20230614-add-sca-result.xml b/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/20230614-add-sca-result.xml new file mode 100644 index 00000000..54f0fb0c --- /dev/null +++ b/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/20230614-add-sca-result.xml @@ -0,0 +1,67 @@ + + + + + + + + + + Create a new sequence es_sca_result_seq + + + + + + + + + + Create a new table es_onboarding_process + + + + + + + + + + + + + + + + + + + + + + + + + + + Create a new index on es_sca_result(identity_verification_id) + + + + + + + + + + + + Create a new index on es_sca_result(process_id) + + + + + + diff --git a/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/db.changelog-version.xml b/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/db.changelog-version.xml index 39cdd4d0..25bba1d7 100644 --- a/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/db.changelog-version.xml +++ b/docs/db/changelog/changesets/enrollment-server-onboarding/1.5.x/db.changelog-version.xml @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/docs/onboarding/Database-Structure.md b/docs/onboarding/Database-Structure.md index fd1317f8..f5c6632c 100644 --- a/docs/onboarding/Database-Structure.md +++ b/docs/onboarding/Database-Structure.md @@ -190,3 +190,24 @@ Stores result of document verification. | `timestamp_created` | `TEXT` | | Timestamp when record was created. | + + + +### SCA Result Table + +Stores result of SCA (Strong Customer Authentication) steps (presence check and OTP verification). + +#### Schema + +| Name | Type | Info | Note | +|----------------------------|---------------|--------------------------------------|---------------------------------------------------------------------------------------------------| +| `id` | `BIGINT` | `NOT NULL PRIMARY KEY` | Autogenerated record identifier (Long). | +| `identity_verification_id` | `VARCHAR(36)` | `NOT NULL` | Identity verification identifier. | +| `process_id` | `VARCHAR(36)` | `NOT NULL` | Process identifier (UUID). | +| `presence_check_result` | `VARCHAR(32)` | | Result of presence check (`SUCCESS`, `FAILED`). | +| `otp_verification_result` | `VARCHAR(32)` | | Result of the last OTP verification (`SUCCESS`, `FAILED`). | +| `sca_result` | `VARCHAR(32)` | | Aggregated result of `presence_check_result` and `otp_verification_result` (`SUCCESS`, `FAILED`). | +| `timestamp_created` | `TIMESTAMP` | `NOT NULL DEFAULT CURRENT_TIMESTAMP` | Timestamp when the SCA was started. | +| `timestamp_last_updated` | `TIMESTAMP` | | Timestamp when record was last updated. | + + diff --git a/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.5.0.md b/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.5.0.md index 786fff4b..e50ca51a 100644 --- a/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.5.0.md +++ b/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.5.0.md @@ -62,7 +62,7 @@ A new column `total_attempts` has been added to the table `es_onboarding_otp`. #### PostgreSQL ```sql -ALTER TABLE es_onboarding_process +ALTER TABLE es_onboarding_otp ADD COLUMN TOTAL_ATTEMPTS INTEGER DEFAULT 0; ``` @@ -70,11 +70,64 @@ ALTER TABLE es_onboarding_process #### Oracle ```sql -ALTER TABLE es_onboarding_process +ALTER TABLE es_onboarding_otp ADD total_attempts INTEGER DEFAULT 0; ``` +### SCA Result + +A new table `es_sca_result` has been created. + + +#### PostgreSQL + +```sql +CREATE SEQUENCE es_sca_result_seq INCREMENT BY 50 START WITH 1; + +CREATE TABLE es_sca_result +( + id BIGINT NOT NULL PRIMARY KEY, + identity_verification_id VARCHAR(36) NOT NULL, + process_id VARCHAR(36) NOT NULL, + presence_check_result VARCHAR(32), + otp_verification_result VARCHAR(32), + sca_result VARCHAR(32), + timestamp_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + timestamp_last_updated TIMESTAMP, + FOREIGN KEY (identity_verification_id) REFERENCES es_identity_verification (id), + FOREIGN KEY (process_id) REFERENCES es_onboarding_process (id) +); + +CREATE INDEX identity_verification_id ON es_sca_result (identity_verification_id); +CREATE INDEX process_id ON es_sca_result (process_id); +``` + + +#### Oracle + +```sql +CREATE SEQUENCE ES_SCA_RESULT_SEQ INCREMENT BY 50 START WITH 1; + +CREATE TABLE ES_SCA_RESULT +( + ID NUMBER(19) NOT NULL PRIMARY KEY, + IDENTITY_VERIFICATION_ID VARCHAR2(36 CHAR) NOT NULL, + PROCESS_ID VARCHAR2(36 CHAR) NOT NULL, + PRESENCE_CHECK_RESULT VARCHAR2(32 CHAR), + OTP_VERIFICATION_RESULT VARCHAR2(32 CHAR), + SCA_RESULT VARCHAR2(32 CHAR), + TIMESTAMP_CREATED TIMESTAMP(6) NOT NULL, + TIMESTAMP_LAST_UPDATED TIMESTAMP(6), + FOREIGN KEY (IDENTITY_VERIFICATION_ID) REFERENCES ES_IDENTITY_VERIFICATION (ID), + FOREIGN KEY (PROCESS_ID) REFERENCES ES_ONBOARDING_PROCESS (ID) +); + +CREATE INDEX IDENTITY_VERIFICATION_ID ON ES_SCA_RESULT (IDENTITY_VERIFICATION_ID); +CREATE INDEX PROCESS_ID ON ES_SCA_RESULT (PROCESS_ID); +``` + + ## Dependencies PostgreSQL JDBC driver is already included in the WAR file. diff --git a/docs/sql/oracle/onboarding/create-schema.sql b/docs/sql/oracle/onboarding/create-schema.sql index e04c2559..53f6d4c2 100644 --- a/docs/sql/oracle/onboarding/create-schema.sql +++ b/docs/sql/oracle/onboarding/create-schema.sql @@ -158,6 +158,25 @@ CREATE TABLE ES_DOCUMENT_RESULT ( -- Oracle does not create indexes on foreign keys automatically CREATE INDEX DOCUMENT_VERIF_RESULT ON ES_DOCUMENT_RESULT (DOCUMENT_VERIFICATION_ID); +CREATE SEQUENCE ES_SCA_RESULT_SEQ INCREMENT BY 50 START WITH 1; + +CREATE TABLE ES_SCA_RESULT +( + ID NUMBER(19) NOT NULL PRIMARY KEY, + IDENTITY_VERIFICATION_ID VARCHAR2(36 CHAR) NOT NULL, + PROCESS_ID VARCHAR2(36 CHAR) NOT NULL, + PRESENCE_CHECK_RESULT VARCHAR2(32 CHAR), + OTP_VERIFICATION_RESULT VARCHAR2(32 CHAR), + SCA_RESULT VARCHAR2(32 CHAR), + TIMESTAMP_CREATED TIMESTAMP(6) NOT NULL, + TIMESTAMP_LAST_UPDATED TIMESTAMP(6), + FOREIGN KEY (IDENTITY_VERIFICATION_ID) REFERENCES ES_IDENTITY_VERIFICATION (ID), + FOREIGN KEY (PROCESS_ID) REFERENCES ES_ONBOARDING_PROCESS (ID) +); + +CREATE INDEX IDENTITY_VERIFICATION_ID ON ES_SCA_RESULT (IDENTITY_VERIFICATION_ID); +CREATE INDEX PROCESS_ID ON ES_SCA_RESULT (PROCESS_ID); + -- Scheduler lock table - https://github.com/lukas-krecan/ShedLock#configure-lockprovider BEGIN EXECUTE IMMEDIATE 'CREATE TABLE shedlock ( name VARCHAR2(64 CHAR) NOT NULL, diff --git a/docs/sql/postgresql/onboarding/create-schema.sql b/docs/sql/postgresql/onboarding/create-schema.sql index 30d8fab2..1defbb88 100644 --- a/docs/sql/postgresql/onboarding/create-schema.sql +++ b/docs/sql/postgresql/onboarding/create-schema.sql @@ -33,7 +33,7 @@ CREATE TABLE es_onboarding_process ( error_origin VARCHAR(256), error_score INTEGER NOT NULL DEFAULT 0, custom_data VARCHAR(1024) NOT NULL, - fds_data TEXT NOT NULL, + fds_data TEXT, timestamp_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, timestamp_last_updated TIMESTAMP, timestamp_finished TIMESTAMP, @@ -162,6 +162,25 @@ CREATE TABLE es_document_result ( -- PostgreSQL does not create indexes on foreign keys automatically CREATE INDEX document_verif_result ON es_document_result (document_verification_id); +CREATE SEQUENCE es_sca_result_seq INCREMENT BY 50 START WITH 1; + +CREATE TABLE es_sca_result +( + id BIGINT NOT NULL PRIMARY KEY, + identity_verification_id VARCHAR(36) NOT NULL, + process_id VARCHAR(36) NOT NULL, + presence_check_result VARCHAR(32), + otp_verification_result VARCHAR(32), + sca_result VARCHAR(32), + timestamp_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + timestamp_last_updated TIMESTAMP, + FOREIGN KEY (identity_verification_id) REFERENCES es_identity_verification (id), + FOREIGN KEY (process_id) REFERENCES es_onboarding_process (id) +); + +CREATE INDEX identity_verification_id ON es_sca_result (identity_verification_id); +CREATE INDEX process_id ON es_sca_result (process_id); + -- Scheduler lock table - https://github.com/lukas-krecan/ShedLock#configure-lockprovider CREATE TABLE IF NOT EXISTS shedlock ( name VARCHAR(64) NOT NULL, diff --git a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStartResponse.java b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStartResponse.java index 475d405c..5053fdce 100644 --- a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStartResponse.java +++ b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStartResponse.java @@ -17,6 +17,7 @@ */ package com.wultra.app.enrollmentserver.api.model.onboarding.response; +import com.wultra.app.enrollmentserver.api.model.onboarding.response.data.ConfigurationDataDto; import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus; import lombok.Data; @@ -30,5 +31,6 @@ public class OnboardingStartResponse { private String processId; private OnboardingStatus onboardingStatus; + private ConfigurationDataDto config; } diff --git a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStatusResponse.java b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStatusResponse.java index f69ffb42..cc9c2f50 100644 --- a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStatusResponse.java +++ b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/OnboardingStatusResponse.java @@ -17,6 +17,7 @@ */ package com.wultra.app.enrollmentserver.api.model.onboarding.response; +import com.wultra.app.enrollmentserver.api.model.onboarding.response.data.ConfigurationDataDto; import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus; import lombok.Data; @@ -30,5 +31,6 @@ public class OnboardingStatusResponse { private String processId; private OnboardingStatus onboardingStatus; + private ConfigurationDataDto config; } diff --git a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/data/ConfigurationDataDto.java b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/data/ConfigurationDataDto.java index 2ea9885a..de22bbb5 100644 --- a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/data/ConfigurationDataDto.java +++ b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/data/ConfigurationDataDto.java @@ -16,6 +16,7 @@ */ package com.wultra.app.enrollmentserver.api.model.onboarding.response.data; +import lombok.Builder; import lombok.Data; /** @@ -24,11 +25,24 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ @Data +@Builder public class ConfigurationDataDto { /** - * OTP resend period (ISO 8601 format) + * OTP resend period (ISO 8601 format). + * + * @deprecated ISO 8601 format does not have native support on mobile platforms, + * leading to complexities in mobile client processing. Use {@link #otpResendPeriodSeconds} + * for a simpler representation in seconds. Scheduled for removal in future versions. + * See https://github.com/wultra/enrollment-server/issues/829 for more details. + * TODO (@jandusil, 2023-08-12, #829) */ + @Deprecated private String otpResendPeriod; + /** + * OTP resend period in seconds for easier parsing on mobile platforms. + */ + private long otpResendPeriodSeconds; + } diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/OnboardingOtpRepository.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/OnboardingOtpRepository.java index 52f956c1..48d1cce2 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/OnboardingOtpRepository.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/OnboardingOtpRepository.java @@ -98,7 +98,4 @@ public interface OnboardingOtpRepository extends CrudRepository. + */ + +package com.wultra.app.onboardingserver.common.database; + +import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; +import com.wultra.app.onboardingserver.common.database.entity.ScaResultEntity; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * Repository for {@link ScaResultEntity}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Repository +public interface ScaResultRepository extends CrudRepository { + + /** + * Count SCA attempts for the given identity verification. + * + * @param identityVerification Identity verification to identify SCA attempts. + * @return count of SCA attempts + */ + int countByIdentityVerification(IdentityVerificationEntity identityVerification); + + /** + * Find the latest SCA attempt. + * + * @param identityVerification Identity verification to identify SCA attempts. + * @return the latest SCA attempt or empty + */ + Optional findTopByIdentityVerificationOrderByTimestampCreatedDesc(IdentityVerificationEntity identityVerification); +} diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentDataEntity.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentDataEntity.java index 539e815a..f43db040 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentDataEntity.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentDataEntity.java @@ -24,6 +24,7 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.UuidGenerator; import java.io.Serial; import java.io.Serializable; @@ -47,8 +48,8 @@ public class DocumentDataEntity implements Serializable { private static final long serialVersionUID = 7685715667785423079L; @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid2") + @GeneratedValue + @UuidGenerator @Column(name = "id", nullable = false) private String id; diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentVerificationEntity.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentVerificationEntity.java index 22358a5b..38ea3173 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentVerificationEntity.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/DocumentVerificationEntity.java @@ -25,6 +25,7 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.UuidGenerator; import java.io.Serial; import java.io.Serializable; @@ -53,8 +54,8 @@ public class DocumentVerificationEntity implements Serializable { * Autogenerated identifier */ @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid2") + @GeneratedValue + @UuidGenerator @Column(name = "id", nullable = false) private String id; diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/IdentityVerificationEntity.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/IdentityVerificationEntity.java index d742717c..c6bb5c5c 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/IdentityVerificationEntity.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/IdentityVerificationEntity.java @@ -28,6 +28,7 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.UuidGenerator; import java.io.Serial; import java.io.Serializable; @@ -60,8 +61,8 @@ public class IdentityVerificationEntity implements Serializable { public static final String CLIENT_EVALUATION_FAILED = "clientEvaluationFailed"; @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid2") + @GeneratedValue + @UuidGenerator @Column(name = "id", nullable = false) private String id; diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingOtpEntity.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingOtpEntity.java index f58c9219..3b4c9783 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingOtpEntity.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingOtpEntity.java @@ -27,6 +27,7 @@ import lombok.Setter; import lombok.ToString; import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.UuidGenerator; import java.io.Serial; import java.io.Serializable; @@ -55,8 +56,8 @@ public class OnboardingOtpEntity implements Serializable { public static final String ERROR_MAX_FAILED_ATTEMPTS = "maxFailedAttemptsOtp"; @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid2") + @GeneratedValue + @UuidGenerator @Column(name = "id", nullable = false) private String id; diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingProcessEntity.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingProcessEntity.java index 8dc6eec1..ca38c0d2 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingProcessEntity.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/OnboardingProcessEntity.java @@ -25,7 +25,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; -import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.UuidGenerator; import java.io.Serial; import java.io.Serializable; @@ -60,8 +60,8 @@ public class OnboardingProcessEntity implements Serializable { public static final String ERROR_USER_LOOKUP = "userLookupFailed"; @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid2") + @GeneratedValue + @UuidGenerator @Column(name = "id", nullable = false) private String id; diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/ScaResultEntity.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/ScaResultEntity.java new file mode 100644 index 00000000..c0452fde --- /dev/null +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/entity/ScaResultEntity.java @@ -0,0 +1,105 @@ +/* + * 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 . + */ + +package com.wultra.app.onboardingserver.common.database.entity; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * SCA (Strong Customer Authentication) consists of presence check and OTP verification. + * This entity stores particular and also aggregated results. + * One SCA attempt is represented by a single entry. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Getter +@Setter +@Entity +@Table(name = "es_sca_result") +@ToString +@EqualsAndHashCode(of = "id") +public class ScaResultEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Autogenerated identifier + */ + @Id + @SequenceGenerator(name = "es_sca_result", sequenceName = "es_sca_result_seq") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "es_sca_result") + private Long id; + + /** + * Identifier of the related identity verification entity. + */ + @ManyToOne + @JoinColumn(name = "identity_verification_id", referencedColumnName = "id", nullable = false) + private IdentityVerificationEntity identityVerification; + + @Column(name = "process_id", nullable = false) + private String processId; + + /** + * Result of presence check. + */ + @Enumerated(EnumType.STRING) + @Column(name = "presence_check_result") + private Result presenceCheckResult; + + /** + * Result of the last OTP verification. + */ + @Enumerated(EnumType.STRING) + @Column(name = "otp_verification_result") + private Result otpVerificationResult; + + /** + * Aggregated result of {@link #presenceCheckResult} and {@link #otpVerificationResult}. + */ + @Enumerated(EnumType.STRING) + @Column(name = "sca_result") + private Result scaResult; + + /** + * Timestamp when the entity was created. + */ + @Column(name = "timestamp_created", nullable = false) + private Date timestampCreated; + + /** + * Timestamp when the entity was last updated. + */ + @Column(name = "timestamp_last_updated") + private Date timestampLastUpdated; + + public enum Result { + SUCCESS, + FAILED + } + +} diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 1ef9043e..d46c044d 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -146,12 +146,6 @@ test - - org.apache.tomcat.embed - tomcat-embed-el - test - - org.springdoc diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java index a6acc6bc..094de983 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java @@ -27,14 +27,14 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; -import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope; 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.annotation.PowerAuthToken; import io.getlime.security.powerauth.rest.api.spring.authentication.PowerAuthApiAuthentication; -import io.getlime.security.powerauth.rest.api.spring.encryption.EciesEncryptionContext; +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; @@ -118,7 +118,7 @@ public ObjectResponse checkIdentityVerificat /** * Submit identity-related documents for verification. * @param request Document submit request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Document submit response. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. * @throws PowerAuthEncryptionException Thrown when request decryption fails. @@ -130,22 +130,22 @@ public ObjectResponse checkIdentityVerificat * @throws OnboardingProcessLimitException Thrown when maximum failed attempts for identity verification have been reached. */ @PostMapping("document/submit") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) @PowerAuthToken(signatureType = { PowerAuthSignatureTypes.POSSESSION }) public Response submitDocuments(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext, + @Parameter(hidden = true) EncryptionContext encryptionContext, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException, PowerAuthEncryptionException, DocumentSubmitException, OnboardingProcessException, IdentityVerificationLimitException, RemoteCommunicationException, IdentityVerificationException, OnboardingProcessLimitException { - return identityVerificationRestService.submitDocuments(request, eciesContext, apiAuthentication); + return identityVerificationRestService.submitDocuments(request, encryptionContext, apiAuthentication); } /** * Upload a single document related to identity verification. This endpoint is used for upload of large documents. * @param requestData Binary request data. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Document upload response. * @throws IdentityVerificationException Thrown when identity verification was not found. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. @@ -154,16 +154,16 @@ public Response submitDocuments(@EncryptedRequestBody ObjectRequest uploadDocument(@EncryptedRequestBody byte[] requestData, - @Parameter(hidden = true) EciesEncryptionContext eciesContext, + @Parameter(hidden = true) EncryptionContext encryptionContext, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, DocumentVerificationException, OnboardingProcessException { - return identityVerificationRestService.uploadDocument(requestData, eciesContext, apiAuthentication); + return identityVerificationRestService.uploadDocument(requestData, encryptionContext, apiAuthentication); } /** @@ -189,7 +189,7 @@ public ObjectResponse checkDocumentStatus(@RequestBody O /** * Initialize document verification SDK for an integration. * @param request Presence check initialization request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param apiAuthentication PowerAuth authentication. * @return Verification SDK initialization response. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. @@ -199,24 +199,24 @@ public ObjectResponse checkDocumentStatus(@RequestBody O * @throws RemoteCommunicationException In case of remote communication error. */ @PostMapping("document/init-sdk") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) @PowerAuth(resourceId = "/api/identity/document/init-sdk", signatureType = { PowerAuthSignatureTypes.POSSESSION }) public ObjectResponse initVerificationSdk( @EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext, + @Parameter(hidden = true) EncryptionContext encryptionContext, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException, DocumentVerificationException, PowerAuthEncryptionException, OnboardingProcessException, RemoteCommunicationException { - return identityVerificationRestService.initVerificationSdk(request, eciesContext, apiAuthentication); + return identityVerificationRestService.initVerificationSdk(request, encryptionContext, apiAuthentication); } /** * Initialize presence check process. * * @param request Presence check initialization request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param apiAuthentication PowerAuth authentication. * @return Presence check initialization response. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. @@ -225,16 +225,16 @@ public ObjectResponse initVerificationSdk( * @throws OnboardingProcessException Thrown when onboarding process is invalid. */ @PostMapping("presence-check/init") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) @PowerAuth(resourceId = "/api/identity/presence-check/init", signatureType = { PowerAuthSignatureTypes.POSSESSION }) public ResponseEntity initPresenceCheck(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext, + @Parameter(hidden = true) EncryptionContext encryptionContext, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, OnboardingProcessException { - return identityVerificationRestService.initPresenceCheck(request, eciesContext, apiAuthentication); + return identityVerificationRestService.initPresenceCheck(request, encryptionContext, apiAuthentication); } /** @@ -278,18 +278,18 @@ public ResponseEntity resendOtp( /** * Verify an OTP code received from the user. * @param request Presence check initialization request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Send OTP response. * @throws PowerAuthEncryptionException Thrown when request decryption fails. * @throws OnboardingProcessException Thrown when onboarding process is not found. */ @PostMapping("otp/verify") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public ObjectResponse verifyOtp(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext) + @Parameter(hidden = true) EncryptionContext encryptionContext) throws PowerAuthEncryptionException, OnboardingProcessException { - return identityVerificationRestService.verifyOtp(request, eciesContext); + return identityVerificationRestService.verifyOtp(request, encryptionContext); } /** diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingController.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingController.java index a8014296..08e29d83 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingController.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingController.java @@ -33,10 +33,10 @@ import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; -import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope; import io.getlime.security.powerauth.rest.api.spring.annotation.EncryptedRequestBody; import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuthEncryption; -import io.getlime.security.powerauth.rest.api.spring.encryption.EciesEncryptionContext; +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.PowerAuthEncryptionException; import io.swagger.v3.oas.annotations.Parameter; import jakarta.servlet.http.HttpServletRequest; @@ -78,7 +78,7 @@ public OnboardingController(final OnboardingServiceImpl onboardingService) { * Start an onboarding process. * * @param request Start onboarding process request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param servletRequest HttpServletRequest. * @return Start onboarding process response. * @throws PowerAuthEncryptionException Thrown when request is invalid. @@ -88,14 +88,14 @@ public OnboardingController(final OnboardingServiceImpl onboardingService) { * @throws InvalidRequestObjectException Thrown in case request is invalid. */ @PostMapping("start") - @PowerAuthEncryption(scope = EciesScope.APPLICATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.APPLICATION_SCOPE) public ObjectResponse startOnboarding( @EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext, + @Parameter(hidden = true) EncryptionContext encryptionContext, final HttpServletRequest servletRequest) throws OnboardingProcessException, OnboardingOtpDeliveryException, PowerAuthEncryptionException, TooManyProcessesException, InvalidRequestObjectException { // Check if the request was correctly decrypted - if (eciesContext == null) { + if (encryptionContext == null) { throw new PowerAuthEncryptionException("ECIES decryption failed during onboarding"); } @@ -113,18 +113,18 @@ public ObjectResponse startOnboarding( * Resend an onboarding OTP code. * * @param request Resend an OTP code request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Response. * @throws PowerAuthEncryptionException Thrown when request decryption fails. * @throws OnboardingProcessException Thrown when onboarding process fails. * @throws OnboardingOtpDeliveryException Thrown when onboarding OTP delivery fails. */ @PostMapping("otp/resend") - @PowerAuthEncryption(scope = EciesScope.APPLICATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.APPLICATION_SCOPE) public Response resendOtp(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext) throws PowerAuthEncryptionException, OnboardingProcessException, OnboardingOtpDeliveryException { + @Parameter(hidden = true) EncryptionContext encryptionContext) throws PowerAuthEncryptionException, OnboardingProcessException, OnboardingOtpDeliveryException { // Check if the request was correctly decrypted - if (eciesContext == null) { + if (encryptionContext == null) { throw new PowerAuthEncryptionException("ECIES decryption failed while resending OTP code"); } @@ -139,17 +139,17 @@ public Response resendOtp(@EncryptedRequestBody ObjectRequest getStatus(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext) throws PowerAuthEncryptionException, OnboardingProcessException { + @Parameter(hidden = true) EncryptionContext encryptionContext) throws PowerAuthEncryptionException, OnboardingProcessException { // Check if the request was correctly decrypted - if (eciesContext == null) { + if (encryptionContext == null) { throw new PowerAuthEncryptionException("ECIES decryption failed while getting status"); } @@ -166,17 +166,17 @@ public ObjectResponse getStatus(@EncryptedRequestBody * Perform cleanup related to an onboarding process. * * @param request Onboarding cleanup request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Onboarding cleanup response. * @throws PowerAuthEncryptionException Thrown when request decryption fails. * @throws OnboardingProcessException Thrown when onboarding process is not found. */ @PostMapping("cleanup") - @PowerAuthEncryption(scope = EciesScope.APPLICATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.APPLICATION_SCOPE) public Response performCleanup(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext) throws PowerAuthEncryptionException, OnboardingProcessException { + @Parameter(hidden = true) EncryptionContext encryptionContext) throws PowerAuthEncryptionException, OnboardingProcessException { // Check if the request was correctly decrypted - if (eciesContext == null) { + if (encryptionContext == null) { throw new PowerAuthEncryptionException("ECIES decryption failed during cleanup"); } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingOtpController.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingOtpController.java index e3d71be3..826ba824 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingOtpController.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/OnboardingOtpController.java @@ -28,10 +28,10 @@ import com.wultra.app.onboardingserver.mock.model.response.OtpDetailResponse; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; -import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope; import io.getlime.security.powerauth.rest.api.spring.annotation.EncryptedRequestBody; import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuthEncryption; -import io.getlime.security.powerauth.rest.api.spring.encryption.EciesEncryptionContext; +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.PowerAuthEncryptionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,16 +64,16 @@ public OnboardingOtpController(OnboardingOtpRepository onboardingOtpRepository) * Get onboarding OTP detail for tests. * * @param request OTP detail request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return OTP detail response. * @throws PowerAuthEncryptionException In case encryption or decryption fails. */ @RequestMapping(value = "otp/detail", method = RequestMethod.POST) - @PowerAuthEncryption(scope = EciesScope.APPLICATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.APPLICATION_SCOPE) public ObjectResponse getOtpDetail(@EncryptedRequestBody ObjectRequest request, - EciesEncryptionContext eciesContext) throws PowerAuthEncryptionException, OnboardingProcessException { + EncryptionContext encryptionContext) throws PowerAuthEncryptionException, OnboardingProcessException { - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationFinishService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationFinishService.java index 2136028e..020a4780 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationFinishService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationFinishService.java @@ -123,7 +123,12 @@ private void sendFinishedEvent(final OnboardingProcessEntity process, final Iden try { logger.info("Publishing finish event, {}", ownerId); final ProcessEventResponse response = onboardingProvider.processEvent(request); - logger.info("Finish event published: {}, {}", response, ownerId); + logger.debug("Got {} for processId={}", response, request.getProcessId()); + if (response.isErrorOccurred()) { + logger.info("Finish event failed to published: {}, {}", response.getErrorDetail(), ownerId); + } else { + logger.info("Finish event published, {}", ownerId); + } } catch (OnboardingProviderException e) { // unsuccessful event publishing does not stop the process logger.info("Unable to publish finished event to the onboarding adapter: {}", e.getMessage()); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationOtpService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationOtpService.java index 16e6d24a..a4b48ac3 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationOtpService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationOtpService.java @@ -17,17 +17,14 @@ */ package com.wultra.app.onboardingserver.impl.service; -import com.fasterxml.jackson.databind.ObjectMapper; 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.database.entity.OnboardingProcessEntityWrapper; +import com.wultra.app.onboardingserver.common.database.ScaResultRepository; +import com.wultra.app.onboardingserver.common.database.entity.*; import com.wultra.app.onboardingserver.common.enumeration.OnboardingProcessError; import com.wultra.app.onboardingserver.common.errorhandling.OnboardingProcessException; import com.wultra.app.onboardingserver.common.service.AuditService; @@ -37,8 +34,8 @@ 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.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -56,67 +53,27 @@ */ @Service @Slf4j +@AllArgsConstructor public class IdentityVerificationOtpService { private final OnboardingProcessRepository onboardingProcessRepository; private final OnboardingOtpRepository onboardingOtpRepository; private final OtpServiceImpl otpService; - private OnboardingProvider onboardingProvider; + private final OnboardingProvider onboardingProvider; private final OnboardingProcessLimitService processLimitService; private final IdentityVerificationRepository identityVerificationRepository; + private final ScaResultRepository scaResultRepository; + private final IdentityVerificationConfig identityVerificationConfig; private final IdentityVerificationService identityVerificationService; private final AuditService auditService; - private final ObjectMapper mapper = new ObjectMapper(); - - /** - * Service constructor. - * - * @param onboardingProcessRepository Onboarding process repository. - * @param onboardingOtpRepository Onboarding OTP repository. - * @param otpService OTP service. - * @param processLimitService Process limit service. - * @param identityVerificationRepository Identity verification repository. - * @param identityVerificationConfig Identity verification config. - * @param identityVerificationService Identity verification service. - * @param auditService Audit service. - */ - public IdentityVerificationOtpService( - final OnboardingProcessRepository onboardingProcessRepository, - final OnboardingOtpRepository onboardingOtpRepository, - final OtpServiceImpl otpService, - final OnboardingProcessLimitService processLimitService, - final IdentityVerificationRepository identityVerificationRepository, - final IdentityVerificationConfig identityVerificationConfig, - final IdentityVerificationService identityVerificationService, - final AuditService auditService) { - - this.onboardingProcessRepository = onboardingProcessRepository; - this.onboardingOtpRepository = onboardingOtpRepository; - this.otpService = otpService; - this.processLimitService = processLimitService; - this.identityVerificationRepository = identityVerificationRepository; - this.identityVerificationConfig = identityVerificationConfig; - this.identityVerificationService = identityVerificationService; - this.auditService = auditService; - } - - /** - * Set onboarding provider via setter injection. - * @param onboardingProvider Onboarding provider. - */ - @Autowired(required = false) - public void setOnboardingProvider(OnboardingProvider onboardingProvider) { - this.onboardingProvider = onboardingProvider; - } - /** * Resends an OTP code for a process during identity verification. * @param identityVerification Identity verification entity. @@ -165,7 +122,10 @@ public OtpVerifyResponse verifyOtpCode(String processId, OwnerId ownerId, String } if (!response.isVerified()) { logger.info("SCA failed, wrong OTP code, process ID: {}", processId); + saveScaOtpResult(ScaResultEntity.Result.FAILED, process, ownerId); return response; + } else { + saveScaOtpResult(ScaResultEntity.Result.SUCCESS, process, ownerId); } return verifyPresenceCheck(process, response, ownerId); } @@ -182,6 +142,16 @@ public boolean isUserVerifiedUsingOtp(String processId) { .isPresent(); } + private void saveScaOtpResult(final ScaResultEntity.Result otpResult, final OnboardingProcessEntity process, final OwnerId ownerId) throws OnboardingProcessException { + final IdentityVerificationEntity identityVerification = getIdentityVerificationEntity(process); + logger.debug("Saving SCA otp verification result: {}, identity verification ID: {}, {}", otpResult, identityVerification.getId(), ownerId); + final ScaResultEntity scaResult = scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(identityVerification).orElseThrow(() -> + new OnboardingProcessException("No ScaResult found, process ID: %s, identity verification ID: %s".formatted(process.getId(), identityVerification.getId()))); + scaResult.setOtpVerificationResult(otpResult); + scaResult.setTimestampLastUpdated(new Date()); + scaResultRepository.save(scaResult); + } + private OtpVerifyResponse verifyPresenceCheck(final OnboardingProcessEntity process, final OtpVerifyResponse response, final OwnerId ownerId) throws OnboardingProcessException { final String processId = process.getId(); if (!identityVerificationConfig.isPresenceCheckEnabled()) { @@ -193,17 +163,21 @@ private OtpVerifyResponse verifyPresenceCheck(final OnboardingProcessEntity proc final IdentityVerificationEntity idVerification = getIdentityVerificationEntity(process); final String errorDetail = idVerification.getErrorDetail(); - final ErrorOrigin errorOrigin = idVerification.getErrorOrigin(); final String rejectReason = idVerification.getRejectReason(); - final RejectOrigin rejectOrigin = idVerification.getRejectOrigin(); - // TODO (racansky, 2022-10-18, #453) this is quite fragile condition, could be improved e.g. with data historization - if (errorOrigin == ErrorOrigin.PRESENCE_CHECK || rejectOrigin == RejectOrigin.PRESENCE_CHECK) { + final ScaResultEntity scaResult = scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(idVerification).orElseThrow(() -> + new OnboardingProcessException("No ScaResult found, process ID: %s, identity verification ID: %s".formatted(process.getId(), idVerification.getId()))); + + if (scaResult.getPresenceCheckResult() != ScaResultEntity.Result.SUCCESS) { logger.info("SCA failed, identity verification ID: {}, {} contains errorDetail: {}, rejectReason: {} from previous step", idVerification.getId(), ownerId, errorDetail, rejectReason); + scaResult.setScaResult(ScaResultEntity.Result.FAILED); + scaResult.setTimestampLastUpdated(new Date()); return moveToPhasePresenceCheck(process, response, idVerification, ownerId); } else { logger.debug("PRESENCE_CHECK without error or reject origin, {}", ownerId); + scaResult.setScaResult(ScaResultEntity.Result.SUCCESS); + scaResult.setTimestampLastUpdated(new Date()); } return response; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheck.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheck.java index 6adaaab8..6f774321 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheck.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheck.java @@ -20,13 +20,16 @@ import com.wultra.app.enrollmentserver.model.enumeration.*; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; import com.wultra.app.onboardingserver.common.database.OnboardingOtpRepository; +import com.wultra.app.onboardingserver.common.database.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; 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.ScaResultEntity; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.statemachine.guard.document.RequiredDocumentTypesCheck; import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -49,6 +52,7 @@ // TODO (racansky, 2022-10-14) consider make it Guard for Spring State Machine @Component @Slf4j +@AllArgsConstructor class IdentityVerificationPrecompleteCheck { private final IdentityVerificationConfig identityVerificationConfig; @@ -57,23 +61,12 @@ class IdentityVerificationPrecompleteCheck { private final OnboardingOtpRepository onboardingOtpRepository; + private final ScaResultRepository scaResultRepository; + private final DocumentVerificationRepository documentVerificationRepository; private final ActivationService activationService; - IdentityVerificationPrecompleteCheck( - final IdentityVerificationConfig identityVerificationConfig, - final RequiredDocumentTypesCheck requiredDocumentTypesCheck, - final OnboardingOtpRepository onboardingOtpRepository, - final DocumentVerificationRepository documentVerificationRepository, - final ActivationService activationService) { - this.identityVerificationConfig = identityVerificationConfig; - this.requiredDocumentTypesCheck = requiredDocumentTypesCheck; - this.onboardingOtpRepository = onboardingOtpRepository; - this.documentVerificationRepository = documentVerificationRepository; - this.activationService = activationService; - } - /** * Evaluate all precomplete conditions. * @@ -85,44 +78,55 @@ Result evaluate(final IdentityVerificationEntity idVerification) throws RemoteCo .findAllDocumentVerifications(idVerification, DocumentStatus.ALL_PROCESSED); final String processId = idVerification.getProcessId(); + final String identityVerificationId = idVerification.getId(); if (!documentVerifications.stream() .map(DocumentVerificationEntity::getStatus) .allMatch(it -> it == DocumentStatus.ACCEPTED)) { - logger.debug("Some documents are not accepted for identity verification ID: {}", processId); + logger.debug("Some documents are not accepted for identity verification ID: {}, process ID: {}", identityVerificationId, processId); return Result.failed("Some documents not accepted"); } - if (!requiredDocumentTypesCheck.evaluate(documentVerifications, idVerification.getId())) { - logger.debug("Not all required documents are present for verification ID: {}", processId); + if (!requiredDocumentTypesCheck.evaluate(documentVerifications, identityVerificationId)) { + logger.debug("Not all required documents are present for verification ID: {}, process ID: {}", identityVerificationId, processId); return Result.failed("Required documents not present"); } if (!isPrecompletePhaseAndStateValid(idVerification)) { - logger.debug("Not valid phase and state for verification ID: {}", processId); + logger.debug("Not valid phase and state for verification ID: {}, process ID: {}", identityVerificationId, processId); return Result.failed("Not valid phase and state"); } if (!isVerificationOtpValid(idVerification)) { - logger.debug("Not valid user verification OTP for verification ID: {}", processId); + logger.debug("Not valid user verification OTP for verification ID: {}, process ID: {}", identityVerificationId, processId); return Result.failed("Not valid user verification OTP"); } if (!isActivationOtpValid(idVerification)) { - logger.debug("Not valid activation OTP for verification ID: {}", processId); + logger.debug("Not valid activation OTP for verification ID: {}, process ID:{}", identityVerificationId, processId); return Result.failed("Not valid activation OTP"); } if (!isActivationValid(idVerification)) { - logger.debug("Activation is not valid for verification ID: {}", processId); + logger.debug("Activation is not valid for verification ID: {}, process ID: {}", identityVerificationId, processId); return Result.failed("Activation is not valid"); } - // TODO (racansky, 2022-10-18, #453) validate presence check when the data available + if (!isVerificationPassedSca(idVerification)) { + logger.debug("Did not pass SCA for verification ID: {}, process ID: {}", identityVerificationId, processId); + return Result.failed("Did not pass SCA"); + } return Result.successful(); } + private boolean isVerificationPassedSca(final IdentityVerificationEntity idVerification) { + return scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(idVerification) + .map(ScaResultEntity::getScaResult) + .filter(it -> it == ScaResultEntity.Result.SUCCESS) + .isPresent(); + } + private boolean isActivationValid(IdentityVerificationEntity idVerification) throws RemoteCommunicationException { final ActivationStatus activationStatus = activationService.fetchActivationStatus(idVerification.getActivationId()); return activationStatus == ActivationStatus.ACTIVE; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java index 6a5f3b0e..2276b5d4 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java @@ -45,7 +45,7 @@ import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; import io.getlime.security.powerauth.rest.api.spring.authentication.PowerAuthApiAuthentication; -import io.getlime.security.powerauth.rest.api.spring.encryption.EciesEncryptionContext; +import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionContext; 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; @@ -125,8 +125,10 @@ public IdentityVerificationRestService( this.presenceCheckService = presenceCheckService; this.stateMachineService = stateMachineService; - this.integrationConfigDto = new ConfigurationDataDto(); - integrationConfigDto.setOtpResendPeriod(onboardingConfig.getOtpResendPeriod().toString()); + this.integrationConfigDto = ConfigurationDataDto.builder() + .otpResendPeriod(onboardingConfig.getOtpResendPeriod().toString()) + .otpResendPeriodSeconds(onboardingConfig.getOtpResendPeriod().toSeconds()) + .build(); } /** @@ -194,7 +196,7 @@ public ObjectResponse checkIdentityVerificat /** * Submit identity-related documents for verification. * @param request Document submit request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Document submit response. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. * @throws PowerAuthEncryptionException Thrown when request decryption fails. @@ -207,17 +209,17 @@ public ObjectResponse checkIdentityVerificat */ @Transactional public Response submitDocuments(ObjectRequest request, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException, PowerAuthEncryptionException, DocumentSubmitException, OnboardingProcessException, IdentityVerificationLimitException, RemoteCommunicationException, IdentityVerificationException, OnboardingProcessLimitException { final String operationDescription = "submitting documents for verification"; checkApiAuthentication(apiAuthentication, operationDescription); - checkEciesContext(eciesContext, operationDescription); + checkEncryptionContext(encryptionContext, operationDescription); checkRequestObject(request, operationDescription); // Extract user ID from onboarding process for current activation - final OwnerId ownerId = extractOwnerId(eciesContext); + final OwnerId ownerId = extractOwnerId(encryptionContext); final String processId = request.getRequestObject().getProcessId(); logger.debug("Onboarding process will be locked using PESSIMISTIC_WRITE lock, {}", processId); @@ -231,7 +233,7 @@ public Response submitDocuments(ObjectRequest request, /** * Upload a single document related to identity verification. This endpoint is used for upload of large documents. * @param requestData Binary request data. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Document upload response. * @throws IdentityVerificationException Thrown when identity verification was not found. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. @@ -240,17 +242,17 @@ public Response submitDocuments(ObjectRequest request, * @throws OnboardingProcessException Thrown when finished onboarding process is not found. */ public ObjectResponse uploadDocument(byte[] requestData, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, DocumentVerificationException, OnboardingProcessException { final String operationDescription = "uploading document for verification"; checkApiAuthentication(apiAuthentication, operationDescription); - checkEciesContext(eciesContext, operationDescription); + checkEncryptionContext(encryptionContext, operationDescription); checkRequest(requestData, operationDescription); // Extract user ID from onboarding process for current activation - final OwnerId ownerId = extractOwnerId(eciesContext); + final OwnerId ownerId = extractOwnerId(encryptionContext); logger.debug("Onboarding process will not be locked, {}", ownerId); IdentityVerificationEntity idVerification = identityVerificationService.findBy(ownerId); @@ -294,7 +296,7 @@ public ObjectResponse checkDocumentStatus(ObjectRequest< /** * Initialize document verification SDK for an integration. * @param request Presence check initialization request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param apiAuthentication PowerAuth authentication. * @return Verification SDK initialization response. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. @@ -305,13 +307,13 @@ public ObjectResponse checkDocumentStatus(ObjectRequest< */ public ObjectResponse initVerificationSdk( ObjectRequest request, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException, DocumentVerificationException, PowerAuthEncryptionException, OnboardingProcessException, RemoteCommunicationException { final String operationDescription = "initializing document verification SDK"; checkApiAuthentication(apiAuthentication, operationDescription); - checkEciesContext(eciesContext, operationDescription); + checkEncryptionContext(encryptionContext, operationDescription); checkRequestObject(request, operationDescription); final OwnerId ownerId = PowerAuthUtil.getOwnerId(apiAuthentication); @@ -332,7 +334,7 @@ public ObjectResponse initVerificationSdk( * Initialize presence check process. * * @param request Presence check initialization request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param apiAuthentication PowerAuth authentication. * @return Presence check initialization response. * @throws PowerAuthAuthenticationException Thrown when request authentication fails. @@ -342,13 +344,13 @@ public ObjectResponse initVerificationSdk( */ @Transactional public ResponseEntity initPresenceCheck(ObjectRequest request, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, OnboardingProcessException { final String operationDescription = "initializing presence check"; checkApiAuthentication(apiAuthentication, operationDescription); - checkEciesContext(eciesContext, operationDescription); + checkEncryptionContext(encryptionContext, operationDescription); checkRequestObject(request, operationDescription); final OwnerId ownerId = PowerAuthUtil.getOwnerId(apiAuthentication); @@ -421,21 +423,21 @@ public ResponseEntity resendOtp( /** * Verify an OTP code received from the user. * @param request Presence check initialization request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Send OTP response. * @throws PowerAuthEncryptionException Thrown when request decryption fails. * @throws OnboardingProcessException Thrown when onboarding process is not found. */ @Transactional public ObjectResponse verifyOtp(ObjectRequest request, - EciesEncryptionContext eciesContext) + EncryptionContext encryptionContext) throws PowerAuthEncryptionException, OnboardingProcessException { - checkEciesContext(eciesContext, "verifying OTP during identity verification"); + checkEncryptionContext(encryptionContext, "verifying OTP during identity verification"); checkRequestObject(request, "verifying OTP during identity verification"); // Extract user ID from onboarding process for current activation - final OwnerId ownerId = extractOwnerId(eciesContext); + final OwnerId ownerId = extractOwnerId(encryptionContext); final String processId = request.getRequestObject().getProcessId(); logger.debug("Onboarding process will be locked using PESSIMISTIC_WRITE lock, {}", processId); @@ -565,12 +567,12 @@ private void checkApiAuthentication(@Nullable PowerAuthApiAuthentication apiAuth /** * Checks if the request was correctly decrypted - * @param eciesContext ECIES encryption context + * @param encryptionContext ECIES encryption context * @param description Additional description * @throws PowerAuthEncryptionException When the ECIES encryption context does not exist */ - private void checkEciesContext(@Nullable EciesEncryptionContext eciesContext, String description) throws PowerAuthEncryptionException { - if (eciesContext == null) { + private void checkEncryptionContext(@Nullable EncryptionContext encryptionContext, String description) throws PowerAuthEncryptionException { + if (encryptionContext == null) { throw new PowerAuthEncryptionException("ECIES encryption failed when " + description); } } @@ -588,13 +590,13 @@ private void checkRequestObject(@Nullable ObjectRequest request, String descr } /** - * Extract owner identification from an ECIES context. The onboarding process is not locked. + * Extract owner identification from an Encryption context. The onboarding process is not locked. * - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Owner identification. */ - private OwnerId extractOwnerId(EciesEncryptionContext eciesContext) throws OnboardingProcessException { - return extractOwnerId(eciesContext.getActivationId()); + private OwnerId extractOwnerId(EncryptionContext encryptionContext) throws OnboardingProcessException { + return extractOwnerId(encryptionContext.getActivationId()); } /** diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/OnboardingServiceImpl.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/OnboardingServiceImpl.java index 0e99d11a..e252958c 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/OnboardingServiceImpl.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/OnboardingServiceImpl.java @@ -27,6 +27,7 @@ import com.wultra.app.enrollmentserver.api.model.onboarding.response.OnboardingConsentTextResponse; import com.wultra.app.enrollmentserver.api.model.onboarding.response.OnboardingStartResponse; import com.wultra.app.enrollmentserver.api.model.onboarding.response.OnboardingStatusResponse; +import com.wultra.app.enrollmentserver.api.model.onboarding.response.data.ConfigurationDataDto; import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin; import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus; import com.wultra.app.enrollmentserver.model.enumeration.OtpType; @@ -84,6 +85,11 @@ public class OnboardingServiceImpl extends CommonOnboardingService { private final ActivationService activationService; + /** + * Configuration data for client integration + */ + private final ConfigurationDataDto integrationConfigDto; + // Special instance of ObjectMapper for normalized serialization of identification data private final ObjectMapper normalizedMapper = JsonMapper .builder() @@ -121,6 +127,9 @@ public OnboardingServiceImpl( this.otpService = otpService; this.activationService = activationService; this.onboardingProvider = onboardingProvider; + this.integrationConfigDto = ConfigurationDataDto.builder() + .otpResendPeriod(onboardingConfig.getOtpResendPeriod().toString()) + .otpResendPeriodSeconds(onboardingConfig.getOtpResendPeriod().toSeconds()).build(); } /** @@ -175,6 +184,7 @@ public OnboardingStartResponse startOnboarding( OnboardingStartResponse response = new OnboardingStartResponse(); response.setProcessId(process.getId()); response.setOnboardingStatus(process.getStatus()); + response.setConfig(integrationConfigDto); return response; } @@ -223,6 +233,7 @@ public OnboardingStatusResponse getStatus(OnboardingStatusRequest request) throw } response.setOnboardingStatus(process.getStatus()); + response.setConfig(integrationConfigDto); return response; } @@ -358,8 +369,14 @@ public void approveConsent(final OnboardingConsentApprovalRequest request) throw try { final ApproveConsentResponse response = onboardingProvider.approveConsent(providerRequest); - auditService.auditOnboardingProvider(process, "Approve consent text for user: {}", userId); logger.debug("Got {} for processId={}", response, request.getProcessId()); + if (response.isErrorOccurred()) { + final String errorDetail = response.getErrorDetail(); + auditService.auditOnboardingProvider(process, "Consent text approval failed for user: {}, error: {}", userId, errorDetail); + throw new OnboardingProcessException("Consent text approval failed for process: %s, user: %s, error: %s" + .formatted(process.getId(), userId, errorDetail)); + } + auditService.auditOnboardingProvider(process, "Approve consent text for user: {}", userId); } catch (OnboardingProviderException e) { throw new OnboardingProcessException("An error when approving consent.", e); } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckLimitService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckLimitService.java index 997cca1d..241340b5 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckLimitService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckLimitService.java @@ -22,11 +22,11 @@ import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin; import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase; import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus; -import com.wultra.app.enrollmentserver.model.enumeration.OtpType; 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.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.OnboardingProcessEntity; import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; @@ -35,9 +35,8 @@ import com.wultra.app.onboardingserver.common.service.AuditService; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.errorhandling.PresenceCheckLimitException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.FAILED; @@ -48,14 +47,15 @@ * @author Roman Strobl, roman.strobl@wultra.com */ @Service +@Slf4j +@AllArgsConstructor public class PresenceCheckLimitService { - private static final Logger logger = LoggerFactory.getLogger(PresenceCheckLimitService.class); - private final IdentityVerificationConfig identityVerificationConfig; private final OnboardingOtpRepository otpRepository; private final IdentityVerificationRepository identityVerificationRepository; private final OnboardingProcessRepository onboardingProcessRepository; + private final ScaResultRepository scaResultRepository; private final ActivationFlagService activationFlagService; private final IdentityVerificationService identityVerificationService; @@ -63,35 +63,7 @@ public class PresenceCheckLimitService { private final AuditService auditService; /** - * Service constructor. - * @param identityVerificationConfig Identity verification configuration. - * @param otpRepository Onboarding OTP repository. - * @param identityVerificationRepository Identity verification repository. - * @param onboardingProcessRepository Onboarding process repository. - * @param activationFlagService Activation flag service. - * @param identityVerificationService Identity verification service. - */ - @Autowired - public PresenceCheckLimitService( - final IdentityVerificationConfig identityVerificationConfig, - final OnboardingOtpRepository otpRepository, - final IdentityVerificationRepository identityVerificationRepository, - final OnboardingProcessRepository onboardingProcessRepository, - final ActivationFlagService activationFlagService, - final IdentityVerificationService identityVerificationService, - final AuditService auditService) { - - this.identityVerificationConfig = identityVerificationConfig; - this.otpRepository = otpRepository; - this.identityVerificationRepository = identityVerificationRepository; - this.onboardingProcessRepository = onboardingProcessRepository; - this.activationFlagService = activationFlagService; - this.identityVerificationService = identityVerificationService; - this.auditService = auditService; - } - - /** - * Check limit for maximum number of attempts for presence check and OTP verification. + * Check limit for maximum number of attempts for SCA (presence check and OTP verification). * @param ownerId Owner identification. * @param processId Process identifier. * @throws IdentityVerificationException Thrown when identity verification is invalid. @@ -99,12 +71,15 @@ public PresenceCheckLimitService( * @throws RemoteCommunicationException Thrown when communication with PowerAuth server fails. */ public void checkPresenceCheckMaxAttemptLimit(OwnerId ownerId, String processId) throws IdentityVerificationException, PresenceCheckLimitException, RemoteCommunicationException { - final int otpCount = otpRepository.countByProcessIdAndType(processId, OtpType.USER_VERIFICATION); - // TODO (racansky, 2022-10-20, #453) OTP verification could be turned off, logic should be based on another data - if (otpCount >= identityVerificationConfig.getPresenceCheckMaxFailedAttempts()) { - final IdentityVerificationEntity identityVerification = identityVerificationRepository.findFirstByActivationIdOrderByTimestampCreatedDesc(ownerId.getActivationId()) - .orElseThrow(() -> - new IdentityVerificationException("Identity verification was not found, " + ownerId)); + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findFirstByActivationIdOrderByTimestampCreatedDesc(ownerId.getActivationId()) + .orElseThrow(() -> + new IdentityVerificationException("Identity verification was not found, " + ownerId)); + + final int count = scaResultRepository.countByIdentityVerification(identityVerification); + logger.debug("SCA attempts count so far {}, {}", count, ownerId); + + if (count >= identityVerificationConfig.getPresenceCheckMaxFailedAttempts()) { + if (!identityVerification.getProcessId().equals(processId)) { throw new IdentityVerificationException(String.format("Process identifier mismatch for owner %s: %s", ownerId, processId)); } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java index 33ce46f3..f72931cf 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java @@ -22,8 +22,10 @@ import com.wultra.app.enrollmentserver.model.enumeration.*; import com.wultra.app.enrollmentserver.model.integration.*; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; +import com.wultra.app.onboardingserver.common.database.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; +import com.wultra.app.onboardingserver.common.database.entity.ScaResultEntity; import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; import com.wultra.app.onboardingserver.common.errorhandling.OnboardingProcessLimitException; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; @@ -35,14 +37,15 @@ import com.wultra.app.onboardingserver.impl.service.document.DocumentProcessingService; import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService; import com.wultra.app.onboardingserver.provider.PresenceCheckProvider; +import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; @@ -56,6 +59,7 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ @Service +@AllArgsConstructor public class PresenceCheckService { private static final Logger logger = LoggerFactory.getLogger(PresenceCheckService.class); @@ -72,40 +76,7 @@ public class PresenceCheckService { private final PresenceCheckLimitService presenceCheckLimitService; private final AuditService auditService; private final ImageProcessor imageProcessor; - - /** - * Service constructor. - * @param documentVerificationRepository Document verification repository. - * @param documentProcessingService Document processing service. - * @param identityVerificationService Identity verification service. - * @param jsonSerializationService JSON serialization service. - * @param presenceCheckProvider Presence check provider. - * @param presenceCheckLimitService Presence check limit service. - * @param auditService Audit service. - * @param imageProcessor Image processor. - */ - @Autowired - public PresenceCheckService( - final IdentityVerificationConfig identityVerificationConfig, - final DocumentVerificationRepository documentVerificationRepository, - final DocumentProcessingService documentProcessingService, - final IdentityVerificationService identityVerificationService, - final JsonSerializationService jsonSerializationService, - final PresenceCheckProvider presenceCheckProvider, - final PresenceCheckLimitService presenceCheckLimitService, - final AuditService auditService, - final ImageProcessor imageProcessor) { - - this.identityVerificationConfig = identityVerificationConfig; - this.documentVerificationRepository = documentVerificationRepository; - this.documentProcessingService = documentProcessingService; - this.identityVerificationService = identityVerificationService; - this.jsonSerializationService = jsonSerializationService; - this.presenceCheckProvider = presenceCheckProvider; - this.presenceCheckLimitService = presenceCheckLimitService; - this.auditService = auditService; - this.imageProcessor = imageProcessor; - } + private final ScaResultRepository scaResultRepository; /** * Prepares presence check to not initialized state. @@ -163,15 +134,17 @@ public void checkPresenceVerification( final PresenceCheckResult result = presenceCheckProvider.getResult(ownerId, sessionInfo); auditService.auditPresenceCheckProvider(idVerification, "Got presence check result: {} for user: {}", result.getStatus(), ownerId.getUserId()); + // Evaluate the result if (result.getStatus() != PresenceCheckStatus.ACCEPTED) { logger.info("Not accepted presence check, status: {}, {}", result.getStatus(), ownerId); - evaluatePresenceCheckResult(ownerId, idVerification, result); - return; + } else { + logger.debug("Processing a result of an accepted presence check, {}", ownerId); } - logger.debug("Processing a result of an accepted presence check, {}", ownerId); + // Process the photo irrespective of the result status final Image photo = result.getPhoto(); if (photo == null) { + evaluatePresenceCheckResult(ownerId, idVerification, result); throw new PresenceCheckException("Missing person photo from presence verification, " + ownerId); } logger.debug("Obtained a photo from the result, {}", ownerId); @@ -311,15 +284,18 @@ private void evaluatePresenceCheckResult(OwnerId ownerId, final IdentityVerificationPhase phase = idVerification.getPhase(); switch (result.getStatus()) { - case ACCEPTED -> + case ACCEPTED -> { // The timestampFinished parameter is not set yet, there may be other steps ahead - identityVerificationService.moveToPhaseAndStatus(idVerification, phase, ACCEPTED, ownerId); + saveScaResult(ScaResultEntity.Result.SUCCESS, idVerification, ownerId); + identityVerificationService.moveToPhaseAndStatus(idVerification, phase, ACCEPTED, ownerId); + } case FAILED -> { idVerification.setErrorDetail(IdentityVerificationEntity.PRESENCE_CHECK_REJECTED); idVerification.setErrorOrigin(ErrorOrigin.PRESENCE_CHECK); idVerification.setTimestampFailed(ownerId.getTimestamp()); - identityVerificationService.moveToPhaseAndStatus(idVerification, phase, FAILED, ownerId); logger.warn("Presence check failed, {}, errorDetail: '{}'", ownerId, result.getErrorDetail()); + saveScaResult(ScaResultEntity.Result.FAILED, idVerification, ownerId); + identityVerificationService.moveToPhaseAndStatus(idVerification, phase, FAILED, ownerId); } case IN_PROGRESS -> logger.debug("Presence check still in progress, {}", ownerId); @@ -328,6 +304,7 @@ private void evaluatePresenceCheckResult(OwnerId ownerId, idVerification.setRejectOrigin(RejectOrigin.PRESENCE_CHECK); idVerification.setTimestampFinished(ownerId.getTimestamp()); logger.info("Presence check rejected, {}, rejectReason: '{}'", ownerId, result.getRejectReason()); + saveScaResult(ScaResultEntity.Result.FAILED, idVerification, ownerId); identityVerificationService.moveToPhaseAndStatus(idVerification, phase, REJECTED, ownerId); } default -> @@ -336,6 +313,16 @@ private void evaluatePresenceCheckResult(OwnerId ownerId, } } + private void saveScaResult(final ScaResultEntity.Result presenceCheckResult, final IdentityVerificationEntity identityVerification, final OwnerId ownerId) { + logger.debug("Saving SCA presence check result: {}, identity verification ID: {}, {}", presenceCheckResult, identityVerification.getId(), ownerId); + final ScaResultEntity scaResultEntity = new ScaResultEntity(); + scaResultEntity.setPresenceCheckResult(presenceCheckResult); + scaResultEntity.setIdentityVerification(identityVerification); + scaResultEntity.setProcessId(identityVerification.getProcessId()); + scaResultEntity.setTimestampCreated(new Date()); + scaResultRepository.save(scaResultEntity); + } + /** * Fetches a current identity verification for presence check initialization * @param ownerId Owner identification. diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java index 2710de77..4d0e2b73 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java @@ -410,14 +410,14 @@ private void processDocsSubmitResults(OwnerId ownerId, DocumentVerificationEntit docVerification.setErrorOrigin(ErrorOrigin.DOCUMENT_VERIFICATION); logger.info("Document verification ID: {}, failed: {}, {}", docVerification.getId(), docSubmitResult.getErrorDetail(), ownerId); - auditService.audit(docVerification, "Document verification failed for user: {}", ownerId.getUserId()); + auditService.audit(docVerification, "Document verification failed for user: {}, detail: {}", ownerId.getUserId(), docSubmitResult.getErrorDetail()); } else if (StringUtils.isNotBlank(docSubmitResult.getRejectReason())) { docVerification.setStatus(DocumentStatus.REJECTED); docVerification.setRejectReason(ErrorDetail.DOCUMENT_VERIFICATION_REJECTED); docVerification.setRejectOrigin(RejectOrigin.DOCUMENT_VERIFICATION); logger.info("Document verification ID: {}, rejected: {}, {}", - docVerification.getId(), docSubmitResult.getErrorDetail(), ownerId); - auditService.audit(docVerification, "Document verification rejected for user: {}", ownerId.getUserId()); + docVerification.getId(), docSubmitResult.getRejectReason(), ownerId); + auditService.audit(docVerification, "Document verification rejected for user: {}, reason: {}", ownerId.getUserId(), docSubmitResult.getRejectReason()); } else { docVerification.setPhotoId(docsSubmitResults.getExtractedPhotoId()); docVerification.setProviderName(identityVerificationConfig.getDocumentVerificationProvider()); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java index b0c96b91..7f565556 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java @@ -28,6 +28,7 @@ import io.netty.channel.ChannelOption; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; +import org.apache.commons.lang3.StringUtils; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -55,6 +56,7 @@ import org.springframework.web.util.UriComponentsBuilder; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; +import reactor.netty.transport.ProxyProvider; import java.time.Duration; import java.util.Map; @@ -170,7 +172,7 @@ private static ReactiveOAuth2AccessTokenResponseClient { + final ProxyProvider.Builder proxyBuilder = proxySpec + .type(ProxyProvider.Proxy.HTTP) + .host(restClientConfig.getProxyHost()) + .port(restClientConfig.getProxyPort()); + if (StringUtils.isNotBlank(restClientConfig.getProxyUsername())) { + proxyBuilder.username(restClientConfig.getProxyUsername()); + proxyBuilder.password(s -> restClientConfig.getProxyPassword()); + } + + proxyBuilder.build(); + }); + } + final ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); return WebClient.builder() diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ApproveConsentResponse.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ApproveConsentResponse.java index 052f52e4..80df2d13 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ApproveConsentResponse.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ApproveConsentResponse.java @@ -21,6 +21,7 @@ import com.wultra.app.onboardingserver.provider.OnboardingProvider; import com.wultra.app.onboardingserver.provider.model.request.ApproveConsentRequest; import com.wultra.core.annotations.PublicApi; +import lombok.Getter; import lombok.ToString; /** @@ -30,6 +31,16 @@ */ @PublicApi @ToString +@Getter public final class ApproveConsentResponse { - // empty so far, open to change in the future + + /** + * Whether business logic error occurred during consent approval. + */ + private boolean errorOccurred; + + /** + * Error detail to store within onboarding process. + */ + private String errorDetail; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ProcessEventResponse.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ProcessEventResponse.java index 538948f7..3abce44f 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ProcessEventResponse.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/response/ProcessEventResponse.java @@ -35,5 +35,14 @@ @ToString @PublicApi public final class ProcessEventResponse { - // empty so far + + /** + * Whether business logic error occurred during event processing. + */ + private boolean errorOccurred; + + /** + * Error detail to store within onboarding process. + */ + private String errorDetail; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProviderAutoConfiguration.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProviderAutoConfiguration.java index d2c12107..fc283866 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProviderAutoConfiguration.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProviderAutoConfiguration.java @@ -65,7 +65,7 @@ public RestClient restClient( final RestClientConfiguration config = new RestClientConfiguration(); config.setBaseUrl(url); - config.setConnectionTimeout((int) configuration.getConnectionTimeout().toMillis()); + config.setConnectionTimeout(configuration.getConnectionTimeout()); config.setResponseTimeout(configuration.getResponseTimeout()); config.setHandshakeTimeout(configuration.getHandshakeTimeout()); config.setDefaultHttpHeaders(headers); diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheckTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheckTest.java index 9b56f494..89cfb676 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheckTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationPrecompleteCheckTest.java @@ -22,9 +22,11 @@ import com.wultra.app.enrollmentserver.model.enumeration.OtpType; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; import com.wultra.app.onboardingserver.common.database.OnboardingOtpRepository; +import com.wultra.app.onboardingserver.common.database.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; 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.ScaResultEntity; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.statemachine.guard.document.RequiredDocumentTypesCheck; import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus; @@ -66,11 +68,23 @@ class IdentityVerificationPrecompleteCheckTest { @Mock private ActivationService activationService; + @Mock + private ScaResultRepository scaResultRepository; + @InjectMocks private IdentityVerificationPrecompleteCheck tested; @Test void testProcessDocumentVerificationResult_valid() throws Exception { + final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); + idVerification.setProcessId("process-1"); + idVerification.setActivationId("activation-1"); + idVerification.setPhase(OTP_VERIFICATION); + idVerification.setStatus(VERIFICATION_PENDING); + + final ScaResultEntity scaResult = new ScaResultEntity(); + scaResult.setScaResult(ScaResultEntity.Result.SUCCESS); + when(requiredDocumentTypesCheck.evaluate(any(), any())) .thenReturn(true); when(identityVerificationConfig.isVerificationOtpEnabled()) @@ -82,12 +96,8 @@ void testProcessDocumentVerificationResult_valid() throws Exception { .thenReturn(Optional.of(createOtp())); when(activationService.fetchActivationStatus("activation-1")) .thenReturn(ActivationStatus.ACTIVE); - - final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); - idVerification.setProcessId("process-1"); - idVerification.setActivationId("activation-1"); - idVerification.setPhase(OTP_VERIFICATION); - idVerification.setStatus(VERIFICATION_PENDING); + when(scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(idVerification)) + .thenReturn(Optional.of(scaResult)); final var result = tested.evaluate(idVerification); @@ -137,6 +147,15 @@ void testProcessDocumentVerificationResult_invalidActivationOtp() throws Excepti @Test void testProcessDocumentVerificationResult_validStateWithoutOtp() throws Exception { + final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); + idVerification.setProcessId("process-1"); + idVerification.setPhase(PRESENCE_CHECK); + idVerification.setStatus(ACCEPTED); + idVerification.setActivationId("activation-1"); + + final ScaResultEntity scaResult = new ScaResultEntity(); + scaResult.setScaResult(ScaResultEntity.Result.SUCCESS); + when(requiredDocumentTypesCheck.evaluate(any(), any())) .thenReturn(true); when(activationService.fetchActivationStatus("activation-1")) @@ -144,11 +163,8 @@ void testProcessDocumentVerificationResult_validStateWithoutOtp() throws Excepti when(onboardingOtpRepository.findNewestByProcessIdAndType("process-1", OtpType.ACTIVATION)) .thenReturn(Optional.of(createOtp())); - final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); - idVerification.setProcessId("process-1"); - idVerification.setPhase(PRESENCE_CHECK); - idVerification.setStatus(ACCEPTED); - idVerification.setActivationId("activation-1"); + when(scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(idVerification)) + .thenReturn(Optional.of(scaResult)); final var result = tested.evaluate(idVerification); @@ -157,18 +173,23 @@ void testProcessDocumentVerificationResult_validStateWithoutOtp() throws Excepti @Test void testProcessDocumentVerificationResult_validStateWithoutOtpAndPresenceCheck() throws Exception { + final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); + idVerification.setProcessId("process-1"); + idVerification.setActivationId("activation-1"); + idVerification.setPhase(CLIENT_EVALUATION); + idVerification.setStatus(ACCEPTED); + + final ScaResultEntity scaResult = new ScaResultEntity(); + scaResult.setScaResult(ScaResultEntity.Result.SUCCESS); + when(requiredDocumentTypesCheck.evaluate(any(), any())) .thenReturn(true); when(activationService.fetchActivationStatus("activation-1")) .thenReturn(ActivationStatus.ACTIVE); when(onboardingOtpRepository.findNewestByProcessIdAndType("process-1", OtpType.ACTIVATION)) .thenReturn(Optional.of(createOtp())); - - final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); - idVerification.setProcessId("process-1"); - idVerification.setActivationId("activation-1"); - idVerification.setPhase(CLIENT_EVALUATION); - idVerification.setStatus(ACCEPTED); + when(scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(idVerification)) + .thenReturn(Optional.of(scaResult)); final var result = tested.evaluate(idVerification); @@ -246,6 +267,36 @@ void testProcessDocumentVerificationResult_invalidStatus() throws Exception { assertEquals("Not valid phase and state", result.getErrorDetail()); } + @Test + void testProcessDocumentVerificationResult_invalidSca() throws Exception { + final IdentityVerificationEntity idVerification = new IdentityVerificationEntity(); + idVerification.setProcessId("process-1"); + idVerification.setActivationId("activation-1"); + idVerification.setPhase(OTP_VERIFICATION); + idVerification.setStatus(VERIFICATION_PENDING); + + final ScaResultEntity scaResult = new ScaResultEntity(); + scaResult.setScaResult(ScaResultEntity.Result.FAILED); + + when(requiredDocumentTypesCheck.evaluate(any(), any())) + .thenReturn(true); + when(identityVerificationConfig.isVerificationOtpEnabled()) + .thenReturn(true); + + when(onboardingOtpRepository.findNewestByProcessIdAndType("process-1", OtpType.USER_VERIFICATION)) + .thenReturn(Optional.of(createOtp())); + when(onboardingOtpRepository.findNewestByProcessIdAndType("process-1", OtpType.ACTIVATION)) + .thenReturn(Optional.of(createOtp())); + when(activationService.fetchActivationStatus("activation-1")) + .thenReturn(ActivationStatus.ACTIVE); + when(scaResultRepository.findTopByIdentityVerificationOrderByTimestampCreatedDesc(idVerification)) + .thenReturn(Optional.of(scaResult)); + + final var result = tested.evaluate(idVerification); + + assertFalse(result.isSuccessful()); + } + private static OnboardingOtpEntity createOtp() { final OnboardingOtpEntity otp = new OnboardingOtpEntity(); otp.setStatus(OtpStatus.VERIFIED); diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index d711c9bb..7df6e872 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -142,12 +142,6 @@ test - - org.apache.tomcat.embed - tomcat-embed-el - test - - org.springdoc diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/ActivationCodeController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/ActivationCodeController.java index 4c6a41b0..9c12aeec 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/ActivationCodeController.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/ActivationCodeController.java @@ -24,13 +24,13 @@ import com.wultra.app.enrollmentserver.impl.service.ActivationCodeService; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; -import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope; 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.EciesEncryptionContext; +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.swagger.v3.oas.annotations.Parameter; @@ -73,7 +73,7 @@ public ActivationCodeController(ActivationCodeService activationCodeService) { * Controller request handler for requesting the activation code. * * @param request Request with activation OTP. - * @param eciesContext ECIES encryption context. + * @param encryptionContext ECIES encryption context. * @param apiAuthentication Authentication object with user and app details. * @return New activation code, activation code signature and activation ID. * @throws PowerAuthAuthenticationException In case user authentication fails. @@ -82,13 +82,13 @@ public ActivationCodeController(ActivationCodeService activationCodeService) { * @throws ActivationCodeException In case fetching the activation code fails. */ @PostMapping("code") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) @PowerAuth(resourceId = "/api/activation/code", signatureType = { PowerAuthSignatureTypes.POSSESSION_BIOMETRY, PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE }) public ObjectResponse requestActivationCode(@EncryptedRequestBody ObjectRequest request, - @Parameter(hidden = true) EciesEncryptionContext eciesContext, + @Parameter(hidden = true) EncryptionContext encryptionContext, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws PowerAuthAuthenticationException, InvalidRequestObjectException, ActivationCodeException, PowerAuthEncryptionException { // Check if the authentication object is present if (apiAuthentication == null) { @@ -97,7 +97,7 @@ public ObjectResponse requestActivationCode(@EncryptedRe } // Check if the request was correctly decrypted - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("ECIES encryption failed when fetching activation code"); throw new PowerAuthEncryptionException("ECIES decryption failed when fetching activation code"); } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java index 518efff3..35358964 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java @@ -205,7 +205,7 @@ public Response operationApprove( .signatureFactors(signatureFactors) .requestContext(requestContext) .activationFlags(activationFlags) - .proximityCheckOtp(requestObject.getProximityCheckOtp()) + .proximityCheckOtp(fetchProximityCheckOtp(requestObject)) .build(); return mobileTokenService.operationApprove(serviceRequest); @@ -221,6 +221,15 @@ public Response operationApprove( } } + private static String fetchProximityCheckOtp(OperationApproveRequest requestObject) { + if (requestObject.getProximityCheck().isEmpty()) { + return null; + } + final var proximityCheck = requestObject.getProximityCheck().get(); + logger.info("Operation ID: {} using proximity check OTP, timestampRequested: {}, timestampSigned: {}", requestObject.getId(), proximityCheck.getTimestampRequested(), proximityCheck.getTimestampSigned()); + return proximityCheck.getOtp(); + } + /** * Operation reject. * diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/mock/controller/v3/EncryptedDataExchangeController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/mock/controller/v3/EncryptedDataExchangeController.java index 03322c2b..9f827825 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/mock/controller/v3/EncryptedDataExchangeController.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/mock/controller/v3/EncryptedDataExchangeController.java @@ -23,17 +23,16 @@ import com.wultra.app.enrollmentserver.mock.model.response.DataExchangeResponse; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; -import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope; 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.EciesEncryptionContext; +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.PowerAuthSignatureInvalidException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -50,59 +49,58 @@ */ @RestController("encryptedDataExchangeControllerV3") @RequestMapping(value = "/exchange") +@Slf4j public class EncryptedDataExchangeController { - private final static Logger logger = LoggerFactory.getLogger(EncryptedDataExchangeController.class); - /** * Sample encrypted data exchange in application scope. * * @param request Data exchange request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Data exchange response. * @throws PowerAuthEncryptionException In case encryption or decryption fails. */ @RequestMapping(value = "v3/application", method = RequestMethod.POST) - @PowerAuthEncryption(scope = EciesScope.APPLICATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.APPLICATION_SCOPE) public DataExchangeResponse exchangeInApplicationScope(@EncryptedRequestBody DataExchangeRequest request, - EciesEncryptionContext eciesContext) throws PowerAuthEncryptionException { + EncryptionContext encryptionContext) throws PowerAuthEncryptionException { - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } // Return a slightly different String containing original data in response - return new DataExchangeResponse("Server successfully decrypted signed data: " + (request == null ? "''" : request.getData()) + ", scope: " + eciesContext.getEciesScope()); + return new DataExchangeResponse("Server successfully decrypted signed data: " + (request == null ? "''" : request.getData()) + ", scope: " + encryptionContext.getEncryptionScope()); } /** * Sample encrypted data exchange in activation scope. * * @param request Data exchange request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @return Data exchange response. * @throws PowerAuthEncryptionException In case encryption or decryption fails. */ @RequestMapping(value = "v3/activation", method = RequestMethod.POST) - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public DataExchangeResponse exchangeInActivationScope(@EncryptedRequestBody DataExchangeRequest request, - EciesEncryptionContext eciesContext) throws PowerAuthEncryptionException { + EncryptionContext encryptionContext) throws PowerAuthEncryptionException { - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } // Return a slightly different String containing original data in response - return new DataExchangeResponse("Server successfully decrypted signed data: " + (request == null ? "''" : request.getData()) + ", scope: " + eciesContext.getEciesScope()); + return new DataExchangeResponse("Server successfully decrypted signed data: " + (request == null ? "''" : request.getData()) + ", scope: " + encryptionContext.getEncryptionScope()); } /** * Sample signed and encrypted data exchange. * * @param request Data exchange request. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param auth PowerAuth authentication object. * @return Data exchange response. * @throws PowerAuthAuthenticationException In case signature validation fails. @@ -110,9 +108,9 @@ public DataExchangeResponse exchangeInActivationScope(@EncryptedRequestBody Data */ @RequestMapping(value = "v3/signed", method = RequestMethod.POST) @PowerAuth(resourceId = "/exchange/v3/signed") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public DataExchangeResponse exchangeSignedAndEncryptedData(@EncryptedRequestBody DataExchangeRequest request, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication auth) throws PowerAuthAuthenticationException, PowerAuthEncryptionException { if (auth == null || auth.getUserId() == null) { @@ -120,7 +118,7 @@ public DataExchangeResponse exchangeSignedAndEncryptedData(@EncryptedRequestBody throw new PowerAuthSignatureInvalidException(); } - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } @@ -133,7 +131,7 @@ public DataExchangeResponse exchangeSignedAndEncryptedData(@EncryptedRequestBody * Sample signed and encrypted data exchange of String data. * * @param requestData Request with String data. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param auth PowerAuth authentication object. * @return Data exchange response. * @throws PowerAuthAuthenticationException In case signature validation fails. @@ -141,9 +139,9 @@ public DataExchangeResponse exchangeSignedAndEncryptedData(@EncryptedRequestBody */ @RequestMapping(value = "v3/signed/string", method = RequestMethod.POST) @PowerAuth(resourceId = "/exchange/v3/signed/string") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public String exchangeSignedAndEncryptedDataString(@EncryptedRequestBody String requestData, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication auth) throws PowerAuthAuthenticationException, PowerAuthEncryptionException { if (auth == null || auth.getUserId() == null) { @@ -151,7 +149,7 @@ public String exchangeSignedAndEncryptedDataString(@EncryptedRequestBody String throw new PowerAuthSignatureInvalidException(); } - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } @@ -164,7 +162,7 @@ public String exchangeSignedAndEncryptedDataString(@EncryptedRequestBody String * Sample signed and encrypted data exchange of raw data as byte[]. * * @param requestData Request with raw byte[] data. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param auth PowerAuth authentication object. * @return Data exchange response. * @throws PowerAuthAuthenticationException In case signature validation fails. @@ -172,9 +170,9 @@ public String exchangeSignedAndEncryptedDataString(@EncryptedRequestBody String */ @RequestMapping(value = "v3/signed/raw", method = RequestMethod.POST) @PowerAuth(resourceId = "/exchange/v3/signed/raw") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public byte[] exchangeSignedAndEncryptedDataRaw(@EncryptedRequestBody byte[] requestData, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication auth) throws PowerAuthAuthenticationException, PowerAuthEncryptionException { if (auth == null || auth.getUserId() == null) { @@ -182,7 +180,7 @@ public byte[] exchangeSignedAndEncryptedDataRaw(@EncryptedRequestBody byte[] req throw new PowerAuthSignatureInvalidException(); } - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } @@ -195,7 +193,7 @@ public byte[] exchangeSignedAndEncryptedDataRaw(@EncryptedRequestBody byte[] req * Sample signed and encrypted data exchange of generified request. * * @param request Request with generified request data. - * @param eciesContext ECIES context. + * @param encryptionContext Encryption context. * @param auth PowerAuth authentication object. * @return Generified data exchange response. * @throws PowerAuthAuthenticationException In case signature validation fails. @@ -203,16 +201,16 @@ public byte[] exchangeSignedAndEncryptedDataRaw(@EncryptedRequestBody byte[] req */ @RequestMapping(value = "v3/signed/generics", method = RequestMethod.POST) @PowerAuth(resourceId = "/exchange/v3/signed/generics") - @PowerAuthEncryption(scope = EciesScope.ACTIVATION_SCOPE) + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) public ObjectResponse exchangeSignedAndEncryptedDataGenerics(@EncryptedRequestBody ObjectRequest request, - EciesEncryptionContext eciesContext, + EncryptionContext encryptionContext, PowerAuthApiAuthentication auth) throws PowerAuthAuthenticationException, PowerAuthEncryptionException { if (auth == null || auth.getUserId() == null) { logger.info("Signature validation failed"); throw new PowerAuthSignatureInvalidException(); } - if (eciesContext == null) { + if (encryptionContext == null) { logger.error("Encryption failed"); throw new PowerAuthEncryptionException(); } diff --git a/mtoken-model/pom.xml b/mtoken-model/pom.xml index 6e2fa941..7b11a399 100644 --- a/mtoken-model/pom.xml +++ b/mtoken-model/pom.xml @@ -38,6 +38,11 @@ com.fasterxml.jackson.core jackson-annotations + + + io.swagger.core.v3 + swagger-annotations-jakarta + diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java index 699ac73d..08a2c686 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java @@ -18,9 +18,13 @@ package com.wultra.security.powerauth.lib.mtoken.model.request; import com.wultra.security.powerauth.lib.mtoken.model.entity.PreApprovalScreen; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.time.Instant; +import java.util.Optional; + /** * Request for online token signature verification. * @@ -35,7 +39,40 @@ public class OperationApproveRequest { private String data; /** - * Optional OTP used for proximity check. User is instructed by {@link PreApprovalScreen.ScreenType#QR_SCAN}. + * Optional proximity check data. User is instructed by {@link PreApprovalScreen.ScreenType#QR_SCAN}. */ - private String proximityCheckOtp; + @Schema(description = "Optional proximity check data." ) + private ProximityCheck proximityCheck; + + public Optional getProximityCheck() { + return Optional.ofNullable(proximityCheck); + } + + @Data + public static class ProximityCheck { + + @NotNull + @Schema(description = "OTP used for proximity check.") + private String otp; + + @Schema(description = "Source from where the OTP has been gained.") + private Type type; + + /** + * When OTP obtained by the client. An optional hint for possible better estimation of the time shift correction. + */ + @Schema(description = "When OTP requested by the client. An optional hint for possible better estimation of the time shift correction.") + private Instant timestampRequested; + + /** + * When OTP signed by the client. An optional hint for possible better estimation of the time shift correction. + */ + @Schema(description = "When OTP signed by the client. An optional hint for possible better estimation of the time shift correction.") + private Instant timestampSigned; + + public enum Type { + QR_CODE, + DEEPLINK + } + } } diff --git a/pom.xml b/pom.xml index 7335be9a..d4341aa0 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.0 + 3.1.3 @@ -87,19 +87,17 @@ 6.5.0 5.1.0 - 3.2.0 + 3.2.1 2.2.8 2.1.0 1.4.2 - - 3.1.0 1.7.0-SNAPSHOT 1.5.0-SNAPSHOT 1.5.0-SNAPSHOT 1.5.0-SNAPSHOT - 1.74 + 1.76 @@ -182,6 +180,13 @@ ${powerauth-push.version} + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + test + + org.javamoney.moneta moneta-core