diff --git a/Auth0.podspec b/Auth0.podspec index 6032cb0e0..a6d0492b9 100644 --- a/Auth0.podspec +++ b/Auth0.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |s| s.authors = { 'Auth0' => 'support@auth0.com', 'Rita Zerrizuela' => 'rita.zerrizuela@auth0.com' } s.source = { :git => 'https://github.com/auth0/Auth0.swift.git', :tag => s.version.to_s } s.social_media_url = 'https://twitter.com/auth0' - s.source_files = 'Auth0/*.swift' + s.source_files = 'Auth0/**/*.swift' s.resource_bundles = { s.name => 'Auth0/PrivacyInfo.xcprivacy' } s.swift_versions = ['5.0'] diff --git a/Auth0.xcodeproj/project.pbxproj b/Auth0.xcodeproj/project.pbxproj index ebbf65e4c..cf67c532b 100644 --- a/Auth0.xcodeproj/project.pbxproj +++ b/Auth0.xcodeproj/project.pbxproj @@ -46,6 +46,25 @@ 5C0AF09B28330CBA00162044 /* SafariProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0AF09928330CBA00162044 /* SafariProvider.swift */; }; 5C0AF09D2833420200162044 /* WebAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0AF09C2833420200162044 /* WebAuthentication.swift */; }; 5C0AF09E2833420200162044 /* WebAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0AF09C2833420200162044 /* WebAuthentication.swift */; }; + 5C0E5B6D2DE1100000D38F4C /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* Mocks.swift */; }; + 5C1574452DD5083A00BF9373 /* MyAccountSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574442DD5083400BF9373 /* MyAccountSpec.swift */; }; + 5C1574462DD5083A00BF9373 /* MyAccountSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574442DD5083400BF9373 /* MyAccountSpec.swift */; }; + 5C1574472DD5083A00BF9373 /* MyAccountSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574442DD5083400BF9373 /* MyAccountSpec.swift */; }; + 5C1574482DD5083A00BF9373 /* MyAccountSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574442DD5083400BF9373 /* MyAccountSpec.swift */; }; + 5C15744F2DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15744E2DD5181E00BF9373 /* MyAccountErrorSpec.swift */; }; + 5C1574502DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15744E2DD5181E00BF9373 /* MyAccountErrorSpec.swift */; }; + 5C1574512DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15744E2DD5181E00BF9373 /* MyAccountErrorSpec.swift */; }; + 5C1574522DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15744E2DD5181E00BF9373 /* MyAccountErrorSpec.swift */; }; + 5C1574552DD7A90400BF9373 /* PasskeyEnrollmentChallengeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574542DD7A8FD00BF9373 /* PasskeyEnrollmentChallengeSpec.swift */; }; + 5C1574562DD7A90400BF9373 /* PasskeyEnrollmentChallengeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574542DD7A8FD00BF9373 /* PasskeyEnrollmentChallengeSpec.swift */; }; + 5C1574572DD7A90400BF9373 /* PasskeyEnrollmentChallengeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574542DD7A8FD00BF9373 /* PasskeyEnrollmentChallengeSpec.swift */; }; + 5C15745A2DD7AF2400BF9373 /* PasskeyAuthenticationMethodSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574592DD7AF2100BF9373 /* PasskeyAuthenticationMethodSpec.swift */; }; + 5C15745B2DD7AF2400BF9373 /* PasskeyAuthenticationMethodSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574592DD7AF2100BF9373 /* PasskeyAuthenticationMethodSpec.swift */; }; + 5C15745D2DD7AF2400BF9373 /* PasskeyAuthenticationMethodSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1574592DD7AF2100BF9373 /* PasskeyAuthenticationMethodSpec.swift */; }; + 5C15745F2DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15745E2DD7D82D00BF9373 /* MyAccountAuthenticationMethodsSpec.swift */; }; + 5C1574602DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15745E2DD7D82D00BF9373 /* MyAccountAuthenticationMethodsSpec.swift */; }; + 5C1574612DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15745E2DD7D82D00BF9373 /* MyAccountAuthenticationMethodsSpec.swift */; }; + 5C1574622DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C15745E2DD7D82D00BF9373 /* MyAccountAuthenticationMethodsSpec.swift */; }; 5C29743223FDBD5400BC18FA /* Optional+DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3F23D0BA2C00074024 /* Optional+DebugDescription.swift */; }; 5C29743323FDBD5400BC18FA /* Optional+DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3F23D0BA2C00074024 /* Optional+DebugDescription.swift */; }; 5C29743423FDBD5500BC18FA /* Optional+DebugDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3F23D0BA2C00074024 /* Optional+DebugDescription.swift */; }; @@ -53,6 +72,16 @@ 5C354C05276CE1A500ADBC86 /* PasswordlessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C354C03276CE1A500ADBC86 /* PasswordlessType.swift */; }; 5C354C06276CE1A500ADBC86 /* PasswordlessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C354C03276CE1A500ADBC86 /* PasswordlessType.swift */; }; 5C354C07276CE1A500ADBC86 /* PasswordlessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C354C03276CE1A500ADBC86 /* PasswordlessType.swift */; }; + 5C38EA232DA4611B0085AC31 /* MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA222DA461150085AC31 /* MyAccount.swift */; }; + 5C38EA242DA4611B0085AC31 /* MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA222DA461150085AC31 /* MyAccount.swift */; }; + 5C38EA252DA4611B0085AC31 /* MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA222DA461150085AC31 /* MyAccount.swift */; }; + 5C38EA262DA4611B0085AC31 /* MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA222DA461150085AC31 /* MyAccount.swift */; }; + 5C38EA272DA4611B0085AC31 /* MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA222DA461150085AC31 /* MyAccount.swift */; }; + 5C38EA292DA4635B0085AC31 /* MyAccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA282DA463550085AC31 /* MyAccountError.swift */; }; + 5C38EA2A2DA4635B0085AC31 /* MyAccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA282DA463550085AC31 /* MyAccountError.swift */; }; + 5C38EA2B2DA4635B0085AC31 /* MyAccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA282DA463550085AC31 /* MyAccountError.swift */; }; + 5C38EA2C2DA4635B0085AC31 /* MyAccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA282DA463550085AC31 /* MyAccountError.swift */; }; + 5C38EA2D2DA4635B0085AC31 /* MyAccountError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C38EA282DA463550085AC31 /* MyAccountError.swift */; }; 5C3D87E22DB8276000AACC34 /* PublicKeyCredentialCreationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3D87DF2DB8274A00AACC34 /* PublicKeyCredentialCreationOptions.swift */; }; 5C3D87E32DB8276000AACC34 /* PublicKeyCredentialCreationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3D87DF2DB8274A00AACC34 /* PublicKeyCredentialCreationOptions.swift */; }; 5C3D87E42DB8276000AACC34 /* PublicKeyCredentialCreationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3D87DF2DB8274A00AACC34 /* PublicKeyCredentialCreationOptions.swift */; }; @@ -114,7 +143,7 @@ 5C4F552523C8FBA100C89615 /* JWKS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552223C8FBA100C89615 /* JWKS.swift */; }; 5C4F552623C8FBA100C89615 /* JWKS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552223C8FBA100C89615 /* JWKS.swift */; }; 5C4F552E23C9123000C89615 /* Generators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552C23C9123000C89615 /* Generators.swift */; }; - 5C4F553123C9123000C89615 /* CryptoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* CryptoExtensions.swift */; }; + 5C4F553123C9123000C89615 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* Mocks.swift */; }; 5C4F553523C9124200C89615 /* JWKSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F553423C9124200C89615 /* JWKSpec.swift */; }; 5C4F553A23C9125600C89615 /* JWTAlgorithmSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F553923C9125600C89615 /* JWTAlgorithmSpec.swift */; }; 5C53A7E92703A23200A7C0A3 /* UserInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2860D41EEF20F300C75D54 /* UserInfoSpec.swift */; }; @@ -156,8 +185,42 @@ 5CD9FC7A26FE30C8009C2B27 /* Quick.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5CD9FC6C26FE30A6009C2B27 /* Quick.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CD9FC7E26FE30D4009C2B27 /* Nimble.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5CD9FC6B26FE30A6009C2B27 /* Nimble.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CD9FC8026FE30D4009C2B27 /* Quick.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5CD9FC6C26FE30A6009C2B27 /* Quick.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5CDF67252DD3922B00A9B513 /* PasskeyEnrollmentChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67222DD3922300A9B513 /* PasskeyEnrollmentChallenge.swift */; }; + 5CDF67262DD3922B00A9B513 /* PasskeyEnrollmentChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67222DD3922300A9B513 /* PasskeyEnrollmentChallenge.swift */; }; + 5CDF67272DD3922B00A9B513 /* PasskeyEnrollmentChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67222DD3922300A9B513 /* PasskeyEnrollmentChallenge.swift */; }; + 5CDF672A2DD395C700A9B513 /* NewPasskey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67292DD395C300A9B513 /* NewPasskey.swift */; }; + 5CDF672B2DD395C700A9B513 /* NewPasskey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67292DD395C300A9B513 /* NewPasskey.swift */; }; + 5CDF672C2DD395C700A9B513 /* NewPasskey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67292DD395C300A9B513 /* NewPasskey.swift */; }; + 5CDF67362DD3A8D600A9B513 /* Auth0APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */; }; + 5CDF67372DD3A8D600A9B513 /* Auth0APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */; }; + 5CDF67382DD3A8D600A9B513 /* Auth0APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */; }; + 5CDF67392DD3A8D600A9B513 /* Auth0APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */; }; + 5CDF673A2DD3A8D600A9B513 /* Auth0APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */; }; + 5CDF673D2DD3B0E400A9B513 /* PasskeyAuthenticationMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF673B2DD3B0DE00A9B513 /* PasskeyAuthenticationMethod.swift */; }; + 5CDF673E2DD3B0E400A9B513 /* PasskeyAuthenticationMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF673B2DD3B0DE00A9B513 /* PasskeyAuthenticationMethod.swift */; }; + 5CDF67402DD3B0E400A9B513 /* PasskeyAuthenticationMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF673B2DD3B0DE00A9B513 /* PasskeyAuthenticationMethod.swift */; }; + 5CDF67422DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */; }; + 5CDF67432DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */; }; + 5CDF67442DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */; }; + 5CDF67452DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */; }; + 5CDF67462DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */; }; + 5CDF67482DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */; }; + 5CDF67492DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */; }; + 5CDF674A2DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */; }; + 5CDF674B2DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */; }; + 5CDF674C2DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */; }; + 5CDF674E2DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */; }; + 5CDF674F2DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */; }; + 5CDF67502DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */; }; + 5CDF67512DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */; }; + 5CDF67522DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */; }; + 5CDF67542DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */; }; + 5CDF67552DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */; }; + 5CDF67562DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */; }; + 5CDF67572DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */; }; + 5CDF67582DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */; }; 5CE775A2244FCF2000D054A0 /* Generators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552C23C9123000C89615 /* Generators.swift */; }; - 5CE775A3244FCF3600D054A0 /* CryptoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* CryptoExtensions.swift */; }; + 5CE775A3244FCF3600D054A0 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* Mocks.swift */; }; 5CE775A4244FCF3A00D054A0 /* BioAuthenticationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */; }; 5CE775A5244FCF3F00D054A0 /* IDTokenValidatorBaseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D7523D0C15000074024 /* IDTokenValidatorBaseSpec.swift */; }; 5CE775A6244FCF4100D054A0 /* IDTokenValidatorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D5323D0BA4B00074024 /* IDTokenValidatorMocks.swift */; }; @@ -319,10 +382,10 @@ 5FDE876A1D8A424700EA27DC /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874E1D8A424700EA27DC /* Credentials.swift */; }; 5FDE876B1D8A424700EA27DC /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874E1D8A424700EA27DC /* Credentials.swift */; }; 5FDE876C1D8A424700EA27DC /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874E1D8A424700EA27DC /* Credentials.swift */; }; - 5FDE876D1D8A424700EA27DC /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* Handlers.swift */; }; - 5FDE876E1D8A424700EA27DC /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* Handlers.swift */; }; - 5FDE876F1D8A424700EA27DC /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* Handlers.swift */; }; - 5FDE87701D8A424700EA27DC /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* Handlers.swift */; }; + 5FDE876D1D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */; }; + 5FDE876E1D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */; }; + 5FDE876F1D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */; }; + 5FDE87701D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */; }; 5FE118291D8A4A2A00A374BF /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE87461D8A422300EA27DC /* Telemetry.swift */; }; 5FE1182A1D8A4A2B00A374BF /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE87461D8A422300EA27DC /* Telemetry.swift */; }; 5FE1182B1D8A4A2B00A374BF /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE87461D8A422300EA27DC /* Telemetry.swift */; }; @@ -378,7 +441,7 @@ C1B3B9F32C24B6D4004A32A4 /* JWKS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552223C8FBA100C89615 /* JWKS.swift */; }; C1B3B9F42C24B6D4004A32A4 /* UserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2860CD1EEAC30500C75D54 /* UserInfo.swift */; }; C1B3B9F52C24B6D4004A32A4 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874E1D8A424700EA27DC /* Credentials.swift */; }; - C1B3B9F62C24B6D4004A32A4 /* Handlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* Handlers.swift */; }; + C1B3B9F62C24B6D4004A32A4 /* AuthenticationHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */; }; C1B3B9F72C24B6D4004A32A4 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FDE87461D8A422300EA27DC /* Telemetry.swift */; }; C1B3B9F82C24B6D4004A32A4 /* IDTokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3E23D0BA2C00074024 /* IDTokenValidator.swift */; }; C1B3B9F92C24B6D4004A32A4 /* IDTokenValidatorContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3C23D0BA2C00074024 /* IDTokenValidatorContext.swift */; }; @@ -458,7 +521,7 @@ C1B3BA492C24BA37004A32A4 /* BioAuthenticationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */; }; C1B3BA4A2C24BA37004A32A4 /* CredentialsManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1931EC3331A0007300D /* CredentialsManagerSpec.swift */; }; C1B3BA4B2C24BA37004A32A4 /* CredentialsManagerErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C809D95275F878E00F15A67 /* CredentialsManagerErrorSpec.swift */; }; - C1B3BA4C2C24BA37004A32A4 /* CryptoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* CryptoExtensions.swift */; }; + C1B3BA4C2C24BA37004A32A4 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552D23C9123000C89615 /* Mocks.swift */; }; C1B3BA4D2C24BA37004A32A4 /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FBBF0371CC964BC0024D2AF /* Matchers.swift */; }; C1B3BA4E2C24BA37004A32A4 /* Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FBBF03A1CC96AA70024D2AF /* Responses.swift */; }; C1B3BA4F2C24BA37004A32A4 /* Generators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4F552C23C9123000C89615 /* Generators.swift */; }; @@ -737,7 +800,14 @@ 5BEDE1931EC3331A0007300D /* CredentialsManagerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CredentialsManagerSpec.swift; path = Auth0Tests/CredentialsManagerSpec.swift; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C0AF09928330CBA00162044 /* SafariProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariProvider.swift; sourceTree = ""; }; 5C0AF09C2833420200162044 /* WebAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthentication.swift; sourceTree = ""; }; + 5C1574442DD5083400BF9373 /* MyAccountSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountSpec.swift; sourceTree = ""; }; + 5C15744E2DD5181E00BF9373 /* MyAccountErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountErrorSpec.swift; sourceTree = ""; }; + 5C1574542DD7A8FD00BF9373 /* PasskeyEnrollmentChallengeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyEnrollmentChallengeSpec.swift; sourceTree = ""; }; + 5C1574592DD7AF2100BF9373 /* PasskeyAuthenticationMethodSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyAuthenticationMethodSpec.swift; sourceTree = ""; }; + 5C15745E2DD7D82D00BF9373 /* MyAccountAuthenticationMethodsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountAuthenticationMethodsSpec.swift; sourceTree = ""; }; 5C354C03276CE1A500ADBC86 /* PasswordlessType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordlessType.swift; sourceTree = ""; }; + 5C38EA222DA461150085AC31 /* MyAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccount.swift; sourceTree = ""; }; + 5C38EA282DA463550085AC31 /* MyAccountError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountError.swift; sourceTree = ""; }; 5C3D87DF2DB8274A00AACC34 /* PublicKeyCredentialCreationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyCredentialCreationOptions.swift; sourceTree = ""; }; 5C3D87E52DB99C4E00AACC34 /* PasskeySignupChallenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeySignupChallenge.swift; sourceTree = ""; }; 5C3D87F12DB9B3DA00AACC34 /* SignupPasskey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignupPasskey.swift; sourceTree = ""; }; @@ -758,7 +828,7 @@ 5C4F551923C8FB8E00C89615 /* Array+Encode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Encode.swift"; sourceTree = ""; }; 5C4F552223C8FBA100C89615 /* JWKS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWKS.swift; sourceTree = ""; }; 5C4F552C23C9123000C89615 /* Generators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Generators.swift; sourceTree = ""; }; - 5C4F552D23C9123000C89615 /* CryptoExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoExtensions.swift; sourceTree = ""; }; + 5C4F552D23C9123000C89615 /* Mocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; 5C4F553423C9124200C89615 /* JWKSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWKSpec.swift; sourceTree = ""; }; 5C4F553923C9125600C89615 /* JWTAlgorithmSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JWTAlgorithmSpec.swift; sourceTree = ""; }; 5C60412E27482A2600EEF515 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -782,6 +852,14 @@ 5CD9FC6C26FE30A6009C2B27 /* Quick.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Quick.xcframework; path = Carthage/Build/Quick.xcframework; sourceTree = ""; }; 5CD9FC8426FE30EB009C2B27 /* JWTDecode.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = JWTDecode.xcframework; path = Carthage/Build/JWTDecode.xcframework; sourceTree = ""; }; 5CD9FC8526FE30EB009C2B27 /* SimpleKeychain.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SimpleKeychain.xcframework; path = Carthage/Build/SimpleKeychain.xcframework; sourceTree = ""; }; + 5CDF67222DD3922300A9B513 /* PasskeyEnrollmentChallenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyEnrollmentChallenge.swift; sourceTree = ""; }; + 5CDF67292DD395C300A9B513 /* NewPasskey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPasskey.swift; sourceTree = ""; }; + 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth0APIError.swift; sourceTree = ""; }; + 5CDF673B2DD3B0DE00A9B513 /* PasskeyAuthenticationMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasskeyAuthenticationMethod.swift; sourceTree = ""; }; + 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth0MyAccount.swift; sourceTree = ""; }; + 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth0MyAccountAuthenticationMethods.swift; sourceTree = ""; }; + 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountHandlers.swift; sourceTree = ""; }; + 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountAuthenticationMethods.swift; sourceTree = ""; }; 5CF5391C2836CEC00073F623 /* WebAuthenticationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthenticationSpec.swift; sourceTree = ""; }; 5CF5391F2836D9720073F623 /* WebAuthSpies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthSpies.swift; sourceTree = ""; }; 5CF539232836DCC10073F623 /* SafariProviderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariProviderSpec.swift; sourceTree = ""; }; @@ -854,7 +932,7 @@ 5FDE874A1D8A424700EA27DC /* Authentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authentication.swift; sourceTree = ""; }; 5FDE874B1D8A424700EA27DC /* AuthenticationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationError.swift; sourceTree = ""; }; 5FDE874E1D8A424700EA27DC /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; - 5FDE874F1D8A424700EA27DC /* Handlers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Handlers.swift; sourceTree = ""; }; + 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationHandlers.swift; sourceTree = ""; }; 5FE2F8A51CCA9C17003628F4 /* CredentialsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CredentialsSpec.swift; path = Auth0Tests/CredentialsSpec.swift; sourceTree = SOURCE_ROOT; }; 5FE2F8B11CCEAED8003628F4 /* Requestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Requestable.swift; path = Auth0/Requestable.swift; sourceTree = SOURCE_ROOT; }; 5FE2F8B71CD0E910003628F4 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Request.swift; path = Auth0/Request.swift; sourceTree = SOURCE_ROOT; }; @@ -1059,12 +1137,43 @@ name = Providers; sourceTree = ""; }; + 5C1574432DD507DB00BF9373 /* MyAccount */ = { + isa = PBXGroup; + children = ( + 5C1574532DD7A8CB00BF9373 /* AuthenticationMethods */, + 5C15744E2DD5181E00BF9373 /* MyAccountErrorSpec.swift */, + 5C1574442DD5083400BF9373 /* MyAccountSpec.swift */, + ); + path = MyAccount; + sourceTree = ""; + }; + 5C1574532DD7A8CB00BF9373 /* AuthenticationMethods */ = { + isa = PBXGroup; + children = ( + 5C15745E2DD7D82D00BF9373 /* MyAccountAuthenticationMethodsSpec.swift */, + 5C1574592DD7AF2100BF9373 /* PasskeyAuthenticationMethodSpec.swift */, + 5C1574542DD7A8FD00BF9373 /* PasskeyEnrollmentChallengeSpec.swift */, + ); + path = AuthenticationMethods; + sourceTree = ""; + }; + 5C38EA212DA4610A0085AC31 /* MyAccount */ = { + isa = PBXGroup; + children = ( + 5CDF67282DD3925200A9B513 /* AuthenticationMethods */, + 5CDF67412DD3B52A00A9B513 /* Auth0MyAccount.swift */, + 5C38EA222DA461150085AC31 /* MyAccount.swift */, + 5C38EA282DA463550085AC31 /* MyAccountError.swift */, + 5CDF674D2DD3DB4E00A9B513 /* MyAccountHandlers.swift */, + ); + path = MyAccount; + sourceTree = ""; + }; 5C3D88172DC051CF00AACC34 /* Passkeys */ = { isa = PBXGroup; children = ( - 5C3D88232DC1509300AACC34 /* LoginPasskey.swift */, + 5CDF67292DD395C300A9B513 /* NewPasskey.swift */, 5C3D881F2DC1491300AACC34 /* PublicKeyCredentialRequestOptions.swift */, - 5C3D87F12DB9B3DA00AACC34 /* SignupPasskey.swift */, 5C3D87DF2DB8274A00AACC34 /* PublicKeyCredentialCreationOptions.swift */, ); name = Passkeys; @@ -1110,6 +1219,17 @@ name = Validators; sourceTree = ""; }; + 5CDF67282DD3925200A9B513 /* AuthenticationMethods */ = { + isa = PBXGroup; + children = ( + 5CDF67472DD3B73D00A9B513 /* Auth0MyAccountAuthenticationMethods.swift */, + 5CDF67532DD4AD7E00A9B513 /* MyAccountAuthenticationMethods.swift */, + 5CDF673B2DD3B0DE00A9B513 /* PasskeyAuthenticationMethod.swift */, + 5CDF67222DD3922300A9B513 /* PasskeyEnrollmentChallenge.swift */, + ); + path = AuthenticationMethods; + sourceTree = ""; + }; 5CF539222836DC360073F623 /* Providers */ = { isa = PBXGroup; children = ( @@ -1169,6 +1289,7 @@ isa = PBXGroup; children = ( 5FDE87751D8A425300EA27DC /* Authentication */, + 5C38EA212DA4610A0085AC31 /* MyAccount */, 5FDE87451D8A421900EA27DC /* Telemetry */, 5BEDE1581EC1FFE40007300D /* Utils */, 5FCAB16E1D08FFE900331C84 /* Extensions */, @@ -1180,6 +1301,7 @@ 5F3965C01CF679B500CDE7C0 /* WebAuth */, 5F06DDC81CC66B710011842B /* Auth0.swift */, 5FD255B61D14F00900387ECB /* Auth0Error.swift */, + 5CDF67352DD3A8D200A9B513 /* Auth0APIError.swift */, 5C6513A62791CDDE004EBC22 /* Version.swift */, A7DDDF6B2BC9A81E0077B067 /* PrivacyInfo.xcprivacy */, ); @@ -1194,12 +1316,17 @@ 5F28B4651D8300BB0000EB23 /* Logger */, 5FE686A81D1894990075874C /* Telemetry */, 5FBBF0411CCA901B0024D2AF /* Authentication */, + 5C1574432DD507DB00BF9373 /* MyAccount */, 5FADB6011CEC0C1600D4BB50 /* Management */, 5FE2F8C11CD0FA11003628F4 /* Networking */, 5C4F553823C9124800C89615 /* Crypto */, 5FCAB1661D07ABEA00331C84 /* WebAuth */, 5FBBF0331CC95FA40024D2AF /* Utils */, 5F93BC0A1CC6B0DE0031519F /* Auth0Spec.swift */, + 5FBBF0371CC964BC0024D2AF /* Matchers.swift */, + 5FBBF03A1CC96AA70024D2AF /* Responses.swift */, + 5C4F552C23C9123000C89615 /* Generators.swift */, + 5C4F552D23C9123000C89615 /* Mocks.swift */, 5F06DD951CC451430011842B /* Info.plist */, 5FE686A01D1877C10075874C /* Auth0.plist */, ); @@ -1304,10 +1431,6 @@ 5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */, 5BEDE1931EC3331A0007300D /* CredentialsManagerSpec.swift */, 5C809D95275F878E00F15A67 /* CredentialsManagerErrorSpec.swift */, - 5C4F552D23C9123000C89615 /* CryptoExtensions.swift */, - 5FBBF0371CC964BC0024D2AF /* Matchers.swift */, - 5FBBF03A1CC96AA70024D2AF /* Responses.swift */, - 5C4F552C23C9123000C89615 /* Generators.swift */, ); name = Utils; sourceTree = ""; @@ -1383,6 +1506,7 @@ 5FDE87491D8A424700EA27DC /* Auth0Authentication.swift */, 5FDE874A1D8A424700EA27DC /* Authentication.swift */, 5FDE874B1D8A424700EA27DC /* AuthenticationError.swift */, + 5FDE874F1D8A424700EA27DC /* AuthenticationHandlers.swift */, 5C354C03276CE1A500ADBC86 /* PasswordlessType.swift */, 970BC36A25C27095007A7745 /* MultifactorChallenge.swift */, 5C3D88192DC148C000AACC34 /* PasskeyLoginChallenge.swift */, @@ -1392,7 +1516,8 @@ 5FDE874E1D8A424700EA27DC /* Credentials.swift */, 5CFB824F2D5BF31D009FD237 /* APICredentials.swift */, 5CFB826F2D6E640B009FD237 /* SSOCredentials.swift */, - 5FDE874F1D8A424700EA27DC /* Handlers.swift */, + 5C3D88232DC1509300AACC34 /* LoginPasskey.swift */, + 5C3D87F12DB9B3DA00AACC34 /* SignupPasskey.swift */, ); name = Authentication; sourceTree = ""; @@ -1832,7 +1957,6 @@ 5F3965C61CF67DD800CDE7C0 = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1020; - ProvisioningStyle = Manual; }; C1B3B9A72C24B297004A32A4 = { CreatedOnToolsVersion = 15.4; @@ -2161,13 +2285,16 @@ files = ( 5CB41D4423D0BA2C00074024 /* IDTokenSignatureValidator.swift in Sources */, 5CB41D4C23D0BA2C00074024 /* Optional+DebugDescription.swift in Sources */, + 5CDF67482DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */, 5C4F551A23C8FB8E00C89615 /* String+URLSafe.swift in Sources */, + 5CDF67392DD3A8D600A9B513 /* Auth0APIError.swift in Sources */, 5C0AF09D2833420200162044 /* WebAuthentication.swift in Sources */, 5CB41D4823D0BA2C00074024 /* IDTokenValidator.swift in Sources */, 5C354C04276CE1A500ADBC86 /* PasswordlessType.swift in Sources */, 5FCAB1711D09005A00331C84 /* NSURLComponents+OAuth2.swift in Sources */, 5CFB82632D6D221F009FD237 /* Barrier.swift in Sources */, - 5FDE876D1D8A424700EA27DC /* Handlers.swift in Sources */, + 5FDE876D1D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */, + 5C38EA232DA4611B0085AC31 /* MyAccount.swift in Sources */, 5FE2F8BB1CD0EAAD003628F4 /* Response.swift in Sources */, 970BC36B25C27095007A7745 /* MultifactorChallenge.swift in Sources */, 5FDE87591D8A424700EA27DC /* Authentication.swift in Sources */, @@ -2177,22 +2304,27 @@ 5C41F6B1244DCC3B00252548 /* MobileWebAuth.swift in Sources */, 5C3D88252DC1509800AACC34 /* LoginPasskey.swift in Sources */, 5F28B4611D8216180000EB23 /* Loggable.swift in Sources */, + 5CDF67402DD3B0E400A9B513 /* PasskeyAuthenticationMethod.swift in Sources */, D5E9E317273ACCA5000CDB0A /* ChallengeGenerator.swift in Sources */, 5C3D881C2DC148C600AACC34 /* PasskeyLoginChallenge.swift in Sources */, 5C49EB3523EB5A80008D562F /* JWK+RSA.swift in Sources */, 5FDE87551D8A424700EA27DC /* Auth0Authentication.swift in Sources */, 5F53F5CE1CFD157300476A46 /* AuthTransaction.swift in Sources */, + 5CDF672A2DD395C700A9B513 /* NewPasskey.swift in Sources */, 5CB41D6C23D0BBA600074024 /* JWT+Header.swift in Sources */, 5FF465BC1CE2AC4500F7ED8C /* Management.swift in Sources */, 5C0AF09A28330CBA00162044 /* SafariProvider.swift in Sources */, 5F4A1F961D00AABC00C72242 /* OAuth2Grant.swift in Sources */, 5FCAB1731D09009600331C84 /* NSData+URLSafe.swift in Sources */, 5B16D88E1F7141A0009476A5 /* ASProvider.swift in Sources */, + 5CDF67422DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */, 5C80980B275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, 5FD255BA1D14F70B00387ECB /* WebAuthError.swift in Sources */, + 5CDF67272DD3922B00A9B513 /* PasskeyEnrollmentChallenge.swift in Sources */, 5C3D87F42DB9B3DF00AACC34 /* SignupPasskey.swift in Sources */, 5BEDE18A1EC21B040007300D /* CredentialsManager.swift in Sources */, 5B2860CE1EEAC30500C75D54 /* UserInfo.swift in Sources */, + 5C38EA2B2DA4635B0085AC31 /* MyAccountError.swift in Sources */, 5C4F552323C8FBA100C89615 /* JWKS.swift in Sources */, 5C6513A72791CDDE004EBC22 /* Version.swift in Sources */, 5C4F550923C8FADF00C89615 /* JWTAlgorithm.swift in Sources */, @@ -2205,6 +2337,7 @@ 5FDE87691D8A424700EA27DC /* Credentials.swift in Sources */, 5FE2F8B21CCEAED8003628F4 /* Requestable.swift in Sources */, 5C4F551E23C8FB8E00C89615 /* Array+Encode.swift in Sources */, + 5CDF67542DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */, 5B9262C01ECF0CA800F4F6D3 /* BioAuthentication.swift in Sources */, 5CB41D7123D0BED200074024 /* ClaimValidators.swift in Sources */, 5FADB60C1CED7E0800D4BB50 /* UserPatchAttributes.swift in Sources */, @@ -2212,6 +2345,7 @@ 5F74CB401CEFD5E600226823 /* JSONObjectPayload.swift in Sources */, 5FD255B41D14DD2600387ECB /* ManagementError.swift in Sources */, 5FE2F8B81CD0E910003628F4 /* Request.swift in Sources */, + 5CDF67512DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */, 5C41F6A4244DC94E00252548 /* LoginTransaction.swift in Sources */, 5F3965C21CF67CF000CDE7C0 /* WebAuth.swift in Sources */, 5FCAB1761D0900CF00331C84 /* TransactionStore.swift in Sources */, @@ -2234,10 +2368,12 @@ buildActionMask = 2147483647; files = ( 5C41F6D1244F972000252548 /* JWT+Header.swift in Sources */, + 5CDF67442DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */, 5FE2F8BC1CD0EAAD003628F4 /* Response.swift in Sources */, 5B7EE47420FCA00A00367724 /* CredentialsManagerError.swift in Sources */, 5C0AF09E2833420200162044 /* WebAuthentication.swift in Sources */, 5FDE876A1D8A424700EA27DC /* Credentials.swift in Sources */, + 5CDF674A2DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */, 5C354C05276CE1A500ADBC86 /* PasswordlessType.swift in Sources */, 5CFB82512D5BF324009FD237 /* APICredentials.swift in Sources */, 5C3D88262DC1509800AACC34 /* LoginPasskey.swift in Sources */, @@ -2262,6 +2398,7 @@ 5C0AF09B28330CBA00162044 /* SafariProvider.swift in Sources */, 5C3D87E72DB99C5C00AACC34 /* PasskeySignupChallenge.swift in Sources */, 5FE2F8B31CCEAED8003628F4 /* Requestable.swift in Sources */, + 5C38EA2A2DA4635B0085AC31 /* MyAccountError.swift in Sources */, 5B2860CF1EEAC30900C75D54 /* UserInfo.swift in Sources */, 5C4F552423C8FBA100C89615 /* JWKS.swift in Sources */, 5CFB82742D6E640F009FD237 /* SSOCredentials.swift in Sources */, @@ -2271,8 +2408,9 @@ 5C41F6D7244F975A00252548 /* TransactionStore.swift in Sources */, 5FE1182B1D8A4A2B00A374BF /* Telemetry.swift in Sources */, 5C6513A82791CDDE004EBC22 /* Version.swift in Sources */, + 5CDF67522DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */, 5FDE875E1D8A424700EA27DC /* AuthenticationError.swift in Sources */, - 5FDE876E1D8A424700EA27DC /* Handlers.swift in Sources */, + 5FDE876E1D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */, 5C3D87F32DB9B3DF00AACC34 /* SignupPasskey.swift in Sources */, 5C41F6CE244F970500252548 /* IDTokenValidator.swift in Sources */, 5C41F6CA244F96AE00252548 /* LoginTransaction.swift in Sources */, @@ -2282,6 +2420,7 @@ 5F74CB411CEFD5E600226823 /* JSONObjectPayload.swift in Sources */, 5C41F6CB244F96E300252548 /* BioAuthentication.swift in Sources */, 5C41F6C8244F969600252548 /* ASProvider.swift in Sources */, + 5C38EA242DA4611B0085AC31 /* MyAccount.swift in Sources */, 5C41F6DF244FA1EE00252548 /* NSURLComponents+OAuth2.swift in Sources */, 5FE2F8B91CD0E910003628F4 /* Request.swift in Sources */, 5B7EE47320FCA00700367724 /* CredentialsManager.swift in Sources */, @@ -2295,9 +2434,14 @@ 5C41F6DD244F982700252548 /* DesktopWebAuth.swift in Sources */, 5CA541CE2B1A81A700E4284D /* Documentation.docc in Sources */, 5FCAB17A1D09124D00331C84 /* NSURL+Auth0.swift in Sources */, + 5CDF67262DD3922B00A9B513 /* PasskeyEnrollmentChallenge.swift in Sources */, + 5CDF67572DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */, + 5CDF673E2DD3B0E400A9B513 /* PasskeyAuthenticationMethod.swift in Sources */, + 5CDF67362DD3A8D600A9B513 /* Auth0APIError.swift in Sources */, 5FDE87561D8A424700EA27DC /* Auth0Authentication.swift in Sources */, 5FADB6071CED27FB00D4BB50 /* Users.swift in Sources */, 5C41F6C3244F965E00252548 /* Array+Encode.swift in Sources */, + 5CDF672B2DD395C700A9B513 /* NewPasskey.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2308,7 +2452,7 @@ D581CF772757D773007327D1 /* RequestSpec.swift in Sources */, 5F1FBB9C1D8A44C1006B0B85 /* ResponseSpec.swift in Sources */, 5C3D886A2DC2CCA200AACC34 /* PasskeyLoginChallenge.swift in Sources */, - 5C4F553123C9123000C89615 /* CryptoExtensions.swift in Sources */, + 5C4F553123C9123000C89615 /* Mocks.swift in Sources */, 5FE686AA1D1894AA0075874C /* TelemetrySpec.swift in Sources */, 5C41F6B6244DCF2F00252548 /* LoginTransactionSpec.swift in Sources */, 5CB41D6523D0BACF00074024 /* IDTokenValidatorMocks.swift in Sources */, @@ -2316,8 +2460,10 @@ 5CF539202836D9720073F623 /* WebAuthSpies.swift in Sources */, 5CF539282836FB0C0073F623 /* ClearSessionTransactionSpec.swift in Sources */, 5CF5392B283835470073F623 /* ASProviderSpec.swift in Sources */, + 5C1574552DD7A90400BF9373 /* PasskeyEnrollmentChallengeSpec.swift in Sources */, 5FCAB16A1D07AC3500331C84 /* ChallengeGeneratorSpec.swift in Sources */, 5CB41D6923D0BAD600074024 /* IDTokenSignatureValidatorSpec.swift in Sources */, + 5C15745D2DD7AF2400BF9373 /* PasskeyAuthenticationMethodSpec.swift in Sources */, 5CFB82772D6FD28E009FD237 /* SSOCredentialsSpec.swift in Sources */, 5CF5391D2836CEC00073F623 /* WebAuthenticationSpec.swift in Sources */, 5FADB60F1CED7E5200D4BB50 /* UserPatchAttributesSpec.swift in Sources */, @@ -2344,11 +2490,14 @@ 5C809D9A275FA3EF00F15A67 /* ManagementErrorSpec.swift in Sources */, 5FCAB16B1D07AC3500331C84 /* OAuth2GrantSpec.swift in Sources */, 5BEDE1951EC333380007300D /* CredentialsManagerSpec.swift in Sources */, + 5C1574452DD5083A00BF9373 /* MyAccountSpec.swift in Sources */, 5CB41D6223D0BAC900074024 /* IDTokenValidatorSpec.swift in Sources */, 5FD255B11D14A9E000387ECB /* AuthenticationErrorSpec.swift in Sources */, 5C4F553A23C9125600C89615 /* JWTAlgorithmSpec.swift in Sources */, + 5C15744F2DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */, 5C4F552E23C9123000C89615 /* Generators.swift in Sources */, 5CB41D8223D611AE00074024 /* ClaimValidatorsSpec.swift in Sources */, + 5C1574612DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */, 5FADB6031CEC0C3300D4BB50 /* UsersSpec.swift in Sources */, 5F686A771D4AB90900412E3D /* TransactionStoreSpec.swift in Sources */, ); @@ -2361,13 +2510,16 @@ D581CF782757D773007327D1 /* RequestSpec.swift in Sources */, 5FE686AB1D1894AA0075874C /* TelemetrySpec.swift in Sources */, 5FBBF03C1CC96AA70024D2AF /* Responses.swift in Sources */, + 5C1574562DD7A90400BF9373 /* PasskeyEnrollmentChallengeSpec.swift in Sources */, 5CE775AA244FCF4E00D054A0 /* JWTAlgorithmSpec.swift in Sources */, 5CFB82792D6FD28E009FD237 /* SSOCredentialsSpec.swift in Sources */, 5CE775A7244FCF4400D054A0 /* IDTokenValidatorSpec.swift in Sources */, 5CE775AE244FD66600D054A0 /* ChallengeGeneratorSpec.swift in Sources */, 5FADB6101CED7E5200D4BB50 /* UserPatchAttributesSpec.swift in Sources */, + 5C1574472DD5083A00BF9373 /* MyAccountSpec.swift in Sources */, 5CF539212836D9720073F623 /* WebAuthSpies.swift in Sources */, 5CF539292836FB0C0073F623 /* ClearSessionTransactionSpec.swift in Sources */, + 5C15745B2DD7AF2400BF9373 /* PasskeyAuthenticationMethodSpec.swift in Sources */, 5CF5392C283835470073F623 /* ASProviderSpec.swift in Sources */, 5FBBF0391CC964BC0024D2AF /* Matchers.swift in Sources */, 5CE775B4244FD72500D054A0 /* JWKSpec.swift in Sources */, @@ -2392,10 +2544,12 @@ 5CE775B2244FD70B00D054A0 /* TransactionStoreSpec.swift in Sources */, 5CE775A9244FCF4900D054A0 /* ClaimValidatorsSpec.swift in Sources */, 5C809D9B275FA3EF00F15A67 /* ManagementErrorSpec.swift in Sources */, - 5CE775A3244FCF3600D054A0 /* CryptoExtensions.swift in Sources */, + 5CE775A3244FCF3600D054A0 /* Mocks.swift in Sources */, 5CE775B3244FD71000D054A0 /* WebAuthSpec.swift in Sources */, 5C3D880F2DBE7F3D00AACC34 /* PasskeySignupChallengeSpec.swift in Sources */, + 5C15745F2DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */, 5B7EE47220FCA00300367724 /* CredentialsManagerSpec.swift in Sources */, + 5C1574512DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */, 5CE775A6244FCF4100D054A0 /* IDTokenValidatorMocks.swift in Sources */, 5CE775AC244FD66000D054A0 /* LoginTransactionSpec.swift in Sources */, 5FADB6041CEC0C3300D4BB50 /* UsersSpec.swift in Sources */, @@ -2412,18 +2566,23 @@ 5F23E6E91D4ACD9200C3F2D9 /* Auth0.swift in Sources */, 5CC9940424ED9EC90027DC74 /* CredentialsManagerError.swift in Sources */, 5F23E6EA1D4ACD9600C3F2D9 /* Auth0Error.swift in Sources */, - 5FDE876F1D8A424700EA27DC /* Handlers.swift in Sources */, + 5CDF67452DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */, + 5FDE876F1D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */, 970BC36D25C27095007A7745 /* MultifactorChallenge.swift in Sources */, 5FDE87571D8A424700EA27DC /* Auth0Authentication.swift in Sources */, 5C4F552523C8FBA100C89615 /* JWKS.swift in Sources */, 5F23E6E51D4ACD8500C3F2D9 /* Request.swift in Sources */, + 5C38EA2D2DA4635B0085AC31 /* MyAccountError.swift in Sources */, + 5CDF67492DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */, 5F23E6DD1D4ACD6100C3F2D9 /* NSURL+Auth0.swift in Sources */, 5F23E6E71D4ACD8500C3F2D9 /* Response.swift in Sources */, 5B2860D11EEAC30A00C75D54 /* UserInfo.swift in Sources */, 5F28B4631D8216180000EB23 /* Loggable.swift in Sources */, + 5C38EA252DA4611B0085AC31 /* MyAccount.swift in Sources */, 5F23E6E01D4ACD7F00C3F2D9 /* Management.swift in Sources */, 5C29743323FDBD5400BC18FA /* Optional+DebugDescription.swift in Sources */, 5F23E6E11D4ACD7F00C3F2D9 /* UserPatchAttributes.swift in Sources */, + 5CDF674E2DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */, 5F23E6DF1D4ACD7A00C3F2D9 /* Logger.swift in Sources */, 5FDE875B1D8A424700EA27DC /* Authentication.swift in Sources */, 5F23E6E21D4ACD7F00C3F2D9 /* Users.swift in Sources */, @@ -2432,6 +2591,7 @@ 5FE1182A1D8A4A2B00A374BF /* Telemetry.swift in Sources */, 5CC9940324ED9EC50027DC74 /* CredentialsManager.swift in Sources */, 5CFB82712D6E640F009FD237 /* SSOCredentials.swift in Sources */, + 5CDF67382DD3A8D600A9B513 /* Auth0APIError.swift in Sources */, 5F23E6E41D4ACD8500C3F2D9 /* JSONObjectPayload.swift in Sources */, 5C4F551C23C8FB8E00C89615 /* String+URLSafe.swift in Sources */, 5CFB82502D5BF324009FD237 /* APICredentials.swift in Sources */, @@ -2440,6 +2600,7 @@ 5C354C07276CE1A500ADBC86 /* PasswordlessType.swift in Sources */, 5FDE875F1D8A424700EA27DC /* AuthenticationError.swift in Sources */, 5C6513AA2791CDDE004EBC22 /* Version.swift in Sources */, + 5CDF67562DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */, 5B1748761EF2D3A70060E653 /* Helpers.swift in Sources */, 5F23E6E31D4ACD7F00C3F2D9 /* ManagementError.swift in Sources */, 5C80980E275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, @@ -2451,20 +2612,25 @@ buildActionMask = 2147483647; files = ( 5F23E70E1D4B88FC00C3F2D9 /* Request.swift in Sources */, - 5FDE87701D8A424700EA27DC /* Handlers.swift in Sources */, + 5FDE87701D8A424700EA27DC /* AuthenticationHandlers.swift in Sources */, 5C4F551D23C8FB8E00C89615 /* String+URLSafe.swift in Sources */, + 5CDF67432DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */, 970BC36E25C27095007A7745 /* MultifactorChallenge.swift in Sources */, 5FDE87581D8A424700EA27DC /* Auth0Authentication.swift in Sources */, 5F23E7071D4B88EA00C3F2D9 /* NSURL+Auth0.swift in Sources */, 5F23E71A1D4B891E00C3F2D9 /* Auth0.swift in Sources */, 5F23E7101D4B88FC00C3F2D9 /* Response.swift in Sources */, + 5C38EA2C2DA4635B0085AC31 /* MyAccountError.swift in Sources */, + 5CDF674B2DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */, 5B2860D01EEAC30A00C75D54 /* UserInfo.swift in Sources */, 5F28B4641D8216180000EB23 /* Loggable.swift in Sources */, 5B0893E720F8A52400FBF962 /* CredentialsManagerError.swift in Sources */, 5F23E7091D4B88F600C3F2D9 /* Management.swift in Sources */, + 5C38EA272DA4611B0085AC31 /* MyAccount.swift in Sources */, 5F23E70A1D4B88F600C3F2D9 /* UserPatchAttributes.swift in Sources */, 5F23E71B1D4B891E00C3F2D9 /* Auth0Error.swift in Sources */, 5FDE875C1D8A424700EA27DC /* Authentication.swift in Sources */, + 5CDF674F2DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */, 5F23E7081D4B88F000C3F2D9 /* Logger.swift in Sources */, 5C29743223FDBD5400BC18FA /* Optional+DebugDescription.swift in Sources */, 5F23E70B1D4B88F600C3F2D9 /* Users.swift in Sources */, @@ -2473,6 +2639,7 @@ 5FE118291D8A4A2A00A374BF /* Telemetry.swift in Sources */, 5F23E70D1D4B88FC00C3F2D9 /* JSONObjectPayload.swift in Sources */, 5CFB82722D6E640F009FD237 /* SSOCredentials.swift in Sources */, + 5CDF67372DD3A8D600A9B513 /* Auth0APIError.swift in Sources */, 5C4F552623C8FBA100C89615 /* JWKS.swift in Sources */, 5B0893E620F8A52100FBF962 /* CredentialsManager.swift in Sources */, 5CFB82532D5BF324009FD237 /* APICredentials.swift in Sources */, @@ -2481,6 +2648,7 @@ 5C354C06276CE1A500ADBC86 /* PasswordlessType.swift in Sources */, 5FDE87601D8A424700EA27DC /* AuthenticationError.swift in Sources */, 5C6513A92791CDDE004EBC22 /* Version.swift in Sources */, + 5CDF67582DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */, 5B1748771EF2D3A90060E653 /* Helpers.swift in Sources */, 5F23E70C1D4B88F600C3F2D9 /* ManagementError.swift in Sources */, 5C80980D275A7B8600DC0A76 /* CredentialsStorage.swift in Sources */, @@ -2495,10 +2663,12 @@ 5B6EE39620F8AEDB00264AC7 /* CredentialsManagerSpec.swift in Sources */, D581CF792757D773007327D1 /* RequestSpec.swift in Sources */, 5F331B061D4BB7DA00AE4382 /* CredentialsSpec.swift in Sources */, + 5C1574482DD5083A00BF9373 /* MyAccountSpec.swift in Sources */, 5C53A7EA2703A23300A7C0A3 /* UserInfoSpec.swift in Sources */, 5F331B0B1D4BB7F900AE4382 /* UserPatchAttributesSpec.swift in Sources */, 5F331B051D4BB7D400AE4382 /* AuthenticationSpec.swift in Sources */, 5F28B4691D8300D50000EB23 /* LoggerSpec.swift in Sources */, + 5C1574622DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */, 5F1FBB9A1D8A44C0006B0B85 /* ResponseSpec.swift in Sources */, 5F331B0C1D4BB7F900AE4382 /* UsersSpec.swift in Sources */, 5CFB82572D5E9F9B009FD237 /* APICredentialsSpec.swift in Sources */, @@ -2506,7 +2676,9 @@ 5F331B0E1D4BB80700AE4382 /* Matchers.swift in Sources */, 5F331B0A1D4BB7F900AE4382 /* ManagementSpec.swift in Sources */, 5CFB82782D6FD28E009FD237 /* SSOCredentialsSpec.swift in Sources */, + 5C1574502DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */, 5F331B091D4BB7EE00AE4382 /* AuthenticationErrorSpec.swift in Sources */, + 5C0E5B6D2DE1100000D38F4C /* Mocks.swift in Sources */, 5F331B101D4BB80700AE4382 /* Auth0Spec.swift in Sources */, 5C809D9C275FA3F000F15A67 /* ManagementErrorSpec.swift in Sources */, 5C809D98275F878E00F15A67 /* CredentialsManagerErrorSpec.swift in Sources */, @@ -2547,13 +2719,15 @@ 5C3D881D2DC148C600AACC34 /* PasskeyLoginChallenge.swift in Sources */, C1B3B9F32C24B6D4004A32A4 /* JWKS.swift in Sources */, 5CFB82522D5BF324009FD237 /* APICredentials.swift in Sources */, + 5CDF673D2DD3B0E400A9B513 /* PasskeyAuthenticationMethod.swift in Sources */, C1B3B9F42C24B6D4004A32A4 /* UserInfo.swift in Sources */, C1B3B9F52C24B6D4004A32A4 /* Credentials.swift in Sources */, - C1B3B9F62C24B6D4004A32A4 /* Handlers.swift in Sources */, + C1B3B9F62C24B6D4004A32A4 /* AuthenticationHandlers.swift in Sources */, 5C3D87F52DB9B3DF00AACC34 /* SignupPasskey.swift in Sources */, C1B3B9F72C24B6D4004A32A4 /* Telemetry.swift in Sources */, C1B3B9F82C24B6D4004A32A4 /* IDTokenValidator.swift in Sources */, C1B3B9F92C24B6D4004A32A4 /* IDTokenValidatorContext.swift in Sources */, + 5CDF67252DD3922B00A9B513 /* PasskeyEnrollmentChallenge.swift in Sources */, C1B3B9FA2C24B6D4004A32A4 /* IDTokenSignatureValidator.swift in Sources */, C1B3B9FB2C24B6D4004A32A4 /* ClaimValidators.swift in Sources */, C1B3B9FC2C24B6D4004A32A4 /* Helpers.swift in Sources */, @@ -2566,6 +2740,7 @@ C1B3BA022C24B6D4004A32A4 /* String+URLSafe.swift in Sources */, C1B3BA032C24B6D4004A32A4 /* NSData+URLSafe.swift in Sources */, 5CFB82622D6D221F009FD237 /* Barrier.swift in Sources */, + 5CDF67502DD3DB5400A9B513 /* MyAccountHandlers.swift in Sources */, C1B3BA042C24B6D4004A32A4 /* NSURL+Auth0.swift in Sources */, C1B3BA052C24B6D4004A32A4 /* NSURLComponents+OAuth2.swift in Sources */, C1B3BA062C24B6D4004A32A4 /* JWT+Header.swift in Sources */, @@ -2580,14 +2755,21 @@ C1B3BA0D2C24B6D4004A32A4 /* Users.swift in Sources */, C1B3BA0E2C24B6D4004A32A4 /* ManagementError.swift in Sources */, C1B3BA0F2C24B6D4004A32A4 /* JSONObjectPayload.swift in Sources */, + 5CDF67552DD4AD8500A9B513 /* MyAccountAuthenticationMethods.swift in Sources */, C1B3BA102C24B6D4004A32A4 /* Request.swift in Sources */, C1B3BA112C24B6D4004A32A4 /* Requestable.swift in Sources */, + 5C38EA292DA4635B0085AC31 /* MyAccountError.swift in Sources */, + 5CDF674C2DD3B74200A9B513 /* Auth0MyAccountAuthenticationMethods.swift in Sources */, C1B3BA122C24B6D4004A32A4 /* Response.swift in Sources */, 5C3D87EA2DB99C5C00AACC34 /* PasskeySignupChallenge.swift in Sources */, + 5C38EA262DA4611B0085AC31 /* MyAccount.swift in Sources */, + 5CDF67462DD3B52F00A9B513 /* Auth0MyAccount.swift in Sources */, C1B3BA132C24B6D4004A32A4 /* JWTAlgorithm.swift in Sources */, C1B3BA142C24B6D4004A32A4 /* ChallengeGenerator.swift in Sources */, C1B3BA152C24B6D4004A32A4 /* ASProvider.swift in Sources */, + 5CDF672C2DD395C700A9B513 /* NewPasskey.swift in Sources */, C1B3BA172C24B6D4004A32A4 /* LoginTransaction.swift in Sources */, + 5CDF673A2DD3A8D600A9B513 /* Auth0APIError.swift in Sources */, C1B3BA182C24B6D4004A32A4 /* ClearSessionTransaction.swift in Sources */, C1B3BA192C24B6D4004A32A4 /* MobileWebAuth.swift in Sources */, C1B3BA1B2C24B6D4004A32A4 /* AuthTransaction.swift in Sources */, @@ -2612,7 +2794,9 @@ C1B3BA2C2C24BA36004A32A4 /* TelemetrySpec.swift in Sources */, C1B3BA2D2C24BA36004A32A4 /* AuthenticationSpec.swift in Sources */, C1B3BA2E2C24BA36004A32A4 /* CredentialsSpec.swift in Sources */, + 5C1574462DD5083A00BF9373 /* MyAccountSpec.swift in Sources */, C1B3BA2F2C24BA36004A32A4 /* UserInfoSpec.swift in Sources */, + 5C1574572DD7A90400BF9373 /* PasskeyEnrollmentChallengeSpec.swift in Sources */, C1B3BA302C24BA36004A32A4 /* JWKSpec.swift in Sources */, C1B3BA312C24BA36004A32A4 /* AuthenticationErrorSpec.swift in Sources */, C1B3BA322C24BA36004A32A4 /* ManagementSpec.swift in Sources */, @@ -2627,8 +2811,10 @@ C1B3BA3B2C24BA36004A32A4 /* ASProviderSpec.swift in Sources */, C1B3BA3D2C24BA36004A32A4 /* WebAuthErrorSpec.swift in Sources */, C177D7772C2BE00D0094C657 /* StubURLProtocol.swift in Sources */, + 5C1574602DD7D83B00BF9373 /* MyAccountAuthenticationMethodsSpec.swift in Sources */, C1B3BA3E2C24BA36004A32A4 /* ChallengeGeneratorSpec.swift in Sources */, C1B3BA3F2C24BA36004A32A4 /* OAuth2GrantSpec.swift in Sources */, + 5C1574522DD5182400BF9373 /* MyAccountErrorSpec.swift in Sources */, C1B3BA402C24BA36004A32A4 /* WebAuthSpec.swift in Sources */, C177D7722C2BDFE40094C657 /* NetworkStub.swift in Sources */, C1B3BA412C24BA36004A32A4 /* TransactionStoreSpec.swift in Sources */, @@ -2645,11 +2831,12 @@ C1B3BA4A2C24BA37004A32A4 /* CredentialsManagerSpec.swift in Sources */, 5CFB82762D6FD28E009FD237 /* SSOCredentialsSpec.swift in Sources */, C1B3BA4B2C24BA37004A32A4 /* CredentialsManagerErrorSpec.swift in Sources */, - C1B3BA4C2C24BA37004A32A4 /* CryptoExtensions.swift in Sources */, + C1B3BA4C2C24BA37004A32A4 /* Mocks.swift in Sources */, C1B3BA4D2C24BA37004A32A4 /* Matchers.swift in Sources */, 5C3D88112DBE7F3D00AACC34 /* PasskeySignupChallengeSpec.swift in Sources */, C1B3BA4E2C24BA37004A32A4 /* Responses.swift in Sources */, C1B3BA4F2C24BA37004A32A4 /* Generators.swift in Sources */, + 5C15745A2DD7AF2400BF9373 /* PasskeyAuthenticationMethodSpec.swift in Sources */, C1B3BA502C24BA37004A32A4 /* Auth0Spec.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Auth0/Auth0.swift b/Auth0/Auth0.swift index f51e66b2e..e3f60287a 100644 --- a/Auth0/Auth0.swift +++ b/Auth0/Auth0.swift @@ -10,6 +10,11 @@ public typealias AuthenticationResult = Result */ public typealias ManagementResult = Result +/** + `Result` wrapper for My Account API operations. + */ +public typealias MyAccountResult = Result + #if WEB_AUTH_PLATFORM /** `Result` wrapper for Web Auth operations. @@ -28,7 +33,8 @@ public typealias CredentialsManagerResult = Result Authentication { return Auth0Authentication(clientId: clientId, url: .httpsURL(from: domain), session: session) } /** - Auth0 [Authentication API](https://auth0.com/docs/api/authentication) client to authenticate your user using Database, - Social, Enterprise or Passwordless connections. + [Authentication API](https://auth0.com/docs/api/authentication) client to authenticate a user with Database, Social, + Enterprise or Passwordless connections. ## Usage @@ -64,10 +70,10 @@ public func authentication(clientId: String, domain: String, session: URLSession - ClientId - YOUR_AUTH0_CLIENT_ID - Domain - YOUR_AUTH0_DOMAIN + ClientId + YOUR_AUTH0_CLIENT_ID + Domain + YOUR_AUTH0_DOMAIN ``` @@ -75,7 +81,7 @@ public func authentication(clientId: String, domain: String, session: URLSession - Parameters: - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. - bundle: Bundle used to locate the `Auth0.plist` file. Defaults to `Bundle.main`. - - Returns: Auth0 Authentication API client. + - Returns: Authentication API client. - Warning: Calling this method without a valid `Auth0.plist` file will crash your application. */ public func authentication(session: URLSession = .shared, bundle: Bundle = .main) -> Authentication { @@ -84,8 +90,7 @@ public func authentication(session: URLSession = .shared, bundle: Bundle = .main } /** - Auth0 [Management API v2](https://auth0.com/docs/api/management/v2) client to perform operations with the Users - endpoints. + [Management API v2](https://auth0.com/docs/api/management/v2) client for performing operations with the Users endpoints. ## Usage @@ -107,19 +112,19 @@ public func authentication(session: URLSession = .shared, bundle: Bundle = .main - ClientId - YOUR_AUTH0_CLIENT_ID - Domain - YOUR_AUTH0_DOMAIN + ClientId + YOUR_AUTH0_CLIENT_ID + Domain + YOUR_AUTH0_DOMAIN ``` - Parameters: - - token: Management API token with the correct allowed scopes to perform the desired action. + - token: Access token for the Management API with the correct allowed scopes to perform the desired action. - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. - bundle: Bundle used to locate the `Auth0.plist` file. Defaults to `Bundle.main`. - - Returns: Auth0 Management API v2 client. + - Returns: Management API v2 client. - Warning: Calling this method without a valid `Auth0.plist` file will crash your application. */ public func users(token: String, session: URLSession = .shared, bundle: Bundle = .main) -> Users { @@ -128,8 +133,7 @@ public func users(token: String, session: URLSession = .shared, bundle: Bundle = } /** - Auth0 [Management API v2](https://auth0.com/docs/api/management/v2) client to perform operations with the Users - endpoints. + [Management API v2](https://auth0.com/docs/api/management/v2) client for performing operations with the Users endpoints. ## Usage @@ -145,10 +149,10 @@ public func users(token: String, session: URLSession = .shared, bundle: Bundle = * Unlink users - Parameters: - - token: Management API token with the correct allowed scopes to perform the desired action. + - token: Access token for the Management API with the correct allowed scopes to perform the desired action. - domain: Domain of your Auth0 account, for example `samples.us.auth0.com`. - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. - - Returns: Auth0 Management API v2 client. + - Returns: Management API v2 client. */ public func users(token: String, domain: String, session: URLSession = .shared) -> Users { return Management(token: token, url: .httpsURL(from: domain), session: session) @@ -156,7 +160,8 @@ public func users(token: String, domain: String, session: URLSession = .shared) #if WEB_AUTH_PLATFORM /** - Auth0 client for performing web-based authentication with [Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login). + [Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login) client for performing web-based + authentication. ## Usage @@ -171,10 +176,10 @@ public func users(token: String, domain: String, session: URLSession = .shared) - ClientId - YOUR_AUTH0_CLIENT_ID - Domain - YOUR_AUTH0_DOMAIN + ClientId + YOUR_AUTH0_CLIENT_ID + Domain + YOUR_AUTH0_DOMAIN ``` @@ -182,7 +187,7 @@ public func users(token: String, domain: String, session: URLSession = .shared) - Parameters: - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. - bundle: Bundle used to locate the `Auth0.plist` file. Defaults to `Bundle.main`. - - Returns: Auth0 Web Auth client. + - Returns: Web Auth client. - Warning: Calling this method without a valid `Auth0.plist` file will crash your application. */ public func webAuth(session: URLSession = .shared, bundle: Bundle = Bundle.main) -> WebAuth { @@ -191,7 +196,8 @@ public func webAuth(session: URLSession = .shared, bundle: Bundle = Bundle.main) } /** - Auth0 client for performing web-based authentication with [Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login). + [Universal Login](https://auth0.com/docs/authenticate/login/auth0-universal-login) client for performing web-based + authentication. ## Usage @@ -203,7 +209,7 @@ public func webAuth(session: URLSession = .shared, bundle: Bundle = Bundle.main) - clientId: Client ID of your Auth0 application. - domain: Domain of your Auth0 account, for example `samples.us.auth0.com`. - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. - - Returns: Auth0 Web Auth client. + - Returns: Web Auth client. */ public func webAuth(clientId: String, domain: String, session: URLSession = .shared) -> WebAuth { return Auth0WebAuth(clientId: clientId, url: .httpsURL(from: domain), session: session) diff --git a/Auth0/Auth0APIError.swift b/Auth0/Auth0APIError.swift new file mode 100644 index 000000000..74326e78e --- /dev/null +++ b/Auth0/Auth0APIError.swift @@ -0,0 +1,102 @@ +import Foundation + +let apiErrorCode = "code" +let apiErrorDescription = "description" +let apiErrorCause = "cause" + +/// Generic representation of Auth0 API errors. +public protocol Auth0APIError: Auth0Error { + + /// Raw error values. + var info: [String: Any] { get } + + /// Error code. + var code: String { get } + + /// HTTP status code of the response. + var statusCode: Int { get } + + /// Creates an error from a JSON response. + /// + /// - Parameters: + /// - info: JSON response from Auth0. + /// - statusCode: HTTP status code of the response. + /// + /// - Returns: A new `Auth0APIError`. + init(info: [String: Any], statusCode: Int) + +} + +public extension Auth0APIError { + + /// The underlying `Error` value, if any. Defaults to `nil`. + var cause: Error? { + return self.info["cause"] as? Error + } + + /// Whether the request failed due to network issues. + /// + /// Returns `true` when the `URLError` code is one of the following: + /// - [dataNotAllowed](https://developer.apple.com/documentation/foundation/urlerror/datanotallowed) + /// - [notConnectedToInternet](https://developer.apple.com/documentation/foundation/urlerror/notconnectedtointernet) + /// - [networkConnectionLost](https://developer.apple.com/documentation/foundation/urlerror/networkconnectionlost) + /// - [dnsLookupFailed](https://developer.apple.com/documentation/foundation/urlerror/dnslookupfailed) + /// - [cannotFindHost](https://developer.apple.com/documentation/foundation/urlerror/cannotfindhost) + /// - [cannotConnectToHost](https://developer.apple.com/documentation/foundation/urlerror/cannotconnecttohost) + /// - [timedOut](https://developer.apple.com/documentation/foundation/urlerror/timedout) + /// - [internationalRoamingOff](https://developer.apple.com/documentation/foundation/urlerror/internationalroamingoff) + /// - [callIsActive](https://developer.apple.com/documentation/foundation/urlerror/callisactive) + /// + /// The underlying `URLError` is available in the ``cause`` property. + var isNetworkError: Bool { + guard let code = (self.cause as? URLError)?.code else { + return false + } + + return Self.networkErrorCodes.contains(code) + } + +} + +extension Auth0APIError { + + init(info: [String: Any], statusCode: Int = 0) { + self.init(info: info, statusCode: statusCode) + } + + init(cause error: Error, statusCode: Int = 0) { + let info: [String: Any] = [ + apiErrorCode: nonJSONError, + apiErrorDescription: "Unable to complete the operation.", + apiErrorCause: error + ] + self.init(info: info, statusCode: statusCode) + } + + init(description: String?, statusCode: Int = 0) { + let info: [String: Any] = [ + apiErrorCode: description != nil ? nonJSONError : emptyBodyError, + apiErrorDescription: description ?? "Empty response body." + ] + self.init(info: info, statusCode: statusCode) + } + + init(from response: Response) { + self.init(description: string(response.data), statusCode: response.response?.statusCode ?? 0) + } + + static var networkErrorCodes: [URLError.Code] { + return [ + .dataNotAllowed, + .notConnectedToInternet, + .networkConnectionLost, + .dnsLookupFailed, + .cannotFindHost, + .cannotConnectToHost, + .timedOut, + .internationalRoamingOff, + .callIsActive + ] + } + +} diff --git a/Auth0/Auth0Authentication.swift b/Auth0/Auth0Authentication.swift index 5cdd0f371..699b9fee9 100644 --- a/Auth0/Auth0Authentication.swift +++ b/Auth0/Auth0Authentication.swift @@ -41,7 +41,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -60,7 +60,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -77,7 +77,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -99,7 +99,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -117,7 +117,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -141,7 +141,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -202,7 +202,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: signup, method: "POST", - handle: databaseUser, + handle: authenticationDatabaseUser, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -246,7 +246,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -262,7 +262,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -303,7 +303,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -332,7 +332,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -349,7 +349,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: resetPassword, method: "POST", - handle: noBody, + handle: authenticationNoBody, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -367,7 +367,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: start, method: "POST", - handle: noBody, + handle: authenticationNoBody, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -384,7 +384,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: start, method: "POST", - handle: noBody, + handle: authenticationNoBody, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -438,7 +438,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: oauthToken, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -453,7 +453,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: oauthToken, method: "POST", - handle: noBody, + handle: authenticationNoBody, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -464,7 +464,7 @@ struct Auth0Authentication: Authentication { return Request(session: session, url: jwks, method: "GET", - handle: codable, + handle: authenticationDecodable, logger: self.logger, telemetry: self.telemetry) } @@ -489,7 +489,7 @@ private extension Auth0Authentication { return Request(session: session, url: url, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) @@ -503,7 +503,7 @@ private extension Auth0Authentication { return Request(session: session, url: token, method: "POST", - handle: codable, + handle: authenticationDecodable, parameters: payload, logger: self.logger, telemetry: self.telemetry) diff --git a/Auth0/Auth0Error.swift b/Auth0/Auth0Error.swift index ecb0d2ed8..29a75caa1 100644 --- a/Auth0/Auth0Error.swift +++ b/Auth0/Auth0Error.swift @@ -31,65 +31,19 @@ public extension Auth0Error { extension Auth0Error { - func appendCause(to errorMessage: String) -> String { + func appendCause(to message: String) -> String { guard let cause = self.cause else { - return errorMessage + return message } - let separator = errorMessage.hasSuffix(".") ? "" : "." - return "\(errorMessage)\(separator) CAUSE: \(String(describing: cause))" - } - -} - -/// Generic representation of Auth0 API errors. -public protocol Auth0APIError: Auth0Error { - - /// Additional information about the error. - var info: [String: Any] { get } - - /// The code of the error as a string. - var code: String { get } - - /// HTTP status code of the response. - var statusCode: Int { get } - - /// Creates an error from a JSON response. - /// - /// - Parameters: - /// - info: JSON response from Auth0. - /// - statusCode: HTTP status code of the response. - /// - /// - Returns: A new `Auth0APIError`. - init(info: [String: Any], statusCode: Int) - -} - -extension Auth0APIError { - - init(info: [String: Any], statusCode: Int = 0) { - self.init(info: info, statusCode: statusCode) - } - - init(cause error: Error, statusCode: Int = 0) { - let info: [String: Any] = [ - "code": nonJSONError, - "description": "Unable to complete the operation.", - "cause": error - ] - self.init(info: info, statusCode: statusCode) - } + let errorMessage = self.appendPeriod(to: message) + let causeMessage = self.appendPeriod(to: String(describing: cause)) - init(description: String?, statusCode: Int = 0) { - let info: [String: Any] = [ - "code": description != nil ? nonJSONError : emptyBodyError, - "description": description ?? "Empty response body." - ] - self.init(info: info, statusCode: statusCode) + return "\(errorMessage) CAUSE: \(causeMessage)" } - init(from response: Response) { - self.init(description: string(response.data), statusCode: response.response?.statusCode ?? 0) + func appendPeriod(to message: String) -> String { + return message.hasSuffix(".") ? message : "\(message)." } } diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index cb61bb7fb..ce712e240 100644 --- a/Auth0/Authentication.swift +++ b/Auth0/Authentication.swift @@ -541,6 +541,8 @@ public protocol Authentication: Trackable, Loggable { /// - scope: Space-separated list of requested scope values. Defaults to `openid profile email`. /// - Returns: A request that will yield Auth0 user's credentials. /// + /// ## See Also + /// /// - [Authentication API Endpoint](https://auth0.com/docs/native-passkeys-api#authenticate-existing-user) /// - [Native Passkeys for Mobile Applications](https://auth0.com/docs/native-passkeys-for-mobile-applications) /// - [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Connect-to-a-service-with-an-existing-account) @@ -657,6 +659,8 @@ public protocol Authentication: Trackable, Loggable { /// - scope: Space-separated list of requested scope values. Defaults to `openid profile email`. /// - Returns: A request that will yield Auth0 user's credentials. /// + /// ## See Also + /// /// - [Authentication API Endpoint](https://auth0.com/docs/native-passkeys-api#authenticate-new-user) /// - [Native Passkeys for Mobile Applications](https://auth0.com/docs/native-passkeys-for-mobile-applications) /// - [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service) @@ -990,18 +994,20 @@ public protocol Authentication: Trackable, Loggable { You can request credentials for a specific API by passing its audience value. The default scopes configured for the API will be granted if you don't request any specific scopes. + > Important: Currently, only the Auth0 My Account API is supported. Support for other APIs will be added in the future. + ```swift Auth0 .authentication() .renew(withRefreshToken: credentials.refreshToken, - audience: "http://example.com/api", - scope: "read:todos update:todos") + audience: "https://samples.us.auth0.com/me", + scope: "create:me:authentication_methods") .start { print($0) } ``` - Parameters: - refreshToken: The refresh token. - - audience: Identifier of the API that your application is requesting access to. Defaults to `nil`. + - audience: Identifier of the API that your application is requesting access to. Currently, only the Auth0 My Account API is supported. Defaults to `nil`. - scope: Space-separated list of scope values to request. Defaults to `nil`. - Returns: A request that will yield Auth0 user's credentials. diff --git a/Auth0/AuthenticationError.swift b/Auth0/AuthenticationError.swift index a06c14263..6f8c8c6f3 100644 --- a/Auth0/AuthenticationError.swift +++ b/Auth0/AuthenticationError.swift @@ -7,7 +7,7 @@ import Foundation /// - [Standard Error Responses](https://auth0.com/docs/api/authentication#standard-error-responses) public struct AuthenticationError: Auth0APIError, @unchecked Sendable { - /// Additional information about the error. + /// Raw error values. public let info: [String: Any] /// Creates an error from a JSON response. @@ -27,15 +27,9 @@ public struct AuthenticationError: Auth0APIError, @unchecked Sendable { /// HTTP status code of the response. public let statusCode: Int - /// The underlying `Error` value, if any. Defaults to `nil`. - public var cause: Error? { - return self.info["cause"] as? Error - } - - /// The code of the error as a string. + /// Error code. public var code: String { - let code = self.info["error"] ?? self.info["code"] - return code as? String ?? unknownError + return self.info["error"] as? String ?? self.info[apiErrorCode] as? String ?? unknownError } /// Description of the error. @@ -135,37 +129,6 @@ public struct AuthenticationError: Auth0APIError, @unchecked Sendable { return self.code == "login_required" } - /// When the request failed due to network issues. - /// - /// Returns `true` when the `URLError` code is one of the following: - /// - [notConnectedToInternet](https://developer.apple.com/documentation/foundation/urlerror/2293104-notconnectedtointernet) - /// - [networkConnectionLost](https://developer.apple.com/documentation/foundation/urlerror/2293759-networkconnectionlost) - /// - [dnsLookupFailed](https://developer.apple.com/documentation/foundation/urlerror/2293434-dnslookupfailed) - /// - [cannotFindHost](https://developer.apple.com/documentation/foundation/urlerror/2293460-cannotfindhost) - /// - [cannotConnectToHost](https://developer.apple.com/documentation/foundation/urlerror/2293028-cannotconnecttohost) - /// - [timedOut](https://developer.apple.com/documentation/foundation/urlerror/2293002-timedout) - /// - [internationalRoamingOff](https://developer.apple.com/documentation/foundation/urlerror/2292893-internationalroamingoff) - /// - [callIsActive](https://developer.apple.com/documentation/foundation/urlerror/2293147-callisactive) - /// - /// The underlying `URLError` is available in the ``Auth0Error/cause-9wuyi`` property. - public var isNetworkError: Bool { - guard let code = (self.cause as? URLError)?.code else { - return false - } - - let networkErrorCodes: [URLError.Code] = [ - .dataNotAllowed, - .notConnectedToInternet, - .networkConnectionLost, - .dnsLookupFailed, - .cannotFindHost, - .cannotConnectToHost, - .timedOut, - .internationalRoamingOff, - .callIsActive - ] - return networkErrorCodes.contains(code) - } } // MARK: - Error Messages @@ -173,13 +136,12 @@ public struct AuthenticationError: Auth0APIError, @unchecked Sendable { extension AuthenticationError { var message: String { - let description = self.info["description"] ?? self.info["error_description"] - - if let string = description as? String { - return string + if let description = self.info[apiErrorDescription] as? String ?? self.info["error_description"] as? String { + return description } + if self.code == unknownError { - return "Failed with unknown error \(self.info)." + return "Failed with unknown error: \(self.info)." } return "Received error with code \(self.code)." diff --git a/Auth0/AuthenticationHandlers.swift b/Auth0/AuthenticationHandlers.swift new file mode 100644 index 000000000..9b81d39af --- /dev/null +++ b/Auth0/AuthenticationHandlers.swift @@ -0,0 +1,58 @@ +import Foundation + +func authenticationDecodable(from response: Response, + callback: Request.Callback) { + do { + if let data = try response.result()?.data { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + let decodedObject = try decoder.decode(T.self, from: data) + callback(.success(decodedObject)) + } else { + callback(.failure(AuthenticationError(from: response))) + } + } catch let error as AuthenticationError { + callback(.failure(error)) + } catch { + callback(.failure(AuthenticationError(cause: error))) + } +} + +func authenticationObject(from response: Response, callback: Request.Callback) { + do { + if let dictionary = try response.result()?.body as? [String: Any], let object = T(json: dictionary) { + callback(.success(object)) + } else { + callback(.failure(AuthenticationError(from: response))) + } + } catch { + callback(.failure(error)) + } +} + +func authenticationDatabaseUser(from response: Response, + callback: Request.Callback) { + do { + if let dictionary = try response.result()?.body as? [String: Any], let email = dictionary["email"] as? String { + let username = dictionary["username"] as? String + let verified = dictionary["email_verified"] as? Bool ?? false + callback(.success((email: email, username: username, verified: verified))) + } else { + callback(.failure(AuthenticationError(from: response))) + } + } catch { + callback(.failure(error)) + } +} + +func authenticationNoBody(from response: Response, + callback: Request.Callback) { + do { + _ = try response.result() + callback(.success(())) + } catch let error where error.code == emptyBodyError { + callback(.success(())) + } catch { + callback(.failure(error)) + } +} diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index babf074fe..3f024ecbe 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -11,12 +11,14 @@ import LocalAuthentication /// Credentials management utility for securely storing and retrieving the user's credentials from the Keychain. /// -/// - Warning: The Credentials Manager is not thread-safe, except for its -/// ``CredentialsManager/credentials(withScope:minTTL:parameters:headers:callback:)``, -/// ``CredentialsManager/apiCredentials(forAudience:scope:minTTL:parameters:headers:callback:)``, and -/// ``CredentialsManager/ssoCredentials(parameters:headers:callback:)``, and -/// ``CredentialsManager/renew(parameters:headers:callback:)`` methods. To avoid concurrency issues, do not -/// call its non thread-safe methods and properties from different threads without proper synchronization. +/// - Warning: The Credentials Manager is not thread-safe, except for the following methods: +/// - ``CredentialsManager/credentials(withScope:minTTL:parameters:headers:callback:)`` +/// - ``CredentialsManager/apiCredentials(forAudience:scope:minTTL:parameters:headers:callback:)`` +/// - ``CredentialsManager/ssoCredentials(parameters:headers:callback:)`` +/// - ``CredentialsManager/renew(parameters:headers:callback:)`` +/// +/// To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads +/// without proper synchronization. /// /// ## See Also /// diff --git a/Auth0/Handlers.swift b/Auth0/Handlers.swift deleted file mode 100644 index 0ff4d9e6e..000000000 --- a/Auth0/Handlers.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation - -func plainJson(from response: Response, callback: Request<[String: Any], AuthenticationError>.Callback) { - do { - if let dictionary = try response.result() as? [String: Any] { - callback(.success(dictionary)) - } else { - callback(.failure(AuthenticationError(from: response))) - } - } catch let error as AuthenticationError { - callback(.failure(error)) - } catch { - callback(.failure(AuthenticationError(cause: error))) - } -} - -func codable(from response: Response, - callback: Request.Callback) { - do { - if let dictionary = try response.result() as? [String: Any] { - let data = try JSONSerialization.data(withJSONObject: dictionary) - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .secondsSince1970 - let decodedObject = try decoder.decode(T.self, from: data) - callback(.success(decodedObject)) - } else { - callback(.failure(AuthenticationError(from: response))) - } - } catch let error as AuthenticationError { - callback(.failure(error)) - } catch { - callback(.failure(AuthenticationError(cause: error))) - } -} - -func authenticationObject(from response: Response, callback: Request.Callback) { - do { - if let dictionary = try response.result() as? [String: Any], let object = T(json: dictionary) { - callback(.success(object)) - } else { - callback(.failure(AuthenticationError(from: response))) - } - } catch let error as AuthenticationError { - callback(.failure(error)) - } catch { - callback(.failure(AuthenticationError(cause: error))) - } -} - -func databaseUser(from response: Response, callback: Request.Callback) { - do { - if let dictionary = try response.result() as? [String: Any], let email = dictionary["email"] as? String { - let username = dictionary["username"] as? String - let verified = dictionary["email_verified"] as? Bool ?? false - callback(.success((email: email, username: username, verified: verified))) - } else { - callback(.failure(AuthenticationError(from: response))) - } - } catch let error as AuthenticationError { - callback(.failure(error)) - } catch { - callback(.failure(AuthenticationError(cause: error))) - } -} - -func noBody(from response: Response, callback: Request.Callback) { - do { - _ = try response.result() - callback(.success(())) - } catch let error as AuthenticationError where error.code == emptyBodyError { - callback(.success(())) - } catch let error as AuthenticationError { - callback(.failure(error)) - } catch { - callback(.failure(AuthenticationError(cause: error))) - } -} diff --git a/Auth0/Management.swift b/Auth0/Management.swift index efbcee535..338cf2263 100644 --- a/Auth0/Management.swift +++ b/Auth0/Management.swift @@ -26,29 +26,25 @@ struct Management: Trackable, Loggable { func managementObject(response: Response, callback: Request.Callback) { do { - if let dictionary = try response.result() as? ManagementObject { + if let dictionary = try response.result()?.body as? ManagementObject { callback(.success(dictionary)) } else { callback(.failure(ManagementError(from: response))) } - } catch let error as ManagementError { - callback(.failure(error)) } catch { - callback(.failure(ManagementError(cause: error))) + callback(.failure(error)) } } func managementObjects(response: Response, callback: Request<[ManagementObject], ManagementError>.Callback) { do { - if let list = try response.result() as? [ManagementObject] { + if let list = try response.result()?.body as? [ManagementObject] { callback(.success(list)) } else { callback(.failure(ManagementError(from: response))) } - } catch let error as ManagementError { - callback(.failure(error)) } catch { - callback(.failure(ManagementError(cause: error))) + callback(.failure(error)) } } } diff --git a/Auth0/ManagementError.swift b/Auth0/ManagementError.swift index 1f18c9d82..427b086a3 100644 --- a/Auth0/ManagementError.swift +++ b/Auth0/ManagementError.swift @@ -3,7 +3,7 @@ import Foundation /// Represents an error during a request to the Auth0 Management API v2. public struct ManagementError: Auth0APIError, @unchecked Sendable { - /// Additional information about the error. + /// Raw error values. public let info: [String: Any] /// Creates an error from a JSON response. @@ -23,14 +23,9 @@ public struct ManagementError: Auth0APIError, @unchecked Sendable { /// HTTP status code of the response. public let statusCode: Int - /// The underlying `Error` value, if any. Defaults to `nil`. - public var cause: Error? { - return self.info["cause"] as? Error - } - - /// The code of the error as a string. + /// Error code. public var code: String { - return self.info["code"] as? String ?? unknownError + return self.info[apiErrorCode] as? String ?? unknownError } /// Description of the error. @@ -50,7 +45,7 @@ extension ManagementError { if let string = self.info["description"] as? String { return string } - return "Failed with unknown error \(self.info)." + return "Failed with unknown error: \(self.info)." } } diff --git a/Auth0/MyAccount/Auth0MyAccount.swift b/Auth0/MyAccount/Auth0MyAccount.swift new file mode 100644 index 000000000..e8dcdb57b --- /dev/null +++ b/Auth0/MyAccount/Auth0MyAccount.swift @@ -0,0 +1,34 @@ +import Foundation + +struct Auth0MyAccount: MyAccount { + + let url: URL + let session: URLSession + let token: String + + var telemetry: Telemetry + var logger: Logger? + + static let apiVersion = "v1" + + var authenticationMethods: MyAccountAuthenticationMethods { + return Auth0MyAccountAuthenticationMethods(token: self.token, + url: self.url, + session: self.session, + telemetry: self.telemetry, + logger: self.logger) + } + + init(token: String, + url: URL, + session: URLSession = .shared, + telemetry: Telemetry = Telemetry(), + logger: Logger? = nil) { + self.url = url.appending("me/\(Self.apiVersion)") + self.session = session + self.token = token + self.telemetry = telemetry + self.logger = logger + } + +} diff --git a/Auth0/MyAccount/AuthenticationMethods/Auth0MyAccountAuthenticationMethods.swift b/Auth0/MyAccount/AuthenticationMethods/Auth0MyAccountAuthenticationMethods.swift new file mode 100644 index 000000000..a1d3435cc --- /dev/null +++ b/Auth0/MyAccount/AuthenticationMethods/Auth0MyAccountAuthenticationMethods.swift @@ -0,0 +1,79 @@ +import Foundation + +struct Auth0MyAccountAuthenticationMethods: MyAccountAuthenticationMethods { + + let url: URL + let session: URLSession + let token: String + + var telemetry: Telemetry + var logger: Logger? + + init(token: String, + url: URL, + session: URLSession = .shared, + telemetry: Telemetry = Telemetry(), + logger: Logger? = nil) { + self.token = token + self.url = url.appending("authentication-methods") + self.session = session + self.telemetry = telemetry + self.logger = logger + } + + // MARK: - Passkey Enrollment + + #if PASSKEYS_PLATFORM + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func passkeyEnrollmentChallenge(userIdentityId: String?, + connection: String?) -> Request { + var payload: [String: Any] = ["type": "passkey"] + payload["identity_user_id"] = userIdentityId + payload["connection"] = connection + + return Request(session: session, + url: self.url, + method: "POST", + handle: myAcccountDecodable, + parameters: payload, + headers: defaultHeaders, + logger: self.logger, + telemetry: self.telemetry) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func enroll(passkey: NewPasskey, + challenge: PasskeyEnrollmentChallenge) -> Request { + let resourceId = challenge.authenticationMethodId.removingPercentEncoding! + let path = "\(resourceId)/verify" + let credentialId = passkey.credentialID.encodeBase64URLSafe() + + var authenticatorResponse: [String: Any] = [ + "id": credentialId, + "rawId": credentialId, + "type": "public-key", + "response": [ + "clientDataJSON": passkey.rawClientDataJSON.encodeBase64URLSafe(), + "attestationObject": passkey.rawAttestationObject!.encodeBase64URLSafe() + ] + ] + + authenticatorResponse["authenticatorAttachment"] = passkey.attachment.stringValue + + let payload: [String: Any] = [ + "auth_session": challenge.authenticationSession, + "authn_response": authenticatorResponse + ] + + return Request(session: session, + url: self.url.appending(path), + method: "POST", + handle: myAcccountDecodable, + parameters: payload, + headers: defaultHeaders, + logger: self.logger, + telemetry: self.telemetry) + } + #endif + +} diff --git a/Auth0/MyAccount/AuthenticationMethods/MyAccountAuthenticationMethods.swift b/Auth0/MyAccount/AuthenticationMethods/MyAccountAuthenticationMethods.swift new file mode 100644 index 000000000..13437ea8c --- /dev/null +++ b/Auth0/MyAccount/AuthenticationMethods/MyAccountAuthenticationMethods.swift @@ -0,0 +1,133 @@ +/// My Account API sub-client for managing the current user's authentication methods. +/// +/// ## See Also +/// - ``MyAccount`` +/// - ``MyAccountError`` +public protocol MyAccountAuthenticationMethods: MyAccountClient { + + #if PASSKEYS_PLATFORM + /// Requests a challenge for enrolling a new passkey. This is the first part of the enrollment flow. + /// + /// You can specify an optional user identity identifier and an optional database connection name. If a + /// connection name is not specified, your tenant's default directory will be used. + /// + /// ## Availability + /// + /// This feature is currently available in + /// [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). + /// Please reach out to Auth0 support to get it enabled for your tenant. + /// + /// ## Scopes Required + /// + /// `create:me:authentication_methods` + /// + /// ## Usage + /// + /// ```swift + /// Auth0 + /// .myAccount(token: apiCredentials.accessToken) + /// .passkeyEnrollmentChallenge() + /// .start { result in + /// switch result { + /// case .success(let enrollmentChallenge): + /// print("Obtained enrollment challenge: \(enrollmentChallenge)") + /// case .failure(let error): + /// print("Failed with: \(error)") + /// } + /// } + /// ``` + /// + /// Use the challenge with [`ASAuthorizationPlatformPublicKeyCredentialProvider`](https://developer.apple.com/documentation/authenticationservices/asauthorizationplatformpublickeycredentialprovider) + /// from the `AuthenticationServices` framework to generate a new passkey credential. It will be delivered through the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) + /// delegate. Check out [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service) + /// to learn more. + /// + /// ```swift + /// let credentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( + /// relyingPartyIdentifier: enrollmentChallenge.relyingPartyId + /// ) + /// + /// let request = credentialProvider.createCredentialRegistrationRequest( + /// challenge: enrollmentChallenge.challengeData, + /// name: enrollmentChallenge.userName, + /// userID: enrollmentChallenge.userId + /// ) + /// + /// let authController = ASAuthorizationController(authorizationRequests: [request]) + /// authController.delegate = self // ASAuthorizationControllerDelegate + /// authController.presentationContextProvider = self + /// authController.performRequests() + /// ``` + /// + /// Then, call ``enroll(passkey:challenge:)`` with the created passkey credential and the challenge to complete the + /// enrollment. + /// + /// - Parameters: + /// - userIdentityId: Unique identifier of the current user's identity. Needed if the user logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking). + /// Defaults to `nil`. + /// - connection: Name of the database connection where the user is stored. Defaults to `nil`. + /// - Returns: A request that will yield a passkey enrollment challenge. + /// + /// ## See Also + /// + /// - [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service) + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func passkeyEnrollmentChallenge(userIdentityId: String?, + connection: String?) -> Request + + /// Enrolls a new passkey credential. This is the last part of the enrollment flow. + /// + /// ## Availability + /// + /// This feature is currently available in + /// [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). + /// Please reach out to Auth0 support to get it enabled for your tenant. + /// + /// ## Scopes Required + /// + /// `create:me:authentication_methods` + /// + /// ## Usage + /// + /// ```swift + /// Auth0 + /// .myAccount(token: apiCredentials.accessToken) + /// .enroll(passkey: newPasskey, challenge: enrollmentChallenge) + /// .start { result in + /// switch result { + /// case .success(let authenticationMethod): + /// print("Enrolled passkey: \(authenticationMethod)") + /// case .failure(let error): + /// print("Failed with: \(error)") + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - passkey: The new passkey credential obtained from the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate. + /// - challenge: The passkey enrollment challenge obtained from ``passkeyEnrollmentChallenge(userIdentityId:connection:)``. + /// - Returns: A request that will yield an enrolled passkey authentication method. + /// + /// ## See Also + /// + /// - [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service) + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func enroll(passkey: NewPasskey, + challenge: PasskeyEnrollmentChallenge) -> Request + #endif + +} + +// MARK: - Default Parameters + +public extension MyAccountAuthenticationMethods { + + #if PASSKEYS_PLATFORM + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func passkeyEnrollmentChallenge(userIdentityId: String? = nil, + connection: String? = nil) -> Request { + self.passkeyEnrollmentChallenge(userIdentityId: userIdentityId, connection: connection) + } + #endif + +} diff --git a/Auth0/MyAccount/AuthenticationMethods/PasskeyAuthenticationMethod.swift b/Auth0/MyAccount/AuthenticationMethods/PasskeyAuthenticationMethod.swift new file mode 100644 index 000000000..8897ddab4 --- /dev/null +++ b/Auth0/MyAccount/AuthenticationMethods/PasskeyAuthenticationMethod.swift @@ -0,0 +1,131 @@ +#if PASSKEYS_PLATFORM +import Foundation + +/// A passkey authentication method. +public struct PasskeyAuthenticationMethod: Identifiable, Equatable, Sendable { + + /// Unique identifier of the authentication method. + public let id: String + + /// Type of the authentication method. Equals to `passkey`. + public let type: String + + /// Unique identifier of the user identity linked with the authentication method. + public let userIdentityId: String + + /// The user agent of the browser o device used to enroll the passkey. + public let userAgent: String? + + /// Details of the passkey credential. + public let credential: PasskeyCredential + + /// Creation date of the authentication method. + public let createdAt: Date + +} + +/// A passkey credential. +public struct PasskeyCredential: Identifiable, Equatable, Sendable { + + /// Unique identifier of the passkey credential. + public let id: String + + /// Public key of the passkey credential. + public let publicKey: Data // This comes base64-encoded + + /// User handle associated with the passkey credential. + public let userHandle: Data // This comes base64url-encoded + + /// Kind of device the passkey credential is stored on as defined by backup eligibility. + public let deviceType: PasskeyDeviceType + + /// Whether the passkey credential was backed up. + public let isBackedUp: Bool + +} + +/// Kind of device the passkey is stored on as defined by backup eligibility. +public enum PasskeyDeviceType: String, Sendable, Decodable { + + /// Passkey that cannot be backed up and synced to another device. + case singleDevice = "single_device" + + /// Passkey that can be backed up and synced to another device, when enabled by the user. + case multiDevice = "multi_device" + +} + +extension PasskeyAuthenticationMethod: Decodable { + + enum CodingKeys: String, CodingKey { + case id + case type + case userIdentityId = "identity_user_id" + case userAgent = "user_agent" + case credential + case createdAt = "created_at" + } + + /// `Decodable` initializer. + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + let createdAtFormatter = ISO8601DateFormatter() + createdAtFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + guard case let createdAtString = try values.decode(String.self, forKey: .createdAt), + let createdAtDate = createdAtFormatter.date(from: createdAtString) else { + throw DecodingError.dataCorruptedError(forKey: .createdAt, + in: values, + debugDescription: "Format of created_at is not recognized.") + } + + createdAt = createdAtDate + + id = try values.decode(String.self, forKey: .id) + type = try values.decode(String.self, forKey: .type) + userIdentityId = try values.decode(String.self, forKey: .userIdentityId) + userAgent = try values.decodeIfPresent(String.self, forKey: .userAgent) + credential = try decoder.singleValueContainer().decode(PasskeyCredential.self) + } + +} + +extension PasskeyCredential: Decodable { + + enum CodingKeys: String, CodingKey { + case id = "key_id" + case publicKey = "public_key" + case userHandle = "user_handle" + case deviceType = "credential_device_type" + case isBackedUp = "credential_backed_up" + } + + /// `Decodable` initializer. + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + guard case let userHandleString = try values.decode(String.self, forKey: .userHandle), + let userHandleData = userHandleString.a0_decodeBase64URLSafe() else { + throw DecodingError.dataCorruptedError(forKey: .userHandle, + in: values, + debugDescription: "Format of user_handle is not recognized.") + } + + userHandle = userHandleData + + guard case let publicKeyString = try values.decode(String.self, forKey: .publicKey), + let publicKeyData = Data(base64Encoded: publicKeyString) else { + throw DecodingError.dataCorruptedError(forKey: .publicKey, + in: values, + debugDescription: "Format of public_key is not recognized.") + } + + publicKey = publicKeyData + + id = try values.decode(String.self, forKey: .id) + deviceType = try values.decode(PasskeyDeviceType.self, forKey: .deviceType) + isBackedUp = try values.decode(Bool.self, forKey: .isBackedUp) + } + +} +#endif diff --git a/Auth0/MyAccount/AuthenticationMethods/PasskeyEnrollmentChallenge.swift b/Auth0/MyAccount/AuthenticationMethods/PasskeyEnrollmentChallenge.swift new file mode 100644 index 000000000..e45075aec --- /dev/null +++ b/Auth0/MyAccount/AuthenticationMethods/PasskeyEnrollmentChallenge.swift @@ -0,0 +1,58 @@ +#if PASSKEYS_PLATFORM +import Foundation + +/// A passkey enrollment challenge. +public struct PasskeyEnrollmentChallenge { + + /// Unique identifier of the authentication method. + public let authenticationMethodId: String + + /// Unique identifier of the Auth0 session. + public let authenticationSession: String + + /// Custom domain configured in the Auth0 tenant. + public let relyingPartyId: String + + /// Generated unique identifier of the user. + public let userId: Data + + /// A user identifier, like the user's email. + public let userName: String + + /// Enrollment challenge data. + public let challengeData: Data + +} + +extension PasskeyEnrollmentChallenge: Decodable { + + enum CodingKeys: String, CodingKey { + case authenticationSession = "auth_session" + case credentialCreationOptions = "authn_params_public_key" + } + + /// `Decodable` initializer. + public init(from decoder: Decoder) throws { + guard let headers = decoder.userInfo[.headersKey] as? [String: Any], + let locationHeader = headers["Location"] as? String, + let authenticationMethodId = locationHeader.components(separatedBy: "/").last else { + let errorDescription = "Missing authentication method identifier in header 'Location'" + let errorContext = DecodingError.Context(codingPath: [], + debugDescription: errorDescription) + throw DecodingError.dataCorrupted(errorContext) + } + + let values = try decoder.container(keyedBy: CodingKeys.self) + let credentialOptions = try values.decode(PublicKeyCredentialCreationOptions.self, + forKey: .credentialCreationOptions) + + self.init(authenticationMethodId: authenticationMethodId, + authenticationSession: try values.decode(String.self, forKey: .authenticationSession), + relyingPartyId: credentialOptions.relyingParty.id, + userId: credentialOptions.user.id, + userName: credentialOptions.user.name, + challengeData: credentialOptions.challengeData) + } + +} +#endif diff --git a/Auth0/MyAccount/MyAccount.swift b/Auth0/MyAccount/MyAccount.swift new file mode 100644 index 000000000..4b540e9b1 --- /dev/null +++ b/Auth0/MyAccount/MyAccount.swift @@ -0,0 +1,104 @@ +import Foundation + +// MARK: - Factory Methods + +/// Auth0 My Account API client for managing the current user's account. +/// +/// ## Usage +/// +/// ```swift +/// Auth0.myAccount(token: apiCredentials.accessToken, domain: "samples.us.auth0.com") +/// ``` +/// +/// You can use the refresh token to get an access token for the My Account API. Refer to ``CredentialsManager/apiCredentials(forAudience:scope:minTTL:parameters:headers:callback:)``, or alternatively ``Authentication/renew(withRefreshToken:audience:scope:)`` if you are not using the ``CredentialsManager``. +/// +/// > Note: See [Get a refresh token](https://github.com/auth0/Auth0.swift/blob/master/EXAMPLES.md#get-a-refresh-token) +/// to learn how to obtain a refresh token. +/// +/// - Parameters: +/// - token: Access token for the My Account API with the correct scopes to perform the desired action. +/// - domain: Domain of your Auth0 account, for example `samples.us.auth0.com`. +/// - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. +/// - Returns: My Account API client. +public func myAccount(token: String, domain: String, session: URLSession = .shared) -> MyAccount { + return Auth0MyAccount(token: token, url: .httpsURL(from: domain), session: session) +} + +/// Auth0 My Account API client for managing the current user's account. +/// +/// ## Usage +/// +/// ```swift +/// Auth0.myAccount(token: apiCredentials.accessToken) +/// ``` +/// +/// You can use the refresh token to get an access token for the My Account API. Refer to ``CredentialsManager/apiCredentials(forAudience:scope:minTTL:parameters:headers:callback:)``, or alternatively ``Authentication/renew(withRefreshToken:audience:scope:)`` if you are not using the ``CredentialsManager``. +/// +/// > Note: See [Get a refresh token](https://github.com/auth0/Auth0.swift/blob/master/EXAMPLES.md#get-a-refresh-token) +/// to learn how to obtain a refresh token. +/// +/// The Auth0 Domain is loaded from the `Auth0.plist` file in your main bundle. It should have the following content: +/// +/// ```xml +/// +/// +/// +/// +/// ClientId +/// YOUR_AUTH0_CLIENT_ID +/// Domain +/// YOUR_AUTH0_DOMAIN +/// +/// +/// ``` +/// +/// - Parameters: +/// - token: Access token for the My Account API with the correct scopes to perform the desired action. +/// - session: `URLSession` instance used for networking. Defaults to `URLSession.shared`. +/// - bundle: Bundle used to locate the `Auth0.plist` file. Defaults to `Bundle.main` +/// - Returns: My Account API client. +public func myAccount(token: String, session: URLSession = .shared, bundle: Bundle = .main) -> MyAccount { + let values = plistValues(bundle: bundle)! + return myAccount(token: token, domain: values.domain, session: session) +} + +// MARK: - MyAccountClient + +/// A client for the My Account API. +/// Adopting types could be either the root client or a leaf sub-client. +/// +/// ## See Also +/// - ``MyAccountError`` +public protocol MyAccountClient: Trackable, Loggable { + + /// URL of the My Account API. + var url: URL { get } + + /// An access token for My Account API. + var token: String { get } + +} + +extension MyAccountClient { + + var defaultHeaders: [String: String] { + return ["Authorization": "Bearer \(token)"] + } + +} + +// MARK: - MyAccount + +/// My Account API client for managing the current user's account. +/// +/// ## See Also +/// - ``MyAccountError`` +public protocol MyAccount: MyAccountClient { + + /// My Account API sub-client for managing the current user's authentication methods. + var authenticationMethods: MyAccountAuthenticationMethods { get } + + /// Currently supported version of the My Account API. + static var apiVersion: String { get } + +} diff --git a/Auth0/MyAccount/MyAccountError.swift b/Auth0/MyAccount/MyAccountError.swift new file mode 100644 index 000000000..9cb022143 --- /dev/null +++ b/Auth0/MyAccount/MyAccountError.swift @@ -0,0 +1,103 @@ +/// Represents an error during a request to the Auth0 My Account API. +public struct MyAccountError: Auth0APIError, @unchecked Sendable { + + /// A server-side validation error. + public struct ValidationError: Sendable { + + /// Information about the validation error. + let detail: String + + /// The property in the request payload that failed validation. + let pointer: String? + + } + + /// Raw error values. + public let info: [String: Any] + + /// HTTP status code of the response. + public let statusCode: Int + + /// Error code. + public let code: String + + /// Error description. + public let title: String + + /// More information about the error. + public let detail: String + + /// All the server-side validation errors. + public let validationErrors: [ValidationError]? + + /// Creates an error from a JSON response. + /// + /// - Parameters: + /// - info: JSON response from Auth0. + /// - statusCode: HTTP status code of the response. + /// + /// - Returns: A new `NyAccountError`. + public init(info: [String: Any], statusCode: Int) { + self.info = info + self.statusCode = statusCode + self.code = info["type"] as? String ?? info[apiErrorCode] as? String ?? unknownError + self.title = info["title"] as? String ?? info[apiErrorDescription] as? String ?? "" + self.detail = info["detail"] as? String ?? "" + + if let validationErrors = info["validation_errors"] as? [[String: Any]] { + self.validationErrors = validationErrors.map(ValidationError.init(from:)) + } else { + self.validationErrors = nil + } + } + + /// Description of the error. + /// + /// - Important: You should avoid displaying the error description to the user, it's meant for **debugging** only. + public var debugDescription: String { + self.appendCause(to: self.message) + } + +} + +// MARK: - Error Messages + +extension MyAccountError { + + var message: String { + if self.code == unknownError { + return "Failed with unknown error: \(self.info)." + } + + if !self.detail.isEmpty { + return self.appendPeriod(to: "\(self.title): \(self.detail)") + } + + return self.appendPeriod(to: "\(self.title)") + } + +} + +// MARK: - Equatable + +extension MyAccountError: Equatable { + + /// Conformance to `Equatable`. + public static func == (lhs: MyAccountError, rhs: MyAccountError) -> Bool { + return lhs.code == rhs.code + && lhs.statusCode == rhs.statusCode + && lhs.localizedDescription == rhs.localizedDescription + } + +} + +extension MyAccountError.ValidationError { + + init(from dict: [String: Any]) { + let pointer = dict["pointer"] as? String ?? "" + + self.detail = dict["detail"] as? String ?? "" + self.pointer = pointer.isEmpty ? nil : pointer + } + +} diff --git a/Auth0/MyAccount/MyAccountHandlers.swift b/Auth0/MyAccount/MyAccountHandlers.swift new file mode 100644 index 000000000..832e5fb74 --- /dev/null +++ b/Auth0/MyAccount/MyAccountHandlers.swift @@ -0,0 +1,28 @@ +import Foundation + +func myAcccountDecodable(from response: Response, + callback: Request.Callback) { + do { + if let jsonResponse = try response.result() { + let decoder = JSONDecoder() + decoder.userInfo[.headersKey] = jsonResponse.headers + let decodedObject = try decoder.decode(T.self, from: jsonResponse.data) + callback(.success(decodedObject)) + } else { + callback(.failure(MyAccountError(from: response))) + } + } catch let error as MyAccountError { + callback(.failure(error)) + } catch { + callback(.failure(MyAccountError(cause: error))) + } +} + +extension CodingUserInfoKey { + + static var headersKey: CodingUserInfoKey { + // Force-unrapping it because it's never nil. See https://github.com/swiftlang/swift/issues/49302 + return CodingUserInfoKey(rawValue: "headers")! + } + +} diff --git a/Auth0/NSURL+Auth0.swift b/Auth0/NSURL+Auth0.swift index fefeba696..5e0d1c1a5 100644 --- a/Auth0/NSURL+Auth0.swift +++ b/Auth0/NSURL+Auth0.swift @@ -9,4 +9,14 @@ extension URL { return URL(string: urlString)! } + // Once the required minimum platform versions have been adopted, + // replace usages with `appending(path:)` and then remove this helper. + func appending(_ path: String) -> Self { + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + return self.appending(path: path) + } + + return self.appendingPathComponent(path) + } + } diff --git a/Auth0/NewPasskey.swift b/Auth0/NewPasskey.swift new file mode 100644 index 000000000..f2b3924a1 --- /dev/null +++ b/Auth0/NewPasskey.swift @@ -0,0 +1,35 @@ +#if PASSKEYS_PLATFORM +import Foundation +import AuthenticationServices + +/// The signup passkey credential obtained from the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate. +/// Contains the subset of relevant properties from [`ASAuthorizationPlatformPublicKeyCredentialRegistration`](https://developer.apple.com/documentation/authenticationservices/asauthorizationplatformpublickeycredentialregistration). +@available(iOS 16.6, macOS 13.5, visionOS 1.0, *) +public protocol NewPasskey { + + var credentialID: Data { get } + var attachment: ASAuthorizationPublicKeyCredentialAttachment { get } + var rawClientDataJSON: Data { get } + var rawAttestationObject: Data? { get } + +} + +@available(iOS 16.6, macOS 13.5, visionOS 1.0, *) +extension ASAuthorizationPlatformPublicKeyCredentialRegistration: NewPasskey {} + +@available(iOS 16.6, macOS 13.5, visionOS 1.0, *) +extension ASAuthorizationPublicKeyCredentialAttachment { + + public var stringValue: String? { + switch self { + case .platform: + return "platform" + case .crossPlatform: + return "cross-platform" + @unknown default: + return nil + } + } + +} +#endif diff --git a/Auth0/PasskeyLoginChallenge.swift b/Auth0/PasskeyLoginChallenge.swift index 321dc6142..cf7dadc84 100644 --- a/Auth0/PasskeyLoginChallenge.swift +++ b/Auth0/PasskeyLoginChallenge.swift @@ -4,7 +4,7 @@ import Foundation /// A passkey login challenge. public struct PasskeyLoginChallenge: Sendable { - /// Session identifier. + /// Unique identifier of the Auth0 session. public let authenticationSession: String /// Custom domain configured in the Auth0 tenant. diff --git a/Auth0/PasskeySignupChallenge.swift b/Auth0/PasskeySignupChallenge.swift index 2f7d538eb..5f99bd45e 100644 --- a/Auth0/PasskeySignupChallenge.swift +++ b/Auth0/PasskeySignupChallenge.swift @@ -4,16 +4,16 @@ import Foundation /// A passkey signup challenge. public struct PasskeySignupChallenge: Sendable { - /// Session identifier. + /// Unique identifier of the Auth0 session. public let authenticationSession: String /// Custom domain configured in the Auth0 tenant. public let relyingPartyId: String - /// Generated identifier. + /// Generated unique identifier of the user. public let userId: Data - /// User identifier, like the user's email. + /// A user identifier, like the user's email. public let userName: String /// Signup challenge data. diff --git a/Auth0/Response.swift b/Auth0/Response.swift index 1d28d7ddc..06c7c933a 100644 --- a/Auth0/Response.swift +++ b/Auth0/Response.swift @@ -10,12 +10,14 @@ func string(_ data: Data?) -> String? { return String(data: data, encoding: .utf8) } +typealias JSONResponse = (headers: [String: Any], body: Any, data: Data) + struct Response { let data: Data? let response: HTTPURLResponse? let error: Error? - func result() throws -> Any? { + func result() throws(E) -> JSONResponse? { guard error == nil else { throw E(cause: error!, statusCode: response?.statusCode ?? 0) } guard let response = self.response else { throw E(description: nil) } guard (200...300).contains(response.statusCode) else { @@ -32,7 +34,7 @@ struct Response { throw E(description: nil, statusCode: response.statusCode) } if let json = json(data) { - return json + return (headers: response.allHeaderFields.stringDictionary, body: json, data: data) } // This piece of code is dedicated to our friends the backend devs :) if response.url?.lastPathComponent == "change_password" { @@ -41,3 +43,17 @@ struct Response { throw E(from: self) } } + +private extension Dictionary where Key == AnyHashable, Value == Any { + + var stringDictionary: [String: Any] { + var result: [String: Any] = [:] + for (key, value) in self { + if let stringKey = key as? String { + result[stringKey] = value + } + } + return result + } + +} diff --git a/Auth0/SignupPasskey.swift b/Auth0/SignupPasskey.swift index 42f16f538..1a82ea189 100644 --- a/Auth0/SignupPasskey.swift +++ b/Auth0/SignupPasskey.swift @@ -5,31 +5,8 @@ import AuthenticationServices /// The signup passkey credential obtained from the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate. /// Contains the subset of relevant properties from [`ASAuthorizationPlatformPublicKeyCredentialRegistration`](https://developer.apple.com/documentation/authenticationservices/asauthorizationplatformpublickeycredentialregistration). @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) -public protocol SignupPasskey { - - var credentialID: Data { get } - var attachment: ASAuthorizationPublicKeyCredentialAttachment { get } - var rawClientDataJSON: Data { get } - var rawAttestationObject: Data? { get } - -} +public protocol SignupPasskey: NewPasskey {} @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) extension ASAuthorizationPlatformPublicKeyCredentialRegistration: SignupPasskey {} - -@available(iOS 16.6, macOS 13.5, visionOS 1.0, *) -extension ASAuthorizationPublicKeyCredentialAttachment { - - public var stringValue: String? { - switch self { - case .platform: - return "platform" - case .crossPlatform: - return "cross-platform" - @unknown default: - return nil - } - } - -} #endif diff --git a/Auth0/WebAuth.swift b/Auth0/WebAuth.swift index 2389c9309..67c1258a7 100644 --- a/Auth0/WebAuth.swift +++ b/Auth0/WebAuth.swift @@ -426,5 +426,6 @@ public extension WebAuth { return try await self.clearSession(federated: federated) } #endif + } #endif diff --git a/Auth0Tests/Auth0Spec.swift b/Auth0Tests/Auth0Spec.swift index 379349e30..6e8fef3f9 100644 --- a/Auth0Tests/Auth0Spec.swift +++ b/Auth0Tests/Auth0Spec.swift @@ -119,7 +119,7 @@ class Auth0Spec: QuickSpec { } it("should have no logging for management by default") { - expect(Auth0.users(token: "token", domain: Domain).logger).to(beNil()) + expect(Auth0.users(token: Token, domain: Domain).logger).to(beNil()) } it("should enable default logger for auth") { @@ -133,12 +133,12 @@ class Auth0Spec: QuickSpec { } it("should enable default logger for users") { - let users = Auth0.users(token: "token", domain: Domain) + let users = Auth0.users(token: Token, domain: Domain) expect(users.logging(enabled: true).logger).toNot(beNil()) } it("should not enable default logger for users") { - let users = Auth0.users(token: "token", domain: Domain) + let users = Auth0.users(token: Token, domain: Domain) expect(users.logging(enabled: false).logger).to(beNil()) } @@ -150,7 +150,7 @@ class Auth0Spec: QuickSpec { it("should enable custom logger for users") { let logger = MockLogger() - let users = Auth0.users(token: "token", domain: Domain) + let users = Auth0.users(token: Token, domain: Domain) expect(users.using(logger: logger).logger).toNot(beNil()) } @@ -186,8 +186,7 @@ class Auth0Spec: QuickSpec { } it("should return users endpoint") { - let users = Auth0.users(token: "token", domain: Domain) - expect(users.token) == "token" + let users = Auth0.users(token: Token, domain: Domain) expect(users.url.absoluteString) == "https://\(Domain)/" } @@ -221,8 +220,7 @@ class Auth0Spec: QuickSpec { } it("should return users endpoint") { - let users = Auth0.users(token: "token", domain: Domain) - expect(users.token) == "token" + let users = Auth0.users(token: Token, domain: Domain) expect(users.url.absoluteString) == "https://\(Domain)/" } @@ -233,27 +231,3 @@ class Auth0Spec: QuickSpec { } } - -struct MockLogger: Logger { - func trace(url: URL, source: String?) {} - - func trace(response: URLResponse, data: Data?) {} - - func trace(request: URLRequest, session: URLSession) {} -} - -struct MockError: LocalizedError, CustomStringConvertible { - private let message = "foo" - - var description: String { - return self.message - } - - var localizedDescription: String { - return self.message - } - - var errorDescription: String? { - return self.message - } -} diff --git a/Auth0Tests/AuthenticationErrorSpec.swift b/Auth0Tests/AuthenticationErrorSpec.swift index 2b42a862b..77f0a065a 100644 --- a/Auth0Tests/AuthenticationErrorSpec.swift +++ b/Auth0Tests/AuthenticationErrorSpec.swift @@ -86,16 +86,16 @@ class AuthenticationErrorSpec: QuickSpec { expect(error.statusCode) == statusCode } - it("should initialize with a cause") { + it("should initialize with cause") { let cause = MockError() - let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)" + let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)." let error = AuthenticationError(cause: cause) expect(error.cause).toNot(beNil()) expect(error.localizedDescription) == description expect(error.statusCode) == 0 } - it("should initialize with a cause & status code") { + it("should initialize with cause & status code") { let statusCode = 400 let error = AuthenticationError(cause: MockError(), statusCode: statusCode) expect(error.statusCode) == statusCode @@ -415,25 +415,49 @@ class AuthenticationErrorSpec: QuickSpec { } it("should detect network error") { - let networkErrorCodes: [URLError.Code] = [ - .dataNotAllowed, - .notConnectedToInternet, - .networkConnectionLost, - .dnsLookupFailed, - .cannotFindHost, - .cannotConnectToHost, - .timedOut, - .internationalRoamingOff, - .callIsActive - ] - - for errorCode in networkErrorCodes { + for errorCode in AuthenticationError.networkErrorCodes { expect(AuthenticationError(cause: URLError.init(errorCode)).isNetworkError) == true } } } + describe("error message") { + + it("should return the message") { + let description = "foo" + let info: [String: Any] = ["description": description] + let error = AuthenticationError(info: info) + expect(error.localizedDescription) == description + } + + it("should return the default message") { + let info: [String: Any] = ["foo": "bar", "statusCode": 0] + let message = "Failed with unknown error: \(info)." + let error = AuthenticationError(info: info) + expect(error.localizedDescription) == message + } + + it("should append the cause error message") { + let description = "foo." + let cause = MockError(message: "bar.") + let info: [String: Any] = ["description": description, "cause": cause] + let message = "\(description) CAUSE: \(cause.localizedDescription)" + let error = AuthenticationError(info: info) + expect(error.localizedDescription) == message + } + + it("should append the cause error message adding periods") { + let description = "foo" + let cause = MockError(message: "bar") + let info: [String: Any] = ["description": description, "cause": cause] + let message = "\(description). CAUSE: \(cause.localizedDescription)." + let error = AuthenticationError(info: info) + expect(error.localizedDescription) == message + } + + } + } } diff --git a/Auth0Tests/AuthenticationSpec.swift b/Auth0Tests/AuthenticationSpec.swift index c5c0d914b..7263ace09 100644 --- a/Auth0Tests/AuthenticationSpec.swift +++ b/Auth0Tests/AuthenticationSpec.swift @@ -284,17 +284,18 @@ class AuthenticationSpec: QuickSpec { var signature: Data! } + let authSession = "y1PI7ue7QX85WMxoR6Qa-9INuqA3xxKLVoDOxBOD6yYQL1Fl-zgwjFtZIQfRORhY" let userId = "LcICuavHdO2zbcA8zRgnTRIkzPrruI_HQqe0J3RL0ou5VSrWhRybCQqyNMXWj1LDdxOzat6KVf9xpW3qLw5qjw" let credentialId = "mXTk10IfDhdxZnJltERtBRyNUkE" + let credentialType = "public-key" let clientData = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTDRTYVN4eDh0cHFyU2NUX2hicFpYLTUwcW" + "ZLaDEyX294bVNVSUtTR0ZwTSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4ud2lkY2tldC5jb20ifQ-MN8A" let authenticatorData = "lDH4SiOEQFwNz4z4dy3yWLJ5CkueUJPzpqulBxP_X_8dAAAAAA" let signature = "MEUCIH6XVeR9aTIEQZJ1vRv96y2ndS4da75h9K41Gnt6ssd9AiEA0DHoeNMrPw8GBzYkagdQD6I4ySOGONSTWPV" + "YA0FAwII" - let authSession = "y1PI7ue7QX85WMxoR6Qa-9INuqA3xxKLVoDOxBOD6yYQL1Fl-zgwjFtZIQfRORhY" - let challengeData = "L4SaSxx8tpqrScT_hbpZX-50qfKh12_oxmSUIKSGFpM".a0_decodeBase64URLSafe()! + let challengeString = "L4SaSxx8tpqrScT_hbpZX-50qfKh12_oxmSUIKSGFpM" + let challengeData = challengeString.a0_decodeBase64URLSafe()! let authenticatorAttachment = "platform" - let type = "public-key" let passkey = MockLoginPasskey(userID: userId.a0_decodeBase64URLSafe(), credentialID: credentialId.a0_decodeBase64URLSafe()!, @@ -318,7 +319,7 @@ class AuthenticationSpec: QuickSpec { "id": credentialId, "rawId": credentialId, "authenticatorAttachment": authenticatorAttachment, - "type": type, + "type": credentialType, "response": [ "userHandle": userId, "authenticatorData": authenticatorData, @@ -353,7 +354,7 @@ class AuthenticationSpec: QuickSpec { "id": credentialId, "rawId": credentialId, "authenticatorAttachment": authenticatorAttachment, - "type": type, + "type": credentialType, "response": [ "userHandle": userId, "authenticatorData": authenticatorData, @@ -449,7 +450,7 @@ class AuthenticationSpec: QuickSpec { "auth_session": authSession, "authn_response": [ "authenticatorAttachment": authenticatorAttachment, - "type": type, + "type": credentialType, "response": [ "attestationObject": attestationObject, "clientDataJSON": clientData @@ -480,7 +481,7 @@ class AuthenticationSpec: QuickSpec { "auth_session": authSession, "authn_response": [ "authenticatorAttachment": authenticatorAttachment, - "type": type, + "type": credentialType, "response": [ "attestationObject": attestationObject, "clientDataJSON": clientData @@ -514,7 +515,11 @@ class AuthenticationSpec: QuickSpec { "client_id": ClientId, "user_profile": ["email": Email] ]) - }, response: passkeySignupChallengeResponse(identifier: Email)) + }, response: passkeySignupChallengeResponse(authSession: authSession, + rpId: Domain, + userId: userId, + userName: Email, + challenge: challengeString)) waitUntil(timeout: Timeout) { done in auth @@ -532,7 +537,11 @@ class AuthenticationSpec: QuickSpec { "client_id": ClientId, "user_profile": ["phone_number": Phone] ]) - }, response: passkeySignupChallengeResponse(identifier: Phone)) + }, response: passkeySignupChallengeResponse(authSession: authSession, + rpId: Domain, + userId: userId, + userName: Phone, + challenge: challengeString)) waitUntil(timeout: Timeout) { done in auth @@ -550,7 +559,11 @@ class AuthenticationSpec: QuickSpec { "client_id": ClientId, "user_profile": ["username": Username] ]) - }, response: passkeySignupChallengeResponse(identifier: Username)) + }, response: passkeySignupChallengeResponse(authSession: authSession, + rpId: Domain, + userId: userId, + userName: Username, + challenge: challengeString)) waitUntil(timeout: Timeout) { done in auth @@ -569,7 +582,12 @@ class AuthenticationSpec: QuickSpec { "realm": ConnectionName, "user_profile": ["email": Email, "phone_number": Phone, "username": Username, "name": Name] ]) - }, response: passkeySignupChallengeResponse(identifier: Email, name: Name)) + }, response: passkeySignupChallengeResponse(authSession: authSession, + rpId: Domain, + userId: userId, + userName: Email, + userDisplayName: Name, + challenge: challengeString)) waitUntil(timeout: Timeout) { done in auth diff --git a/Auth0Tests/CredentialsManagerErrorSpec.swift b/Auth0Tests/CredentialsManagerErrorSpec.swift index d17e14e60..060a00c81 100644 --- a/Auth0Tests/CredentialsManagerErrorSpec.swift +++ b/Auth0Tests/CredentialsManagerErrorSpec.swift @@ -138,12 +138,19 @@ class CredentialsManagerErrorSpec: QuickSpec { } it("should append the cause error message") { - let cause = MockError() + let cause = MockError(message: "foo bar.") let message = "The revocation of the refresh token failed. CAUSE: \(cause.localizedDescription)" let error = CredentialsManagerError(code: .revokeFailed, cause: cause) expect(error.localizedDescription) == message } + it("should append the cause error message adding a period") { + let cause = MockError(message: "foo bar") + let message = "The revocation of the refresh token failed. CAUSE: \(cause.localizedDescription)." + let error = CredentialsManagerError(code: .revokeFailed, cause: cause) + expect(error.localizedDescription) == message + } + } } diff --git a/Auth0Tests/CryptoExtensions.swift b/Auth0Tests/CryptoExtensions.swift deleted file mode 100644 index 9a1b08154..000000000 --- a/Auth0Tests/CryptoExtensions.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import Security - -@testable import Auth0 - -extension SecKey { - func export() -> Data { - return SecKeyCopyExternalRepresentation(self, nil)! as Data - } -} diff --git a/Auth0Tests/Generators.swift b/Auth0Tests/Generators.swift index 37af582e7..48b910e40 100644 --- a/Auth0Tests/Generators.swift +++ b/Auth0Tests/Generators.swift @@ -200,3 +200,9 @@ func generateRSAJWK(from publicKey: SecKey = TestKeys.rsaPublic, keyId: String = return asn(unsafeRawBufferPointer.bindMemory(to: UInt8.self).baseAddress!)! } } + +extension SecKey { + func export() -> Data { + return SecKeyCopyExternalRepresentation(self, nil)! as Data + } +} diff --git a/Auth0Tests/ManagementErrorSpec.swift b/Auth0Tests/ManagementErrorSpec.swift index 419b74dc0..faa4eb035 100644 --- a/Auth0Tests/ManagementErrorSpec.swift +++ b/Auth0Tests/ManagementErrorSpec.swift @@ -79,16 +79,16 @@ class ManagementErrorSpec: QuickSpec { expect(error.statusCode) == statusCode } - it("should initialize with a cause") { + it("should initialize with cause") { let cause = MockError() - let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)" + let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)." let error = AuthenticationError(cause: cause) expect(error.cause).toNot(beNil()) expect(error.localizedDescription) == description expect(error.statusCode) == 0 } - it("should initialize with a cause & status code") { + it("should initialize with cause & status code") { let statusCode = 400 let error = AuthenticationError(cause: MockError(), statusCode: statusCode) expect(error.statusCode) == statusCode @@ -159,13 +159,41 @@ class ManagementErrorSpec: QuickSpec { it("should return the default message") { let info: [String: Any] = ["foo": "bar", "statusCode": 0] - let message = "Failed with unknown error \(info)." + let message = "Failed with unknown error: \(info)." + let error = ManagementError(info: info) + expect(error.localizedDescription) == message + } + + it("should append the cause error message") { + let description = "foo." + let cause = MockError(message: "bar.") + let info: [String: Any] = ["description": description, "cause": cause] + let message = "\(description) CAUSE: \(cause.localizedDescription)" + let error = ManagementError(info: info) + expect(error.localizedDescription) == message + } + + it("should append the cause error message adding periods") { + let description = "foo" + let cause = MockError(message: "bar") + let info: [String: Any] = ["description": description, "cause": cause] + let message = "\(description). CAUSE: \(cause.localizedDescription)." let error = ManagementError(info: info) expect(error.localizedDescription) == message } } + describe("error cases") { + + it("should detect network error") { + for errorCode in ManagementError.networkErrorCodes { + expect(ManagementError(cause: URLError.init(errorCode)).isNetworkError) == true + } + } + + } + } } diff --git a/Auth0Tests/ManagementSpec.swift b/Auth0Tests/ManagementSpec.swift index 693783ec7..830eef2ea 100644 --- a/Auth0Tests/ManagementSpec.swift +++ b/Auth0Tests/ManagementSpec.swift @@ -111,9 +111,9 @@ class ManagementSpec: QuickSpec { expect(actual).toEventually(haveManagementError(description: "Empty response body.", code: emptyBodyError, statusCode: statusCode)) } - it("should yield error with a cause") { + it("should yield error with cause") { let cause = MockError() - let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)" + let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)." let response = Response(data: nil, response: nil, error: cause) var actual: ManagementResult? = nil management.managementObject(response: response) { actual = $0 } diff --git a/Auth0Tests/Matchers.swift b/Auth0Tests/Matchers.swift index 8678a152a..999329d01 100644 --- a/Auth0Tests/Matchers.swift +++ b/Auth0Tests/Matchers.swift @@ -163,12 +163,64 @@ func haveSSOCredentials(_ sessionTransferToken: String, #if PASSKEYS_PLATFORM func havePasskeySignupChallenge(identifier: String) -> Nimble.Matcher> { let definition = "have passkey signup challenge with user identifier <\(identifier)>" + return Matcher>.define(definition) { expression, failureMessage -> MatcherResult in return try beSuccessful(expression, failureMessage) { (created: PasskeySignupChallenge) -> Bool in return created.userName == identifier } } } + +func havePasskeyEnrollmentChallenge(authenticationMethodId: String, + authenticationSession: String, + relyingPartyId: String, + userId: Data, + userName: String, + challengeData: Data) -> Nimble.Matcher> { + let definition = "have passkey enrollment challenge with " + + "authentication method identifier <\(authenticationMethodId)>, " + + "authentication session <\(authenticationSession)>, " + + "relying party identifier <\(relyingPartyId)>, " + + "and user name <\(userName)>" + + return Matcher>.define(definition) { expression, failureMessage -> MatcherResult in + return try beSuccessful(expression, failureMessage) { (created: PasskeyEnrollmentChallenge) -> Bool in + return authenticationMethodId == created.authenticationMethodId && + authenticationSession == created.authenticationSession && + relyingPartyId == created.relyingPartyId && + userId == created.userId && + userName == created.userName && + challengeData == created.challengeData + } + } +} + +func havePasskeyAuthenticationMethod(id: String, + userIdentityId: String, + credentialId: String, + credentialPublicKey: Data, + credentialUserHandle: Data, + credentialDeviceType: PasskeyDeviceType, + createdAt: Date) -> Nimble.Matcher> { + let definition = "have passkey authentication method with " + + "identifier <\(id)>, " + + "user identity identifier <\(userIdentityId)>, " + + "credential identifier <\(credentialPublicKey)>, " + + "credential device type <\(credentialDeviceType.rawValue)>, " + + "and creation date <\(createdAt)>" + + return Matcher>.define(definition) { expression, failureMessage -> MatcherResult in + return try beSuccessful(expression, failureMessage) { (created: PasskeyAuthenticationMethod) -> Bool in + return id == created.id && + userIdentityId == created.userIdentityId && + credentialId == created.credential.id && + credentialPublicKey == created.credential.publicKey && + credentialUserHandle == created.credential.userHandle && + credentialDeviceType == created.credential.deviceType && + createdAt == created.createdAt + } + } +} #endif func haveCreatedUser(_ email: String, username: String? = nil) -> Nimble.Matcher> { @@ -216,6 +268,17 @@ func beUnsuccessful(_ cause: String? = nil) -> Nimble.Matcher(_ cause: String? = nil) -> Nimble.Matcher> { + return Matcher>.define("be a failure result") { expression, failureMessage -> MatcherResult in + if let cause = cause { + _ = failureMessage.appended(message: " with cause \(cause)") + } else { + _ = failureMessage.appended(message: " from my account api") + } + return try beUnsuccessful(expression, failureMessage) + } +} + #if WEB_AUTH_PLATFORM func beUnsuccessful(_ cause: String? = nil) -> Nimble.Matcher> { return Matcher>.define("be a failure result") { expression, failureMessage -> MatcherResult in @@ -416,6 +479,11 @@ extension URLRequest { return isHost(domain) && isPath(path) } + func isMyAccountAuthenticationMethods(_ domain: String, _ endpoint: String = "", token: String) -> Bool { + let subpath = endpoint.isEmpty ? endpoint : "/\(endpoint)" + let path = "/me/\(Auth0MyAccount.apiVersion)/authentication-methods\(subpath)" + return isHost(domain) && isPath(path) && hasBearerToken(token) + } func isLinkPath(_ domain: String, identifier: String) -> Bool { return isHost(domain) && isPath("/api/v2/users/\(identifier)/identities") diff --git a/Auth0Tests/Mocks.swift b/Auth0Tests/Mocks.swift new file mode 100644 index 000000000..32a1d0c6d --- /dev/null +++ b/Auth0Tests/Mocks.swift @@ -0,0 +1,31 @@ +import Foundation + +@testable import Auth0 + +struct MockLogger: Logger { + func trace(url: URL, source: String?) {} + + func trace(response: URLResponse, data: Data?) {} + + func trace(request: URLRequest, session: URLSession) {} +} + +struct MockError: LocalizedError, CustomStringConvertible { + private let message: String + + init(message: String = "foo") { + self.message = message + } + + var description: String { + return self.message + } + + var localizedDescription: String { + return self.message + } + + var errorDescription: String? { + return self.message + } +} diff --git a/Auth0Tests/MyAccount/AuthenticationMethods/MyAccountAuthenticationMethodsSpec.swift b/Auth0Tests/MyAccount/AuthenticationMethods/MyAccountAuthenticationMethodsSpec.swift new file mode 100644 index 000000000..5480e24d6 --- /dev/null +++ b/Auth0Tests/MyAccount/AuthenticationMethods/MyAccountAuthenticationMethodsSpec.swift @@ -0,0 +1,269 @@ +import Foundation +import Quick +import Nimble + +#if PASSKEYS_PLATFORM +import AuthenticationServices +#endif + +@testable import Auth0 + +private let ClientId = "CLIENT_ID" +private let Domain = "samples.auth0.com" +private let DomainURL = URL(string: "https://\(Domain)")! +private let AccessToken = UUID().uuidString.replacingOccurrences(of: "-", with: "") +private let AuthenticationMethodId = "PASSKEY_ID" +private let Connection = "Username-Password-Authentication" +private let Email = "user@example.com" +private let Timeout: NimbleTimeInterval = .seconds(2) + +class MyAccountAuthenticationMethodsSpec: QuickSpec { + override class func spec() { + + let myAccount = myAccount(token: AccessToken, domain: Domain) + let authMethods = myAccount.authenticationMethods + + beforeEach { + URLProtocol.registerClass(StubURLProtocol.self) + } + + afterEach { + NetworkStub.clearStubs() + URLProtocol.unregisterClass(StubURLProtocol.self) + } + + describe("init") { + + it("should init with token and url") { + let authMethods = Auth0MyAccountAuthenticationMethods(token: AccessToken, url: DomainURL) + expect(authMethods.token) == AccessToken + expect(authMethods.url) == DomainURL.appending("authentication-methods") + } + + it("should init with token, url, and session") { + let session = URLSession(configuration: URLSession.shared.configuration) + let authMethods = Auth0MyAccountAuthenticationMethods(token: AccessToken, + url: DomainURL, + session: session) + expect(authMethods.session).to(be(session)) + } + + it("should init with token, url, and telemetry") { + let telemetryInfo = "info" + var telemetry = Telemetry() + telemetry.info = telemetryInfo + let authMethods = Auth0MyAccountAuthenticationMethods(token: AccessToken, + url: DomainURL, + telemetry: telemetry) + expect(authMethods.telemetry.info) == telemetryInfo + } + + } + + #if PASSKEYS_PLATFORM + if #available(iOS 16.6, macOS 13.5, visionOS 1.0, *) { + struct MockNewPasskey: NewPasskey { + let credentialID: Data + let attachment: ASAuthorizationPublicKeyCredentialAttachment = .platform + let rawAttestationObject: Data? + let rawClientDataJSON: Data + } + + let authSession = "y1PI7ue7QX85WMxoR6Qa-9INuqA3xxKLVoDOxBOD6yYQL1Fl-zgwjFtZIQfRORhY" + let userId = "LcICuavHdO2zbcA8zRgnTRIkzPrruI_HQqe0J3RL0ou5VSrWhRybCQqyNMXWj1LDdxOzat6KVf9xpW3qLw5qjw" + let userIdData = userId.a0_decodeBase64URLSafe()! + let userIdentityId = "681359da4a20c7993310ff1d" + let challenge = "L4SaSxx8tpqrScT_hbpZX-50qfKh12_oxmSUIKSGFpM" + let challengeData = challenge.a0_decodeBase64URLSafe()! + + describe("enroll passkey") { + + let endpoint = "\(AuthenticationMethodId)/verify" + let credentialId = "mXTk10IfDhdxZnJltERtBRyNUkE" + let credentialType = "public-key" + let authenticatorAttachment = "platform" + let attestationObject = "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYlDH4SiOEQFwNz4z4dy3yWLJ5CkueUJPzpqu" + + "lBxP_X_9dAAAAAPv8MAcVTk7MjAtuAgVX170AFJl05NdCHw4XcWZyZbREbQUcjVJBpQECAyYgASFYII53hB2t9eUcxo6B4PdeSa" + + "WKQCb-sQRSSJIsSl1iXE6VIlgg9SFUiFdAPMrCwC-RQaNKVwNrMFzsRkiu0Djz-GPjDfA" + let clientData = "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTDRTYVN4eDh0cHFyU2NUX2hicFpYLT" + + "UwcWZLaDEyX294bVNVSUtTR0ZwTSIsIm9yaWdpbiI6Imh0dHBzOi8vbG9naW4ud2lkY2tldC5jb20ifQ-MN8A" + + let newPasskey = MockNewPasskey(credentialID: credentialId.a0_decodeBase64URLSafe()!, + rawAttestationObject: attestationObject.a0_decodeBase64URLSafe(), + rawClientDataJSON: clientData.a0_decodeBase64URLSafe()!) + let enrollmentChallenge = PasskeyEnrollmentChallenge(authenticationMethodId: AuthenticationMethodId, + authenticationSession: authSession, + relyingPartyId: Domain, + userId: userIdData, + userName: Email, + challengeData: challengeData) + + it("should enroll passkey") { + let publicKey = "pQECAyYgASFYIGK0OMbKXIHgb1Es/MrVoCTrGDzi96vGxUpAGJOhUOp4IlggxIbnS81JDZHWv+NZtWV" + + "7wMzbg7sTOJbACvk7xY6DE7A=" + let publicKeyData = Data(base64Encoded: publicKey)! + let createdAt = "2025-05-15T13:29:32.321Z" + let createdAtDateFormatter = ISO8601DateFormatter() + createdAtDateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let createdAtDate = createdAtDateFormatter.date(from: createdAt)! + + NetworkStub.addStub(condition: { + $0.isMyAccountAuthenticationMethods(Domain, endpoint, token: AccessToken) && + $0.isMethodPOST && + $0.hasAtLeast([ + "auth_session": authSession, + "authn_response": [ + "authenticatorAttachment": authenticatorAttachment, + "type": credentialType, + "response": [ + "attestationObject": attestationObject, + "clientDataJSON": clientData + ] + ] + ]) + }, response: passkeyAuthenticationMethodResponse(id: AuthenticationMethodId, + userIdentityId: userIdentityId, + userHandle: userId, + keyId: credentialId, + publicKey: publicKey, + credentialDeviceType: .singleDevice, + createdAt: createdAt)) + + waitUntil(timeout: Timeout) { done in + authMethods + .enroll(passkey: newPasskey, challenge: enrollmentChallenge) + .start { result in + expect(result) + .to(havePasskeyAuthenticationMethod(id: AuthenticationMethodId, + userIdentityId: userIdentityId, + credentialId: credentialId, + credentialPublicKey: publicKeyData, + credentialUserHandle: userIdData, + credentialDeviceType: .singleDevice, + createdAt: createdAtDate)) + done() + } + } + } + + it("should fail to enroll passkey") { + NetworkStub.addStub(condition: { + $0.isMyAccountAuthenticationMethods(Domain, endpoint, token: AccessToken) && + $0.isMethodPOST && + $0.hasAtLeast([ + "auth_session": authSession, + "authn_response": [ + "authenticatorAttachment": authenticatorAttachment, + "type": credentialType, + "response": [ + "attestationObject": attestationObject, + "clientDataJSON": clientData + ] + ] + ]) + }, response: apiFailureResponse()) + + waitUntil(timeout: Timeout) { done in + authMethods + .enroll(passkey: newPasskey, challenge: enrollmentChallenge) + .start { result in + expect(result).to(beUnsuccessful()) + done() + } + } + } + + } + + describe("passkey enrollment challenge") { + + let locationHeader = "https://example.com/foo/\(AuthenticationMethodId)" + + it("should request passkey enrollment challenge with default parameters") { + NetworkStub.addStub(condition: { + $0.isMyAccountAuthenticationMethods(Domain, token: AccessToken) && + $0.isMethodPOST && + $0.hasAllOf(["type": "passkey"]) + }, response: passkeyEnrollmentChallengeResponse(authSession: authSession, + rpId: Domain, + userId: userId, + userName: Email, + challenge: challenge, + headers: ["Location": locationHeader])) + + waitUntil(timeout: Timeout) { done in + authMethods + .passkeyEnrollmentChallenge() + .start { result in + expect(result) + .to(havePasskeyEnrollmentChallenge(authenticationMethodId: AuthenticationMethodId, + authenticationSession: authSession, + relyingPartyId: Domain, + userId: userIdData, + userName: Email, + challengeData: challengeData)) + done() + } + } + } + + it("should request passkey signup challenge with all parameters") { + let identityUserId = "681359da4a20c7993310ff1d" + + NetworkStub.addStub(condition: { + $0.isMyAccountAuthenticationMethods(Domain, token: AccessToken) && + $0.isMethodPOST && + $0.hasAllOf([ + "type": "passkey", + "connection": Connection, + "identity_user_id": identityUserId + ]) + }, response: passkeyEnrollmentChallengeResponse(authSession: authSession, + rpId: Domain, + userId: userId, + userName: Email, + challenge: challenge, + headers: ["Location": locationHeader])) + + waitUntil(timeout: Timeout) { done in + authMethods + .passkeyEnrollmentChallenge(userIdentityId: identityUserId, connection: Connection) + .start { result in + expect(result) + .to(havePasskeyEnrollmentChallenge(authenticationMethodId: AuthenticationMethodId, + authenticationSession: authSession, + relyingPartyId: Domain, + userId: userIdData, + userName: Email, + challengeData: challengeData)) + done() + } + } + + } + + it("should fail to request passkey signup challenge") { + NetworkStub.addStub(condition: { + $0.isMyAccountAuthenticationMethods(Domain, token: AccessToken) && + $0.isMethodPOST && + $0.hasAllOf(["type": "passkey"]) + }, response: apiFailureResponse()) + + waitUntil(timeout: Timeout) { done in + authMethods + .passkeyEnrollmentChallenge() + .start { result in + expect(result).to(beUnsuccessful()) + done() + } + } + + } + + } + + } + #endif + + } +} diff --git a/Auth0Tests/MyAccount/AuthenticationMethods/PasskeyAuthenticationMethodSpec.swift b/Auth0Tests/MyAccount/AuthenticationMethods/PasskeyAuthenticationMethodSpec.swift new file mode 100644 index 000000000..bd4ca8aa5 --- /dev/null +++ b/Auth0Tests/MyAccount/AuthenticationMethods/PasskeyAuthenticationMethodSpec.swift @@ -0,0 +1,124 @@ +import Foundation +import Quick +import Nimble + +@testable import Auth0 + +private let PasskeyId = "passkey|dev_rMJhr6SjnUUcHCxW" +private let PasskeyType = "passkey" +private let PasskeyCreatedAt = "2025-05-01T11:24:10.172Z" +private let PasskeyUserIdentityId = "681359da4a20c7993310ff1d" +private let PasskeyUserHandle = "uq9owlgkg7papmbVSah3y968mhZfLh6xSBtLxx9rGouTF3ZIw59_VqC3CyRJQCI76Fp07n-5p2I4ew1D-0b" + +"OBA" +private let PasskeyUserAgent = "OAuth2/1 CFNetwork/3826.500.111.2.2 Darwin/24.4.0" +private let PasskeyKeyId = "XqQWiLjlWv6SPbFapFvkwYrSDCc" +private let PasskeyPublicKey = "pQECAyYgASFYIGK0OMbKXIHgb1Es/MrVoCTrGDzi96vGxUpAGJOhUOp4IlggxIbnS81JDZHWv+NZtWV7wMzb" + +"g7sTOJbACvk7xY6DE7A=" +private let PasskeyCredentialDeviceType = "multi_device" +private let PasskeyIsCredentialBackedUp = true + +private let JSON = """ +{ + "id": "\(PasskeyId)", + "type": "\(PasskeyType)", + "created_at": "\(PasskeyCreatedAt)", + "identity_user_id": "\(PasskeyUserIdentityId)", + "user_handle": "\(PasskeyUserHandle)", + "user_agent": "\(PasskeyUserAgent)", + "key_id": "\(PasskeyKeyId)", + "public_key": "\(PasskeyPublicKey)", + "credential_device_type": "\(PasskeyCredentialDeviceType)", + "credential_backed_up": \(PasskeyIsCredentialBackedUp) +} +""" + +class PasskeyAuthenticationMethodSpec: QuickSpec { + override class func spec() { + + describe("decode from json") { + + var decoder: JSONDecoder! + + beforeEach { + decoder = JSONDecoder() + } + + it("should have all properties") { + let jsonData = JSON.data(using: .utf8)! + let challenge = try decoder.decode(PasskeyAuthenticationMethod.self, from: jsonData) + let createdAtDecoder = ISO8601DateFormatter() + createdAtDecoder.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let createdAtDate = createdAtDecoder.date(from: PasskeyCreatedAt)! + + expect(challenge.id) == PasskeyId + expect(challenge.type) == PasskeyType + expect(challenge.userIdentityId) == PasskeyUserIdentityId + expect(challenge.userAgent) == PasskeyUserAgent + expect(challenge.credential.id) == PasskeyKeyId + expect(challenge.credential.publicKey) == PasskeyPublicKey.a0_decodeBase64URLSafe() + expect(challenge.credential.userHandle) == PasskeyUserHandle.a0_decodeBase64URLSafe() + expect(challenge.credential.deviceType) == .multiDevice + expect(challenge.credential.isBackedUp) == PasskeyIsCredentialBackedUp + expect(challenge.createdAt).to(beCloseTo(createdAtDate, within: 0.01)) + } + + it("should have non-optional properties only") { + let json = """ + { + "id": "\(PasskeyId)", + "type": "\(PasskeyType)", + "created_at": "\(PasskeyCreatedAt)", + "identity_user_id": "\(PasskeyUserIdentityId)", + "user_handle": "\(PasskeyUserHandle)", + "key_id": "\(PasskeyKeyId)", + "public_key": "\(PasskeyPublicKey)", + "credential_device_type": "\(PasskeyCredentialDeviceType)", + "credential_backed_up": \(PasskeyIsCredentialBackedUp) + } + """ + let jsonData = json.data(using: .utf8)! + let challenge = try decoder.decode(PasskeyAuthenticationMethod.self, from: jsonData) + let createdAtDecoder = ISO8601DateFormatter() + createdAtDecoder.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let createdAtDate = createdAtDecoder.date(from: PasskeyCreatedAt)! + + expect(challenge.id) == PasskeyId + expect(challenge.type) == PasskeyType + expect(challenge.userIdentityId) == PasskeyUserIdentityId + expect(challenge.userAgent).to(beNil()) + expect(challenge.credential.id) == PasskeyKeyId + expect(challenge.credential.publicKey) == PasskeyPublicKey.a0_decodeBase64URLSafe() + expect(challenge.credential.userHandle) == PasskeyUserHandle.a0_decodeBase64URLSafe() + expect(challenge.credential.deviceType) == .multiDevice + expect(challenge.credential.isBackedUp) == PasskeyIsCredentialBackedUp + expect(challenge.createdAt).to(beCloseTo(createdAtDate, within: 0.01)) + } + + it("should fail when the user handle is invalid") { + let json = JSON.replacingOccurrences(of: "\"\(PasskeyUserHandle)\"", with: "1000") + let jsonData = json.data(using: .utf8)! + let context = DecodingError.Context(codingPath: [], + debugDescription: "Format of user_handle is not recognized.") + let expectedError = DecodingError.dataCorrupted(context) + + expect({ + try decoder.decode(PasskeyAuthenticationMethod.self, from: jsonData) + }).to(throwError(expectedError)) + } + + it("should fail when the public key is invalid") { + let json = JSON.replacingOccurrences(of: "\"\(PasskeyPublicKey)\"", with: "1000") + let jsonData = json.data(using: .utf8)! + let context = DecodingError.Context(codingPath: [], + debugDescription: "Format of public_key is not recognized.") + let expectedError = DecodingError.dataCorrupted(context) + + expect({ + try decoder.decode(PasskeyAuthenticationMethod.self, from: jsonData) + }).to(throwError(expectedError)) + } + + } + + } +} diff --git a/Auth0Tests/MyAccount/AuthenticationMethods/PasskeyEnrollmentChallengeSpec.swift b/Auth0Tests/MyAccount/AuthenticationMethods/PasskeyEnrollmentChallengeSpec.swift new file mode 100644 index 000000000..1defe13d4 --- /dev/null +++ b/Auth0Tests/MyAccount/AuthenticationMethods/PasskeyEnrollmentChallengeSpec.swift @@ -0,0 +1,113 @@ +import Foundation +import Quick +import Nimble + +@testable import Auth0 + +private let AuthenticationMethodId = "baz" +private let AuthenticationSession = "7p0knAl_iQedbOuG2vBrU0QKREz8J300DkuCzTRRGuFsQ_Z6XqcADPdrB26RxqDx" +private let RelyingPartyIdentifier = "example.com" +private let UserIdentifier = "LcICuavHdO2zbcA8zRgnTRIkzPrruI_HQqe0J3RL0ou5VSrWhRybCQqyNMXWj1LDdxOzat6KVf9xpW3qLw5qjw" +private let UserName = "user@example.com" +private let Challenge = "wC-Pos1D-2xf9H5JjeoNJDWKhToOwrlwJ2mguvhnshw" +private let JSON = """ +{ + "auth_session": "\(AuthenticationSession)", + "authn_params_public_key": { + "rp": { + "id": "\(RelyingPartyIdentifier)", + "name": "Foo", + + }, + "user": { + "id": "\(UserIdentifier)", + "name": "\(UserName)", + "displayName": "Bar" + }, + "challenge": "\(Challenge)", + "pubKeyCredParams": [{ "type": "public-key", "alg": -257 }] + } +} +""" + +class PasskeyEnrollmentChallengeSpec: QuickSpec { + override class func spec() { + + describe("decode from json") { + + var decoder: JSONDecoder! + + beforeEach { + decoder = JSONDecoder() + decoder.userInfo[.headersKey] = ["Location": "https://example.com/foo/bar/\(AuthenticationMethodId)"] + } + + it("should have all properties") { + let jsonData = JSON.data(using: .utf8)! + let challenge = try decoder.decode(PasskeyEnrollmentChallenge.self, from: jsonData) + + expect(challenge.authenticationMethodId) == AuthenticationMethodId + expect(challenge.authenticationSession) == AuthenticationSession + expect(challenge.relyingPartyId) == RelyingPartyIdentifier + expect(challenge.userId) == UserIdentifier.a0_decodeBase64URLSafe() + expect(challenge.userName) == UserName + expect(challenge.challengeData) == Challenge.a0_decodeBase64URLSafe() + } + + it("should fail when the headers are missing") { + decoder = JSONDecoder() + + let jsonData = JSON.data(using: .utf8)! + let errorDescription = "Missing authentication method identifier in header 'Location'" + let context = DecodingError.Context(codingPath: [], + debugDescription: errorDescription) + let expectedError = DecodingError.dataCorrupted(context) + + expect({ + try decoder.decode(PasskeyEnrollmentChallenge.self, from: jsonData) + }).to(throwError(expectedError)) + } + + it("should fail when the Location header is missing") { + decoder = JSONDecoder() + decoder.userInfo[.headersKey] = ["Foo": "Bar"] + + let jsonData = JSON.data(using: .utf8)! + let errorDescription = "Missing authentication method identifier in header 'Location'" + let context = DecodingError.Context(codingPath: [], + debugDescription: errorDescription) + let expectedError = DecodingError.dataCorrupted(context) + + expect({ + try decoder.decode(PasskeyEnrollmentChallenge.self, from: jsonData) + }).to(throwError(expectedError)) + } + + it("should fail when the user id is invalid") { + let json = JSON.replacingOccurrences(of: "\"\(UserIdentifier)\"", with: "1000") + let jsonData = json.data(using: .utf8)! + let context = DecodingError.Context(codingPath: [], + debugDescription: "Format of user id is not recognized.") + let expectedError = DecodingError.dataCorrupted(context) + + expect({ + try decoder.decode(PasskeyEnrollmentChallenge.self, from: jsonData) + }).to(throwError(expectedError)) + } + + it("should fail when the challenge is invalid") { + let json = JSON.replacingOccurrences(of: "\"\(Challenge)\"", with: "1000") + let jsonData = json.data(using: .utf8)! + let context = DecodingError.Context(codingPath: [], + debugDescription: "Format of challenge is not recognized.") + let expectedError = DecodingError.dataCorrupted(context) + + expect({ + try decoder.decode(PasskeyEnrollmentChallenge.self, from: jsonData) + }).to(throwError(expectedError)) + } + + } + + } +} diff --git a/Auth0Tests/MyAccount/MyAccountErrorSpec.swift b/Auth0Tests/MyAccount/MyAccountErrorSpec.swift new file mode 100644 index 000000000..78014c93e --- /dev/null +++ b/Auth0Tests/MyAccount/MyAccountErrorSpec.swift @@ -0,0 +1,333 @@ +import Foundation +import Quick +import Nimble + +@testable import Auth0 + +class MyAccountErrorSpec: QuickSpec { + + override class func spec() { + + describe("init") { + + it("should initialize with info") { + let info: [String: Any] = ["foo": "bar"] + let error = MyAccountError(info: info) + + expect(error.info["foo"] as? String) == "bar" + expect(error.info.count) == 1 + expect(error.statusCode) == 0 + expect(error.cause).to(beNil()) + } + + it("should initialize with info and status code") { + let info: [String: Any] = ["foo": "bar"] + let statusCode = 400 + let error = MyAccountError(info: info, statusCode: statusCode) + + expect(error.statusCode) == statusCode + } + + it("should initialize with cause") { + let cause = NSError(domain: "com.auth0", code: -99999, userInfo: nil) + let error = MyAccountError(cause: cause) + + expect(error.cause).to(matchError(cause)) + expect(error.statusCode) == 0 + } + + it("should initialize with cause and status code") { + let cause = NSError(domain: "com.auth0", code: -99999, userInfo: nil) + let statusCode = 400 + let error = MyAccountError(cause: cause, statusCode: statusCode) + + expect(error.statusCode) == statusCode + } + + it("should initialize with description") { + let description = "foo" + let error = MyAccountError(description: description) + + expect(error.localizedDescription) == "\(description)." + expect(error.statusCode) == 0 + expect(error.cause).to(beNil()) + } + + it("should initialize with description and status code") { + let description = "foo" + let statusCode = 400 + let error = MyAccountError(description: description, statusCode: statusCode) + + expect(error.statusCode) == statusCode + } + + it("should initialize with response") { + let description = "foo" + let data = description.data(using: .utf8)! + let response = Response(data: data, response: nil, error: nil) + let error = MyAccountError(from: response) + + expect(error.localizedDescription) == "\(description)." + expect(error.statusCode) == 0 + expect(error.cause).to(beNil()) + } + + it("should initialize with response and status code") { + let description = "foo" + let data = description.data(using: .utf8)! + let statusCode = 400 + let httpResponse = HTTPURLResponse(url: URL(string: "example.com")!, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil) + let response = Response(data: data, response: httpResponse, error: nil) + let error = MyAccountError(from: response) + + expect(error.localizedDescription) == "\(description)." + expect(error.statusCode) == statusCode + } + + it("should initialize with cause") { + let cause = MockError() + let description = "Unable to complete the operation. CAUSE: \(cause.localizedDescription)." + let error = MyAccountError(cause: cause) + + expect(error.cause).toNot(beNil()) + expect(error.localizedDescription) == description + expect(error.statusCode) == 0 + } + + it("should initialize with cause and status code") { + let statusCode = 400 + let error = MyAccountError(cause: MockError(), statusCode: statusCode) + + expect(error.statusCode) == statusCode + } + + } + + describe("operators") { + + it("should be equal") { + let info: [String: Any] = ["type": "foo", "title": "bar", "detail": "baz"] + let statusCode = 400 + let error = MyAccountError(info: info, statusCode: statusCode) + + expect(error) == MyAccountError(info: info, statusCode: statusCode) + } + + it("should not be equal to an error with a different type") { + let title = "bar" + let detail = "baz" + let statusCode = 400 + let error = MyAccountError(info: ["type": "foo", "title": title, "detail": detail], + statusCode: statusCode) + + expect(error) != MyAccountError(info: ["type": "qux", "title": title, "detail": detail], + statusCode: statusCode) + } + + it("should not be equal to an error with a different title") { + let type = "foo" + let detail = "baz" + let statusCode = 400 + let error = MyAccountError(info: ["type": type, "title": "bar", "detail": detail], + statusCode: statusCode) + + expect(error) != MyAccountError(info: ["type": type, "title": "qux", "detail": detail], + statusCode: statusCode) + } + + it("should not be equal to an error with a different detail") { + let type = "foo" + let title = "bar" + let statusCode = 400 + let error = MyAccountError(info: ["type": type, "title": title, "detail": "baz"], + statusCode: statusCode) + + expect(error) != MyAccountError(info: ["type": type, "title": title, "detail": "qux"], + statusCode: statusCode) + } + + it("should not be equal to an error with a different status code") { + let info: [String: Any] = ["type": "foo", "title": "bar", "detail": "baz"] + let error = MyAccountError(info: info, statusCode: 400) + + expect(error) != MyAccountError(info: info, statusCode: 500) + } + + it("should access the internal info dictionary") { + let info: [String: Any] = ["foo": "bar"] + let error = MyAccountError(info: info) + + expect(error.info["foo"] as? String) == "bar" + } + + } + + describe("error code") { + + it("should return the type") { + let type = "foo" + let info: [String: Any] = ["type": type] + let error = MyAccountError(info: info) + + expect(error.code) == type + } + + it("should return the code") { + let code = "foo" + let info: [String: Any] = ["code": code] + let error = MyAccountError(info: info) + + expect(error.code) == code + } + + it("should return the default code") { + let error = MyAccountError(info: [:]) + + expect(error.code) == unknownError + } + + } + + describe("error title") { + + it("should return the title") { + let title = "foo" + let info: [String: Any] = ["title": title] + let error = MyAccountError(info: info) + + expect(error.title) == title + } + + it("should return the description") { + let description = "foo" + let info: [String: Any] = ["description": description] + let error = MyAccountError(info: info) + + expect(error.title) == description + } + + it("should return an empty title") { + let info: [String: Any] = [:] + let error = MyAccountError(info: info) + + expect(error.title).to(beEmpty()) + } + + } + + describe("error detail") { + + it("should return the title") { + let detail = "foo" + let info: [String: Any] = ["detail": detail] + let error = MyAccountError(info: info) + + expect(error.detail) == detail + } + + it("should return an empty detail") { + let info: [String: Any] = [:] + let error = MyAccountError(info: info) + + expect(error.detail).to(beEmpty()) + } + + } + + describe("error message") { + + it("should return the title and detail") { + let title = "foo" + let detail = "bar." + let info: [String: Any] = ["title": title, "detail": detail, "type": "baz"] + let error = MyAccountError(info: info) + + expect(error.localizedDescription) == "\(title): \(detail)" + } + + it("should return the title and detail adding a period") { + let title = "foo" + let detail = "bar" + let info: [String: Any] = ["title": title, "detail": detail, "type": "baz"] + let error = MyAccountError(info: info) + + expect(error.localizedDescription) == "\(title): \(detail)." + } + + it("should return the title") { + let title = "foo." + let info: [String: Any] = ["title": title, "type": "bar"] + let error = MyAccountError(info: info) + + expect(error.localizedDescription) == "\(title)" + } + + it("should return the title adding period") { + let title = "foo" + let info: [String: Any] = ["title": title, "type": "bar"] + let error = MyAccountError(info: info) + + expect(error.localizedDescription) == "\(title)." + } + + it("should return the default message") { + let info: [String: Any] = [:] + let message = "Failed with unknown error: \(info)." + let error = MyAccountError(info: info) + + expect(error.localizedDescription) == message + } + + } + + describe("validation errors") { + + it("should return the validation errors") { + let expectedValidationErrors: [(json: [String: String], error: MyAccountError.ValidationError)] = [ + (json: ["detail": ""], error: .init(detail: "", pointer: nil)), + (json: ["detail": "foo"], error: .init(detail: "foo", pointer: nil)), + (json: ["detail": "foo", "pointer": ""], error: .init(detail: "foo", pointer: nil)), + (json: ["detail": "foo", "pointer": "baz"], error: .init(detail: "foo", pointer: "baz")), + ] + let info: [String: Any] = ["validation_errors": expectedValidationErrors.map(\.json)] + let error = MyAccountError(info: info) + + expect(error.validationErrors).to(haveCount(4)) + + for (index, actualError) in error.validationErrors!.enumerated() { + let expectedError = expectedValidationErrors[index].error + expect(actualError.detail) == expectedError.detail + + if (expectedError.pointer == nil) { + expect(actualError.pointer).to(beNil()) + } else { + expect(actualError.pointer) == expectedError.pointer + } + } + } + + it("should return nil when there are no validation errors") { + let info: [String: Any] = [:] + let error = MyAccountError(info: info) + + expect(error.validationErrors).to(beNil()) + } + + } + + describe("error cases") { + + it("should detect network error") { + for errorCode in MyAccountError.networkErrorCodes { + expect(MyAccountError(cause: URLError.init(errorCode)).isNetworkError) == true + } + } + + } + + } + +} diff --git a/Auth0Tests/MyAccount/MyAccountSpec.swift b/Auth0Tests/MyAccount/MyAccountSpec.swift new file mode 100644 index 000000000..5c2902dbd --- /dev/null +++ b/Auth0Tests/MyAccount/MyAccountSpec.swift @@ -0,0 +1,151 @@ +import Foundation +import Quick +import Nimble + +@testable import Auth0 + +private let Domain = "samples.auth0.com" +private let Token = "TOKEN" + +class MyAccountSpec: QuickSpec { + + override class func spec() { + + describe("global functions") { + + it("should return my account client with token and domain") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.token) == Token + expect(myAccount.url.absoluteString) == "https://\(Domain)/me/v1" + } + + it("should return my account client with token, domain, and session") { + let session = URLSession(configuration: URLSession.shared.configuration) + let myAccount = Auth0.myAccount(token: Token, domain: Domain, session: session) as! Auth0MyAccount + + expect(myAccount.session).to(be(session)) + } + + #if !SWIFT_PACKAGE + it("should return my account client with bundle") { + let bundle = Bundle(for: MyAccountSpec.self) + let myAccount = Auth0.myAccount(token: Token, bundle: bundle) + + expect(myAccount.url.absoluteString) == "https://\(Domain)/me/v1" + } + #endif + + } + + describe("endpoint") { + + it("should return my account endpoint without trailing slash") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.url.absoluteString) == "https://\(Domain)/me/v1" + } + + it("should return my account endpoint with trailing slash") { + let myAccount = Auth0.myAccount(token: Token, domain: "\(Domain)/") + + expect(myAccount.url.absoluteString) == "https://\(Domain)/me/v1" + } + + } + + describe("logging") { + + it("should have no logging by default") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.logger).to(beNil()) + } + + it("should enable default logger") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.logging(enabled: true).logger).toNot(beNil()) + } + + it("should not enable default logger") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.logging(enabled: false).logger).to(beNil()) + } + + it("should enable custom logger") { + let logger = MockLogger() + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.using(logger: logger).logger).toNot(beNil()) + } + + } + + describe("authentication methods sub-client") { + + it("should return authentication methods sub-client") { + let session = URLSession(configuration: URLSession.shared.configuration) + let myAccount = Auth0.myAccount(token: Token, domain: Domain, session: session) + let authenticationMethods = myAccount.authenticationMethods as! Auth0MyAccountAuthenticationMethods + + expect(authenticationMethods.token) == Token + expect(authenticationMethods.url) == myAccount.url.appending("authentication-methods") + expect(authenticationMethods.session).to(be(session)) + } + + context("endpoint") { + + it("should return my account endpoint without trailing slash") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + let myAccountURL = myAccount.url.absoluteString + let authenticationMethodsUrl = myAccount.authenticationMethods.url.absoluteString + + expect(authenticationMethodsUrl) == "\(myAccountURL)/authentication-methods" + } + + it("should return my account endpoint with trailing slash") { + let myAccount = Auth0.myAccount(token: Token, domain: "\(Domain)/") + let myAccountURL = myAccount.url.absoluteString + let authenticationMethodsUrl = myAccount.authenticationMethods.url.absoluteString + + expect(authenticationMethodsUrl) == "\(myAccountURL)/authentication-methods" + } + + } + + context("logging") { + + it("should have no logging by default") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.authenticationMethods.logger).to(beNil()) + } + + it("should enable default logger") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.authenticationMethods.logging(enabled: true).logger).toNot(beNil()) + } + + it("should not enable default logger") { + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.authenticationMethods.logging(enabled: false).logger).to(beNil()) + } + + it("should enable custom logger") { + let logger = MockLogger() + let myAccount = Auth0.myAccount(token: Token, domain: Domain) + + expect(myAccount.authenticationMethods.using(logger: logger).logger).toNot(beNil()) + } + + } + + } + + } + +} diff --git a/Auth0Tests/PasskeyLoginChallenge.swift b/Auth0Tests/PasskeyLoginChallenge.swift index ab7ee40ac..e34e2d919 100644 --- a/Auth0Tests/PasskeyLoginChallenge.swift +++ b/Auth0Tests/PasskeyLoginChallenge.swift @@ -50,7 +50,7 @@ class PasskeyLoginChallengeSpec: QuickSpec { } } """.data(using: .utf8)! - let context = DecodingError.Context(codingPath: [SSOCredentials.CodingKeys.expiresIn], + let context = DecodingError.Context(codingPath: [], debugDescription: "Format of challenge is not recognized.") let expectedError = DecodingError.dataCorrupted(context) diff --git a/Auth0Tests/PasskeySignupChallengeSpec.swift b/Auth0Tests/PasskeySignupChallengeSpec.swift index dffc83d6b..af30bfded 100644 --- a/Auth0Tests/PasskeySignupChallengeSpec.swift +++ b/Auth0Tests/PasskeySignupChallengeSpec.swift @@ -70,7 +70,7 @@ class PasskeySignupChallengeSpec: QuickSpec { } } """.data(using: .utf8)! - let context = DecodingError.Context(codingPath: [SSOCredentials.CodingKeys.expiresIn], + let context = DecodingError.Context(codingPath: [], debugDescription: "Format of user id is not recognized.") let expectedError = DecodingError.dataCorrupted(context) @@ -97,7 +97,7 @@ class PasskeySignupChallengeSpec: QuickSpec { } } """.data(using: .utf8)! - let context = DecodingError.Context(codingPath: [SSOCredentials.CodingKeys.expiresIn], + let context = DecodingError.Context(codingPath: [], debugDescription: "Format of challenge is not recognized.") let expectedError = DecodingError.dataCorrupted(context) diff --git a/Auth0Tests/RequestSpec.swift b/Auth0Tests/RequestSpec.swift index a4a160448..59ef0d9e6 100644 --- a/Auth0Tests/RequestSpec.swift +++ b/Auth0Tests/RequestSpec.swift @@ -233,3 +233,16 @@ class RequestSpec: QuickSpec { } } + +func plainJson(from response: Response, + callback: Request<[String: Any], AuthenticationError>.Callback) { + do { + if let dictionary = try response.result()?.body as? [String: Any] { + callback(.success(dictionary)) + } else { + callback(.failure(AuthenticationError(from: response))) + } + } catch { + callback(.failure(error)) + } +} diff --git a/Auth0Tests/ResponseSpec.swift b/Auth0Tests/ResponseSpec.swift index ad314228f..0b937ae8e 100644 --- a/Auth0Tests/ResponseSpec.swift +++ b/Auth0Tests/ResponseSpec.swift @@ -8,7 +8,7 @@ private let URL = Foundation.URL(string: "https://samples.auth0.com")! private let JSONData = try! JSONSerialization.data(withJSONObject: ["attr": "value"], options: []) private func http(_ statusCode: Int, url: Foundation.URL = URL) -> HTTPURLResponse { - return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "1.0", headerFields: [:])! + return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "1.0", headerFields: ["Foo": "Bar"])! } class ResponseSpec: QuickSpec { @@ -24,6 +24,11 @@ class ResponseSpec: QuickSpec { it("should handle valid JSON") { let response = Response(data: JSONData, response: http(200), error: nil) expect(try? response.result()).toNot(beNil()) + + let result: JSONResponse = try response.result()! + expect(result.headers).toNot(beEmpty()) + expect(result.body as? [String: Any]).toNot(beEmpty()) + expect(result.data).toNot(beEmpty()) } it("should handle string as json for reset password") { @@ -68,5 +73,6 @@ class ResponseSpec: QuickSpec { } } + } } diff --git a/Auth0Tests/Responses.swift b/Auth0Tests/Responses.swift index 4ba3b13d7..fe92c9e87 100644 --- a/Auth0Tests/Responses.swift +++ b/Auth0Tests/Responses.swift @@ -37,10 +37,14 @@ func catchAllResponse() -> RequestResponse { } } -func apiSuccessResponse(json: [AnyHashable: Any] = [:]) -> RequestResponse { +func apiSuccessResponse(json: [AnyHashable: Any] = [:], headers: [String: String] = [:]) -> RequestResponse { return { request in let data = try! JSONSerialization.data(withJSONObject: json, options: []) - let response = HTTPURLResponse(url: request.url!, statusCode: APISuccessStatusCode, httpVersion: nil, headerFields: APIResponseHeaders)! + let headers = headers.merging(APIResponseHeaders, uniquingKeysWith: { (_, last) in last }) + let response = HTTPURLResponse(url: request.url!, + statusCode: APISuccessStatusCode, + httpVersion: nil, + headerFields: headers)! return (data, response, nil) } } @@ -48,7 +52,10 @@ func apiSuccessResponse(json: [AnyHashable: Any] = [:]) -> RequestResponse { func apiSuccessResponse(jsonArray: [Any]) -> RequestResponse { return { request in let data = try! JSONSerialization.data(withJSONObject: jsonArray, options: []) - let response = HTTPURLResponse(url: request.url!, statusCode: APISuccessStatusCode, httpVersion: nil, headerFields: APIResponseHeaders)! + let response = HTTPURLResponse(url: request.url!, + statusCode: APISuccessStatusCode, + httpVersion: nil, + headerFields: APIResponseHeaders)! return (data, response, nil) } } @@ -56,7 +63,10 @@ func apiSuccessResponse(jsonArray: [Any]) -> RequestResponse { func apiSuccessResponse(string: String) -> RequestResponse { return { request in let data = string.data(using: .utf8) - let response = HTTPURLResponse(url: request.url!, statusCode: APISuccessStatusCode, httpVersion: nil, headerFields: APIResponseHeaders)! + let response = HTTPURLResponse(url: request.url!, + statusCode: APISuccessStatusCode, + httpVersion: nil, + headerFields: APIResponseHeaders)! return (data, response, nil) } } @@ -64,7 +74,10 @@ func apiSuccessResponse(string: String) -> RequestResponse { func apiFailureResponse(json: [AnyHashable: Any] = [:], statusCode: Int = 400) -> RequestResponse { return { request in let data = try! JSONSerialization.data(withJSONObject: json, options: []) - let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: APIResponseHeaders)! + let response = HTTPURLResponse(url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: APIResponseHeaders)! return (data, response, nil) } } @@ -72,7 +85,10 @@ func apiFailureResponse(json: [AnyHashable: Any] = [:], statusCode: Int = 400) - func apiFailureResponse(string: String, statusCode: Int) -> RequestResponse { return { request in let data = string.data(using: .utf8) - let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: APIResponseHeaders)! + let response = HTTPURLResponse(url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: APIResponseHeaders)! return (data, response, nil) } } @@ -164,6 +180,7 @@ func multifactorChallengeResponse(challengeType: String, oobCode: String? = nil, return apiSuccessResponse(json: json) } +#if PASSKEYS_PLATFORM func passkeyLoginChallengeResponse() -> RequestResponse { let json: [String: Any] = [ "auth_session": "u5WSCyajq719ZSkLiEH13OJpa-Jsh8YZ75-NsBXph5pS-_gvqA0Z1MZyXL_sellw", @@ -178,20 +195,41 @@ func passkeyLoginChallengeResponse() -> RequestResponse { return apiSuccessResponse(json: json) } -func passkeySignupChallengeResponse(identifier: String, name: String = Support) -> RequestResponse { +func passkeySignupChallengeResponse(authSession: String, + rpId: String, + userId: String, + userName: String, + userDisplayName: String = Support, + challenge: String) -> RequestResponse { + return passkeyEnrollmentChallengeResponse(authSession: authSession, + rpId: rpId, + userId: userId, + userName: userName, + userDisplayName: userDisplayName, + challenge: challenge, + headers: [:]) +} + +func passkeyEnrollmentChallengeResponse(authSession: String, + rpId: String, + userId: String, + userName: String, + userDisplayName: String = Support, + challenge: String, + headers: [String: String]) -> RequestResponse { let json: [String: Any] = [ - "auth_session": "72CaO4uYxnfBz2JZ8zWfUkV2DFnC1TRZQWqEctPFJ8xIkPcTkZ82BhB6nKmj85xh", + "auth_session": authSession, "authn_params_public_key": [ "rp": [ - "id": "example.com", - "name": "example.com" + "id": rpId, + "name": rpId ], "user": [ - "id": "s54lWOzf1JezfejtMqklA6xZQDAwIWIgSbEM4WeZXPMlBhk2K_Ojp3nKSy0AUo1Bph2RepN8IzbGQGQruI6ibQ", - "name": identifier, - "displayName": name + "id": userId, + "name": userName, + "displayName": userDisplayName ], - "challenge": "0UAd1U-liMt27la7xtyaBnzwxFSrFsWSBzZMvx3tCGI", + "challenge": challenge, "pubKeyCredParams": [ ["type": "public-key", "alg": -8], ["type": "public-key", "alg": -7], @@ -205,5 +243,28 @@ func passkeySignupChallengeResponse(identifier: String, name: String = Support) ] ] + return apiSuccessResponse(json: json, headers: headers) +} + +func passkeyAuthenticationMethodResponse(id: String, + userIdentityId: String, + userHandle: String, + keyId: String, + publicKey: String, + credentialDeviceType: PasskeyDeviceType, + createdAt: String) -> RequestResponse { + let json: [String: Any] = [ + "id": id, + "type": "passkey", + "identity_user_id": userIdentityId, + "user_handle": userHandle, + "key_id": keyId, + "public_key": publicKey, + "credential_device_type": credentialDeviceType.rawValue, + "credential_backed_up": true, + "created_at": createdAt + ] + return apiSuccessResponse(json: json) } +#endif diff --git a/Auth0Tests/SSOCredentialsSpec.swift b/Auth0Tests/SSOCredentialsSpec.swift index c2e46c79d..711431c37 100644 --- a/Auth0Tests/SSOCredentialsSpec.swift +++ b/Auth0Tests/SSOCredentialsSpec.swift @@ -126,7 +126,7 @@ class SSOCredentialsSpec: QuickSpec { "id_token": "\(IdToken)" } """.data(using: .utf8)! - let context = DecodingError.Context(codingPath: [SSOCredentials.CodingKeys.expiresIn], + let context = DecodingError.Context(codingPath: [], debugDescription: "Format of expires_in is not recognized.") let expectedError = DecodingError.dataCorrupted(context) diff --git a/Auth0Tests/WebAuthErrorSpec.swift b/Auth0Tests/WebAuthErrorSpec.swift index b67b74748..0de977269 100644 --- a/Auth0Tests/WebAuthErrorSpec.swift +++ b/Auth0Tests/WebAuthErrorSpec.swift @@ -142,16 +142,17 @@ class WebAuthErrorSpec: QuickSpec { } it("should append the cause error message") { - let cause = MockError() - let message = "An unexpected error occurred. CAUSE: \(cause.localizedDescription)" - let error = WebAuthError(code: .other, cause: cause) + let description = "foo." + let cause = MockError(message: "foo bar.") + let message = "\(description) CAUSE: \(cause.localizedDescription)" + let error = WebAuthError(code: .unknown(description), cause: cause) expect(error.localizedDescription) == message } - it("should append the cause error message with a separator") { + it("should append the cause error message adding periods") { let description = "foo" - let cause = MockError() - let message = "\(description). CAUSE: \(cause.localizedDescription)" + let cause = MockError(message: "foo bar") + let message = "\(description). CAUSE: \(cause.localizedDescription)." let error = WebAuthError(code: .unknown(description), cause: cause) expect(error.localizedDescription) == message } diff --git a/Documentation.docc/Documentation.md b/Documentation.docc/Documentation.md index 5d21876a9..cd5dc5c04 100644 --- a/Documentation.docc/Documentation.md +++ b/Documentation.docc/Documentation.md @@ -7,4 +7,5 @@ SDK for Apple platforms. - ``Auth0/WebAuth``: Web-based authentication client. - ``Auth0/CredentialsManager``: Credentials management utility for securely storing and retrieving the user's credentials from the Keychain. - ``Auth0/Authentication``: Client for the [Auth0 Authentication API](https://auth0.com/docs/api/authentication). -- ``Auth0/Users``: Client for the Users endpoints of the Auth0 [Management API v2](https://auth0.com/docs/api/management/v2). +- ``Auth0/MyAccount``: Client for the My Account API. +- ``Auth0/Users``: Client for the Users endpoints of the [Management API v2](https://auth0.com/docs/api/management/v2). diff --git a/EXAMPLES.md b/EXAMPLES.md index a216f9f5e..dd06496af 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,15 +1,16 @@ # Examples -- [Web Auth (iOS / macOS)](#web-auth-ios--macos) -- [Credentials Manager (iOS / macOS / tvOS / watchOS)](#credentials-manager-ios--macos--tvos--watchos) -- [Authentication API (iOS / macOS / tvOS / watchOS)](#authentication-api-ios--macos--tvos--watchos) -- [Management API (Users) (iOS / macOS / tvOS / watchOS)](#management-api-users-ios--macos--tvos--watchos) +- [Web Auth (iOS / macOS / visionOS)](#web-auth-ios--macos--visionos) +- [Credentials Manager (iOS / macOS / TVOS / watchOS / visionOS)](#credentials-manager-ios--macos--tvos--watchos--visionos) +- [Authentication API (iOS / macOS / TVOS / watchOS / visionOS)](#authentication-api-ios--macos--tvos--watchos--visionos) +- [My Account API (iOS / macOS / tvOS / watchOS / visionOS) [EA]](#my-account-api-ios--macos--tvos--watchos--visionos-ea) +- [Management API (Users) (iOS / macOS / TVOS / watchOS / visionOS)](#management-api-users-ios--macos--tvos--watchos--visionos) - [Logging](#logging) - [Advanced Features](#advanced-features) --- -## Web Auth (iOS / macOS) +## Web Auth (iOS / macOS / visionOS) **See all the available features in the [API documentation ↗](https://auth0.github.io/Auth0.swift/documentation/auth0/webauth)** @@ -274,7 +275,7 @@ Web Auth will only produce `WebAuthError` error values. You can find the underly [Go up ⤴](#examples) -## Credentials Manager (iOS / macOS / tvOS / watchOS) +## Credentials Manager (iOS / macOS / tvOS / watchOS / visionOS) **See all the available features in the [API documentation ↗](https://auth0.github.io/Auth0.swift/documentation/auth0/credentialsmanager)** @@ -295,7 +296,14 @@ let credentialsManager = CredentialsManager(authentication: Auth0.authentication ``` > [!CAUTION] -> The Credentials Manager is not thread-safe, except for its `credentials()`, `apiCredentials()`, `ssoCredentials()`, and `renew()` methods. To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads without proper synchronization. +> The Credentials Manager is not thread-safe, except for the following methods: +> +> - `credentials()` +> - `apiCredentials()` +> - `ssoCredentials()` +> - `renew()` +> +> To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads without proper synchronization. ### Store credentials @@ -473,6 +481,67 @@ credentialsManager.enableBiometrics(withTitle: "Unlock with Face ID or passcode" ### Other credentials +#### API credentials [EA] + +> [!NOTE] +> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant. + +When the user logs in, you can request an access token for a specific API by passing its API identifier as the [audience](#add-an-audience-value) value. The access token in the resulting credentials can then be used to make authenticated requests to that API. + +However, if you need an access token for a different API, you can exchange the [refresh token](https://auth0.com/docs/secure/tokens/refresh-tokens) for credentials containing an access token specific to this other API. **This method is thread-safe**. + +> [!IMPORTANT] +> Currently, only the Auth0 My Account API is supported. Support for other APIs will be added in the future. + +```swift +credentialsManager.apiCredentials(forAudience: "https://samples.us.auth0.com/me", + scope: "create:me:authentication_methods") { result in + switch result { + case .success(let apiCredentials): + print("Obtained API credentials: \(apiCredentials)") + case .failure(let error): + print("Failed with: \(error)") + } +} +``` + +
+ Using async/await + +```swift +do { + let apiCredentials = try await credentialsManager.apiCredentials(forAudience: "https://samples.us.auth0.com/me", + scope: "create:me:authentication_methods") + print("Obtained API credentials: \(apiCredentials)") +} catch { + print("Failed with: \(error)") +} +``` +
+ +
+ Using Combine + +```swift +credentialsManager + .apiCredentials(forAudience: "https://samples.us.auth0.com/me", + scope: "create:me:authentication_methods") + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Failed with: \(error)") + } + }, receiveValue: { apiCredentials in + print("Obtained API credentials: \(apiCredentials)") + }) + .store(in: &cancellables) +``` +
+ +See [Get a refresh token](#get-a-refresh-token) to learn how to obtain a refresh token. + +> [!CAUTION] +> To ensure that no concurrent exchange requests get made, do not call this method from multiple Credentials Manager instances. The Credentials Manager cannot synchronize requests across instances. + #### SSO credentials [EA] > [!NOTE] @@ -560,7 +629,7 @@ The Credentials Manager will only produce `CredentialsManagerError` error values [Go up ⤴](#examples) -## Authentication API (iOS / macOS / tvOS / watchOS) +## Authentication API (iOS / macOS / tvOS / watchOS / visionOS) **See all the available features in the [API documentation ↗](https://auth0.github.io/Auth0.swift/documentation/auth0/authentication)** @@ -795,11 +864,13 @@ authController.performRequests() The resulting passkey credential will be delivered through the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate. ```swift -func authorizationController(controller: ASAuthorizationController, - didCompleteWithAuthorization authorization: ASAuthorization) { - guard let loginPasskey = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion else { +public func authorizationController(controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let loginPasskey as ASAuthorizationPlatformPublicKeyCredentialAssertion: + // ... + default: print("Unrecognized credential: \(authorization.credential)") - return } // ... @@ -962,11 +1033,13 @@ authController.performRequests() The created passkey credential will be delivered through the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate. ```swift -func authorizationController(controller: ASAuthorizationController, - didCompleteWithAuthorization authorization: ASAuthorization) { - guard let signupPasskey = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration else { +public func authorizationController(controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let signupPasskey as ASAuthorizationPlatformPublicKeyCredentialRegistration: + // ... + default: print("Unrecognized credential: \(authorization.credential)") - return } // ... @@ -1388,14 +1461,206 @@ Auth0 ### Authentication API client errors -The Authentication API client will only produce `AuthenticationError` error values. You can find the error information in the `info` dictionary of the error value. Check the [API documentation](https://auth0.github.io/Auth0.swift/documentation/auth0/authenticationerror) to learn more about the available `AuthenticationError` properties. +The Authentication API client will only produce `AuthenticationError` error values. + +- The `cause` property contains the underlying error value –if any. +- Use the `isNetworkError` property to check if the request failed due to networking issues. +- Find more information about the error in the `info` dictionary. + +Check the [API documentation](https://auth0.github.io/Auth0.swift/documentation/auth0/authenticationerror) to learn more about the available `AuthenticationError` properties. > [!WARNING] > Do not parse or otherwise rely on the error messages to handle the errors. The error messages are not part of the API and can change. Use the [error types](https://auth0.github.io/Auth0.swift/documentation/auth0/authenticationerror/#topics) instead, which are part of the API. [Go up ⤴](#examples) -## Management API (Users) (iOS / macOS / tvOS / watchOS) +## My Account API (iOS / macOS / tvOS / watchOS / visionOS) [EA] + +**See all the available features in the [API documentation ↗](https://auth0.github.io/Auth0.swift/documentation/auth0/myaccount)** + +- [Enroll a new passkey](#enroll-a-new-passkey) +- [My Account API client errors](#my-account-api-client-errors) + +> [!NOTE] +> The My Account API is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant. + +Use the Auth0 My Account API to manage the current user's account. + +To call the My Account API, you need an access token issued specifically for this API. See [API credentials [EA]](#api-credentials-ea) to learn how to obtain one. + +### Enroll a new passkey + +**Scopes required:** `create:me:authentication_methods` + +Enrolling a new passkey is a three-step process that requires the **Passkeys** grant to be enabled for your Auth0 application. Check [our documentation](https://auth0.com/docs/native-passkeys-for-mobile-applications#prepare-your-application) for more information. + +First, you request an enrollment challenge from Auth0. Then, you pass that challenge to Apple's [`AuthenticationServices`](https://developer.apple.com/documentation/authenticationservices) APIs to create a new passkey credential. Finally, you use the created passkey credential and the original challenge to enroll the passkey with Auth0. + +#### 1. Request an enrollment challenge + +You can specify an optional user identity identifier and/or a database connection name to help Auth0 find the user. The user identity identifier will be needed if the user logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking). + +```swift +Auth0 + .myAccount(token: apiCredentials.accessToken) + .authenticationMethods + .passkeyEnrollmentChallenge() + .start { result in + switch result { + case .success(let enrollmentChallenge): + print("Obtained enrollment challenge: \(enrollmentChallenge)") + case .failure(let error): + print("Failed with: \(error)") + } + } +``` + +
+ Using async/await + +```swift +do { + let enrollmentChallenge = try await Auth0 + .myAccount(token: apiCredentials.accessToken) + .authenticationMethods + .passkeyEnrollmentChallenge() + .start() + print("Obtained enrollment challenge: \(enrollmentChallenge)") +} catch { + print("Failed with: \(error)") +} +``` +
+ +
+ Using Combine + +```swift +Auth0 + .myAccount(token: apiCredentials.accessToken) + .authenticationMethods + .passkeyEnrollmentChallenge() + .start() + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Failed with: \(error)") + } + }, receiveValue: { enrollmentChallenge in + print("Obtained enrollment challenge: \(enrollmentChallenge)") + }) + .store(in: &cancellables) +``` +
+ +#### 2. Create a new passkey credential + +Use the enrollment challenge with [`ASAuthorizationPlatformPublicKeyCredentialProvider`](https://developer.apple.com/documentation/authenticationservices/asauthorizationplatformpublickeycredentialprovider) from the `AuthenticationServices` framework to generate a new passkey credential. Check out [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys#Register-a-new-account-on-a-service) to learn more. + +```swift +let credentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( + relyingPartyIdentifier: enrollmentChallenge.relyingPartyId +) + +let request = credentialProvider.createCredentialRegistrationRequest( + challenge: enrollmentChallenge.challengeData, + name: enrollmentChallenge.userName, + userID: enrollmentChallenge.userId +) + +let authController = ASAuthorizationController(authorizationRequests: [request]) +authController.delegate = self // ASAuthorizationControllerDelegate +authController.presentationContextProvider = self +authController.performRequests() +``` + +The created passkey credential will be delivered through the [`ASAuthorizationControllerDelegate`](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontrollerdelegate) delegate. + +```swift +public func authorizationController(controller: ASAuthorizationController, + didCompleteWithAuthorization authorization: ASAuthorization) { + switch authorization.credential { + case let newPasskey as ASAuthorizationPlatformPublicKeyCredentialRegistration: + // ... + default: + print("Unrecognized credential: \(authorization.credential)") + } + + // ... +} +``` + +#### 3. Enroll the passkey + +Use the created passkey credential and the enrollment challenge to enroll the passkey with Auth0. + +```swift +Auth0 + .myAccount(token: apiCredentials.accessToken) + .authenticationMethods + .enroll(passkey: newPasskey, + challenge: enrollmentChallenge) + .start { result in + switch result { + case .success(let authenticationMethod): + print("Enrolled passkey: \(authenticationMethod)") + case .failure(let error): + print("Failed with: \(error)") + } + } +``` + +
+ Using async/await + +```swift +do { + let authenticationMethod = try await Auth0 + .myAccount(token: apiCredentials.accessToken) + .authenticationMethods + .enroll(passkey: newPasskey, + challenge: enrollmentChallenge) + .start() + print("Enrolled passkey: \(authenticationMethod)") +} catch { + print("Failed with: \(error)") +} +``` +
+ +
+ Using Combine + +```swift +Auth0 + .myAccount(token: apiCredentials.accessToken) + .authenticationMethods + .enroll(passkey: newPasskey, + challenge: enrollmentChallenge) + .start() + .sink(receiveCompletion: { completion in + if case .failure(let error) = completion { + print("Failed with: \(error)") + } + }, receiveValue: { authenticationMethod in + print("Enrolled passkey: \(authenticationMethod)") + }) + .store(in: &cancellables) +``` +
+ +### My Account API client errors + +The My Account API client will only produce `MyAccountError` error values. + +- The `cause` property contains the underlying error value –if any. +- Use the `isNetworkError` property to check if the request failed due to networking issues. +- Find more information about the error in the `info` dictionary. + +See the [API documentation](https://auth0.github.io/Auth0.swift/documentation/auth0/myaccounterror) to learn more about the available `MyAccountError` properties. + +[Go up ⤴](#examples) + +## Management API (Users) (iOS / macOS / tvOS / watchOS / visionOS) **See all the available features in the [API documentation ↗](https://auth0.github.io/Auth0.swift/documentation/auth0/users)** @@ -1630,7 +1895,13 @@ Auth0 ### Management API client errors -The Management API client will only produce `ManagementError` error values. You can find the error information in the `info` dictionary of the error value. Check the [API documentation](https://auth0.github.io/Auth0.swift/documentation/auth0/managementerror) to learn more about the available `ManagementError` properties. +The Management API client will only produce `ManagementError` error values. + +- The `cause` property contains the underlying error value –if any. +- Use the `isNetworkError` property to check if the request failed due to networking issues. +- Find more information about the error in the `info` dictionary. + +Check the [API documentation](https://auth0.github.io/Auth0.swift/documentation/auth0/managementerror) to learn more about the available `ManagementError` properties. [Go up ⤴](#examples)