Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #852: Allow scoping operation to a single activation ID #1127

Merged
merged 11 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/PowerAuth-Server-1.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ and `pa_application` or `pa_operation`. Make sure that `pa_operation_application
existing `pa_application.id` and `pa_operation_application.operation_id` contains references to
existing `pa_operation.id`. If necessary, manually remove orphaned records in `pa_operation_application`. Consider
creating a backup before this operation.

### Add application_id column

Add new column `application_id` to `pa_operation` table. Storing `application_id` brings enhancements for developers:

* Create a new operation on a specific mobile device (activation ID).
* Approve the operation just on that specific mobile device (activation ID).
1 change: 1 addition & 0 deletions docs/WebServices-Methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,7 @@ REST endpoint: `POST /rest/v3/operation/create`
| `String` | `externalId` | External identifier of the operation, i.e., ID from transaction system |
| `Map<String, String>` | `parameters` | Parameters of the operation, will be filled to the operation data |
| `Boolean` | `proximityCheckEnabled` | Whether proximity check should be used. Overrides configuration from operation template. |
| `String` | `activationId` | Activation Id of a specific device. |

#### Response

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

<changeSet id="1" logicalFilePath="powerauth-java-server/1.6.x/20231112-add-activation-id.xml" author="Jan Dusil">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="pa_operation" columnName="activation_id" />
</not>
</preConditions>
<comment>Add activation_id column to pa_operation</comment>
<addColumn tableName="pa_operation">
<column name="activation_id" type="varchar(37)" />
jandusil marked this conversation as resolved.
Show resolved Hide resolved
</addColumn>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<include file="20231018-add-constraint-operation-template-name.xml" relativeToChangelogFile="true" />
<include file="20231103-add-activation-name-history.xml" relativeToChangelogFile="true" />
<include file="20231106-add-foreign-keys.xml" relativeToChangelogFile="true" />
<include file="20231112-add-activation-id.xml" relativeToChangelogFile="true" />

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ public class OperationCreateRequest {
private String externalId;
private final Map<String, String> parameters = new LinkedHashMap<>();
private Boolean proximityCheckEnabled;
private String activationId;
jandusil marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ public class OperationEntity implements Serializable {
@Column(name = "totp_seed")
private String totpSeed;

/**
* Optional activationId of a device.
*/
@Column(name = "activation_id")
private String activationId;

/**
* Get operation ID.
* @return Operation ID.
Expand Down Expand Up @@ -419,6 +425,24 @@ public void setTotpSeed(String totpSeed) {
this.totpSeed = totpSeed;
}

/**
* Get the activation ID.
*
* @return Activation ID.
*/
public String getActivationId() {
return activationId;
}

/**
* Set the activation ID.
*
* @param activationId Activation ID to set.
*/
public void setActivationId(String activationId) {
this.activationId = activationId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ public OperationDetailResponse createOperation(OperationCreateRequest request) t
final Date timestampExpiresRequest = request.getTimestampExpires();
final Map<String, String> parameters = request.getParameters() != null ? request.getParameters() : new LinkedHashMap<>();
final String externalId = request.getExternalId();
final String activationId = request.getActivationId();

// Prepare current timestamp in advance
final Date currentTimestamp = new Date();
Expand Down Expand Up @@ -188,6 +189,7 @@ public OperationDetailResponse createOperation(OperationCreateRequest request) t
operationEntity.setTimestampFinalized(null); // empty initially
operationEntity.setRiskFlags(templateEntity.getRiskFlags());
operationEntity.setTotpSeed(generateTotpSeed(request, templateEntity));
operationEntity.setActivationId(activationId);

final AuditDetail auditDetail = AuditDetail.builder()
.type(AuditType.OPERATION.getCode())
Expand All @@ -205,6 +207,7 @@ public OperationDetailResponse createOperation(OperationCreateRequest request) t
.param("maxFailureCount", operationEntity.getMaxFailureCount())
.param("timestampExpires", timestampExpires)
.param("proximityCheckEnabled", operationEntity.getTotpSeed() != null)
.param("activationId", activationId)
.build();
audit.log(AuditLevel.INFO, "Operation created with ID: {}", auditDetail, operationId);

Expand Down Expand Up @@ -250,13 +253,15 @@ public OperationUserActionResponse attemptApproveOperation(OperationApproveReque
// Check the operation properties match the request
final PowerAuthSignatureTypes factorEnum = PowerAuthSignatureTypes.getEnumFromString(signatureType.toString());
final ProximityCheckResult proximityCheckResult = fetchProximityCheckResult(operationEntity, request, currentInstant);
final boolean activationIdMatches = activationIdMatches(request, operationEntity.getActivationId());

if (operationEntity.getUserId().equals(userId) // correct user approved the operation
&& operationEntity.getApplications().contains(application.get()) // operation is approved by the expected application
&& isDataEqual(operationEntity, data) // operation data matched the expected value
&& factorsAcceptable(operationEntity, factorEnum) // auth factors are acceptable
&& operationEntity.getMaxFailureCount() > operationEntity.getFailureCount() // operation has sufficient attempts left (redundant check)
&& proximityCheckPassed(proximityCheckResult)){
&& proximityCheckPassed(proximityCheckResult)
&& activationIdMatches){ // either Operation does not have assigned activationId or it has one, and it matches activationId from request

// Approve the operation
operationEntity.setStatus(OperationStatusDo.APPROVED);
Expand All @@ -277,6 +282,7 @@ && proximityCheckPassed(proximityCheckResult)){
.param("failureCount", operationEntity.getFailureCount())
.param("proximityCheckResult", proximityCheckResult)
.param("currentTimestamp", currentTimestamp)
.param("activationIdOperation", operationEntity.getActivationId())
.build();
audit.log(AuditLevel.INFO, "Operation approved with ID: {}", auditDetail, operationId);

Expand Down Expand Up @@ -310,6 +316,9 @@ && proximityCheckPassed(proximityCheckResult)){
.param("failureCount", operationEntity.getFailureCount())
.param("proximityCheckResult", proximityCheckResult)
.param("currentTimestamp", currentTimestamp)
.param("activationIdMatches", activationIdMatches)
.param("activationIdOperation", operationEntity.getActivationId())
.param("activationIdRequest", additionalData.get("activationId"))
.build();
audit.log(AuditLevel.INFO, "Operation approval failed with ID: {}, failed attempts count: {}", auditDetail, operationId, operationEntity.getFailureCount());

Expand Down Expand Up @@ -340,6 +349,9 @@ && proximityCheckPassed(proximityCheckResult)){
.param("maxFailureCount", operationEntity.getMaxFailureCount())
.param("proximityCheckResult", proximityCheckResult)
.param("currentTimestamp", currentTimestamp)
.param("activationIdMatches", activationIdMatches)
.param("activationIdOperation", operationEntity.getActivationId())
.param("activationIdRequest", additionalData.get("activationId"))
.build();
audit.log(AuditLevel.INFO, "Operation failed with ID: {}", auditDetail, operationId);

Expand Down Expand Up @@ -791,6 +803,10 @@ private static boolean proximityCheckPassed(final ProximityCheckResult proximity
return proximityCheckResult == ProximityCheckResult.SUCCESS || proximityCheckResult == ProximityCheckResult.DISABLED;
}

private static boolean activationIdMatches(final OperationApproveRequest operationApproveRequest, String activationId) {
return activationId == null || activationId.equals(operationApproveRequest.getAdditionalData().get("activationId"));
}

private ProximityCheckResult fetchProximityCheckResult(final OperationEntity operation, final OperationApproveRequest request, final Instant now) {
final String seed = operation.getTotpSeed();
if (seed == null) {
Expand Down
Loading