diff --git a/docs/sql/mysql/enrollment/create-schema.sql b/docs/sql/mysql/enrollment/create-schema.sql index 0e545826f..6dd247086 100644 --- a/docs/sql/mysql/enrollment/create-schema.sql +++ b/docs/sql/mysql/enrollment/create-schema.sql @@ -23,7 +23,8 @@ CREATE TABLE es_operation_template ( language VARCHAR(8) NOT NULL, title VARCHAR(255) NOT NULL, message TEXT NOT NULL, - attributes TEXT + attributes TEXT, + ui TEXT ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX es_operation_template_placeholder ON es_operation_template(placeholder, language); diff --git a/docs/sql/oracle/enrollment/create-schema.sql b/docs/sql/oracle/enrollment/create-schema.sql index 41d3d7b99..3e7deb27b 100644 --- a/docs/sql/oracle/enrollment/create-schema.sql +++ b/docs/sql/oracle/enrollment/create-schema.sql @@ -24,7 +24,8 @@ CREATE TABLE ES_OPERATION_TEMPLATE ( LANGUAGE VARCHAR2(8 CHAR) NOT NULL, TITLE VARCHAR2(255 CHAR) NOT NULL, MESSAGE CLOB NOT NULL, - ATTRIBUTES CLOB + ATTRIBUTES CLOB, + UI CLOB ); CREATE UNIQUE INDEX ES_OPERATION_TEMPLATE_PLACEHOLDER ON ES_OPERATION_TEMPLATE(PLACEHOLDER, LANGUAGE); diff --git a/docs/sql/postgresql/enrollment/create-schema.sql b/docs/sql/postgresql/enrollment/create-schema.sql index 1afc79ded..0a9ddf2f5 100644 --- a/docs/sql/postgresql/enrollment/create-schema.sql +++ b/docs/sql/postgresql/enrollment/create-schema.sql @@ -28,5 +28,6 @@ CREATE TABLE es_operation_template ( language VARCHAR(8) NOT NULL, title VARCHAR(255) NOT NULL, message TEXT NOT NULL, - attributes TEXT + attributes TEXT, + ui TEXT ); diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/entity/OperationTemplateEntity.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/entity/OperationTemplateEntity.java index 9c9becf8f..1470c8b57 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/entity/OperationTemplateEntity.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/entity/OperationTemplateEntity.java @@ -57,6 +57,9 @@ public class OperationTemplateEntity implements Serializable { @Column(name = "message", nullable = false) private String message; + @Column(name = "ui") + private String ui; + @Column(name = "attributes") private String attributes; @@ -69,11 +72,12 @@ public boolean equals(Object o) { && Objects.equals(language, that.language) && Objects.equals(title, that.title) && Objects.equals(message, that.message) + && Objects.equals(ui, that.ui) && Objects.equals(attributes, that.attributes); } @Override public int hashCode() { - return Objects.hash(placeholder, language, title, message, attributes); + return Objects.hash(placeholder, language, title, message, ui, attributes); } } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java index e3ceeee0e..a3008f990 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java @@ -25,15 +25,13 @@ import com.wultra.app.enrollmentserver.errorhandling.MobileTokenConfigurationException; import com.wultra.security.powerauth.client.model.enumeration.SignatureType; import com.wultra.security.powerauth.client.model.response.OperationDetailResponse; -import com.wultra.security.powerauth.lib.mtoken.model.entity.AllowedSignatureType; -import com.wultra.security.powerauth.lib.mtoken.model.entity.FormData; -import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation; +import com.wultra.security.powerauth.lib.mtoken.model.entity.*; import com.wultra.security.powerauth.lib.mtoken.model.entity.attributes.*; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringSubstitutor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.util.ArrayList; @@ -46,9 +44,12 @@ * @author Petr Dvorak, petr@wultra.com */ @Component +@Slf4j public class MobileTokenConverter { - private static final Logger logger = LoggerFactory.getLogger(MobileTokenConverter.class); + private static final String RISK_FLAG_FLIP_BUTTONS = "X"; + private static final String RISK_FLAG_BLOCK_APPROVAL_ON_CALL = "C"; + private static final String RISK_FLAG_FRAUD_WARNING = "F"; private final ObjectMapper objectMapper; @@ -75,6 +76,15 @@ private AllowedSignatureType convert(List signatureType) { return allowedSignatureType; } + /** + * Convert operation detail from PowerAuth Server and operation template from Enrollment Server into an + * operation in API response. + * + * @param operationDetail Operation detail response obtained from PowerAuth Server. + * @param operationTemplate Operation template obtained from Enrollment Server. + * @return Operation for API response. + * @throws MobileTokenConfigurationException In case there is an error in configuration data. + */ public Operation convert(OperationDetailResponse operationDetail, OperationTemplateEntity operationTemplate) throws MobileTokenConfigurationException { try { final Operation operation = new Operation(); @@ -86,6 +96,8 @@ public Operation convert(OperationDetailResponse operationDetail, OperationTempl operation.setOperationExpires(operationDetail.getTimestampExpires()); operation.setStatus(operationDetail.getStatus().name()); + operation.setUi(convertUiExtension(operationDetail, operationTemplate)); + // Prepare title and message with substituted attributes final FormData formData = new FormData(); final Map parameters = operationDetail.getParameters(); @@ -120,6 +132,32 @@ public Operation convert(OperationDetailResponse operationDetail, OperationTempl } } + private UiExtensions convertUiExtension(final OperationDetailResponse operationDetail, final OperationTemplateEntity operationTemplate) throws JsonProcessingException { + if (StringUtils.hasText(operationTemplate.getUi())) { + final String uiJsonString = operationTemplate.getUi(); + logger.debug("Deserializing ui: '{}' of OperationTemplate ID: {} to UiExtensions", uiJsonString, operationTemplate.getId()); + return objectMapper.readValue(uiJsonString, UiExtensions.class); + } else if (StringUtils.hasText(operationDetail.getRiskFlags())) { + final String riskFlags = operationDetail.getRiskFlags(); + logger.debug("Converting riskFlags: '{}' of OperationDetail ID: {} to UiExtensions", riskFlags, operationDetail.getId()); + final UiExtensions ui = new UiExtensions(); + if (riskFlags.contains(RISK_FLAG_FLIP_BUTTONS)) { + ui.setFlipButtons(true); + } + if (riskFlags.contains(RISK_FLAG_BLOCK_APPROVAL_ON_CALL)) { + ui.setBlockApprovalOnCall(true); + } + if (riskFlags.contains(RISK_FLAG_FRAUD_WARNING)) { + final PreApprovalScreen preApprovalScreen = new PreApprovalScreen(); + preApprovalScreen.setType(PreApprovalScreen.ScreenType.WARNING); + ui.setPreApprovalScreen(preApprovalScreen); + } + return ui; + } else { + return null; + } + } + private Attribute buildAttribute(OperationTemplateParam templateParam, Map params) { final String type = templateParam.getType(); final String id = templateParam.getId(); diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java new file mode 100644 index 000000000..24de62812 --- /dev/null +++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java @@ -0,0 +1,162 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2022 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.enrollmentserver.impl.service.converter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity; +import com.wultra.security.powerauth.client.model.enumeration.OperationStatus; +import com.wultra.security.powerauth.client.model.enumeration.SignatureType; +import com.wultra.security.powerauth.client.model.response.OperationDetailResponse; +import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation; +import com.wultra.security.powerauth.lib.mtoken.model.entity.PreApprovalScreen; +import com.wultra.security.powerauth.lib.mtoken.model.entity.UiExtensions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for {@link MobileTokenConverter}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +class MobileTokenConverterTest { + + private static final String TEMPLATE_UI = "{\n" + + " \"flipButtons\": true,\n" + + " \"blockApprovalOnCall\": false,\n" + + " \"preApprovalScreen\": {\n" + + " \"type\": \"WARNING\",\n" + + " \"heading\": \"Watch out!\",\n" + + " \"message\": \"You may become a victim of an attack.\",\n" + + " \"items\": [\n" + + " \"You activate a new app and allow access to your accounts\",\n" + + " \"Make sure the activation takes place on your device\",\n" + + " \"If you have been prompted for this operation in connection with a payment, decline it\"\n" + + " ],\n" + + " \"approvalType\": \"SLIDER\"\n" + + " }\n" + + "}"; + + private final MobileTokenConverter tested = new MobileTokenConverter(new ObjectMapper()); + + @Test + void testConvertUiNull() throws Exception { + final OperationDetailResponse operationDetail = new OperationDetailResponse(); + operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE)); + operationDetail.setStatus(OperationStatus.APPROVED); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + + final Operation result = tested.convert(operationDetail, operationTemplate); + + assertNull(result.getUi()); + } + + @Test + void testConvertUiOverriddenByEnrollment() throws Exception { + final OperationDetailResponse operationDetail = new OperationDetailResponse(); + operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE)); + operationDetail.setStatus(OperationStatus.APPROVED); + operationDetail.setRiskFlags("C"); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setUi(TEMPLATE_UI); + + final Operation result = tested.convert(operationDetail, operationTemplate); + + assertNotNull(result.getUi()); + + final UiExtensions ui = result.getUi(); + assertEquals(true, ui.getFlipButtons()); + assertEquals(false, ui.getBlockApprovalOnCall()); + assertNotNull(ui.getPreApprovalScreen()); + + final PreApprovalScreen preApprovalScreen = ui.getPreApprovalScreen(); + assertEquals(PreApprovalScreen.ScreenType.WARNING, preApprovalScreen.getType()); + assertEquals("Watch out!", preApprovalScreen.getHeading()); + assertEquals("You may become a victim of an attack.", preApprovalScreen.getMessage()); + assertEquals(PreApprovalScreen.ApprovalType.SLIDER, preApprovalScreen.getApprovalType()); + assertNotNull(preApprovalScreen.getItems()); + + final List items = preApprovalScreen.getItems(); + assertEquals(3, items.size()); + assertEquals("You activate a new app and allow access to your accounts", items.get(0)); + } + + @Test + void testConvertUiRiskFlagsX() throws Exception { + final OperationDetailResponse operationDetail = new OperationDetailResponse(); + operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE)); + operationDetail.setStatus(OperationStatus.APPROVED); + operationDetail.setRiskFlags("X"); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + + final Operation result = tested.convert(operationDetail, operationTemplate); + + assertNotNull(result.getUi()); + + final UiExtensions ui = result.getUi(); + assertEquals(true, ui.getFlipButtons()); + assertNull(ui.getBlockApprovalOnCall()); + assertNull(ui.getPreApprovalScreen()); + } + + @Test + void testConvertUiRiskFlagsC() throws Exception { + final OperationDetailResponse operationDetail = new OperationDetailResponse(); + operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE)); + operationDetail.setStatus(OperationStatus.APPROVED); + operationDetail.setRiskFlags("C"); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + + final Operation result = tested.convert(operationDetail, operationTemplate); + + assertNotNull(result.getUi()); + + final UiExtensions ui = result.getUi(); + assertNull(ui.getFlipButtons()); + assertEquals(true, ui.getBlockApprovalOnCall()); + assertNull(ui.getPreApprovalScreen()); + } + + @Test + void testConvertUiRiskFlagsXCF() throws Exception { + final OperationDetailResponse operationDetail = new OperationDetailResponse(); + operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE)); + operationDetail.setStatus(OperationStatus.APPROVED); + operationDetail.setRiskFlags("XCF"); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + + final Operation result = tested.convert(operationDetail, operationTemplate); + + assertNotNull(result.getUi()); + + final UiExtensions ui = result.getUi(); + assertEquals(true, ui.getFlipButtons()); + assertEquals(true, ui.getBlockApprovalOnCall()); + assertNotNull(ui.getPreApprovalScreen()); + + final PreApprovalScreen preApprovalScreen = ui.getPreApprovalScreen(); + assertEquals(PreApprovalScreen.ScreenType.WARNING, preApprovalScreen.getType()); + } +} diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/Operation.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/Operation.java index 6ed849d88..9262acb60 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/Operation.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/Operation.java @@ -39,6 +39,7 @@ public class Operation { private String status; private Date operationCreated; private Date operationExpires; + private UiExtensions ui; @NotNull private AllowedSignatureType allowedSignatureType; @NotNull diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/PreApprovalScreen.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/PreApprovalScreen.java new file mode 100644 index 000000000..6469bbd13 --- /dev/null +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/PreApprovalScreen.java @@ -0,0 +1,87 @@ +/* + * PowerAuth Mobile Token Model + * Copyright (C) 2022 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.security.powerauth.lib.mtoken.model.entity; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Information about screen displayed before an operation approval. + * + * @author Petr Dvorak, petr@wultra.com + */ +@Data +public class PreApprovalScreen { + + /** + * Type of the pre-approval screen. + */ + public enum ScreenType { + /** + * The purpose of the screen is to warn user about a potential problem. + */ + WARNING, + + /** + * The purpose of the screen is to inform user about a some specific operation context. + */ + INFO + } + + /** + * Type of the approval user experience. + */ + public enum ApprovalType { + /** + * The user needs to slide a UI slider ("Slide to unlock") to proceed to the operation approval screen. + */ + SLIDER + } + + /** + * Type of the pre-approval screen. + */ + @NotNull + private ScreenType type; + + /** + * Approval screen heading. + */ + @NotNull + private String heading; + + /** + * Approval screen message displayed under heading. + */ + @NotNull + private String message; + + /** + * List of additional text items displayed. + */ + @NotNull + private List items; + + /** + * Type of the approval element. + */ + private ApprovalType approvalType; + +} diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/UiExtensions.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/UiExtensions.java new file mode 100644 index 000000000..5589a5c64 --- /dev/null +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/entity/UiExtensions.java @@ -0,0 +1,51 @@ +/* + * PowerAuth Mobile Token Model + * Copyright (C) 2022 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.security.powerauth.lib.mtoken.model.entity; + +import lombok.Data; + +/** + * Data object representing UI extensions displayed during the operation approval. + * + * @author Petr Dvorak, petr@wultra.com + */ +@Data +public class UiExtensions { + + /** + * Property that hints the mobile app that the "Approve" and "Reject" buttons should be flipped, + * raising the CTA priority of the "Reject" button over the "Approve" button. + */ + private Boolean flipButtons; + + /** + * Property that hints the mobile app that the operation approval should be blocked in case that + * there is an ongoing call on the device. The UI should still allow pressing the "Approve" button, + * but instead of displaying a PIN code / biometric authentication, user should be presented with + * a screen informing the user that the operation cannot be approved while the user is on the phone. + */ + private Boolean blockApprovalOnCall; + + /** + * Property that defines a screen content that is displayed before the user sees the operation + * approval screen. The purpose of the screen could be to provide an additional warning before + * approving an operation, or to display generic information related to the operation approval. + */ + private PreApprovalScreen preApprovalScreen; + +}