diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff1b089..f8ab76c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,11 @@ on: jobs: build: name: Build - runs-on: macos-11.0 + runs-on: macos-12 steps: - name: Checkout the repo uses: actions/checkout@v2 - name: Building + env: + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./scripts/build.sh \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0316421..a6e9ec5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ on: jobs: pod: name: Pod Lib Lint - runs-on: macos-11.0 + runs-on: macos-12 steps: - name: Checkout the repo uses: actions/checkout@v2 @@ -18,7 +18,7 @@ jobs: run: pod lib lint --allow-warnings swift: name: Swift Lint - runs-on: macos-11.0 + runs-on: macos-12 steps: - name: Checkout the repo uses: actions/checkout@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4bb5823..98d63be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ on: jobs: publish: name: Publish - runs-on: macos-latest + runs-on: macos-12 steps: - name: Checkout the repo uses: actions/checkout@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f6f699..3895d51 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,18 +13,20 @@ on: jobs: tests: name: Tests - runs-on: macos-11.0 + runs-on: macos-12 steps: - name: Checkout the repo uses: actions/checkout@v2 - name: Runing Tests env: + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} APP_KEY: ${{ secrets.TESTS_APP_KEY }} APP_SECRET: ${{ secrets.TESTS_APP_SECRET }} MASTER_SERVER_PUBLIC_KEY : ${{ secrets.TESTS_MASTER_SERVER_PUBLIC_KEY }} CL_URL: ${{ secrets.TESTS_CL_URL }} CL_LGN: ${{ secrets.TESTS_CL_LGN }} CL_PWD: ${{ secrets.TESTS_CL_PWD }} + CL_AID: ${{ secrets.TESTS_CL_AID }} OP_URL: ${{ secrets.TESTS_OP_URL }} ER_URL: ${{ secrets.TESTS_ER_URL }} - run: ./scripts/test.sh -destination "platform=iOS Simulator,OS=15.2,name=iPhone SE (2nd generation)" -appkey "$APP_KEY" -appsecret "$APP_SECRET" -masterspk "$MASTER_SERVER_PUBLIC_KEY" -er "$ER_URL" -op "$OP_URL" -cl "$CL_URL" -clu "$CL_LGN" -clp "$CL_PWD" \ No newline at end of file + run: ./scripts/test.sh -appkey "$APP_KEY" -appsecret "$APP_SECRET" -masterspk "$MASTER_SERVER_PUBLIC_KEY" -er "$ER_URL" -op "$OP_URL" -cl "$CL_URL" -clu "$CL_LGN" -clp "$CL_PWD" -cla "$CL_AID" \ No newline at end of file diff --git a/Cartfile b/Cartfile index b2d21ab..fc5ba05 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1 @@ -github "wultra/powerauth-mobile-sdk" "release/1.6.x" -github "wultra/networking-apple" "release/1.0.x" +github "wultra/networking-apple" "release/1.1.x" diff --git a/Cartfile.resolved b/Cartfile.resolved index 606a284..3be1e51 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,3 @@ -github "wultra/networking-apple" "7543f158013c92889fcb349ec789ce90a9f22910" -github "wultra/powerauth-mobile-sdk" "b86c15ced7303dd446619a8d714470660d55407b" +binary "https://raw.githubusercontent.com/wultra/powerauth-mobile-sdk-spm/develop/PowerAuth2.json" "1.7.1" +binary "https://raw.githubusercontent.com/wultra/powerauth-mobile-sdk-spm/develop/PowerAuthCore.json" "1.7.1" +github "wultra/networking-apple" "eaf413b0ef0fcf2c5cef9c3ff0735b2643450e8c" diff --git a/Deploy/WultraMobileTokenSDK.podspec b/Deploy/WultraMobileTokenSDK.podspec index 2cf9e71..76c8b8d 100644 --- a/Deploy/WultraMobileTokenSDK.podspec +++ b/Deploy/WultraMobileTokenSDK.podspec @@ -18,8 +18,8 @@ Pod::Spec.new do |s| # 'Common' subspec s.subspec 'Common' do |sub| sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' - sub.dependency 'PowerAuth2', '>= 1.6' - sub.dependency 'WultraPowerAuthNetworking', '>= 1.0.2' + sub.dependency 'PowerAuth2', '>= 1.7' + sub.dependency 'WultraPowerAuthNetworking', '>= 1.1.4' end # 'Operations' subspec diff --git a/Package.swift b/Package.swift index 3e0512d..2bcd145 100644 --- a/Package.swift +++ b/Package.swift @@ -11,8 +11,8 @@ let package = Package( .library(name: "WultraMobileTokenSDK", targets: ["WultraMobileTokenSDK"]) ], dependencies: [ - .package(name: "PowerAuth2", url: "https://github.com/wultra/powerauth-mobile-sdk-spm.git", .upToNextMinor(from: "1.6.2")), - .package(name: "WultraPowerAuthNetworking", url: "https://github.com/wultra/networking-apple.git", .upToNextMinor(from: "1.1.3")) + .package(name: "PowerAuth2", url: "https://github.com/wultra/powerauth-mobile-sdk-spm.git", .upToNextMinor(from: "1.7.0")), + .package(name: "WultraPowerAuthNetworking", url: "https://github.com/wultra/networking-apple.git", .upToNextMinor(from: "1.1.4")) ], targets: [ .target( diff --git a/WultraMobileTokenSDK.podspec b/WultraMobileTokenSDK.podspec index 9aff681..9f90b0c 100644 --- a/WultraMobileTokenSDK.podspec +++ b/WultraMobileTokenSDK.podspec @@ -18,8 +18,8 @@ Pod::Spec.new do |s| # 'Common' subspec s.subspec 'Common' do |sub| sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' - sub.dependency 'PowerAuth2', '>= 1.6' - sub.dependency 'WultraPowerAuthNetworking', '>= 1.0.2' + sub.dependency 'PowerAuth2', '>= 1.7' + sub.dependency 'WultraPowerAuthNetworking', '>= 1.1.4' end # 'Operations' subspec diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index 0c6a22f..cbf0751 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - DC059A3E244DDC0900B24878 /* WMTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC059A3D244DDC0900B24878 /* WMTConfig.swift */; }; DC06D01F25AC74E400F2EA69 /* WMTLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC06D01E25AC74E400F2EA69 /* WMTLock.swift */; }; DC395C0A24E55B9B0007C36E /* PushParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC395C0924E55B9B0007C36E /* PushParserTests.swift */; }; DC3D0B372480F3C7000DC4D9 /* WMTOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3D0B362480F3C7000DC4D9 /* WMTOperation.swift */; }; @@ -63,7 +62,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - DC059A3D244DDC0900B24878 /* WMTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTConfig.swift; sourceTree = ""; }; DC06D01E25AC74E400F2EA69 /* WMTLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTLock.swift; sourceTree = ""; }; DC395C0924E55B9B0007C36E /* PushParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushParserTests.swift; sourceTree = ""; }; DC3D0B362480F3C7000DC4D9 /* WMTOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperation.swift; sourceTree = ""; }; @@ -165,13 +163,13 @@ isa = PBXGroup; children = ( DC5F6DE724E14FA100D351D3 /* Configs */, - DC616235248508F8000DED17 /* QROperationParserTests.swift */, DC616237248508F8000DED17 /* Info.plist */, DC61624124852B6D000DED17 /* NetworkingObjectsTests.swift */, DCE660D024CEBECA00870E53 /* IntegrationTests.swift */, DCE660D224CEF56400870E53 /* IntegrationUtils.swift */, DC395C0924E55B9B0007C36E /* PushParserTests.swift */, DC6EDB7825A49ED900A229E4 /* OperationExpirationTests.swift */, + DC616235248508F8000DED17 /* QROperationParserTests.swift */, ); path = WultraMobileTokenSDKTests; sourceTree = ""; @@ -246,7 +244,6 @@ DC81D1CE24502E0300F80CD6 /* Common */ = { isa = PBXGroup; children = ( - DC059A3D244DDC0900B24878 /* WMTConfig.swift */, DCC5CCCD244DB0AD004679AC /* WMTLogger.swift */, DC06D01E25AC74E400F2EA69 /* WMTLock.swift */, DC9511F826EA02C100FF40AD /* WPNIntegration.swift */, @@ -366,6 +363,7 @@ DCC5CC962449EE21004679AC /* Sources */, DCC5CC972449EE21004679AC /* Frameworks */, DCC5CC982449EE21004679AC /* Resources */, + DCDA0A3128A6851400EDB6D4 /* Swift Lint */, ); buildRules = ( ); @@ -432,6 +430,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + DCDA0A3128A6851400EDB6D4 /* Swift Lint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Swift Lint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if ! [ -x \"$(command -v swiftlint)\" ]; then\n echo 'warning: swiftlint is not installed on this computer.' >&2\n exit 0\nfi\n\nswiftlint\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ DC61622F248508F8000DED17 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -455,7 +474,6 @@ DCC5CCB52449F8E9004679AC /* WMTOperationAttributeAmount.swift in Sources */, DCC5CCD6244DBB7F004679AC /* WMTPushRegistrationData.swift in Sources */, DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */, - DC059A3E244DDC0900B24878 /* WMTConfig.swift in Sources */, DCD8B336246C1BAF00385F02 /* WMTRejectionReason.swift in Sources */, DCC5CCD8244DBBBD004679AC /* WMTAuthorizationData.swift in Sources */, DC3D0B372480F3C7000DC4D9 /* WMTOperation.swift in Sources */, diff --git a/WultraMobileTokenSDK/Common/WMTConfig.swift b/WultraMobileTokenSDK/Common/WMTConfig.swift deleted file mode 100644 index 70458df..0000000 --- a/WultraMobileTokenSDK/Common/WMTConfig.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright 2020 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 -import WultraPowerAuthNetworking - -/// Configuration class used for configuring WultraMobileTokenSDK services. -public struct WMTConfig { - - /// Base URL for service requests. - public let baseUrl: URL - - /// SSL validation strategy for the request. - public let sslValidation: WMTSSLValidationStrategy - - public init(baseUrl: URL, sslValidation: WMTSSLValidationStrategy) { - self.baseUrl = baseUrl - self.sslValidation = sslValidation - } - - internal func buildURL(_ endpoint: String) -> URL { - - var relativePath = endpoint - var url = baseUrl - - // if relative path starts with "/", lets remove it to create valid URL - if relativePath.hasPrefix("/") { - relativePath.removeFirst() - } - - url.appendPathComponent(relativePath) - - return url - } - - internal var wpnConfig: WPNConfig { WPNConfig(baseUrl: baseUrl, sslValidation: sslValidation) } -} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTAllowedOperationSignature.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTAllowedOperationSignature.swift index f2d02cf..8c88b5d 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTAllowedOperationSignature.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTAllowedOperationSignature.swift @@ -19,23 +19,23 @@ import Foundation /// Allowed signature types that can be used for operation approval. public class WMTAllowedOperationSignature: Codable { - /// If operation should be signed with 1 or 2 factor authentication + /// If operation should be signed with 1 or 2 factor authentication. public let signatureType: SignatureType - /// What factors ("password" or/and "biometry") can be used for signing this operation. + /// What factors are needed to signing this operation. public let signatureFactors: [SignatureFactors] - /// Helper getter if biometry factor is allowed. - public var isBiometryAllowed: Bool { return signatureFactors.contains(.possessionBiometry) } - // MARK: - INNER CLASSES public enum SignatureType: String, Codable { case singleFactor = "1FA" case twoFactors = "2FA" + // 3-factor scheme is not used in mobile token + // case threeFactors = "3FA" } public enum SignatureFactors: String, Codable { + case possession = "possession" case possessionKnowledge = "possession_knowledge" case possessionBiometry = "possession_biometry" } @@ -58,3 +58,8 @@ public class WMTAllowedOperationSignature: Codable { case signatureFactors = "variants" } } + +public extension WMTAllowedOperationSignature { + /// Helper getter if biometry factor is allowed. + var isBiometryAllowed: Bool { return signatureFactors.contains(.possessionBiometry) } +} diff --git a/WultraMobileTokenSDK/Operations/QR/WMTQROperation.swift b/WultraMobileTokenSDK/Operations/QR/WMTQROperation.swift index eecd88b..4a8bfb3 100644 --- a/WultraMobileTokenSDK/Operations/QR/WMTQROperation.swift +++ b/WultraMobileTokenSDK/Operations/QR/WMTQROperation.swift @@ -51,10 +51,6 @@ public struct WMTQROperation { return nonce } - internal var uriIdForOfflineSigning: String { - return "/operation/authorize/offline" - } - internal var dataForOfflineSigning: Data { return "\(operationId)&\(operationData.sourceString)".data(using: .utf8)! } diff --git a/WultraMobileTokenSDK/Operations/QR/WMTQROperationParser.swift b/WultraMobileTokenSDK/Operations/QR/WMTQROperationParser.swift index 644d3c7..ae117a7 100644 --- a/WultraMobileTokenSDK/Operations/QR/WMTQROperationParser.swift +++ b/WultraMobileTokenSDK/Operations/QR/WMTQROperationParser.swift @@ -303,19 +303,13 @@ public class WMTQROperationParser { bic = nil } let allowedChars = "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" - for c in iban { - if allowedChars.firstIndex(of: c) == nil { - // Invalid character in IBAN - return nil - } + if iban.contains(where: { !allowedChars.contains($0) }) { + // Invalid character in IBAN + return nil } - if let bic = bic { - for c in bic { - if allowedChars.firstIndex(of: c) == nil { - // Invalid character in BIC - return nil - } - } + if bic?.contains(where: { !allowedChars.contains($0) }) == true { + // Invalid character in BIC + return nil } return .account(iban: iban, bic: bic) } diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift index e79ce30..708e9c8 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift @@ -25,11 +25,22 @@ public extension PowerAuthSDK { /// Creates instance of the `WMTOperations` on top of the PowerAuth instance. /// - Parameters: - /// - config: Operations service config + /// - networkingConfig: Networking service config /// - pollingOptions: Polling feature configuration /// - Returns: Operations service - func createWMTOperations(config: WMTConfig, pollingOptions: WMTOperationsPollingOptions = []) -> WMTOperations { - return WMTOperationsImpl(powerAuth: self, config: config, pollingOptions: pollingOptions) + func createWMTOperations(networkingConfig: WPNConfig, pollingOptions: WMTOperationsPollingOptions = []) -> WMTOperations { + return WMTOperationsImpl(networking: WPNNetworkingService(powerAuth: self, config: networkingConfig, serviceName: "WMTOperations"), pollingOptions: pollingOptions) + } +} + +public extension WPNNetworkingService { + + /// Creates instance of the `WMTOperations` on top of the WPNNetworkingService/PowerAuth instance. + /// - Parameters: + /// - pollingOptions: Polling feature configuration + /// - Returns: Operations service + func createWMTOperations(pollingOptions: WMTOperationsPollingOptions = []) -> WMTOperations { + return WMTOperationsImpl(networking: self, pollingOptions: pollingOptions) } } @@ -56,14 +67,13 @@ public extension WMTErrorReason { class WMTOperationsImpl: WMTOperations { // Dependencies - private let powerAuth: PowerAuthSDK + private lazy var powerAuth = networking.powerAuth private let networking: WPNNetworkingService private let qrQueue: OperationQueue = { let q = OperationQueue() q.name = "WMTOperationsQRQueue" return q }() - let config: WMTConfig /// If operation loading is currently in progress private(set) var isLoadingOperations = false { @@ -73,7 +83,7 @@ class WMTOperationsImpl: WMTOperations { } } - var isPollingOperations: Bool { return pollingLock.synchronized { self.isPollingOperationsInternal } } + var isPollingOperations: Bool { return pollingLock.synchronized { isPollingOperationsInternal } } private var isPollingOperationsInternal: Bool { pollingTimer != nil } let pollingOptions: WMTOperationsPollingOptions @@ -101,16 +111,14 @@ class WMTOperationsImpl: WMTOperations { /// Methods of the delegate are always called on the main thread. weak var delegate: WMTOperationsDelegate? - init(powerAuth: PowerAuthSDK, config: WMTConfig, pollingOptions: WMTOperationsPollingOptions = []) { - self.powerAuth = powerAuth - self.networking = WPNNetworkingService(powerAuth: powerAuth, config: config.wpnConfig, serviceName: "WMTOperations") - self.config = config + init(networking: WPNNetworkingService, pollingOptions: WMTOperationsPollingOptions = []) { + self.networking = networking self.pollingOptions = pollingOptions #if os(iOS) if pollingOptions.contains(.pauseWhenOnBackground) { notificationObservers.append(NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let `self` = self else { + guard let self = self else { return } self.pollingLock.synchronized { @@ -120,7 +128,7 @@ class WMTOperationsImpl: WMTOperations { } }) notificationObservers.append(NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let `self` = self else { + guard let self = self else { return } self.pollingLock.synchronized { @@ -144,9 +152,6 @@ class WMTOperationsImpl: WMTOperations { // MARK: - service API - /// Refreshes operations, but does not return any result. For the result, you can - /// add a delegate to `delegates` property. - /// If operations are already loading, the function does nothing. func refreshOperations() { DispatchQueue.main.async { // no need to start new operation loading if there is already one in progress @@ -156,51 +161,37 @@ class WMTOperationsImpl: WMTOperations { } } - /// Retrieves user operations and calls task when finished. - /// - /// - Parameter completion: To be called when operations are loaded. - /// This completion is always called on the main thread. - /// - Returns: Control object in case the operations needs to be canceled. - /// - /// Note: be sure to call this method on the main thread! @discardableResult func getOperations(completion: @escaping GetOperationsCompletion) -> Cancellable { - // getOperations should always be called from main thread to ensure - // order of operations - assert(Thread.isMainThread) - let task = GetOperationsTask(completion: completion) - // register block - self.tasks.append(task) - - // if there is loading in progress, just exit and wait for result - if isLoadingOperations == false { + DispatchQueue.main.async { - isLoadingOperations = true + // register block + self.tasks.append(task) + + // if there is loading in progress, just exit and wait for result + if self.isLoadingOperations == false { - fetchOps { result in - // this callback should be called from main thread to prevent inconsistent state when multiple - // getOperations are called - assert(Thread.isMainThread) - // call all registered blocks and clear them - self.tasks.filter({ $0.isCanceled == false }).forEach { $0.finish(result) } - self.tasks.removeAll() - // reset the state - self.isLoadingOperations = false + self.isLoadingOperations = true + + self.fetchOperations { result in + // this callback should be called from main thread to prevent inconsistent state when multiple + // getOperations are called + assert(Thread.isMainThread) + // call all registered blocks and clear them + self.tasks.filter({ $0.isCanceled == false }).forEach { $0.finish(result) } + self.tasks.removeAll() + // reset the state + self.isLoadingOperations = false + } } } return task } - /// Retrieves the history of user operations with its current status. - /// - Parameters: - /// - authentication: Authentication object for signing. - /// - completion: Result completion. - /// This completion is always called on the main thread. - /// - Returns: Operation object for its state observation. func getHistory(authentication: PowerAuthAuthentication, completion: @escaping (Result<[WMTOperationHistoryEntry], WMTError>) -> Void) -> Operation? { if !powerAuth.hasValidActivation() { @@ -211,29 +202,31 @@ class WMTOperationsImpl: WMTOperations { } return networking.post(data: .init(), signedWith: authentication, to: WMTOperationEndpoints.History.endpoint) { response, error in - DispatchQueue.main.async { - if let result = response?.responseObject { - completion(.success(result)) - } else { - completion(.failure(error ?? WMTError(reason: .unknown))) - } + assert(Thread.isMainThread) + if let result = response?.responseObject { + completion(.success(result)) + } else { + completion(.failure(error ?? WMTError(reason: .unknown))) } } } - /// Authorize operation with given PowerAuth authentication object. - /// - /// - Parameters: - /// - operation: Operation that should be authorized. - /// - authentication: Authentication object for signing. - /// - completion: Result callback (nil on success). - /// This completion is always called on the main thread. - /// - Returns: Operation object for its state observation. func authorize(operation: WMTOperation, authentication: PowerAuthAuthentication, completion: @escaping(WMTError?) -> Void) -> Operation? { + return authorize(operation: operation, with: authentication) { result in + switch result { + case .success: + completion(nil) + case .failure(let error): + completion(error) + } + } + } + + func authorize(operation: WMTOperation, with authentication: PowerAuthAuthentication, completion: @escaping (Result) -> Void) -> Operation? { guard powerAuth.hasValidActivation() else { DispatchQueue.main.async { - completion(WMTError(reason: .missingActivation)) + completion(.failure(WMTError(reason: .missingActivation))) } return nil } @@ -242,58 +235,54 @@ class WMTOperationsImpl: WMTOperations { return networking.post(data: .init(data), signedWith: authentication, to: WMTOperationEndpoints.Authorize.endpoint) { _, error in assert(Thread.isMainThread) - if error == nil { + if let error = error { + completion(.failure(self.adjustOperationError(error, auth: true))) + } else { self.operationsRegister.remove(operation: operation) + completion(.success(())) } - completion(self.adjustOperationError(error, auth: true)) - } } - /// Reject operation with a reason. - /// - /// - Parameters: - /// - operation: Operation that should be rejected. - /// - reason: Reason for the rejection. - /// - completion: Result callback (nil on success). - /// This completion is always called on the main thread. - /// - Returns: Operation object for its state observation. func reject(operation: WMTOperation, reason: WMTRejectionReason, completion: @escaping(WMTError?) -> Void) -> Operation? { + return reject(operation: operation, with: reason) { result in + switch result { + case .success: + completion(nil) + case .failure(let error): + completion(error) + } + } + } + + func reject(operation: WMTOperation, with reason: WMTRejectionReason, completion: @escaping(Result) -> Void) -> Operation? { guard powerAuth.hasValidActivation() else { DispatchQueue.main.async { - completion(WMTError(reason: .missingActivation)) + completion(.failure(WMTError(reason: .missingActivation))) } return nil } - - let auth = PowerAuthAuthentication() - auth.usePossession = true - - return networking.post(data: .init(.init(operationId: operation.id, reason: reason)), signedWith: auth, to: WMTOperationEndpoints.Reject.endpoint) { _, error in - if error == nil { + + return networking.post( + data: .init(.init(operationId: operation.id, reason: reason)), + signedWith: .possession(), + to: WMTOperationEndpoints.Reject.endpoint + ) { _, error in + assert(Thread.isMainThread) + if let error = error { self.operationsRegister.remove(operation: operation) + completion(.failure(self.adjustOperationError(error, auth: false))) + } else { + completion(.success(())) } - completion(self.adjustOperationError(error, auth: false)) } } - /// Will sign the given QR operation with authentication object. - /// - /// Note that the operation will be signed even if the authentication object is - /// not valid as it cannot be verified on the server. - /// - /// - Parameters: - /// - qrOperation: QR operation data - /// - authentication: Authentication object for signing. - /// - completion: Result completion. - /// This completion is always called on the main thread. - /// - Returns: Operation object for its state observation. - func authorize(qrOperation: WMTQROperation, authentication: PowerAuthAuthentication, completion: @escaping (Result) -> Void) -> Operation { + func authorize(qrOperation: WMTQROperation, uriId: String, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void) -> Operation { let op = WPNAsyncBlockOperation { _, markFinished in do { - let uriId = qrOperation.uriIdForOfflineSigning let body = qrOperation.dataForOfflineSigning let nonce = qrOperation.nonceForOfflineSigning let signature = try self.powerAuth.offlineSignature(with: authentication, uriId: uriId, body: body, nonce: nonce) @@ -312,7 +301,6 @@ class WMTOperationsImpl: WMTOperations { return op } - /// Start operations polling func startPollingOperations(interval: TimeInterval, delayStart: Bool) { pollingLock.synchronized { self.startPollingOperationsInternal(interval: interval, delayStart: delayStart) @@ -360,60 +348,41 @@ class WMTOperationsImpl: WMTOperations { // MARK: - private functions - private func fetchAvailableOperations(completion: @escaping ([WMTUserOperation]?, WMTError?) -> Void) { + private func fetchOperations(completion: @escaping GetOperationsCompletion) { + + assert(Thread.isMainThread) if !powerAuth.hasValidActivation() { - completion(nil, WMTError(reason: .missingActivation)) + completion(.failure(WMTError(reason: .missingActivation))) return } - let auth = PowerAuthAuthentication() - auth.usePossession = true - - networking.post(data: .init(), signedWith: auth, to: WMTOperationEndpoints.List.endpoint) { response, error in - completion(response?.responseObject, error) - } - } - - private func shouldContinueLoading() -> Bool { - assert(Thread.isMainThread) // main thread to sync this method with getOperations calls - return tasks.contains { $0.isCanceled == false } - } - - private func fetchOps(completion: @escaping GetOperationsCompletion) { - - fetchAvailableOperations { ops, error in - - guard self.shouldContinueLoading() else { - self.processFetchResult(nil, nil, completion) + networking.post(data: .init(), signedWith: .possession(), to: WMTOperationEndpoints.List.endpoint) { response, error in + assert(Thread.isMainThread) + // if all tasks were canceled, just ignore the result. + guard self.tasks.contains(where: { $0.isCanceled == false }) else { + completion(.failure(WMTError(reason: .unknown))) return } - self.processFetchResult(ops, error, completion) - } - } - - private func processFetchResult(_ operations: [WMTUserOperation]?, _ error: WMTError?, _ completion: GetOperationsCompletion) { - - if let ops = operations { - lastFetchResult = .success(ops) - operationsRegister.replace(with: ops) - } else { - let err = error ?? WMTError(reason: .unknown) - lastFetchResult = .failure(err) - delegate?.operationsFailed(error: err) + let result: GetOperationsResult + + if let ops = response?.responseObject { + result = .success(ops) + self.operationsRegister.replace(with: ops) + } else { + let err = error ?? WMTError(reason: .unknown) + result = .failure(err) + self.delegate?.operationsFailed(error: err) + } + + self.lastFetchResult = result + completion(result) } - - completion(lastFetchResult!) } /// If request for operation fails at known error code, then this private function adjusts description of given AuthError. - /// The provided string is then typically presented into the UI. - private func adjustOperationError(_ error: WMTError?, auth: Bool) -> WMTError? { - - guard let error = error else { - return nil - } + private func adjustOperationError(_ error: WMTError, auth: Bool) -> WMTError { var reason: WMTErrorReason? @@ -454,11 +423,10 @@ private class OperationsRegister { private var currentOperationsSet = Set() /// Returns true if register is empty - var isEmpty: Bool { - return self.currentOperations.isEmpty - } + var isEmpty: Bool { currentOperations.isEmpty } typealias OnChangeCallback = (_ operations: [WMTUserOperation], _ added: [WMTUserOperation], _ removed: [WMTUserOperation]) -> Void + /// Callback that is called everytime that register changed private let onChangeCallback: OnChangeCallback @@ -474,32 +442,28 @@ private class OperationsRegister { // Process received list of operations to build an array of added objects var addedOperations = [WMTUserOperation]() var addedOperationsSet = Set() - for newOp in operations { - if !self.currentOperationsSet.contains(newOp.id) { - // identifier is not in current set - addedOperations.append(newOp) - addedOperationsSet.insert(newOp.id) - } + for newOp in operations where currentOperationsSet.contains(newOp.id) == false { + // identifier is not in current set + addedOperations.append(newOp) + addedOperationsSet.insert(newOp.id) } // Build a list of removed operations let newOperationsSet = Set(operations.map { $0.id }) var removedOperations = [WMTUserOperation]() - for op in self.currentOperations { - if !newOperationsSet.contains(op.id) { - removedOperations.append(op) - } + for op in currentOperations where newOperationsSet.contains(op.id) == false { + removedOperations.append(op) } // Now remove no longer valid operations for removedOp in removedOperations { - if let index = self.currentOperations.firstIndex(where: { $0.id == removedOp.id }) { - self.currentOperations.remove(at: index) - self.currentOperationsSet.remove(removedOp.id) + if let index = currentOperations.firstIndex(where: { $0.id == removedOp.id }) { + currentOperations.remove(at: index) + currentOperationsSet.remove(removedOp.id) } } // ...and append new objects - self.currentOperations.append(contentsOf: addedOperations) - self.currentOperationsSet.formUnion(addedOperationsSet) + currentOperations.append(contentsOf: addedOperations) + currentOperationsSet.formUnion(addedOperationsSet) // we need to call onChanged even if nothing changed, because the objects are replaced by different insntances onChangeCallback(currentOperations, addedOperations, removedOperations) @@ -510,10 +474,10 @@ private class OperationsRegister { /// Removes an operation from register func remove(operation: WMTOperation) { assert(Thread.isMainThread) - if let index = self.currentOperationsSet.firstIndex(of: operation.id) { + if let index = currentOperationsSet.firstIndex(of: operation.id) { currentOperationsSet.remove(at: index) } - if let index = self.currentOperations.firstIndex(where: { $0.id == operation.id }) { + if let index = currentOperations.firstIndex(where: { $0.id == operation.id }) { let removedOperation = currentOperations.remove(at: index) onChangeCallback(currentOperations, [], [removedOperation]) } diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift b/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift index 1a22dfe..956e10f 100644 --- a/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift +++ b/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift @@ -109,13 +109,11 @@ public class WMTOperationExpirationWatcher { return lock.synchronized { let currentDate = currentDateProvider.currentDate - for op in operations { + for op in operations where op.isExpired(currentDate) { // we do not remove expired operations. // Operation can expire during the networking communication. Such operation // would be lost and never reported as expired. - if op.isExpired(currentDate) { - D.warning("WMTOperationExpirationWatcher: You're adding an expired operation to watch.") - } + D.warning("WMTOperationExpirationWatcher: You're adding an expired operation to watch.") } guard operations.isEmpty == false else { diff --git a/WultraMobileTokenSDK/Operations/WMTOperations.swift b/WultraMobileTokenSDK/Operations/WMTOperations.swift index d71dbf0..ba69a8b 100644 --- a/WultraMobileTokenSDK/Operations/WMTOperations.swift +++ b/WultraMobileTokenSDK/Operations/WMTOperations.swift @@ -25,9 +25,6 @@ public protocol WMTOperations: AnyObject { /// Methods of the delegate are always called on the main thread. var delegate: WMTOperationsDelegate? { get set } - /// Configuration for the service. - var config: WMTConfig { get } - /// Configuration of the polling feature var pollingOptions: WMTOperationsPollingOptions { get } @@ -55,8 +52,6 @@ public protocol WMTOperations: AnyObject { /// - Parameter completion: To be called when operations are loaded. /// This completion is always called on the main thread. /// - Returns: Control object in case the operations needs to be canceled. - /// - /// Note: be sure to call this method on the main thread! @discardableResult func getOperations(completion: @escaping GetOperationsCompletion) -> Cancellable @@ -78,21 +73,35 @@ public protocol WMTOperations: AnyObject { /// This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult + @available(*, deprecated, message: "Use method with the result completion instead") func authorize(operation: WMTOperation, authentication: PowerAuthAuthentication, completion: @escaping(WMTError?) -> Void) -> Operation? - /// Will sign the given QR operation with authentication object. + /// Authorize operation with given PowerAuth authentication object. + /// + /// - Parameters: + /// - operation: Operation that should be authorized. + /// - authentication: Authentication object for signing. + /// - completion: Result callback. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + func authorize(operation: WMTOperation, with: PowerAuthAuthentication, completion: @escaping(Result) -> Void) -> Operation? + + /// Will sign the given QR operation with URI ID and authentication object. /// /// Note that the operation will be signed even if the authentication object is /// not valid as it cannot be verified on the server. /// /// - Parameters: - /// - qrOperation: QR operation data + /// - qrOperation: QR operation data. + /// - uriId: Custom signature URI ID of the operation. Use URI ID under which the operation was + /// created on the server. Usually something like `/confirm/offline/operation`. /// - authentication: Authentication object for signing. /// - completion: Result completion. /// This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func authorize(qrOperation: WMTQROperation, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void) -> Operation + func authorize(qrOperation: WMTQROperation, uriId: String, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void) -> Operation /// Reject operation with a reason. /// @@ -103,8 +112,20 @@ public protocol WMTOperations: AnyObject { /// This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult + @available(*, deprecated, message: "Use method with the Result completion instead") func reject(operation: WMTOperation, reason: WMTRejectionReason, completion: @escaping(WMTError?) -> Void) -> Operation? + /// Reject operation with a reason. + /// + /// - Parameters: + /// - operation: Operation that should be rejected. + /// - reason: Reason for the rejection. + /// - completion: Result callback. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + func reject(operation: WMTOperation, with: WMTRejectionReason, completion: @escaping(Result) -> Void) -> Operation? + /// If the service is polling operations var isPollingOperations: Bool { get } @@ -121,6 +142,27 @@ public protocol WMTOperations: AnyObject { func stopPollingOperations() } +public extension WMTOperations { + + /// Will sign the given QR operation with authentication object. + /// + /// Default operation URI ID `/operation/authorize/offline` is used. To customize this value, use + /// the method with `uriId` parameter. + /// + /// Note that the operation will be signed even if the authentication object is + /// not valid as it cannot be verified on the server. + /// + /// - Parameters: + /// - qrOperation: QR operation data + /// - authentication: Authentication object for signing. + /// - completion: Result completion. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + func authorize(qrOperation: WMTQROperation, authentication: PowerAuthAuthentication, completion: @escaping (Result) -> Void) -> Operation { + return authorize(qrOperation: qrOperation, uriId: "/operation/authorize/offline", authentication: authentication, completion: completion) + } +} + public typealias GetOperationsResult = Result<[WMTUserOperation], WMTError> public typealias GetOperationsCompletion = (GetOperationsResult) -> Void diff --git a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift b/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift index 033fc38..eaec8cd 100644 --- a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift +++ b/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift @@ -21,10 +21,19 @@ import WultraPowerAuthNetworking public extension PowerAuthSDK { /// Creates instance of the `WMTPush` on top of the PowerAuth instance. - /// - Parameter config: Push service config + /// - Parameter networkingConfig: Networking service config /// - Returns: Push service - func createWMTPush(config: WMTConfig) -> WMTPush { - return WMTPushImpl(powerAuth: self, config: config) + func createWMTPush(networkingConfig: WPNConfig) -> WMTPush { + return WMTPushImpl(networking: WPNNetworkingService(powerAuth: self, config: networkingConfig, serviceName: "WMTPush")) + } +} + +public extension WPNNetworkingService { + + /// Creates instance of the `WMTPush` on top of the WPNNetworkingService instance. + /// - Returns: Push service + func createWMTPush() -> WMTPush { + return WMTPushImpl(networking: self) } } @@ -36,9 +45,8 @@ public extension WMTErrorReason { class WMTPushImpl: WMTPush { // Dependencies - private let powerAuth: PowerAuthSDK + private lazy var powerAuth = networking.powerAuth private let networking: WPNNetworkingService - let config: WMTConfig private(set) var pushNotificationsRegisteredOnServer = false // Contains true if push notifications were already registered private var pendingRegistrationForRemotePushNotifications = false // Contains true if there's pending registration for push notifications @@ -48,10 +56,8 @@ class WMTPushImpl: WMTPush { set { networking.acceptLanguage = newValue } } - init(powerAuth: PowerAuthSDK, config: WMTConfig) { - self.powerAuth = powerAuth - self.networking = WPNNetworkingService(powerAuth: powerAuth, config: config.wpnConfig, serviceName: "WMTPush") - self.config = config + init(networking: WPNNetworkingService) { + self.networking = networking } @discardableResult @@ -70,15 +76,12 @@ class WMTPushImpl: WMTPush { return nil } - let auth = PowerAuthAuthentication() - auth.usePossession = true - pendingRegistrationForRemotePushNotifications = true pushNotificationsRegisteredOnServer = false let data = WMTPushRegistrationData(token: HexadecimalString.encodeData(token)) - return networking.post(data: .init(data), signedWith: auth, to: WMTPushEndpoints.RegisterDevice.endpoint) { _, error in + return networking.post(data: .init(data), signedWith: .possession(), to: WMTPushEndpoints.RegisterDevice.endpoint) { _, error in self.pendingRegistrationForRemotePushNotifications = false if error == nil { self.pushNotificationsRegisteredOnServer = true diff --git a/WultraMobileTokenSDK/Push/WMTPush.swift b/WultraMobileTokenSDK/Push/WMTPush.swift index 1589fc8..3bd9d6c 100644 --- a/WultraMobileTokenSDK/Push/WMTPush.swift +++ b/WultraMobileTokenSDK/Push/WMTPush.swift @@ -23,9 +23,6 @@ public protocol WMTPush: AnyObject { /// If there was already made an successful request. var pushNotificationsRegisteredOnServer: Bool { get } - /// Configuration for the service. - var config: WMTConfig { get } - /// Accept language for the outgoing requests headers. /// Default value is "en". var acceptLanguage: String { get set } diff --git a/WultraMobileTokenSDKTests/Configs/Readme.md b/WultraMobileTokenSDKTests/Configs/Readme.md index 65e3d39..64c336a 100644 --- a/WultraMobileTokenSDKTests/Configs/Readme.md +++ b/WultraMobileTokenSDKTests/Configs/Readme.md @@ -7,6 +7,7 @@ _Example config:_ "cloudServerUrl" : "https://url-to-my-cloud.com/powerauth-cloud", "cloudServerLogin" : "admin", "cloudServerPassword" : "admin", + "cloudApplicationId" : "dev", "enrollmentServerUrl" : "https://url-to-my-cloud.com/enrollment-server", "operationsServerUrl" : "https://url-to-my-cloud.com/enrollment-server", "appKey" : "14w+JmBGbhluACWuJMAzXp==", diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index d2b1176..2fd92ec 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -199,18 +199,19 @@ class IntegrationTests: XCTestCase { let auth = PowerAuthAuthentication() auth.usePossession = true auth.usePassword = "xxxx" // wrong password on purpose - self.ops.authorize(operation: ops.first!, authentication: auth) { error in - if error != nil { + self.ops.authorize(operation: ops.first!, with: auth) { result in + switch result { + case .failure: let auth = PowerAuthAuthentication() auth.usePossession = true auth.usePassword = Self.pin - self.ops.authorize(operation: ops.first!, authentication: auth) { error in - if let error = error { + self.ops.authorize(operation: ops.first!, with: auth) { result in + if case .failure(let error) = result { XCTFail("Failed to authorize op: \(error.description)") } exp.fulfill() } - } else { + case .success: XCTFail("Operation approved with wrong password") exp.fulfill() } @@ -247,8 +248,8 @@ class IntegrationTests: XCTestCase { exp.fulfill() return } - self.ops.reject(operation: opToReject, reason: .unexpectedOperation) { error in - if let error = error { + self.ops.reject(operation: opToReject, with: .unexpectedOperation) { result in + if case .failure(let error) = result { XCTFail("Failed to reject op: \(error.description)") } exp.fulfill() @@ -396,6 +397,67 @@ class IntegrationTests: XCTestCase { XCTFail("expectation should not have been met") } } + + func testQROperation() { + let exp = expectation(description: "QR Operation integration test") + + // create regular operation + IntegrationUtils.createOperation { op in + + guard let op = op else { + XCTFail("Failed to create operation") + exp.fulfill() + return + } + + // get QR data of the operation + IntegrationUtils.getQROperation(operation: op) { qrData in + guard let qrData = qrData else { + XCTFail("Failed to retrieve QR data") + exp.fulfill() + return + } + + // parse the data + switch WMTQROperationParser().parse(string: qrData.operationQrCodeData) { + case .success(let qrOp): + + let auth = PowerAuthAuthentication() + auth.usePossession = true + auth.usePassword = Self.pin + + // get the OTP with the "offline" signing + _ = self.ops.authorize(qrOperation: qrOp, authentication: auth) { qrAuthResult in + switch qrAuthResult { + case .success(let otp): + + // verify the operation on the backend with the OTP + IntegrationUtils.verifyQROperation(operation: op, operationData: qrData, otp: otp) { verified in + + print("Operation verified with \(verified?.otpValid.description ?? "ERROR") result") + + // success? + if verified?.otpValid == true { + exp.fulfill() + } else { + XCTFail("Failed to verify QR operation") + exp.fulfill() + } + } + case .failure: + XCTFail("Failed to authorize QR operation") + exp.fulfill() + } + } + case .failure: + XCTFail("Failed to parse QR operation") + exp.fulfill() + } + } + } + // there are 3 backend calls, give it some time... + waitForExpectations(timeout: 20, handler: nil) + } } private class OpDelegate: WMTOperationsDelegate { diff --git a/WultraMobileTokenSDKTests/IntegrationUtils.swift b/WultraMobileTokenSDKTests/IntegrationUtils.swift index 2e44521..853a2ec 100644 --- a/WultraMobileTokenSDKTests/IntegrationUtils.swift +++ b/WultraMobileTokenSDKTests/IntegrationUtils.swift @@ -16,11 +16,13 @@ import PowerAuth2 import WultraMobileTokenSDK +import WultraPowerAuthNetworking class IntegrationUtils { private static var config: IntegrationConfig! private static let activationName = UUID().uuidString + private static var registrationId = "" // will be filled when activation is created typealias Callback = (_ instances: (PowerAuthSDK, WMTOperations)?, _ error: String?) -> Void @@ -44,8 +46,8 @@ class IntegrationUtils { if let error = error { callback(nil, error) } else { - let wmtconf = WMTConfig(baseUrl: URL(string: config.operationsServerUrl)!, sslValidation: .noValidation) - callback((pa,pa.createWMTOperations(config: wmtconf, pollingOptions: [.pauseWhenOnBackground])), nil) + let wpnConf = WPNConfig(baseUrl: URL(string: config.operationsServerUrl)!, sslValidation: .noValidation) + callback((pa,pa.createWMTOperations(networkingConfig: wpnConf, pollingOptions: [.pauseWhenOnBackground])), nil) } } } @@ -64,7 +66,7 @@ class IntegrationUtils { opBody = """ { "userId": "\(activationName)", - "template": "login-tpp", + "template": "login", "parameters": { "party.id": "666", "party.name": "Datová schránka", @@ -75,7 +77,26 @@ class IntegrationUtils { """ } - completion(self.makeRequest(url: URL(string: "\(config.cloudServerUrl)/operations")!, body: opBody)) + completion(self.makeRequest(url: URL(string: "\(config.cloudServerUrl)/v2/operations")!, body: opBody)) + } + } + + class func getQROperation(operation: OperationObject, completion: @escaping (QROperationData?) -> Void) { + DispatchQueue.global().async { + completion(self.makeRequest(url: URL(string: "\(config.cloudServerUrl)/v2/operations/\(operation.operationId)/offline/qr?registrationId=\(registrationId)")!, body: "", httpMethod: "GET")) + } + } + + class func verifyQROperation(operation: OperationObject, operationData: QROperationData, otp: String, completion: @escaping (QROperationVerify?) -> Void) { + DispatchQueue.global().async { + let body = """ + { + "otp": "\(otp)", + "nonce": "\(operationData.nonce)", + "registrationId": "\(registrationId)" + } + """ + completion(self.makeRequest(url: URL(string: "\(config.cloudServerUrl)/v2/operations/\(operation.operationId)/offline/otp")!, body: body)) } } @@ -116,6 +137,7 @@ class IntegrationUtils { callback("Create activation on server failed.") return } + registrationId = act.registrationId pa.createActivation(withName: "tests", activationCode: act.activationCode()!) { result, error in guard let _ = result else { callback("Create activation failed.") @@ -127,7 +149,7 @@ class IntegrationUtils { callback("Commit activation locally failed.") return } - guard let _ = commitActivationOnServer() else { + guard let _ = commitActivationOnServer(registrationId: act.registrationId) else { callback("Commit on server failed.") return } @@ -138,26 +160,29 @@ class IntegrationUtils { private class func createActivation() -> RegistrationObject? { let body = """ { - "userId": "\(activationName)" + "userId": "\(activationName)", + "flags": [], + "appId": "\(config.cloudApplicationId)" } """ - let resp: RegistrationObject? = makeRequest(url: URL(string: "\(config.cloudServerUrl)/registration")!, body: body) + let resp: RegistrationObject? = makeRequest(url: URL(string: "\(config.cloudServerUrl)/v2/registrations")!, body: body) return resp } - private class func commitActivationOnServer() -> CommitObject? { + private class func commitActivationOnServer(registrationId: String) -> CommitObject? { let body = """ { - "userId": "\(activationName)" + "externalUserId": "test" } """ - let resp: CommitObject? = makeRequest(url: URL(string: "\(config.cloudServerUrl)/registration/commit")!, body: body) + let resp: CommitObject? = makeRequest(url: URL(string: "\(config.cloudServerUrl)/v2/registrations/\(registrationId)/commit")!, body: body) return resp } } private struct RegistrationObject: Codable { let activationQrCodeData: String + let registrationId: String func activationCode() -> String? { return PowerAuthActivationCodeUtil.parse(fromActivationCode: activationQrCodeData)?.activationCode } } @@ -182,9 +207,26 @@ private struct IntegrationConfig: Codable { let cloudServerUrl: String let cloudServerLogin: String let cloudServerPassword: String + let cloudApplicationId: String let enrollmentServerUrl: String let operationsServerUrl: String let appKey: String let appSecret: String let masterServerPublicKey: String } + +struct QROperationData: Codable { + let operationQrCodeData: String + let nonce: String +} + +struct QROperationVerify: Codable { + let otpValid: Bool + let userId: String + let registrationId: String + let registrationStatus: String + let signatureType: String + let remainingAttempts: Int + // let flags: [] + // let application +} diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 4c4cc66..06dec5a 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -27,24 +27,27 @@ Operations Service communicates with a backend via [Mobile Token API endpoints]( ## Creating an Instance -To create an instance of an operations service, use the following snippet: - +### On Top of the `PowerAuthSDK` instance ```swift import WultraMobileTokenSDK +import WultraPowerAuthNetworking -let opsConfig = WMTConfig( +let networkingConfig = WPNConfig( baseUrl: URL(string: "https://myservice.com/mtoken/operations/api/")!, - sslValidation: .default, - pollingOptions: [.pauseWhenOnBackground] + sslValidation: .default ) -let opsService = powerAuth.createWMTOperations(config: config) +// powerAuth is instance of PowerAuthSDK +let opsService = powerAuth.createWMTOperations(networkingConfig: networkingConfig, pollingOptions: [.pauseWhenOnBackground]) ``` -The `sslValidation` parameter is used when validating HTTPS requests. Following strategies can be used. +### On Top of the `WPNNetworkingService` instance +```swift +import WultraMobileTokenSDK +import WultraPowerAuthNetworking -- `WMTSSLValidationStrategy.default` -- `WMTSSLValidationStrategy.noValidation` -- `WMTSSLValidationStrategy.sslPinning` +// networkingService is instance of WPNNetworkingService +let opsService = networkingService.createWMTOperations(pollingOptions: [.pauseWhenOnBackground]) +``` The `pollingOptions` parameter is used for polling feature configuration. The default value is empty `[]`. Possible options are: @@ -104,10 +107,11 @@ class MyOperationsManager: WMTOperationsDelegate { private let ops: WMTOperations init(powerAuth: PowerAuthSDK) { - let opsConfig = WMTConfig( - baseUrl: URL(string: "https://myservice.com/mtoken/api/")!, - sslValidation: .default) - self.ops = powerAuth.createWMTOperations(config: opsConfig) + let networkingConfig = WPNConfig( + baseUrl: URL(string: "https://myservice.com/mtoken/operations/api/")!, + sslValidation: .default + ) + self.ops = powerAuth.createWMTOperations(networkingConfig: networkingConfig) self.ops.delegate = self } @@ -140,9 +144,7 @@ import PowerAuth2 // Approve operation with password func approve(operation: WMTOperation, password: String) { - let auth = PowerAuthAuthentication() - auth.usePossession = true - auth.usePassword = password + let auth = PowerAuthAuthentication.possessionWithPassword(password: password) operationService.authorize(operation: operation, authentication: auth) { error in if let error = error { @@ -163,10 +165,7 @@ import PowerAuth2 // Approve operation with password func approveWithBiometry(operation: WMTOperation) { - let auth = PowerAuthAuthentication() - auth.usePossession = true - auth.useBiometry = true - auth.biometryPrompt = "Confirm operation." + let auth = PowerAuthAuthentication.possessionWithBiometry(prompt: "Confirm operation.") operationService.authorize(operation: operation, authentication: auth) { error in if let error = error { @@ -208,9 +207,7 @@ import PowerAuth2 // Retrieve operation history with password func history(password: String) { - let auth = PowerAuthAuthentication() - auth.usePossession = true - auth.usePassword = password + let auth = PowerAuthAuthentication.possessionWithPassword(password: password) operationService.getHistory(authentication: auth) { result in switch result { case .success(let operations): @@ -258,6 +255,10 @@ case .failure(let error): An offline operation needs to be __always__ approved with __2-factor scheme__ (password or biometry). + +Each offline operation created on the server has an __URI ID__ to define its purpose and configuration. The default value used here is `/operation/authorize/offline` and can be modified with the `uriId` parameter in the `authrorize` method. + + #### With Password ```swift @@ -266,15 +267,13 @@ import PowerAuth2 func approveQROperation(operation: WMTQROperation, password: String) { - let auth = PowerAuthAuthentication() - auth.usePossession = true - auth.usePassword = password + let auth = PowerAuthAuthentication.possessionWithPassword(password: password) operationsService.authorize(qrOperation: operation, authentication: auth) { result in switch result { case .success(let code): // Display the signature to the user so it can be manually rewritten. - // Note that the operation will be signed even with the wrong password! + // Note that the operation will be signed even with a wrong password! case .failure(let error): // Failed to sign the operation } @@ -286,6 +285,29 @@ func approveQROperation(operation: WMTQROperation, password: String) { An offline operation can and will be signed even with an incorrect password. The signature cannot be used for manual approval in such a case. This behavior cannot be detected, so you should warn the user that an incorrect password will result in an incorrect "approval code". +#### With Password and Custom `uriId` + +```swift +import WultraMobileTokenSDK +import PowerAuth2 + +func approveQROperation(operation: WMTQROperation, password: String) { + + let auth = PowerAuthAuthentication.possessionWithPassword(password: password) + + // using the authorize method with custom uriId + operationsService.authorize(qrOperation: operation, uriId: "/confirm/offline/operation", authentication: auth) { result in + switch result { + case .success(let code): + // Display the signature to the user so it can be manually rewritten. + // Note that the operation will be signed even with a wrong password! + case .failure(let error): + // Failed to sign the operation + } + } +} +``` + #### With Biometry To approve offline operations with biometry, your PowerAuth instance [need to be configured with biometry factor](https://github.com/wultra/powerauth-mobile-sdk/blob/develop/docs/PowerAuth-SDK-for-iOS.md#biometry-setup). @@ -302,10 +324,7 @@ func approveQROperationWithBiometry(operation: WMTQROperation) { return } - let auth = PowerAuthAuthentication() - auth.usePossession = true - auth.useBiometry = true - auth.biometryPrompt = "Confirm operation." + let auth = PowerAuthAuthentication.possessionWithBiometry(prompt: "Confirm operation.") operationsService.authorize(qrOperation: operation, authentication: auth) { result in switch result { @@ -323,7 +342,6 @@ func approveQROperationWithBiometry(operation: WMTQROperation) { All available methods and attributes of `WMTOperations` API are: - `delegate` - Delegate object that receives info about operation loading. Methods of the delegate are always called on the main thread. -- `config` - Config object, that was used for initialization. - `acceptLanguage` - Language settings, that will be sent along with each request. The server will return properly localized content based on this value. Value follows standard RFC [Accept-Language](https://tools.ietf.org/html/rfc7231#section-5.3.5) - `lastFetchResult()` - Cached last operations result. - `isLoadingOperations` - Indicates if the service is loading pending operations. @@ -337,19 +355,24 @@ All available methods and attributes of `WMTOperations` API are: - `interval` - How often should operations be refreshed. - `delayStart` - When true, polling starts after the first `interval` time passes. - `stopPollingOperations()` - Stops the periodic operation polling. -- `authorize(operation: WMTOperation, authentication: PowerAuthAuthentication, completion: @escaping(WMTError?)->Void)` - Authorize provided operation. +- `authorize(operation: WMTOperation, with: PowerAuthAuthentication, completion: @escaping(Result) -> Void)` - Authorize provided operation. - `operation` - An operation to approve, retrieved from `getOperations` call or [created locally](#creating-a-custom-operation). - - `authentication` - PowerAuth authentication object for operation signing. + - `with` - PowerAuth authentication object for operation signing. - `completion` - Called when authorization request finishes. Always called on the main thread. -- `reject(operation: WMTOperation, reason: WMTRejectionReason, completion: @escaping(WMTError?)->Void)` - Reject provided operation. +- `reject(operation: WMTOperation, with: WMTRejectionReason, completion: @escaping(Result) -> Void)` - Reject provided operation. - `operation` - An operation to reject, retrieved from `getOperations` call or [created locally](#creating-a-custom-operation). - - `reason` - Rejection reason + - `with` - Rejection reason - `completion` - Called when rejection request finishes. Always called on the main thread. - `getHistory(authentication: PowerAuthAuthentication, completion: @escaping(Result<[WMTOperationHistoryEntry],WMTError>) -> Void)` - Retrieves operation history - `authentication` - PowerAuth authentication object for operation signing. - `completion` - Called when rejection request finishes. Always called on the main thread. - `authorize(qrOperation: WMTQROperation, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void)` - Sign offline (QR) operation. - - `operation` - Offline operation that can be retrieved via `WMTQROperationParser.parse` method. + - `qrOperation ` - Offline operation that can be retrieved via `WMTQROperationParser.parse` method. + - `authentication` - PowerAuth authentication object for operation signing. + - `completion` - Called when authentication finishes. Always called on the main thread. +- `authorize(qrOperation: WMTQROperation, uriId: String, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void)` - Sign offline (QR) operation. + - `qrOperation ` - Offline operation that can be retrieved via `WMTQROperationParser.parse` method. + - `uriId` - Custom signature URI ID of the operation. Use URI ID under which the operation was created on the server. Usually something like `/confirm/offline/operation`. - `authentication` - PowerAuth authentication object for operation signing. - `completion` - Called when authentication finishes. Always called on the main thread. diff --git a/docs/Using-Push-Service.md b/docs/Using-Push-Service.md index 77d7129..e82576f 100644 --- a/docs/Using-Push-Service.md +++ b/docs/Using-Push-Service.md @@ -21,30 +21,32 @@ Push Service communicates with [Mobile Push Registration API](https://github.com ## Creating an Instance -To create an instance of the push service, use the following snippet: - +### On Top of the `PowerAuthSDK` instance ```swift import WultraMobileTokenSDK +import WultraPowerAuthNetworking -let opsConfig = WMTConfig( +let networkingConfig = WPNConfig( baseUrl: URL(string: "https://myservice.com/mtoken/push/api/")!, sslValidation: .default ) -let pushService = powerAuth.createWMTPush(config: config) +// powerAuth is instance of PowerAuthSDK +let pushService = powerAuth.createWMTPush(networkingConfig: networkingConfig) ``` -`sslValidation` property is used when validating HTTPS requests. Following strategies can be used. +### On Top of the `WPNNetworkingService` instance +```swift +import WultraMobileTokenSDK -- `WMTSSLValidationStrategy.default` -- `WMTSSLValidationStrategy.noValidation` -- `WMTSSLValidationStrategy.sslPinning` +// networkingService is instance of WPNNetworkingService +let pushService = networkingService.createWMTPush() +``` ## Push Service API Reference All available methods of the `WMTPush` API are: - `pushNotificationsRegisteredOnServer` - If there was already made an successful request. -- `config` - Config object, that was used for initialization. - `acceptLanguage` - Language settings, that will be sent along with each request. - `registerDeviceTokenForPushNotifications(token: Data, completionHandler: @escaping (_ success: Bool, _ error: WMTError?) -> Void)` - Registers push token on the backend. - `token` - token data retrieved from APNS. diff --git a/scripts/test.sh b/scripts/test.sh index c91743b..de4c25e 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,10 +6,13 @@ set -u # stop when undefined variable is used SCRIPT_FOLDER=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -DESTINATION="platform=iOS Simulator,OS=15.0,name=iPhone SE (2nd generation)" +IOS_VERSION=$(xcrun simctl list | grep "\-\- iOS" | tr -d - | tr -d " " | tr -d "iOS") +DESTINATION="platform=iOS Simulator,OS=${IOS_VERSION},name=iPhone 13 mini" + CL_URL="" CL_LGN="" CL_PWD="" +CL_AID="" ER_URL="" OP_URL="" APPKEY="" @@ -40,6 +43,11 @@ do shift shift ;; + -cla) + CL_AID="$2" + shift + shift + ;; -er) ER_URL="$2" shift @@ -84,6 +92,7 @@ echo """{ \"cloudServerUrl\" : \"${CL_URL}\", \"cloudServerLogin\" : \"${CL_LGN}\", \"cloudServerPassword\" : \"${CL_PWD}\", + \"cloudApplicationId\" : \"${CL_AID}\", \"enrollmentServerUrl\" : \"${ER_URL}\", \"operationsServerUrl\" : \"${OP_URL}\", \"appKey\" : \"${APPKEY}\",