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

PAC with claim #132

Merged
merged 19 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 17 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
5 changes: 4 additions & 1 deletion WultraMobileTokenSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */; };
EA6DDF1A29F804D60011E234 /* WMTPostApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */; };
EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; };
EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */; };
EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; };
EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; };
EAB7054A2AF1161500756AC2 /* PACParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB705492AF1161500756AC2 /* PACParserTests.swift */; };
Expand Down Expand Up @@ -157,6 +158,7 @@
EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPreApprovalScreen.swift; sourceTree = "<group>"; };
EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = "<group>"; };
EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = "<group>"; };
EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = "<group>"; };
EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = "<group>"; };
EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = "<group>"; };
EAB705492AF1161500756AC2 /* PACParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PACParserTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -433,6 +435,7 @@
children = (
DCC5CCD7244DBBBD004679AC /* WMTAuthorizationData.swift */,
DCC5CCD9244DBBE2004679AC /* WMTRejectionData.swift */,
EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */,
);
path = Requests;
sourceTree = "<group>";
Expand Down Expand Up @@ -506,7 +509,6 @@
DCC5CC912449EE21004679AC /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1140;
LastUpgradeCheck = 1500;
ORGANIZATIONNAME = Wultra;
Expand Down Expand Up @@ -652,6 +654,7 @@
EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */,
DC48803D292282FF00DB844B /* WMTInbox.swift in Sources */,
DC488042292282FF00DB844B /* WMTInboxImpl.swift in Sources */,
EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Copyright 2023 Wultra s.r.o.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions
// and limitations under the License.
//

import Foundation

/// Claim payload
class WMTOperationDetailRequest: Codable {

/// Operation Id
let operationId: String

init(operationId: String) {
self.operationId = operationId
}

enum CodingKeys: String, CodingKey {
case operationId = "id"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,14 @@ enum WMTOperationEndpoints {
typealias EndpointType = WPNEndpointSigned<WPNRequest<WMTRejectionData>, WPNResponseBase>
static let endpoint: EndpointType = WPNEndpointSigned(endpointURLPath: "/api/auth/token/app/operation/cancel", uriId: "/operation/cancel")
}

enum OperationDetail {
typealias EndpointType = WPNEndpointSignedWithToken<WPNRequest<WMTOperationDetailRequest>, WPNResponse<WMTUserOperation>>
static let endpoint: EndpointType = WPNEndpointSignedWithToken(endpointURLPath: "/api/auth/token/app/operation/detail", tokenName: "possession_universal")
}

enum OperationClaim {
typealias EndpointType = WPNEndpointSignedWithToken<WPNRequest<WMTOperationDetailRequest>, WPNResponse<WMTUserOperation>>
static let endpoint: EndpointType = WPNEndpointSignedWithToken(endpointURLPath: "/api/auth/token/app/operation/detail/claim", tokenName: "possession_universal")
}
}
53 changes: 52 additions & 1 deletion WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,46 @@ class WMTOperationsImpl<T: WMTUserOperation>: WMTOperations, WMTService {
}
}

func getDetail(operationId: String, completion: @escaping (Result<WMTUserOperation, WMTError>) -> Void) -> Operation? {
guard validateActivation(completion) else {
return nil
}

let detailData = WMTOperationDetailRequest(operationId: operationId)

return networking.post(data: .init(detailData), signedWith: .possession(), to: WMTOperationEndpoints.OperationDetail.endpoint) { response, error in
self.processResult(response: response, error: error) { result in
switch result {
case .success(let operation):
completion(.success(operation))
case .failure(let err):
completion(.failure(self.adjustOperationError(err, auth: false)))
}
}
}
}

func claim(operationId: String, completion: @escaping(Result<WMTUserOperation, WMTError>) -> Void) -> Operation? {

guard validateActivation(completion) else {
return nil
}

let claimData = WMTOperationDetailRequest(operationId: operationId)

return networking.post(data: .init(claimData), signedWith: .possession(), to: WMTOperationEndpoints.OperationClaim.endpoint) { response, error in
self.processResult(response: response, error: error) { result in
switch result {
case .success(let operation):
self.operationsRegister.add(operation)
completion(.success(operation))
case .failure(let err):
completion(.failure(self.adjustOperationError(err, auth: false)))
}
}
}
}

