diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index a41e3c8..9ec25d6 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -72,8 +72,8 @@ EA6DDF1A29F804D60011E234 /* WMTPostApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */; }; EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; - EA9CE2C22AEBDB0D00FE4E35 /* WMTTOTPUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTTOTPUtils.swift */; }; - EAB7054A2AF1161500756AC2 /* TOTPParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB705492AF1161500756AC2 /* TOTPParserTests.swift */; }; + EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; + EAB7054A2AF1161500756AC2 /* PACParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB705492AF1161500756AC2 /* PACParserTests.swift */; }; EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */; }; /* End PBXBuildFile section */ @@ -158,8 +158,8 @@ EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = ""; }; EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.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 = ""; }; - EAB705492AF1161500756AC2 /* TOTPParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPParserTests.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 = ""; }; EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTJsonValue.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -277,7 +277,7 @@ DC395C0924E55B9B0007C36E /* PushParserTests.swift */, DC6EDB7825A49ED900A229E4 /* OperationExpirationTests.swift */, DC616235248508F8000DED17 /* QROperationParserTests.swift */, - EAB705492AF1161500756AC2 /* TOTPParserTests.swift */, + EAB705492AF1161500756AC2 /* PACParserTests.swift */, ); path = WultraMobileTokenSDKTests; sourceTree = ""; @@ -295,7 +295,7 @@ DC6E52D4259C959900FC25BE /* Utils */ = { isa = PBXGroup; children = ( - EA9CE2C12AEBDB0D00FE4E35 /* WMTTOTPUtils.swift */, + EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */, DC6E52D5259C964600FC25BE /* WMTOperationExpirationWatcher.swift */, EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */, ); @@ -587,7 +587,7 @@ DC61624224852B6D000DED17 /* NetworkingObjectsTests.swift in Sources */, DC395C0A24E55B9B0007C36E /* PushParserTests.swift in Sources */, DC6EDB7925A49ED900A229E4 /* OperationExpirationTests.swift in Sources */, - EAB7054A2AF1161500756AC2 /* TOTPParserTests.swift in Sources */, + EAB7054A2AF1161500756AC2 /* PACParserTests.swift in Sources */, DC616236248508F8000DED17 /* QROperationParserTests.swift in Sources */, EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */, DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */, @@ -628,7 +628,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 */, diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift new file mode 100644 index 0000000..d625a5a --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Utils/WMTPACUtils.swift @@ -0,0 +1,122 @@ +// +// 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 \(url)") + return nil + } + + guard let queryItems = components.queryItems else { + 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?.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) + } else { + D.error("Failed to get Query Items values for parsing") + return nil + } + } + + /// Method accepts scanned code as a String and returns PAC data - it can be in deeplink format or JWT + public static func parseQRCode(code: String) -> WMTPACData? { + guard let encodedURLString = code.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: encodedURLString) else { + return parseJWT(code: code) + } + // if the QR code is in the deeplink format parse it the same way as the deeplink + if url.scheme != nil { + return parseDeeplink(url: url) + } else { + 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 dataPayload = Data(base64Encoded: jwtBase64String.addBase64Padding) { + 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 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? + + enum Keys: String, CodingKey { + case totp = "totp" + case potp = "potp" // to keep backward compatibility + case operationId = "oid" + } + + public init(operationId: String, totp: String?) { + self.operationId = operationId + self.totp = totp + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Keys.self) + operationId = try container.decode(String.self, forKey: .operationId) + 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/WultraMobileTokenSDK/Operations/Utils/WMTTOTPUtils.swift b/WultraMobileTokenSDK/Operations/Utils/WMTTOTPUtils.swift deleted file mode 100644 index 8f3e1d0..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 = "potp" - 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/WultraMobileTokenSDKTests/PACParserTests.swift b/WultraMobileTokenSDKTests/PACParserTests.swift new file mode 100644 index 0000000..2ed3cb2 --- /dev/null +++ b/WultraMobileTokenSDKTests/PACParserTests.swift @@ -0,0 +1,137 @@ +// +// 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 XCTest +import WultraMobileTokenSDK + +final class PACParserTest: XCTestCase { + + func testQRPACParserWithEmptyCode() { + let code = "" + + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)) + } + + func testQRPACParserWithShortInvalidCode() { + let code = "abc" + + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)) + } + + func testQRTPACParserWithValidDeeplinkCode() { + let code = "scheme://operation?oid=6a1cb007-ff75-4f40-a21b-0b546f0f6cad&potp=73743194" + + 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==" + 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" + + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)) + } + + func testDeeplinkPACParserWithInvalidURL() { + let url = URL(string: "scheme://an-invalid-url.com")! + XCTAssertNil(WMTPACUtils.parseDeeplink(url: url)) + } + + func testDeeplinkParserWithValidURLButInvalidQuery() { + let url = URL(string: "scheme://operation?code=abc")! + + XCTAssertNil(WMTPACUtils.parseDeeplink(url: url)) + } + + func testDeeplinkPACParserWithValidJWTCode() { + let url = URL(string: "scheme://operation?code=eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiIzYjllZGZkMi00ZDgyLTQ3N2MtYjRiMy0yMGZhNWM5OWM5OTMiLCJwb3RwIjoiMTQzNTc0NTgifQ==")! + + XCTAssertEqual(WMTPACUtils.parseDeeplink(url: url)?.totp, "14357458", "Parsing of totp failed") + XCTAssertEqual(WMTPACUtils.parseDeeplink(url: url)?.operationId, "3b9edfd2-4d82-477c-b4b3-20fa5c99c993", "Parsing of operationId failed") + } + + func testDeeplinkParserWithValidPACCode() { + let url = URL(string: "scheme://operation?oid=df6128fc-ca51-44b7-befa-ca0e1408aa63&potp=56725494")! + + 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") + } + + func testDeeplinkPACParserWithValidAnonymousDeeplinkQRCode() { + let code = "scheme://operation?oid=df6128fc-ca51-44b7-befa-ca0e1408aa63" + + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)?.totp) + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "df6128fc-ca51-44b7-befa-ca0e1408aa63", "Parsing of operationId failed") + } + + func testDeeplinkPACParserWithAnonymousJWTQRCodeWithOnlyOperationId() { + let code = "eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiI1YWM0YjNlOC05MjZmLTQ1ZjAtYWUyOC1kMWJjN2U2YjA0OTYifQ==" + + XCTAssertNil(WMTPACUtils.parseQRCode(code: code)?.totp) + XCTAssertEqual(WMTPACUtils.parseQRCode(code: code)?.operationId, "5ac4b3e8-926f-45f0-ae28-d1bc7e6b0496", "Parsing of operationId failed") + } +} diff --git a/WultraMobileTokenSDKTests/TOTPParserTests.swift b/WultraMobileTokenSDKTests/TOTPParserTests.swift deleted file mode 100644 index 1e5afea..0000000 --- a/WultraMobileTokenSDKTests/TOTPParserTests.swift +++ /dev/null @@ -1,59 +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 XCTest -import WultraMobileTokenSDK - -final class TOTPParserTest: XCTestCase { - - func testQRTOTPParserWithEmptyCode() { - let code = "" - - XCTAssertNil(WMTTOTPUtils.parseQRCode(code: code)) - } - - func testQRTOTPParserWithShortCode() { - let code = "abc" - - XCTAssertNil(WMTTOTPUtils.parseQRCode(code: code)) - } - - func testQRTOTPParserWithValidCode() { - let code = "eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiI5OWZjZjc5Mi1mMjhiLTRhZGEtYmVlNy1mYjY4ZDE5ZTA1OGYiLCJwb3RwIjoiNjI2NTY0MTMifQ==" - - XCTAssertEqual(WMTTOTPUtils.parseQRCode(code: code)?.totp, "62656413", "Parsing of totp failed") - XCTAssertEqual(WMTTOTPUtils.parseQRCode(code: code)?.operationId, "99fcf792-f28b-4ada-bee7-fb68d19e058f", "Parsing of operationId failed") - } - - - func testDeeplinkTOTPParserWithInvalidURL() { - let url = URL(string: "mtoken://an-invalid-url.com")! - XCTAssertNil(WMTTOTPUtils.parseDeeplink(url: url)) - } - - func testDeeplinkTOTPParserWithInvalidJWTCode() { - let url = URL(string: "mtoken://login?code=abc")! - - XCTAssertNil(WMTTOTPUtils.parseDeeplink(url: url)) - } - - func testDeeplinkTOTPParserWithValidJWTCode() { - let url = URL(string: "mtoken://login?code=eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9.eyJvaWQiOiI5OWZjZjc5Mi1mMjhiLTRhZGEtYmVlNy1mYjY4ZDE5ZTA1OGYiLCJwb3RwIjoiNjI2NTY0MTMifQ==")! - - XCTAssertEqual(WMTTOTPUtils.parseDeeplink(url: url)?.totp, "62656413", "Parsing of totp failed") - XCTAssertEqual(WMTTOTPUtils.parseDeeplink(url: url)?.operationId, "99fcf792-f28b-4ada-bee7-fb68d19e058f", "Parsing of operationId failed") - } -} diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 69e9de8..1fa0006 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,29 @@ 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? +} +``` + +- 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`! + ## 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).