From 0e865ad6bc3fbf785984a3fded8a1b545b03c522 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Fri, 24 Nov 2023 12:34:21 +0100 Subject: [PATCH 01/16] Implement new Operation Endpoint Claim and extend WMTOperations with claim function. Rename TOTPParser for PACParser --- Package.resolved | 23 +++++ .../project.pbxproj | 14 ++- .../Model/Requests/WMTClaimData.swift | 29 ++++++ .../Service/WMTOperationEndpoints.swift | 5 + .../Service/WMTOperationsImpl.swift | 21 +++++ .../Operations/Utils/WMTPACUtils.swift | 63 +++++++++++++ .../Operations/Utils/WMTTOTPUtils.swift | 91 ------------------- .../Operations/WMTOperations.swift | 9 ++ .../TOTPParserTests.swift | 24 ++--- 9 files changed, 171 insertions(+), 108 deletions(-) create mode 100644 Package.resolved create mode 100644 WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift create mode 100644 WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift delete mode 100644 WultraMobileTokenSDK/Operations/Utils/WMTTOTPUtils.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..26bccf3 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "networking-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/wultra/networking-apple.git", + "state" : { + "revision" : "b7ebe23f441e614d13bd6bc803d658e66767e31a", + "version" : "1.2.0" + } + }, + { + "identity" : "powerauth-mobile-sdk-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/wultra/powerauth-mobile-sdk-spm.git", + "state" : { + "revision" : "095be6adfc057501a7cb9a7351697a1b6ed9e64b", + "version" : "1.7.8" + } + } + ], + "version" : 2 +} diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index 06ff6ba..027701b 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -71,8 +71,9 @@ 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 /* WMTClaimData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTClaimData.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; - EA9CE2C22AEBDB0D00FE4E35 /* WMTTOTPUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTTOTPUtils.swift */; }; + EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; EAB7054A2AF1161500756AC2 /* TOTPParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB705492AF1161500756AC2 /* TOTPParserTests.swift */; }; EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */; }; /* End PBXBuildFile section */ @@ -157,8 +158,9 @@ EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPreApprovalScreen.swift; sourceTree = ""; }; EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = ""; }; EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; + EA7A6E572B0E639800C1D4F4 /* WMTClaimData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTClaimData.swift; sourceTree = ""; }; EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = ""; }; - EA9CE2C12AEBDB0D00FE4E35 /* WMTTOTPUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTTOTPUtils.swift; sourceTree = ""; }; + EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = ""; }; EAB705492AF1161500756AC2 /* TOTPParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPParserTests.swift; sourceTree = ""; }; EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTJsonValue.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -295,7 +297,7 @@ DC6E52D4259C959900FC25BE /* Utils */ = { isa = PBXGroup; children = ( - EA9CE2C12AEBDB0D00FE4E35 /* WMTTOTPUtils.swift */, + EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */, DC6E52D5259C964600FC25BE /* WMTOperationExpirationWatcher.swift */, EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */, ); @@ -433,6 +435,7 @@ children = ( DCC5CCD7244DBBBD004679AC /* WMTAuthorizationData.swift */, DCC5CCD9244DBBE2004679AC /* WMTRejectionData.swift */, + EA7A6E572B0E639800C1D4F4 /* WMTClaimData.swift */, ); path = Requests; sourceTree = ""; @@ -626,7 +629,7 @@ BFEEB2092937A2680047941D /* WMTInboxGetList.swift in Sources */, BFEEB20729379F960047941D /* WMTInboxSetMessageRead.swift in Sources */, EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */, - EA9CE2C22AEBDB0D00FE4E35 /* WMTTOTPUtils.swift in Sources */, + EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */, EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */, DCC5CCB32449F8CD004679AC /* WMTOperationAttribute.swift in Sources */, DCC5CCAC2449F765004679AC /* WMTOperationsImpl.swift in Sources */, @@ -650,6 +653,7 @@ EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */, DC48803D292282FF00DB844B /* WMTInbox.swift in Sources */, DC488042292282FF00DB844B /* WMTInboxImpl.swift in Sources */, + EA7A6E582B0E639800C1D4F4 /* WMTClaimData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift b/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift new file mode 100644 index 0000000..990dd8a --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift @@ -0,0 +1,29 @@ +// +// 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 WMTClaimData: Codable { + + /// Operation Id + let operationId: String + + init(operationId: String) { + self.operationId = operationId + } +} diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift index 22f5f57..ef4c1ad 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift @@ -38,4 +38,9 @@ enum WMTOperationEndpoints { typealias EndpointType = WPNEndpointSigned, WPNResponseBase> static let endpoint: EndpointType = WPNEndpointSigned(endpointURLPath: "/api/auth/token/app/operation/cancel", uriId: "/operation/cancel") } + + enum Claim { + typealias EndpointType = WPNEndpointBasic, WPNResponse> + static let endpoint: EndpointType = WPNEndpointBasic(endpointURLPath: "api/auth/token/app/operation/detail/claim") + } } diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift index 9a6d98a..90910f4 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift @@ -237,6 +237,27 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } + func claim(operationId: String, completion: @escaping(Result) -> Void) -> Operation? { + + guard validateActivation(completion) else { + return nil + } + + let claimData = WMTClaimData(operationId: operationId) + + return networking.post(data: .init(claimData), to: WMTOperationEndpoints.Claim.endpoint) { response, error in + self.processResult(response: response, error: error) { result in + switch result { + case .success(let operation): + self.operationsRegister.replace(with: [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) -> Operation? { guard validateActivation(completion) else { diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift new file mode 100644 index 0000000..dad2518 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift @@ -0,0 +1,63 @@ +// +// 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 + +/// Utility class used for handling Proximity Antifraud Check +public class WMTPACUtils { + + /// Method accepts deeplink URL and returns PAC data + public static func parseDeeplink(url: URL) -> WMTPACData? { + + guard let components = URLComponents(string: url.absoluteString) else { + D.error("Failed to get URLComponents: URLString is malformed") + return nil + } + + guard let queryItems = components.queryItems else { + D.error("Failed to get URLComponents queryItems") + return nil + } + + if let operationId = queryItems.first(where: { $0.name == "oid" })?.value { + let totp = queryItems.first(where: { $0.name == "totp" })?.value + return WMTPACData(operationId: operationId, totp: totp) + } else { + D.error("Failed to get operationId from query items") + return nil + } + } + + /// Method accepts scanned code as a String and returns PAC data + public static func parseQRCode(code: String) -> WMTPACData? { + if let encodedURLString = code.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedURLString) { + return parseDeeplink(url: url) + } else { + D.error("Failed to created URL from QR code String.") + return nil + } + } +} + +/// Data which is return after parsing PAC code +public struct WMTPACData { + + /// The ID of the operation associated with the PAC + public let operationId: String + + /// Time-based one time password used for Proximity antifraud check + public let totp: String? +} diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTTOTPUtils.swift b/WultraMobileTokenSDK/Operations/Utils/WMTTOTPUtils.swift deleted file mode 100644 index 0ffb766..0000000 --- a/WultraMobileTokenSDK/Operations/Utils/WMTTOTPUtils.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// 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 - -/// Utility class used for handling TOTP -public class WMTTOTPUtils { - - /// Method accepts deeeplink URL and returns payload data - public static func parseDeeplink(url: URL) -> WMTOperationTOTPData? { - - guard let components = URLComponents(string: url.absoluteString) else { - D.error("Failed to get URLComponents: URLString is malformed") - return nil - } - - guard let queryItems = components.queryItems else { - D.error("Failed to get URLComponents queryItems") - return nil - } - - guard let code = queryItems.first?.value else { - D.error("Failed to get Query Items value for parsing") - return nil - } - - guard let data = parseJWT(code: code) else { return nil } - - return data - } - - /// Method accepts scanned code as a String and returns payload data - public static func parseQRCode(code: String) -> WMTOperationTOTPData? { - return parseJWT(code: code) - } - - private static func parseJWT(code: String) -> WMTOperationTOTPData? { - let jwtParts = code.split(separator: ".") - - // At this moment we dont care about header, we want only payload which is the second part of JWT - let jwtBase64String = jwtParts.count > 1 ? String(jwtParts[1]) : "" - - if let base64EncodedData = jwtBase64String.data(using: .utf8), - let dataPayload = Data(base64Encoded: base64EncodedData) { - do { - return try JSONDecoder().decode(WMTOperationTOTPData.self, from: dataPayload) - } catch { - D.error("Failed to decode JWT from: \(code)") - D.error("With error: \(error)") - return nil - } - } - - D.error("Failed to decode QR JWT from: \(jwtBase64String)") - return nil - } -} - -/// Data payload which is returned from JWT parser -public struct WMTOperationTOTPData: Codable { - - /// The actual Time-based one time password - public let totp: String - - /// The ID of the operations associated with the TOTP - public let operationId: String - - public enum Keys: String, CodingKey { - case totp = "totp" - case operationId = "oid" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: Keys.self) - totp = try container.decode(String.self, forKey: .totp) - operationId = try container.decode(String.self, forKey: .operationId) - } -} diff --git a/WultraMobileTokenSDK/Operations/WMTOperations.swift b/WultraMobileTokenSDK/Operations/WMTOperations.swift index 139dc7b..5f7ad76 100644 --- a/WultraMobileTokenSDK/Operations/WMTOperations.swift +++ b/WultraMobileTokenSDK/Operations/WMTOperations.swift @@ -76,6 +76,15 @@ public protocol WMTOperations: AnyObject { @discardableResult func getHistory(authentication: PowerAuthAuthentication, completion: @escaping(Result<[WMTOperationHistoryEntry], 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) -> Void) -> Operation? + /// Authorize operation with given PowerAuth authentication object. /// /// - Parameters: diff --git a/WultraMobileTokenSDKTests/TOTPParserTests.swift b/WultraMobileTokenSDKTests/TOTPParserTests.swift index 586de14..53751da 100644 --- a/WultraMobileTokenSDKTests/TOTPParserTests.swift +++ b/WultraMobileTokenSDKTests/TOTPParserTests.swift @@ -22,38 +22,38 @@ final class TOTPParserTest: XCTestCase { func testQRTOTPParserWithEmptyCode() { let code = "" - XCTAssertNil(WMTTOTPUtils.parseQRCode(code: code)) + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)) } func testQRTOTPParserWithShortCode() { let code = "abc" - XCTAssertNil(WMTTOTPUtils.parseQRCode(code: code)) + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)) } func testQRTOTPParserWithValidCode() { - let code = "eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiI2YTFjYjAwNy1mZjc1LTRmNDAtYTIxYi0wYjU0NmYwZjZjYWQiLCJ0b3RwIjoiNzM3NDMxOTQifQ==" + let code = "mtoken://login?oid=6a1cb007-ff75-4f40-a21b-0b546f0f6cad&totp=73743194" - XCTAssertEqual(WMTTOTPUtils.parseQRCode(code: code)?.totp, "73743194", "Parsing of totp failed") - XCTAssertEqual(WMTTOTPUtils.parseQRCode(code: code)?.operationId, "6a1cb007-ff75-4f40-a21b-0b546f0f6cad", "Parsing of operationId failed") + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.totp, "73743194", "Parsing of totp failed") + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "6a1cb007-ff75-4f40-a21b-0b546f0f6cad", "Parsing of operationId failed") } func testDeeplinkTOTPParserWithInvalidURL() { let url = URL(string: "mtoken://an-invalid-url.com")! - XCTAssertNil(WMTTOTPUtils.parseDeeplink(url: url)) + XCTAssertNil(WMTPACUtils.parseDeeplink(url: url)) } - func testDeeplinkTOTPParserWithInvalidJWTCode() { + func testDeeplinkParserWithInvalidPACCode() { let url = URL(string: "mtoken://login?code=abc")! - XCTAssertNil(WMTTOTPUtils.parseDeeplink(url: url)) + XCTAssertNil(WMTPACUtils.parseDeeplink(url: url)) } - func testDeeplinkTOTPParserWithValidJWTCode() { - let url = URL(string: "mtoken://login?code=eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiJkZjYxMjhmYy1jYTUxLTQ0YjctYmVmYS1jYTBlMTQwOGFhNjMiLCJ0b3RwIjoiNTY3MjU0OTQifQ==")! + func testDeeplinkParserWithValidPACCode() { + let url = URL(string: "mtoken://login?oid=df6128fc-ca51-44b7-befa-ca0e1408aa63&totp=56725494")! - XCTAssertEqual(WMTTOTPUtils.parseDeeplink(url: url)?.totp, "56725494", "Parsing of totp failed") - XCTAssertEqual(WMTTOTPUtils.parseDeeplink(url: url)?.operationId, "df6128fc-ca51-44b7-befa-ca0e1408aa63", "Parsing of operationId failed") + XCTAssertEqual(WMTPACUtils.parseDeeplink(url: url)?.totp, "56725494", "Parsing of totp failed") + XCTAssertEqual(WMTPACUtils.parseDeeplink(url: url)?.operationId, "df6128fc-ca51-44b7-befa-ca0e1408aa63", "Parsing of operationId failed") } } From bb6975def739e035d2a1257b0dc5c248fe33a3f9 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 28 Nov 2023 11:48:35 +0100 Subject: [PATCH 02/16] #125: update parser so it is able to parse deeplink and also JWT in QR code --- .../Operations/Utils/WMTPACUtils.swift | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift index dad2518..54dea06 100644 --- a/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift +++ b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift @@ -33,10 +33,12 @@ public class WMTPACUtils { } if let operationId = queryItems.first(where: { $0.name == "oid" })?.value { - let totp = queryItems.first(where: { $0.name == "totp" })?.value + let totp = queryItems.first(where: { $0.name == "potp" })?.value return WMTPACData(operationId: operationId, totp: totp) + } else if let code = queryItems.first?.value { + return parseJWT(code: code) } else { - D.error("Failed to get operationId from query items") + D.error("Failed to get Query Items values for parsing") return nil } } @@ -46,14 +48,36 @@ public class WMTPACUtils { if let encodedURLString = code.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedURLString) { return parseDeeplink(url: url) } else { - D.error("Failed to created URL from QR code String.") - return nil + return parseJWT(code: code) } } + + private static func parseJWT(code: String) -> WMTPACData? { + let jwtParts = code.split(separator: ".") + + // At this moment we dont care about header, we want only payload which is the second part of JWT + let jwtBase64String = jwtParts.count > 1 ? String(jwtParts[1]) : "" + + if let base64EncodedData = jwtBase64String.data(using: .utf8), + let dataPayload = Data(base64Encoded: base64EncodedData) { + do { + return try JSONDecoder().decode(WMTPACData.self, from: dataPayload) + } catch { + D.error("Failed to decode JWT from: \(code)") + D.error("With error: \(error)") + return nil + } + } + + D.error("Failed to decode QR JWT from: \(jwtBase64String)") + return nil + } + + } /// Data which is return after parsing PAC code -public struct WMTPACData { +public struct WMTPACData: Decodable { /// The ID of the operation associated with the PAC public let operationId: String From b35394509b1e2b2a886cce4c34cf6023f3142c93 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 28 Nov 2023 15:27:31 +0100 Subject: [PATCH 03/16] Implement Detail endpoint and getDetail method to OperationsImpl --- .../Model/Requests/WMTClaimData.swift | 1 - .../Service/WMTOperationEndpoints.swift | 9 ++++++-- .../Service/WMTOperationsImpl.swift | 21 ++++++++++++++++++- .../Operations/WMTOperations.swift | 9 ++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift b/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift index 990dd8a..69fcbb7 100644 --- a/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift +++ b/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift @@ -14,7 +14,6 @@ // and limitations under the License. // - import Foundation /// Claim payload diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift index ef4c1ad..80799f1 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift @@ -39,8 +39,13 @@ enum WMTOperationEndpoints { static let endpoint: EndpointType = WPNEndpointSigned(endpointURLPath: "/api/auth/token/app/operation/cancel", uriId: "/operation/cancel") } - enum Claim { + enum OperationDetail { + typealias EndpointType = WPNEndpointBasic, WPNResponseBase> + static let endpoint: EndpointType = WPNEndpointBasic(endpointURLPath: "/api/auth/token/app/operation/detail") + } + + enum OperationClaim { typealias EndpointType = WPNEndpointBasic, WPNResponse> - static let endpoint: EndpointType = WPNEndpointBasic(endpointURLPath: "api/auth/token/app/operation/detail/claim") + static let endpoint: EndpointType = WPNEndpointBasic(endpointURLPath: "/api/auth/token/app/operation/detail/claim") } } diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift index 90910f4..9ca2511 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift @@ -237,6 +237,25 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } + func getDetail(operationId: String, completion: @escaping (Result) -> Void) -> Operation? { + guard validateActivation(completion) else { + return nil + } + + let detailData = WMTClaimData(operationId: operationId) + + return networking.post(data: .init(detailData), to: WMTOperationEndpoints.OperationClaim.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) -> Void) -> Operation? { guard validateActivation(completion) else { @@ -245,7 +264,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { let claimData = WMTClaimData(operationId: operationId) - return networking.post(data: .init(claimData), to: WMTOperationEndpoints.Claim.endpoint) { response, error in + return networking.post(data: .init(claimData), to: WMTOperationEndpoints.OperationClaim.endpoint) { response, error in self.processResult(response: response, error: error) { result in switch result { case .success(let operation): diff --git a/WultraMobileTokenSDK/Operations/WMTOperations.swift b/WultraMobileTokenSDK/Operations/WMTOperations.swift index 5f7ad76..36999e7 100644 --- a/WultraMobileTokenSDK/Operations/WMTOperations.swift +++ b/WultraMobileTokenSDK/Operations/WMTOperations.swift @@ -76,6 +76,15 @@ 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) -> 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 From 38cdc2554945727d220444fe08ac5a70590de9aa Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 28 Nov 2023 21:41:57 +0100 Subject: [PATCH 04/16] Add docs for PACUtils --- docs/Using-Operations-Service.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 69e9de8..b9ee493 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -11,7 +11,7 @@ - [Operations API Reference](#operations-api-reference) - [WMTUserOperation](#wmtuseroperation) - [Creating a Custom Operation](#creating-a-custom-operation) -- [TOTP WMTProximityCheck](#totp-wmtproximitycheck) +- [WMTProximityCheck](#wmtproximitycheck) - [Error handling](#error-handling) ## Introduction @@ -601,7 +601,7 @@ public extension WMTOperation { } ``` -## TOTP WMTProximityCheck +## WMTProximityCheck Two-Factor Authentication (2FA) using Time-Based One-Time Passwords (TOTP) in the Operations Service is facilitated through the use of WMTProximityCheck. This allows secure approval of operations through QR code scanning or deeplink handling. @@ -622,6 +622,30 @@ Once the QR code is scanned or match from the deeplink is found, create a `WMTPr - Authorizing the WMTProximityCheck When authorization, the SDK will by default add `timestampSigned` to the `WMTProximityCheck` object. This timestamp indicates when the operation was signed. +### WMTPACUtils +- For convenience, utility class for parsing and extracting data from QR codes and deeplinks used in the PAC (Proximity Anti-fraud Check), is provided. +```swift + /// Data which is returned from parsing PAC code + public struct WMTPACData: Decodable { + + /// The ID of the operation associated with the PAC + public let operationId: String + + /// Time-based one time password used for Proximity antifraud check + public let totp: String? + + public enum Keys: String, CodingKey { + case totp = "potp" + case operationId = "oid" + } + } +``` + + - two methods are provided: + - `parseDeeplink(url: URL) -> WMTPACData?` - uri is expected to be in format `"scheme://code=$JWT"` or `scheme://operation?oid=5b753d0d-d59a-49b7-bec4-eae258566dbb&potp=12345678}` + - `parseQRCode(code: String) -> WMTPACData?` - code is to be expected in the same format as deeplink formats or as a plain JWT + - mentioned JWT should be in format `{“typ”:”JWT”, “alg”:”none”}.{“oid”:”5b753d0d-d59a-49b7-bec4-eae258566dbb”, “potp”:”12345678”} ` + ## Error handling Every error produced by the Operations Service is of a `WMTError` type. For more information see detailed [error handling documentation](Error-Handling.md). From 2fddeaff6a4e896880947a240e5008b42a711c76 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 29 Nov 2023 10:01:57 +0100 Subject: [PATCH 05/16] Remove WMTPACUtils Keys from docs and add description of topt format in JWT/query --- docs/Using-Operations-Service.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index b9ee493..faf524d 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -633,11 +633,6 @@ When authorization, the SDK will by default add `timestampSigned` to the `WMTPro /// Time-based one time password used for Proximity antifraud check public let totp: String? - - public enum Keys: String, CodingKey { - case totp = "potp" - case operationId = "oid" - } } ``` @@ -645,7 +640,10 @@ When authorization, the SDK will by default add `timestampSigned` to the `WMTPro - `parseDeeplink(url: URL) -> WMTPACData?` - uri is expected to be in format `"scheme://code=$JWT"` or `scheme://operation?oid=5b753d0d-d59a-49b7-bec4-eae258566dbb&potp=12345678}` - `parseQRCode(code: String) -> WMTPACData?` - code is to be expected in the same format as deeplink formats or as a plain JWT - mentioned JWT should be in format `{“typ”:”JWT”, “alg”:”none”}.{“oid”:”5b753d0d-d59a-49b7-bec4-eae258566dbb”, “potp”:”12345678”} ` - + + - Accepted formats: + - notice that totp key in JWT and in query shall be `potp`! + ## Error handling Every error produced by the Operations Service is of a `WMTError` type. For more information see detailed [error handling documentation](Error-Handling.md). From a1dc916d19cf3f7d8ce6367bcd3740477eb2f01f Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 29 Nov 2023 10:09:21 +0100 Subject: [PATCH 06/16] Remove unneccesary spaces from doc --- docs/Using-Operations-Service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index faf524d..d7954e7 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -642,7 +642,7 @@ When authorization, the SDK will by default add `timestampSigned` to the `WMTPro - mentioned JWT should be in format `{“typ”:”JWT”, “alg”:”none”}.{“oid”:”5b753d0d-d59a-49b7-bec4-eae258566dbb”, “potp”:”12345678”} ` - Accepted formats: - - notice that totp key in JWT and in query shall be `potp`! + - notice that totp key in JWT and in query shall be `potp`! ## Error handling From 55273e784a6ea35239e436e058baef5250307834 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 29 Nov 2023 10:25:05 +0100 Subject: [PATCH 07/16] Format docs --- docs/Using-Operations-Service.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index d7954e7..1fa0006 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -624,25 +624,26 @@ When authorization, the SDK will by default add `timestampSigned` to the `WMTPro ### WMTPACUtils - For convenience, utility class for parsing and extracting data from QR codes and deeplinks used in the PAC (Proximity Anti-fraud Check), is provided. + ```swift - /// Data which is returned from parsing PAC code - public struct WMTPACData: Decodable { - - /// The ID of the operation associated with the PAC - public let operationId: String - - /// Time-based one time password used for Proximity antifraud check - public let totp: String? - } +/// Data which is returned from parsing PAC code +public struct WMTPACData: Decodable { + + /// The ID of the operation associated with the PAC + public let operationId: String + + /// Time-based one time password used for Proximity antifraud check + public let totp: String? +} ``` - - two methods are provided: +- two methods are provided: - `parseDeeplink(url: URL) -> WMTPACData?` - uri is expected to be in format `"scheme://code=$JWT"` or `scheme://operation?oid=5b753d0d-d59a-49b7-bec4-eae258566dbb&potp=12345678}` - `parseQRCode(code: String) -> WMTPACData?` - code is to be expected in the same format as deeplink formats or as a plain JWT - mentioned JWT should be in format `{“typ”:”JWT”, “alg”:”none”}.{“oid”:”5b753d0d-d59a-49b7-bec4-eae258566dbb”, “potp”:”12345678”} ` - - Accepted formats: - - notice that totp key in JWT and in query shall be `potp`! +- Accepted formats: + - notice that totp key in JWT and in query shall be `potp`! ## Error handling From 0b6cd5740899fbfff55be2e7af41761857b07dda Mon Sep 17 00:00:00 2001 From: Jan Kobersky Date: Wed, 29 Nov 2023 15:29:53 +0100 Subject: [PATCH 08/16] Improved PAC Utils # Conflicts: # WultraMobileTokenSDKTests/TOTPParserTests.swift --- .../Operations/Utils/WMTPACUtils.swift | 40 ++++++++++----- .../TOTPParserTests.swift | 51 +++++++++++++++++-- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift index 93d42d4..1d5d398 100644 --- a/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift +++ b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift @@ -23,18 +23,18 @@ public class WMTPACUtils { public static func parseDeeplink(url: URL) -> WMTPACData? { guard let components = URLComponents(string: url.absoluteString) else { - D.error("Failed to get URLComponents: URLString is malformed") + D.error("Failed to get URLComponents: URLString is malformed \(url)") return nil } guard let queryItems = components.queryItems else { - D.error("Failed to get URLComponents queryItems") + D.error("Failed to get URLComponents queryItems for \(url)") return nil } // Deeplink can have two query items with operationId & optional totp or single query item with JWT value - if let operationId = queryItems.first(where: { $0.name == "oid" })?.value { - let totp = queryItems.first(where: { $0.name == "potp" })?.value + if let operationId = queryItems.first(where: { $0.name == "oid" })?.value?.removingPercentEncoding { + let totp = queryItems.first(where: { $0.name == "totp" || $0.name == "potp" })?.value?.removingPercentEncoding return WMTPACData(operationId: operationId, totp: totp) } else if let code = queryItems.first?.value { return parseJWT(code: code) @@ -59,12 +59,11 @@ public class WMTPACUtils { private static func parseJWT(code: String) -> WMTPACData? { let jwtParts = code.split(separator: ".") - + // At this moment we dont care about header, we want only payload which is the second part of JWT let jwtBase64String = jwtParts.count > 1 ? String(jwtParts[1]) : "" - - if let base64EncodedData = jwtBase64String.data(using: .utf8), - let dataPayload = Data(base64Encoded: base64EncodedData) { + + if let dataPayload = Data(base64Encoded: jwtBase64String.addBase64Padding) { do { return try JSONDecoder().decode(WMTPACData.self, from: dataPayload) } catch { @@ -73,7 +72,7 @@ public class WMTPACUtils { return nil } } - + D.error("Failed to decode QR JWT from: \(jwtBase64String)") return nil } @@ -88,8 +87,9 @@ public struct WMTPACData: Decodable { /// Time-based one time password used for Proximity antifraud check public let totp: String? - public enum Keys: String, CodingKey { - case totp = "potp" + enum Keys: String, CodingKey { + case totp = "totp" + case potp = "potp" // to keep backward compatibility case operationId = "oid" } @@ -101,6 +101,22 @@ public struct WMTPACData: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: Keys.self) operationId = try container.decode(String.self, forKey: .operationId) - totp = try container.decodeIfPresent(String.self, forKey: .totp) + if let t = try container.decodeIfPresent(String.self, forKey: .totp) { + totp = t + } else if let p = try container.decodeIfPresent(String.self, forKey: .potp) { + totp = p + } else { + totp = nil + } + } +} + +private extension String { + var addBase64Padding: String { + let offset = count % 4 + if offset > 0 { + return padding(toLength: count + 4 - offset, withPad: "=", startingAt: 0) + } + return self } } diff --git a/WultraMobileTokenSDKTests/TOTPParserTests.swift b/WultraMobileTokenSDKTests/TOTPParserTests.swift index 7c0c1ed..39c3ed0 100644 --- a/WultraMobileTokenSDKTests/TOTPParserTests.swift +++ b/WultraMobileTokenSDKTests/TOTPParserTests.swift @@ -36,15 +36,60 @@ final class TOTPParserTest: XCTestCase { XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.totp, "73743194", "Parsing of totp failed") XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "6a1cb007-ff75-4f40-a21b-0b546f0f6cad", "Parsing of operationId failed") - } + } + + func testQRTPACParserWithValidDeeplinkCodeAndBase64OID() { + let code = "scheme://operation?oid=E/+DRFVmd4iZABEiM0RVZneImQARIjNEVWZ3iJkAESIzRFVmd4iZAA=&totp=12345678" + + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.totp, "12345678", "Parsing of totp failed") + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "E/+DRFVmd4iZABEiM0RVZneImQARIjNEVWZ3iJkAESIzRFVmd4iZAA=", "Parsing of operationId failed") + } + + func testQRTPACParserWithValidDeeplinkCodeAndBase64EncodedOID() { + let code = "scheme://operation?oid=E%2F%2BDRFVmd4iZABEiM0RVZneImQARIjNEVWZ3iJkAESIzRFVmd4iZAA%3D&totp=12345678" + + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.totp, "12345678", "Parsing of totp failed") + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "E/+DRFVmd4iZABEiM0RVZneImQARIjNEVWZ3iJkAESIzRFVmd4iZAA=", "Parsing of operationId failed") + } func testQRPACParserWithValidJWT() { let code = "eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiIzYjllZGZkMi00ZDgyLTQ3N2MtYjRiMy0yMGZhNWM5OWM5OTMiLCJwb3RwIjoiMTQzNTc0NTgifQ==" - - XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.totp, "14357458", "Parsing of totp failed") + let parsed = WMTPACUtils.parseQRCode(code: code) + XCTAssertEqual(parsed?.totp, "14357458", "Parsing of totp failed") XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "3b9edfd2-4d82-477c-b4b3-20fa5c99c993", "Parsing of operationId failed") } + func testQRPACParserWithValidJWTWithoutPadding() { + let code = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJvaWQiOiJMRG5JY0NjRGhjRHdHNVNLejhLeWdQeG9PbXh3dHpJc29zMEUrSFBYUHlvIiwicG90cCI6IjU4NTkwMDU5In0" + let parsed = WMTPACUtils.parseQRCode(code: code) + XCTAssertEqual(parsed?.totp, "58590059", "Parsing of totp failed") + XCTAssertEqual(parsed?.operationId, "LDnIcCcDhcDwG5SKz8KygPxoOmxwtzIsos0E+HPXPyo", "Parsing of operationId failed") + } + + func testQRPACParserWithInvalidJWT() { + let code = "eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9eyJvaWQiOiIzYjllZGZkMi00ZDgyLTQ3N2MtYjRiMy0yMGZhNWM5OWM5OTMiLCJwb3RwIjoiMTQzNTc0NTgifQ==" + let parsed = WMTPACUtils.parseQRCode(code: code) + XCTAssertNil(parsed, "Parsing of should fail") + } + + func testQRPACParserWithInvalidJWT2() { + let code = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.1eyJvaWQiOiJMRG5JY0NjRGhjRHdHNVNLejhLeWdQeG9PbXh3dHpJc29zMEUrSFBYUHlvIiwicG90cCI6IjU4NTkwMDU5In0" + let parsed = WMTPACUtils.parseQRCode(code: code) + XCTAssertNil(parsed, "Parsing of should fail") + } + + func testQRPACParserWithInvalidJWT3() { + let code = "" + let parsed = WMTPACUtils.parseQRCode(code: code) + XCTAssertNil(parsed, "Parsing of should fail") + } + + func testQRPACParserWithInvalidJWT4() { + let code = "eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.1eyJvaWQiOiJMRG5JY0NjR.GhjRHdHNVNLejhLeWdQeG9PbXh3dHpJc29zMEUrSFBYUHlvIiwicG90cCI6IjU4NTkwMDU5In0=====" + let parsed = WMTPACUtils.parseQRCode(code: code) + XCTAssertNil(parsed, "Parsing of should fail") + } + func testDeeplinkParserWithInvalidPACCode() { let code = "operation?oid=df6128fc-ca51-44b7-befa-ca0e1408aa63&potp=56725494" From e31c92300ebb60c7405bb8335dfa3c845bf9dd8d Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 4 Dec 2023 15:32:27 +0100 Subject: [PATCH 09/16] Remove `Package.resolved` --- Package.resolved | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 26bccf3..0000000 --- a/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "networking-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/wultra/networking-apple.git", - "state" : { - "revision" : "b7ebe23f441e614d13bd6bc803d658e66767e31a", - "version" : "1.2.0" - } - }, - { - "identity" : "powerauth-mobile-sdk-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/wultra/powerauth-mobile-sdk-spm.git", - "state" : { - "revision" : "095be6adfc057501a7cb9a7351697a1b6ed9e64b", - "version" : "1.7.8" - } - } - ], - "version" : 2 -} From cab00ae69d241f1ea787da183e55e3bb0a193da2 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 6 Dec 2023 15:26:06 +0100 Subject: [PATCH 10/16] Fix endpoints to be signed with token --- .../Model/Requests/WMTClaimData.swift | 6 ++++- .../Service/WMTOperationEndpoints.swift | 8 +++---- .../Service/WMTOperationsImpl.swift | 23 ++++++++++++++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift b/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift index 69fcbb7..3cd5a0a 100644 --- a/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift +++ b/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift @@ -17,7 +17,7 @@ import Foundation /// Claim payload -class WMTClaimData: Codable { +class WMTOperationDetailRequest: Codable { /// Operation Id let operationId: String @@ -25,4 +25,8 @@ class WMTClaimData: Codable { init(operationId: String) { self.operationId = operationId } + + enum CodingKeys: String, CodingKey { + case operationId = "id" + } } diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift index 80799f1..5e8ba54 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift @@ -40,12 +40,12 @@ enum WMTOperationEndpoints { } enum OperationDetail { - typealias EndpointType = WPNEndpointBasic, WPNResponseBase> - static let endpoint: EndpointType = WPNEndpointBasic(endpointURLPath: "/api/auth/token/app/operation/detail") + typealias EndpointType = WPNEndpointSignedWithToken, WPNResponse> + static let endpoint: EndpointType = WPNEndpointSignedWithToken(endpointURLPath: "/api/auth/token/app/operation/detail", tokenName: "possession_universal") } enum OperationClaim { - typealias EndpointType = WPNEndpointBasic, WPNResponse> - static let endpoint: EndpointType = WPNEndpointBasic(endpointURLPath: "/api/auth/token/app/operation/detail/claim") + typealias EndpointType = WPNEndpointSignedWithToken, WPNResponse> + static let endpoint: EndpointType = WPNEndpointSignedWithToken(endpointURLPath: "/api/auth/token/app/operation/detail/claim", tokenName: "possession_universal") } } diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift index 9ca2511..93115a2 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift @@ -242,9 +242,9 @@ class WMTOperationsImpl: WMTOperations, WMTService { return nil } - let detailData = WMTClaimData(operationId: operationId) + let detailData = WMTOperationDetailRequest(operationId: operationId) - return networking.post(data: .init(detailData), to: WMTOperationEndpoints.OperationClaim.endpoint) { response, error in + 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): @@ -262,13 +262,13 @@ class WMTOperationsImpl: WMTOperations, WMTService { return nil } - let claimData = WMTClaimData(operationId: operationId) + let claimData = WMTOperationDetailRequest(operationId: operationId) - return networking.post(data: .init(claimData), to: WMTOperationEndpoints.OperationClaim.endpoint) { response, error in + 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.replace(with: [operation]) + self.operationsRegister.add(operation) completion(.success(operation)) case .failure(let err): completion(.failure(self.adjustOperationError(err, auth: false))) @@ -512,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 @@ -543,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) From 91f8e0eb7e515c576a579228406a884ab99b5980 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 6 Dec 2023 15:55:02 +0100 Subject: [PATCH 11/16] Update docs --- docs/Using-Operations-Service.md | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 1fa0006..987541e 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -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) +- [Claim an Operation](#claim-an-operation) - [Off-line Authorization](#off-line-authorization) - [Operations API Reference](#operations-api-reference) - [WMTUserOperation](#wmtuseroperation) @@ -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 + +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) { + 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. From 183f3e957433a167972885353c527646f81eb6f3 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 6 Dec 2023 15:55:24 +0100 Subject: [PATCH 12/16] Implement detail and claim integration tests --- .../IntegrationProxy.swift | 32 ++++++++++++ .../IntegrationTests.swift | 52 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index 8c7162a..532bd03 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -89,6 +89,28 @@ class IntegrationProxy { } } + func createNonPersonalisedOperation(_ factors: Factors = .F_2FA, completion: @escaping (NonPersonalisedOperationObject?) -> Void) { + DispatchQueue.global().async { + let opBody: String + switch factors { + case .F_2FA: + opBody = """ + { + "template": "login", + "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 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")) @@ -244,6 +266,16 @@ struct OperationObject: Codable { let timestampExpires: Int } +struct NonPersonalisedOperationObject: Codable { + let operationId: String + let status: String + let operationType: String + let failureCount: Int + let maxFailureCount: Int + let timestampCreated: Int + let timestampExpires: Int +} + private struct IntegrationConfig: Codable { let cloudServerUrl: String let cloudServerLogin: String diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index 3ae7e82..a94d94a 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -91,6 +91,58 @@ class IntegrationTests: XCTestCase { waitForExpectations(timeout: 20, handler: nil) } + /// Operation IDs should be equal + func testDetail() { + let exp = expectation(description: "Operation detail") + + proxy.createNonPersonalisedOperation { 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.createNonPersonalisedOperation { op in + if let op { + DispatchQueue.main.async { + _ = self.ops.claim(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) + } + /// `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") From cb0e438a96a9655da2b72fe61d58e74c7be9a982 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 6 Dec 2023 17:04:39 +0100 Subject: [PATCH 13/16] Rename WMTClaim file --- WultraMobileTokenSDK.xcodeproj/project.pbxproj | 11 +++++------ ...laimData.swift => WMTOperationDetailRequest.swift} | 0 2 files changed, 5 insertions(+), 6 deletions(-) rename WultraMobileTokenSDK/Operations/Model/Requests/{WMTClaimData.swift => WMTOperationDetailRequest.swift} (100%) diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index be1ff5b..c91279e 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -71,7 +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 /* WMTClaimData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTClaimData.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 */; }; @@ -158,7 +158,7 @@ EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPreApprovalScreen.swift; sourceTree = ""; }; EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = ""; }; EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; - EA7A6E572B0E639800C1D4F4 /* WMTClaimData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTClaimData.swift; sourceTree = ""; }; + EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = ""; }; EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = ""; }; EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = ""; }; EAB705492AF1161500756AC2 /* PACParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PACParserTests.swift; sourceTree = ""; }; @@ -435,7 +435,7 @@ children = ( DCC5CCD7244DBBBD004679AC /* WMTAuthorizationData.swift */, DCC5CCD9244DBBE2004679AC /* WMTRejectionData.swift */, - EA7A6E572B0E639800C1D4F4 /* WMTClaimData.swift */, + EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */, ); path = Requests; sourceTree = ""; @@ -509,7 +509,6 @@ DCC5CC912449EE21004679AC /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1140; LastUpgradeCheck = 1500; ORGANIZATIONNAME = Wultra; @@ -655,7 +654,7 @@ EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */, DC48803D292282FF00DB844B /* WMTInbox.swift in Sources */, DC488042292282FF00DB844B /* WMTInboxImpl.swift in Sources */, - EA7A6E582B0E639800C1D4F4 /* WMTClaimData.swift in Sources */, + EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift b/WultraMobileTokenSDK/Operations/Model/Requests/WMTOperationDetailRequest.swift similarity index 100% rename from WultraMobileTokenSDK/Operations/Model/Requests/WMTClaimData.swift rename to WultraMobileTokenSDK/Operations/Model/Requests/WMTOperationDetailRequest.swift From 356c588e630a7cc877c3545ff2586c675f5963fd Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 7 Dec 2023 12:15:48 +0100 Subject: [PATCH 14/16] Improve claim integration test --- .../IntegrationProxy.swift | 14 +++++-- .../IntegrationTests.swift | 42 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index 532bd03..6869fce 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -89,14 +89,15 @@ class IntegrationProxy { } } - func createNonPersonalisedOperation(_ factors: Factors = .F_2FA, completion: @escaping (NonPersonalisedOperationObject?) -> Void) { + func createNonPersonalisedPACOperation(_ factors: Factors = .F_2FA, completion: @escaping (NonPersonalisedTOTPOperationObject?) -> Void) { DispatchQueue.global().async { let opBody: String switch factors { case .F_2FA: opBody = """ { - "template": "login", + "template": "login_preApproval", + "proximityCheckEnabled": true, "parameters": { "party.id": "666", "party.name": "Datová schránka", @@ -111,6 +112,12 @@ class IntegrationProxy { } } + 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")) @@ -266,7 +273,7 @@ struct OperationObject: Codable { let timestampExpires: Int } -struct NonPersonalisedOperationObject: Codable { +struct NonPersonalisedTOTPOperationObject: Codable { let operationId: String let status: String let operationType: String @@ -274,6 +281,7 @@ struct NonPersonalisedOperationObject: Codable { let maxFailureCount: Int let timestampCreated: Int let timestampExpires: Int + let proximityOtp: String? } private struct IntegrationConfig: Codable { diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index a94d94a..137fdaf 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -95,7 +95,7 @@ class IntegrationTests: XCTestCase { func testDetail() { let exp = expectation(description: "Operation detail") - proxy.createNonPersonalisedOperation { op in + proxy.createNonPersonalisedPACOperation { op in if let op { DispatchQueue.main.async { _ = self.ops.getDetail(operationId: op.operationId) { result in @@ -121,19 +121,47 @@ class IntegrationTests: XCTestCase { func testClaim() { let exp = expectation(description: "Operation Claim should return UserOperation with operation.id") - proxy.createNonPersonalisedOperation { op in + proxy.createNonPersonalisedPACOperation { op in if let op { DispatchQueue.main.async { _ = self.ops.claim(operationId: op.operationId) { result in switch result { case .success(let operation): - XCTAssertEqual(op.operationId, operation.id) - case .failure(let err): - XCTFail(err.description) + 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() + } } - exp.fulfill() } - } } else { XCTFail("Failed to get operation detail") exp.fulfill() From 4d52fc6baeb865af5daf3ec855c878b78a7abcba Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 8 Jan 2024 11:21:13 +0100 Subject: [PATCH 15/16] Improve docs --- .../Operations/WMTOperations.swift | 2 +- docs/Using-Operations-Service.md | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/WMTOperations.swift b/WultraMobileTokenSDK/Operations/WMTOperations.swift index 36999e7..7d19638 100644 --- a/WultraMobileTokenSDK/Operations/WMTOperations.swift +++ b/WultraMobileTokenSDK/Operations/WMTOperations.swift @@ -85,7 +85,7 @@ public protocol WMTOperations: AnyObject { @discardableResult func getDetail(operationId: String, completion: @escaping(Result) -> Void) -> Operation? - /// Claims the "anonymous" operation to be assigned to the user + /// Assigns the 'non-personalized' operation to the user /// - Parameters: /// - operationId: Operation ID which will be claimed to belong to the user /// - completion: Result completion. diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index bf2b35f..fa89f15 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -7,8 +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) -- [Claim an Operation](#claim-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) @@ -212,7 +212,7 @@ func reject(operation: WMTOperation, reason: WMTRejectionReason) { } ``` -## Detail of an Operation +## 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. @@ -220,9 +220,9 @@ To get a detail of an operation based on operation ID use `WMTOperations.getDeta import WultraMobileTokenSDK import PowerAuth2 -// Reject operation with some reason -func getDetail(operationId: String, reason: WMTRejectionReason) { - operationService.getDetail(operationId: operationId) { error in +// 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 @@ -235,21 +235,28 @@ func getDetail(operationId: String, reason: WMTRejectionReason) { } ``` -## Claim an Operation +## 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. -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. +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) { - operationService.reject(operation: operation, reason: reason) { error in - if let error = error { - // show error UI - } else { - // show success UI +// Assigns the 'non-personalized' operation to the user +func claim(operationId: String) { + operationService.claim(operation: operation, reason: reason) { result in + switch result { + case .success(let operation): + // process operation + break + case .failure(let error): + // process error + break } } } From 5aa35d012c9de0a4a565be0c756fd3cbd9027eca Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 9 Jan 2024 11:57:04 +0100 Subject: [PATCH 16/16] Fix claim docs --- docs/Using-Operations-Service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index fa89f15..9740dc2 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -249,7 +249,7 @@ import PowerAuth2 // Assigns the 'non-personalized' operation to the user func claim(operationId: String) { - operationService.claim(operation: operation, reason: reason) { result in + operationService.claim(operationId: operationId) { result in switch result { case .success(let operation): // process operation