func authorize(operation: WMTOperation, with authentication: PowerAuthAuthentication, completion: @escaping (Result<Void, WMTError>) -> Void) -> Operation? {

guard validateActivation(completion) else {
Expand Down Expand Up @@ -472,6 +512,17 @@ private class OperationsRegister {
onChangeCallback = callback
}

/// Adds an operation from register
func add(_ operation: WMTUserOperation) {

// Check if the ID of the operation is already in the list otherwise add it
if currentOperations.contains(where: { $0.id == operation.id }) == false {
currentOperations.append(operation)
currentOperationsSet.insert(operation.id)
onChangeCallback(currentOperations, [operation], [])
}
}

/// Adds a multiple operations to the register.
/// Returns list of added and removed operations.
@discardableResult
Expand Down Expand Up @@ -503,7 +554,7 @@ private class OperationsRegister {
currentOperations.append(contentsOf: addedOperations)
currentOperationsSet.formUnion(addedOperationsSet)

// we need to call onChanged even if nothing changed, because the objects are replaced by different insntances
// we need to call onChanged even if nothing changed, because the objects are replaced by different instances
onChangeCallback(currentOperations, addedOperations, removedOperations)
// Returns list of operations
return (addedOperations, removedOperations)
Expand Down
18 changes: 18 additions & 0 deletions WultraMobileTokenSDK/Operations/WMTOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ public protocol WMTOperations: AnyObject {
@discardableResult
func getHistory(authentication: PowerAuthAuthentication, completion: @escaping(Result<[WMTOperationHistoryEntry], WMTError>) -> Void) -> Operation?

/// Retrieves operation detail based on operation ID
/// - Parameters:
/// - operationId: Operation ID to get
/// - completion: Result completion.
/// This completion is always called on the main thread.
/// - Returns: Operation object for its state observation.
@discardableResult
func getDetail(operationId: String, completion: @escaping(Result<WMTUserOperation, WMTError>) -> Void) -> Operation?

/// Claims the "anonymous" operation to be assigned to the user
/// - Parameters:
/// - operationId: Operation ID which will be claimed to belong to the user
/// - completion: Result completion.
/// This completion is always called on the main thread.
/// - Returns: Operation object for its state observation.
@discardableResult
func claim(operationId: String, completion: @escaping(Result<WMTUserOperation, WMTError>) -> Void) -> Operation?

/// Authorize operation with given PowerAuth authentication object.
///
/// - Parameters:
Expand Down
40 changes: 40 additions & 0 deletions WultraMobileTokenSDKTests/IntegrationProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,35 @@ class IntegrationProxy {
}
}

func createNonPersonalisedPACOperation(_ factors: Factors = .F_2FA, completion: @escaping (NonPersonalisedTOTPOperationObject?) -> Void) {
DispatchQueue.global().async {
let opBody: String
switch factors {
case .F_2FA:
opBody = """
{
"template": "login_preApproval",
"proximityCheckEnabled": true,
"parameters": {
"party.id": "666",
"party.name": "Datová schránka",
"session.id": "123",
"session.ip-address": "192.168.0.1"
}
}
"""
}

completion(self.makeRequest(url: URL(string: "\(self.config.cloudServerUrl)/v2/operations")!, body: opBody))
}
}

func getOperation(operation: NonPersonalisedTOTPOperationObject, completion: @escaping (NonPersonalisedTOTPOperationObject?) -> Void) {
DispatchQueue.global().async {
completion(self.makeRequest(url: URL(string: "\(self.config.cloudServerUrl)/v2/operations/\(operation.operationId)")!, body: "", httpMethod: "GET"))
}
}

func getQROperation(operation: OperationObject, completion: @escaping (QROperationData?) -> Void) {
DispatchQueue.global().async {
completion(self.makeRequest(url: URL(string: "\(self.config.cloudServerUrl)/v2/operations/\(operation.operationId)/offline/qr?registrationId=\(self.registrationId)")!, body: "", httpMethod: "GET"))
Expand Down Expand Up @@ -244,6 +273,17 @@ struct OperationObject: Codable {
let timestampExpires: Int
}

struct NonPersonalisedTOTPOperationObject: Codable {
let operationId: String
let status: String
let operationType: String
let failureCount: Int
let maxFailureCount: Int
let timestampCreated: Int
let timestampExpires: Int
let proximityOtp: String?
}

private struct IntegrationConfig: Codable {
let cloudServerUrl: String
let cloudServerLogin: String
Expand Down
80 changes: 80 additions & 0 deletions WultraMobileTokenSDKTests/IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,86 @@ class IntegrationTests: XCTestCase {
waitForExpectations(timeout: 20, handler: nil)
}

/// Operation IDs should be equal
func testDetail() {
let exp = expectation(description: "Operation detail")

proxy.createNonPersonalisedPACOperation { op in
if let op {
DispatchQueue.main.async {
_ = self.ops.getDetail(operationId: op.operationId) { result in
switch result {
case .success(let operation):
XCTAssertEqual(op.operationId, operation.id)
case .failure(let err):
XCTFail(err.description)
}
exp.fulfill()
}
}
} else {
XCTFail("Failed to get operation detail")
exp.fulfill()
}
}

waitForExpectations(timeout: 20, handler: nil)
}

/// Operation IDs should be equal
func testClaim() {
let exp = expectation(description: "Operation Claim should return UserOperation with operation.id")

proxy.createNonPersonalisedPACOperation { op in
if let op {
DispatchQueue.main.async {
_ = self.ops.claim(operationId: op.operationId) { result in
switch result {
case .success(let operation):
if operation.ui?.preApprovalScreen?.type == .qr {
self.proxy.getOperation(operation: op) { totpOP in
XCTAssertNotNil(totpOP?.proximityOtp, "Even with proximityCheckEnabled: true, in proximityOtp nil")
if let totpOP = totpOP, let proximityOtp = totpOP.proximityOtp {
operation.proximityCheck = WMTProximityCheck(totp: proximityOtp, type: .qrCode)
// wrong password on purpose
let auth = PowerAuthAuthentication.possessionWithPassword(password: "xxxx")
self.ops.authorize(operation: operation, with: auth) { result in
switch result {
case .failure:
let auth = PowerAuthAuthentication.possessionWithPassword(password: self.pin)
self.ops.authorize(operation: operation, with: auth) { result in
if case .failure(let error) = result {
XCTFail("Failed to authorize op: \(error.description)")
}
exp.fulfill()
}
case .success:
XCTFail("Operation approved with wrong password")
exp.fulfill()
}
}
} else {
XCTFail("Operation or TOTP is NIL")
exp.fulfill()
}
}
}

case .failure(let err):
XCTFail(err.description)
exp.fulfill()
}
}
}
} else {
XCTFail("Failed to get operation detail")
exp.fulfill()
}
}

waitForExpectations(timeout: 20, handler: nil)
}

/// `currentServerDate` is nil by default and after ops fetch, it should be set
func testCurrentServerDate() {
let exp = expectation(description: "Server date should be set after operation fetch")
Expand Down
45 changes: 45 additions & 0 deletions docs/Using-Operations-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- [Start Periodic Polling](#start-periodic-polling)
- [Approve an Operation](#approve-an-operation)
- [Reject an Operation](#reject-an-operation)
- [Detail of an Operation](#detail-of-an-operation)
Hopsaheysa marked this conversation as resolved.
Show resolved Hide resolved
- [Claim an Operation](#claim-an-operation)
Hopsaheysa marked this conversation as resolved.
Show resolved Hide resolved
- [Off-line Authorization](#off-line-authorization)
- [Operations API Reference](#operations-api-reference)
- [WMTUserOperation](#wmtuseroperation)
Expand Down Expand Up @@ -210,6 +212,49 @@ func reject(operation: WMTOperation, reason: WMTRejectionReason) {
}
```

## Detail of an Operation

To get a detail of an operation based on operation ID use `WMTOperations.getDetail`. Operation detail is confirmed by the possession factor so there is no need for creating `PowerAuthAuthentication` object. The returned result is the operation and its current status.

```swift
import WultraMobileTokenSDK
import PowerAuth2

// Reject operation with some reason
func getDetail(operationId: String, reason: WMTRejectionReason) {
operationService.getDetail(operationId: operationId) { error in
switch result {
case .success(let operation):
// process operation
break
case .failure(let error):
// process error
break
}
}
}
```

## Claim an Operation

Hopsaheysa marked this conversation as resolved.
Show resolved Hide resolved
To claim a non-persolized operation use `WMTOperations.claim`. Operation claim is confirmed by the possession factor so there is no need for creating `PowerAuthAuthentication` object. The returned result is the operation and its current status and also the claimed operation is inserted into the operation list. You can simply use it with the following example.

```swift
import WultraMobileTokenSDK
import PowerAuth2

// Reject operation with some reason
func reject(operation: WMTOperation, reason: WMTRejectionReason) {
Hopsaheysa marked this conversation as resolved.
Show resolved Hide resolved
operationService.reject(operation: operation, reason: reason) { error in
if let error = error {
// show error UI
} else {
// show success UI
}
}
}
```

## Operation History

You can retrieve an operation history via the `WMTOperations.getHistory` method. The returned result is operations and their current status.
Expand Down
Loading