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

1.7.x PAC with claim #167

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions 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 @@ -651,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?

/// Assigns the 'non-personalized' operation 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 @@ -245,6 +274,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
52 changes: 52 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)
- [Operation detail](#operation-detail)
- [Claim the Operation](#claim-the-operation)
- [Off-line Authorization](#off-line-authorization)
- [Operations API Reference](#operations-api-reference)
- [WMTUserOperation](#wmtuseroperation)
Expand Down Expand Up @@ -210,6 +212,56 @@ func reject(operation: WMTOperation, reason: WMTRejectionReason) {
}
```

## Operation detail

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

// Retrieve operation details based on the operation ID.
func getDetail(operationId: String) {
operationService.getDetail(operationId: operationId) { result in
switch result {
case .success(let operation):
// process operation
break
case .failure(let error):
// process error
break
}
}
}
```

## Claim the Operation

To claim a non-persolized operation use `WMTOperations.claim`.

A non-personalized operation refers to an operation that is initiated without a specific operationId. In this state, the operation is not tied to a particular user and lacks a unique identifier.

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

// Assigns the 'non-personalized' operation to the user
func claim(operationId: String) {
operationService.claim(operationId: operationId) { result in
switch result {
case .success(let operation):
// process operation
break
case .failure(let error):
// process error
break
}
}
}
```

## 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