From 2cf6765e133667a72c336048156a0b7d92360ebe Mon Sep 17 00:00:00 2001 From: nandan_prabhu Date: Wed, 5 Aug 2026 16:32:42 +0530 Subject: [PATCH 01/20] feat: Upgrade Auth0.swift to v3.0.2 (#909) --- auth0_flutter/darwin/auth0_flutter.podspec | 4 +- .../darwin/auth0_flutter/Package.swift | 4 +- .../AuthAPI/AuthAPIHandler.swift | 28 +- .../AuthAPILoginWithOTPMethodHandler.swift | 4 +- ...APIMultifactorChallengeMethodHandler.swift | 12 +- ...sskeyCredentialExchangeMethodHandler.swift | 2 +- .../AuthAPIUserInfoMethodHandler.swift | 2 +- .../AuthAPI/SSOExchangeMethodHandler.swift | 2 +- .../ClearApiCredentialsMethodHandler.swift | 7 +- ...CredentialsManagerClearMethodHandler.swift | 7 +- .../CredentialsManagerSaveMethodHandler.swift | 7 +- ...dentialsManagerUserInfoMethodHandler.swift | 9 +- .../SSOCredentialsMethodHandler.swift | 2 +- .../Sources/auth0_flutter/Extensions.swift | 10 +- .../MfaAPI/MfaEnrollPushMethodHandler.swift | 2 +- .../MfaAPI/MfaEnrollTotpMethodHandler.swift | 2 +- .../MfaAPI/MfaVerifyMethodHandler.swift | 2 +- ...ccountConfirmEnrollmentMethodHandler.swift | 2 +- .../MyAccountVerifyOtpMethodHandler.swift | 2 +- .../WebAuth/WebAuthExtensions.swift | 7 +- .../WebAuth/WebAuthLoginMethodHandler.swift | 12 +- .../WebAuth/WebAuthLogoutMethodHandler.swift | 2 +- auth0_flutter/example/ios/Podfile | 3 + .../ios/Runner.xcodeproj/project.pbxproj | 48 +- .../xcshareddata/swiftpm/Package.resolved | 32 -- .../xcshareddata/swiftpm/Package.resolved | 32 -- auth0_flutter/example/ios/Runner/Info.plist | 19 + .../example/ios/Runner/SceneDelegate.swift | 6 + ...ustomTokenExchangeMethodHandlerTests.swift | 2 +- .../Tests/AuthAPI/AuthAPIHandlerTests.swift | 60 ++- ...ginUsernameOrEmailMethodHandlerTests.swift | 2 +- ...uthAPILoginWithOTPMethodHandlerTests.swift | 41 +- ...ltifactorChallengeMethodHandlerTests.swift | 77 ++- .../AuthAPIRenewMethodHandlerTests.swift | 2 +- ...AuthAPISSOExchangeMethodHandlerTests.swift | 2 +- .../ios/Tests/AuthAPI/AuthAPISpies.swift | 88 ++-- .../AuthAPIUserInfoMethodHandlerTests.swift | 2 +- ...agerApiCredentialsMethodHandlerTests.swift | 18 +- ...learApiCredentialsMethodHandlerTests.swift | 18 +- ...ntialsManagerClearMethodHandlerTests.swift | 12 +- ...dentialsManagerGetMethodHandlerTests.swift | 26 +- ...alsManagerHasValidMethodHandlerTests.swift | 16 +- ...ntialsManagerRenewMethodHandlerTests.swift | 4 +- ...agerSSOCredentialsMethodHandlerTests.swift | 22 +- ...entialsManagerSaveMethodHandlerTests.swift | 12 +- .../CredentialsManagerSpies.swift | 15 +- ...alsManagerUserInfoMethodHandlerTests.swift | 121 ++--- .../example/ios/Tests/ExtensionsTests.swift | 60 +-- ...aGetAuthenticatorsMethodHandlerTests.swift | 17 +- .../example/ios/Tests/Mfa/MfaSpies.swift | 66 +-- auth0_flutter/example/ios/Tests/Mocks.swift | 31 ++ .../ios/Tests/MyAccount/MyAccountSpies.swift | 75 ++- .../WebAuth/WebAuthExtensionsTests.swift | 14 +- .../WebAuthLoginMethodHandlerTests.swift | 2 +- .../ios/Tests/WebAuth/WebAuthSpies.swift | 22 +- auth0_flutter/example/lib/example_app.dart | 4 +- .../macos/Runner.xcodeproj/project.pbxproj | 41 ++ .../xcshareddata/swiftpm/Package.resolved | 32 -- .../xcshareddata/swiftpm/Package.resolved | 32 -- auth0_flutter/ios/auth0_flutter.podspec | 4 +- auth0_flutter/macos/auth0_flutter.podspec | 4 +- ...windows_web_authentication_test.mocks.dart | 7 + .../test/mobile/authentication_api_test.dart | 17 - .../mobile/authentication_api_test.mocks.dart | 86 ++- .../credentials_manager_test.mocks.dart | 86 +-- .../test/mobile/mfa_api_test.mocks.dart | 193 ++++--- .../mobile/passwordless_api_test.mocks.dart | 489 ++++++++++-------- .../mobile/web_authentication_test.mocks.dart | 230 +++++--- .../auth_multifactor_challenge_options.dart | 4 +- ...ethod_channel_auth0_flutter_auth_test.dart | 37 +- 70 files changed, 1348 insertions(+), 1016 deletions(-) delete mode 100644 auth0_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 auth0_flutter/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 auth0_flutter/example/ios/Runner/SceneDelegate.swift delete mode 100644 auth0_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 auth0_flutter/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/auth0_flutter/darwin/auth0_flutter.podspec b/auth0_flutter/darwin/auth0_flutter.podspec index ff18cfe73..d728c1105 100644 --- a/auth0_flutter/darwin/auth0_flutter.podspec +++ b/auth0_flutter/darwin/auth0_flutter.podspec @@ -19,8 +19,8 @@ Pod::Spec.new do |s| s.osx.deployment_target = '11.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '2.23.0' - s.dependency 'JWTDecode', '3.3.0' + s.dependency 'Auth0', '3.0.2' + s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' # Flutter.framework does not contain a i386 slice. diff --git a/auth0_flutter/darwin/auth0_flutter/Package.swift b/auth0_flutter/darwin/auth0_flutter/Package.swift index 8f0083901..ca61ab968 100644 --- a/auth0_flutter/darwin/auth0_flutter/Package.swift +++ b/auth0_flutter/darwin/auth0_flutter/Package.swift @@ -11,8 +11,8 @@ let package = Package( .library(name: "auth0-flutter", targets: ["auth0_flutter"]), ], dependencies: [ - .package(url: "https://github.com/auth0/Auth0.swift", exact: "2.23.0"), - .package(url: "https://github.com/auth0/JWTDecode.swift", exact: "3.3.0"), + .package(url: "https://github.com/auth0/Auth0.swift", exact: "3.0.2"), + .package(url: "https://github.com/auth0/JWTDecode.swift", exact: "4.0.0"), .package(url: "https://github.com/auth0/SimpleKeychain", exact: "1.3.0"), ], targets: [ diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIHandler.swift index 5b6120acc..274f4d0bf 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIHandler.swift @@ -9,6 +9,7 @@ import FlutterMacOS // MARK: - Providers typealias AuthAPIClientProvider = (_ account: Account, _ userAgent: UserAgent, _ arguments: [String: Any]) -> Authentication +typealias AuthAPIMFAClientProvider = (_ account: Account, _ userAgent: UserAgent, _ arguments: [String: Any]) -> MFAClient typealias AuthAPIMethodHandlerProvider = (_ method: AuthAPIHandler.Method, _ client: Authentication) -> MethodHandler // MARK: - Auth Auth Handler @@ -64,11 +65,21 @@ public class AuthAPIHandler: NSObject, FlutterPlugin { return client } + var mfaClientProvider: AuthAPIMFAClientProvider = { account, userAgent, arguments in + var client = Auth0.mfa(clientId: account.clientId, domain: account.domain) + client.using(inLibrary: userAgent.name, version: userAgent.version) + + let useDPoP = arguments["useDPoP"] as? Bool ?? false + if useDPoP { + client = client.useDPoP() + } + + return client + } + var methodHandlerProvider: AuthAPIMethodHandlerProvider = { method, client in switch method { case .loginWithUsernameOrEmail: return AuthAPILoginUsernameOrEmailMethodHandler(client: client) - case .loginWithOTP: return AuthAPILoginWithOTPMethodHandler(client: client) - case .multifactorChallenge: return AuthAPIMultifactorChallengeMethodHandler(client: client) case .signup: return AuthAPISignupMethodHandler(client: client) case .userInfo: return AuthAPIUserInfoMethodHandler(client: client) case .renew: return AuthAPIRenewMethodHandler(client: client) @@ -102,6 +113,7 @@ public class AuthAPIHandler: NSObject, FlutterPlugin { case .passkeyLoginChallenge, .passkeySignupChallenge, .passkeyCredentialExchange: return UnsupportedMethodHandler() #endif + case .loginWithOTP, .multifactorChallenge: return UnsupportedMethodHandler() } } @@ -121,8 +133,16 @@ public class AuthAPIHandler: NSObject, FlutterPlugin { return result(FlutterMethodNotImplemented) } - let client = clientProvider(account, userAgent, arguments) - let methodHandler = methodHandlerProvider(method, client) + let methodHandler: MethodHandler + switch method { + case .loginWithOTP: + methodHandler = AuthAPILoginWithOTPMethodHandler(client: mfaClientProvider(account, userAgent, arguments)) + case .multifactorChallenge: + methodHandler = AuthAPIMultifactorChallengeMethodHandler(client: mfaClientProvider(account, userAgent, arguments)) + default: + let client = clientProvider(account, userAgent, arguments) + methodHandler = methodHandlerProvider(method, client) + } methodHandler.handle(with: arguments, callback: result) } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift index e1a36cf8f..a792325bf 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift @@ -12,7 +12,7 @@ struct AuthAPILoginWithOTPMethodHandler: MethodHandler { case mfaToken } - let client: Authentication + let client: MFAClient func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { guard let otp = arguments[Argument.otp] as? String else { @@ -23,7 +23,7 @@ struct AuthAPILoginWithOTPMethodHandler: MethodHandler { } client - .login(withOTP: otp, mfaToken: mfaToken) + .verify(otp: otp, mfaToken: mfaToken) .start { switch $0 { case let .success(credentials): callback(result(from: credentials)) diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift index a409a4670..28d50e6f0 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift @@ -13,7 +13,7 @@ enum ChallengeProperty: String { } fileprivate extension MethodHandler { - func result(from challenge: Challenge) -> Any? { + func result(from challenge: MFAChallenge) -> Any? { var data: [String: Any] = [ChallengeProperty.challengeType.rawValue: challenge.challengeType] data[ChallengeProperty.oobCode] = challenge.oobCode data[ChallengeProperty.bindingMethod] = challenge.bindingMethod @@ -28,18 +28,18 @@ struct AuthAPIMultifactorChallengeMethodHandler: MethodHandler { case authenticatorId } - let client: Authentication + let client: MFAClient func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { guard let mfaToken = arguments[Argument.mfaToken] as? String else { return callback(FlutterError(from: .requiredArgumentMissing(Argument.mfaToken.rawValue))) } - - let types = arguments[Argument.types] as? [String] - let authenticatorId = arguments[Argument.authenticatorId] as? String + guard let authenticatorId = arguments[Argument.authenticatorId] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing(Argument.authenticatorId.rawValue))) + } client - .multifactorChallenge(mfaToken: mfaToken, types: types, authenticatorId: authenticatorId) + .challenge(with: authenticatorId, mfaToken: mfaToken) .start { switch $0 { case let .success(challenge): callback(result(from: challenge)) diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIPasskeyCredentialExchangeMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIPasskeyCredentialExchangeMethodHandler.swift index 5849d52eb..5464af6b2 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIPasskeyCredentialExchangeMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIPasskeyCredentialExchangeMethodHandler.swift @@ -71,7 +71,7 @@ struct AuthAPIPasskeyCredentialExchangeMethodHandler: MethodHandler { let response = credentialMap["response"] as? [String: Any] let isSignup = response?["attestationObject"] != nil - let request: Request + let request: any TokenRequestable if isSignup { guard let challenge = Self.reconstructSignupChallenge(from: challengeMap) else { return callback(FlutterError(code: "PASSKEY_ERROR", diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIUserInfoMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIUserInfoMethodHandler.swift index 2790d109a..4ae51b313 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIUserInfoMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIUserInfoMethodHandler.swift @@ -7,7 +7,7 @@ import FlutterMacOS #endif fileprivate extension MethodHandler { - func result(from userInfo: UserInfo) -> Any? { + func result(from userInfo: UserProfile) -> Any? { return userInfo.asDictionary() } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/SSOExchangeMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/SSOExchangeMethodHandler.swift index 6cf5c08f4..286164bcf 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/SSOExchangeMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/SSOExchangeMethodHandler.swift @@ -36,7 +36,7 @@ struct SSOExchangeMethodHandler: MethodHandler { var response: [String: Any] = [ "sessionTransferToken": ssoCredentials.sessionTransferToken, "tokenType": ssoCredentials.issuedTokenType, - "expiresIn": Int(ssoCredentials.expiresIn.timeIntervalSinceNow) + "expiresIn": Int(ssoCredentials.expiresAt.timeIntervalSinceNow) ] response["idToken"] = ssoCredentials.idToken if let refreshToken = ssoCredentials.refreshToken { diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/ClearApiCredentialsMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/ClearApiCredentialsMethodHandler.swift index ef3fdbea7..634c82510 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/ClearApiCredentialsMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/ClearApiCredentialsMethodHandler.swift @@ -21,7 +21,12 @@ struct ClearApiCredentialsMethodHandler: MethodHandler { } let scope = arguments[Argument.scope] as? String - callback(self.credentialsManager.clear(forAudience: audience, scope: scope)) + do { + try self.credentialsManager.clear(forAudience: audience, scope: scope) + callback(nil) + } catch { + callback(FlutterError(from: (error as? CredentialsManagerError) ?? .unknown)) + } } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearMethodHandler.swift index 000136322..efb2b6a9c 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearMethodHandler.swift @@ -10,6 +10,11 @@ struct CredentialsManagerClearMethodHandler: MethodHandler { let credentialsManager: CredentialsManager func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { - callback(self.credentialsManager.clear()) + do { + try self.credentialsManager.clear() + callback(true) + } catch { + callback(FlutterError(from: (error as? CredentialsManagerError) ?? .unknown)) + } } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerSaveMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerSaveMethodHandler.swift index 723a0d072..8926e060f 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerSaveMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerSaveMethodHandler.swift @@ -19,6 +19,11 @@ struct CredentialsManagerSaveMethodHandler: MethodHandler { return callback(FlutterError(from: .requiredArgumentMissing(Argument.credentials.rawValue))) } - callback(self.credentialsManager.store(credentials: credentials)) + do { + try self.credentialsManager.store(credentials: credentials) + callback(true) + } catch { + callback(FlutterError(from: (error as? CredentialsManagerError) ?? .unknown)) + } } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerUserInfoMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerUserInfoMethodHandler.swift index f09591825..0991782bf 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerUserInfoMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerUserInfoMethodHandler.swift @@ -11,10 +11,11 @@ struct CredentialsManagerUserInfoMethodHandler: MethodHandler { let credentialsManager: CredentialsManager func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { - if let user = credentialsManager.user { - callback(user.asDictionary()) - } else { - callback(nil) + do { + let user = try credentialsManager.userProfile() + callback(user?.asDictionary()) + } catch { + callback(FlutterError(from: (error as? CredentialsManagerError) ?? .unknown)) } } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/SSOCredentialsMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/SSOCredentialsMethodHandler.swift index 8e6457183..a863e61e6 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/SSOCredentialsMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/SSOCredentialsMethodHandler.swift @@ -29,7 +29,7 @@ struct SSOCredentialsMethodHandler: MethodHandler { var response: [String: Any] = [ "sessionTransferToken": ssoCredentials.sessionTransferToken, "tokenType": ssoCredentials.issuedTokenType, - "expiresIn": Int(ssoCredentials.expiresIn.timeIntervalSinceNow) + "expiresIn": Int(ssoCredentials.expiresAt.timeIntervalSinceNow) ] response["idToken"] = ssoCredentials.idToken if let refreshToken = ssoCredentials.refreshToken { diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift index 40b8478ca..2d6536b76 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift @@ -75,7 +75,7 @@ extension Credentials { tokenType: tokenType, idToken: idToken, refreshToken: dictionary[CredentialsProperty.refreshToken] as? String, - expiresIn: expiresIn, + expiresAt: expiresIn, scope: scopes.isEmpty ? nil : scopes.asSpaceSeparatedString, recoveryCode: nil) } @@ -85,9 +85,9 @@ extension Credentials { var data: [String: Any] = [ CredentialsProperty.accessToken.rawValue: accessToken, CredentialsProperty.idToken.rawValue: idToken, - CredentialsProperty.expiresAt.rawValue: expiresIn.asISO8601String, + CredentialsProperty.expiresAt.rawValue: expiresAt.asISO8601String, CredentialsProperty.scopes.rawValue: scope?.split(separator: " ").map(String.init) ?? [], - CredentialsProperty.userProfile.rawValue: UserInfo(json: jwt.body)?.asDictionary() ?? [:], + CredentialsProperty.userProfile.rawValue: UserProfile(json: jwt.body)?.asDictionary() ?? [:], CredentialsProperty.tokenType.rawValue: tokenType ] data[CredentialsProperty.refreshToken] = refreshToken @@ -100,13 +100,13 @@ extension APICredentials { return [ "accessToken": accessToken, "tokenType": tokenType, - "expiresAt": expiresIn.asISO8601String, + "expiresAt": expiresAt.asISO8601String, "scopes": scope.split(separator: " ").map(String.init) ] } } -extension UserInfo { +extension UserProfile { func asDictionary() -> [String: Any] { let claimsToFilter = ["aud", "iss", diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollPushMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollPushMethodHandler.swift index 99a89f013..e8808a429 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollPushMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollPushMethodHandler.swift @@ -14,7 +14,7 @@ struct MfaEnrollPushMethodHandler: MethodHandler { return callback(FlutterError(from: .requiredArgumentMissing("mfaToken"))) } - let request: Request = + let request: any Requestable = client.enroll(mfaToken: mfaToken) request.start { switch $0 { diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollTotpMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollTotpMethodHandler.swift index b3a0c434b..f940ab48d 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollTotpMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaEnrollTotpMethodHandler.swift @@ -14,7 +14,7 @@ struct MfaEnrollTotpMethodHandler: MethodHandler { return callback(FlutterError(from: .requiredArgumentMissing("mfaToken"))) } - let request: Request = + let request: any Requestable = client.enroll(mfaToken: mfaToken) request.start { switch $0 { diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaVerifyMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaVerifyMethodHandler.swift index f36176d7d..c635f7837 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaVerifyMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MfaAPI/MfaVerifyMethodHandler.swift @@ -17,7 +17,7 @@ struct MfaVerifyMethodHandler: MethodHandler { return callback(FlutterError(from: .requiredArgumentMissing("grantType"))) } - let request: Request + let request: any TokenRequestable switch grantType { case "otp": diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountConfirmEnrollmentMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountConfirmEnrollmentMethodHandler.swift index bb41c2ef0..ff80a9854 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountConfirmEnrollmentMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountConfirmEnrollmentMethodHandler.swift @@ -21,7 +21,7 @@ struct MyAccountConfirmEnrollmentMethodHandler: MethodHandler { return callback(FlutterError(from: .requiredArgumentMissing("factorType"))) } - let request: Request + let request: any Requestable if factorType == "push-notification" { request = client.authenticationMethods.confirmPushNotificationEnrollment(id: id, authSession: authSession) } else { diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift index 69ccbcf0c..2f305e3f4 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift @@ -24,7 +24,7 @@ struct MyAccountVerifyOtpMethodHandler: MethodHandler { return callback(FlutterError(from: .requiredArgumentMissing("factorType"))) } - let request: Request + let request: any Requestable switch factorType { case "email": request = client.authenticationMethods.confirmEmailEnrollment(id: id, authSession: authSession, otpCode: otp) diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthExtensions.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthExtensions.swift index 6f706b998..93a55bb74 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthExtensions.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthExtensions.swift @@ -10,12 +10,11 @@ extension FlutterError { convenience init(from webAuthError: WebAuthError) { var code: String switch webAuthError { - case WebAuthError.noBundleIdentifier: code = "NO_BUNDLE_IDENTIFIER" - case WebAuthError.invalidInvitationURL: code = "INVALID_INVITATION_URL" case WebAuthError.userCancelled: code = "USER_CANCELLED" - case WebAuthError.noAuthorizationCode: code = "NO_AUTHORIZATION_CODE" - case WebAuthError.pkceNotAllowed: code = "PKCE_NOT_ALLOWED" + case WebAuthError.authenticationFailed: code = "AUTHENTICATION_FAILED" + case WebAuthError.codeExchangeFailed: code = "CODE_EXCHANGE_FAILED" case WebAuthError.idTokenValidationFailed: code = "ID_TOKEN_VALIDATION_FAILED" + case WebAuthError.credentialsManagerError: code = "CREDENTIALS_MANAGER_ERROR" case WebAuthError.transactionActiveAlready: code = "TRANSACTION_ACTIVE_ALREADY" case WebAuthError.other: code = "OTHER" default: code = "UNKNOWN" diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLoginMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLoginMethodHandler.swift index b0898c8c4..f9a49b90b 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLoginMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLoginMethodHandler.swift @@ -8,7 +8,7 @@ import FlutterMacOS #endif #if os(iOS) -typealias WebAuthProviderFunction = (UIModalPresentationStyle) -> WebAuthProvider +typealias WebAuthProviderFunction = @MainActor (UIModalPresentationStyle) -> WebAuthProvider #endif struct WebAuthLoginMethodHandler: MethodHandler { @@ -35,7 +35,9 @@ struct WebAuthLoginMethodHandler: MethodHandler { #if os(iOS) let safariProvider: WebAuthProviderFunction - init(client: WebAuth, safariProvider: @escaping WebAuthProviderFunction = WebAuthentication.safariProvider) { + init(client: WebAuth, safariProvider: @escaping WebAuthProviderFunction = { style in + WebAuthentication.safariProvider(style: style) + }) { self.client = client self.safariProvider = safariProvider } @@ -103,7 +105,11 @@ struct WebAuthLoginMethodHandler: MethodHandler { #if os(iOS) if let safariViewControllerDictionary = arguments[SafariViewController.key] as? [String: Any?] { let safariViewController = SafariViewController(from: safariViewControllerDictionary) - webAuth = webAuth.provider(self.safariProvider(safariViewController.presentationStyle)) + // Flutter dispatches method calls on the main thread, so this is safe to assert. + let provider = MainActor.assumeIsolated { + self.safariProvider(safariViewController.presentationStyle) + } + webAuth = webAuth.provider(provider) } #endif diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLogoutMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLogoutMethodHandler.swift index af59de4e1..be2976c06 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLogoutMethodHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/WebAuth/WebAuthLogoutMethodHandler.swift @@ -34,7 +34,7 @@ struct WebAuthLogoutMethodHandler: MethodHandler { webAuth = webAuth.redirectURL(url) } - webAuth.clearSession(federated: federated) { // Pass federated here + webAuth.logout(federated: federated) { // Pass federated here switch $0 { case .success: callback(nil) case let .failure(error): callback(FlutterError(from: error)) diff --git a/auth0_flutter/example/ios/Podfile b/auth0_flutter/example/ios/Podfile index 23eab8798..9131006df 100644 --- a/auth0_flutter/example/ios/Podfile +++ b/auth0_flutter/example/ios/Podfile @@ -39,5 +39,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end end end diff --git a/auth0_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/auth0_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 98be7b56a..1f501669e 100644 --- a/auth0_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/auth0_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,9 +8,10 @@ /* Begin PBXBuildFile section */ 06FCF69BF7F84D607295C71B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C5483C34779CA72998DEA52 /* Pods_RunnerTests.framework */; }; + 0AA9CE2720467CE301D1C118 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7BE2C01B563724E04C1E9 /* SceneDelegate.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D82163828F9A89300467FD1 /* AuthAPILoginWithOTPMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D82163728F9A89300467FD1 /* AuthAPILoginWithOTPMethodHandlerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 51B33CFEAF2B34075EB0D5BE /* CredentialsManagerUserInfoMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3D36CB4A98386C0AF7BC50 /* CredentialsManagerUserInfoMethodHandlerTests.swift */; }; 5C08DBC8288A7646000D2F37 /* CredentialsManagerExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C08DBC7288A7646000D2F37 /* CredentialsManagerExtensionsTests.swift */; }; 5C328B4827F7822600451E70 /* WebAuthHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C328B4727F7822600451E70 /* WebAuthHandlerTests.swift */; }; 5C328B5027F7B0E700451E70 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C328B4F27F7B0E600451E70 /* Utilities.swift */; }; @@ -36,11 +37,11 @@ 5C59DA8B2809386B00365CDB /* AuthAPIExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C59DA8A2809386B00365CDB /* AuthAPIExtensionsTests.swift */; }; 5C59DA8D280938BE00365CDB /* WebAuthExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C59DA8C280938BE00365CDB /* WebAuthExtensionsTests.swift */; }; 5C59DA97280958FB00365CDB /* SmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C59DA96280958FB00365CDB /* SmokeTests.swift */; }; - 5CA2853A29C14EBA008A06B8 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA2853929C14EBA008A06B8 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */; }; 5CAAA4A2281A0C7D007666F1 /* ModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAAA4A1281A0C7D007666F1 /* ModelsTests.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 82FCE04B4D34064CD03272F0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22621E40BED0AB508F0967BC /* Pods_Runner.framework */; }; + 85FC7505D30A68D7A3D61920 /* AuthAPILoginWithOTPMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262746525770F2BE30882696 /* AuthAPILoginWithOTPMethodHandlerTests.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -71,6 +72,7 @@ B0CB00020000000000000006 /* MfaEnrollPushMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CB00010000000000000006 /* MfaEnrollPushMethodHandlerTests.swift */; }; B0CB00020000000000000007 /* MfaChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CB00010000000000000007 /* MfaChallengeMethodHandlerTests.swift */; }; B0CB00020000000000000008 /* MfaVerifyMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CB00010000000000000008 /* MfaVerifyMethodHandlerTests.swift */; }; + ED52E6C0A84A015AA6D08CF0 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1332D8749DF3C853F772E7A /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */; }; PK00000000000000000001 /* AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PK00000000000000000003 /* AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift */; }; PK00000000000000000005 /* AuthAPIPasskeyExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PK00000000000000000006 /* AuthAPIPasskeyExtensionsTests.swift */; }; PK10000000000000000001 /* AuthAPIPasskeySignupChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PK10000000000000000003 /* AuthAPIPasskeySignupChallengeMethodHandlerTests.swift */; }; @@ -109,10 +111,11 @@ /* Begin PBXFileReference section */ 0385195E6E4B40D5D2CAE9CF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 0B3D36CB4A98386C0AF7BC50 /* CredentialsManagerUserInfoMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CredentialsManagerUserInfoMethodHandlerTests.swift; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 22621E40BED0AB508F0967BC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 2D82163728F9A89300467FD1 /* AuthAPILoginWithOTPMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthAPILoginWithOTPMethodHandlerTests.swift; sourceTree = ""; }; + 262746525770F2BE30882696 /* AuthAPILoginWithOTPMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthAPILoginWithOTPMethodHandlerTests.swift; sourceTree = ""; }; 30E92729F56505552C6FEAB6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3C5483C34779CA72998DEA52 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -145,7 +148,6 @@ 5C59DA94280958FA00365CDB /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5C59DA96280958FB00365CDB /* SmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokeTests.swift; sourceTree = ""; }; 5C5BFA8C2B96422A0078D945 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; }; - 5CA2853929C14EBA008A06B8 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthAPIMultifactorChallengeMethodHandlerTests.swift; sourceTree = ""; }; 5CAAA4A1281A0C7D007666F1 /* ModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelsTests.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -187,6 +189,8 @@ B0CB00010000000000000007 /* MfaChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MfaChallengeMethodHandlerTests.swift; sourceTree = ""; }; B0CB00010000000000000008 /* MfaVerifyMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MfaVerifyMethodHandlerTests.swift; sourceTree = ""; }; B53DC26EF5BF473819173843 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + DDC7BE2C01B563724E04C1E9 /* SceneDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = SceneDelegate.swift; sourceTree = ""; }; + E1332D8749DF3C853F772E7A /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthAPIMultifactorChallengeMethodHandlerTests.swift; sourceTree = ""; }; ED10EF5476E15471E11081E4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; F4387C1CC70C6144C6484B93 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; PK00000000000000000003 /* AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift; sourceTree = ""; }; @@ -265,7 +269,6 @@ 5C335E4627FE68A500EDDE3A /* AuthAPI */ = { isa = PBXGroup; children = ( - 2D82163728F9A89300467FD1 /* AuthAPILoginWithOTPMethodHandlerTests.swift */, 5C335E4727FE68BC00EDDE3A /* AuthAPISpies.swift */, 5C59DA6427FFCF0600365CDB /* AuthAPIHandlerTests.swift */, 5C59DA6627FFE75600365CDB /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift */, @@ -278,7 +281,8 @@ PK10000000000000000003 /* AuthAPIPasskeySignupChallengeMethodHandlerTests.swift */, PK20000000000000000002 /* AuthAPIPasskeyCredentialExchangeMethodHandlerTests.swift */, 5C59DA8A2809386B00365CDB /* AuthAPIExtensionsTests.swift */, - 5CA2853929C14EBA008A06B8 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */, + 262746525770F2BE30882696 /* AuthAPILoginWithOTPMethodHandlerTests.swift */, + E1332D8749DF3C853F772E7A /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */, ); path = AuthAPI; sourceTree = ""; @@ -296,6 +300,7 @@ A09B2E3F1D4C7891BA000001 /* CredentialsManagerSSOCredentialsMethodHandlerTests.swift */, A0AC1D0F2E0000000000AC01 /* CredentialsManagerApiCredentialsMethodHandlerTests.swift */, A0AC1D0F2E0000000000AC03 /* CredentialsManagerClearApiCredentialsMethodHandlerTests.swift */, + 0B3D36CB4A98386C0AF7BC50 /* CredentialsManagerUserInfoMethodHandlerTests.swift */, ); path = CredentialsManager; sourceTree = ""; @@ -371,6 +376,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + DDC7BE2C01B563724E04C1E9 /* SceneDelegate.swift */, ); path = Runner; sourceTree = ""; @@ -643,8 +649,6 @@ 5C59DA8B2809386B00365CDB /* AuthAPIExtensionsTests.swift in Sources */, 5C59DA8D280938BE00365CDB /* WebAuthExtensionsTests.swift in Sources */, 5C335E3F27FBCD1D00EDDE3A /* Auth0FlutterPluginTests.swift in Sources */, - 5CA2853A29C14EBA008A06B8 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift in Sources */, - 2D82163828F9A89300467FD1 /* AuthAPILoginWithOTPMethodHandlerTests.swift in Sources */, 5C4E65C5286D1CFB00141449 /* CredentialsManagerHasValidMethodHandlerTests.swift in Sources */, 5C328B4827F7822600451E70 /* WebAuthHandlerTests.swift in Sources */, 5C328B5327F7B12F00451E70 /* WebAuthSpies.swift in Sources */, @@ -694,6 +698,9 @@ B0CB00020000000000000006 /* MfaEnrollPushMethodHandlerTests.swift in Sources */, B0CB00020000000000000007 /* MfaChallengeMethodHandlerTests.swift in Sources */, B0CB00020000000000000008 /* MfaVerifyMethodHandlerTests.swift in Sources */, + 85FC7505D30A68D7A3D61920 /* AuthAPILoginWithOTPMethodHandlerTests.swift in Sources */, + ED52E6C0A84A015AA6D08CF0 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift in Sources */, + 51B33CFEAF2B34075EB0D5BE /* CredentialsManagerUserInfoMethodHandlerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -711,6 +718,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 0AA9CE2720467CE301D1C118 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -790,7 +798,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -810,7 +818,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -838,7 +846,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -867,7 +875,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.sdks.Flutter.Tests; @@ -893,7 +901,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.sdks.Flutter.Tests; @@ -918,7 +926,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -947,7 +955,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.sdks.Flutter.RunnerUITests; @@ -973,7 +981,7 @@ DEVELOPMENT_TEAM = ""; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.sdks.Flutter.RunnerUITests; @@ -1032,7 +1040,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1081,7 +1089,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -1104,7 +1112,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1128,7 +1136,7 @@ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/auth0_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/auth0_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 6a183a3ce..000000000 --- a/auth0_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,32 +0,0 @@ -{ - "pins" : [ - { - "identity" : "auth0.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/Auth0.swift", - "state" : { - "revision" : "8fa0ccce3920717843ed4dd95839bf4cbf22c662", - "version" : "2.23.0" - } - }, - { - "identity" : "jwtdecode.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/JWTDecode.swift", - "state" : { - "revision" : "36a5ce735a61c4bc119593f43ce2c027b4ca7392", - "version" : "3.3.0" - } - }, - { - "identity" : "simplekeychain", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/SimpleKeychain", - "state" : { - "revision" : "776c4a6db74d5c6c143974be91c383680d468630", - "version" : "1.3.0" - } - } - ], - "version" : 2 -} diff --git a/auth0_flutter/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/auth0_flutter/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 6a183a3ce..000000000 --- a/auth0_flutter/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,32 +0,0 @@ -{ - "pins" : [ - { - "identity" : "auth0.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/Auth0.swift", - "state" : { - "revision" : "8fa0ccce3920717843ed4dd95839bf4cbf22c662", - "version" : "2.23.0" - } - }, - { - "identity" : "jwtdecode.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/JWTDecode.swift", - "state" : { - "revision" : "36a5ce735a61c4bc119593f43ce2c027b4ca7392", - "version" : "3.3.0" - } - }, - { - "identity" : "simplekeychain", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/SimpleKeychain", - "state" : { - "revision" : "776c4a6db74d5c6c143974be91c383680d468630", - "version" : "1.3.0" - } - } - ], - "version" : 2 -} diff --git a/auth0_flutter/example/ios/Runner/Info.plist b/auth0_flutter/example/ios/Runner/Info.plist index 5aab9811a..f3e9291e7 100644 --- a/auth0_flutter/example/ios/Runner/Info.plist +++ b/auth0_flutter/example/ios/Runner/Info.plist @@ -39,6 +39,25 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/auth0_flutter/example/ios/Runner/SceneDelegate.swift b/auth0_flutter/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 000000000..b9ce8ea2b --- /dev/null +++ b/auth0_flutter/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPICustomTokenExchangeMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPICustomTokenExchangeMethodHandlerTests.swift index 65ac489d6..438d83135 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPICustomTokenExchangeMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPICustomTokenExchangeMethodHandlerTests.swift @@ -48,7 +48,7 @@ extension AuthAPICustomTokenExchangeMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(), + expiresAt: Date(), scope: "openid profile email") let expectation = self.expectation(description: "Produced credentials") spy.credentialsResult = .success(credentials) diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIHandlerTests.swift index d76a6a273..2ee2baaa7 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIHandlerTests.swift @@ -82,6 +82,54 @@ extension AuthAPIHandlerTests { wait(for: [expectation]) } + // MARK: AuthAPIMFAClientProvider + + func testCallsMFAClientProviderForLoginWithOTP() { + let methodName = AuthAPIHandler.Method.loginWithOTP.rawValue + let accountDictionary = [AccountProperty.clientId.rawValue: "foo", AccountProperty.domain.rawValue: "bar"] + let userAgentDictionary = [UserAgentProperty.name.rawValue: "baz", UserAgentProperty.version.rawValue: "qux"] + let argumentsDictionary: [String: Any] = [ + Account.key: accountDictionary, + UserAgent.key: userAgentDictionary, + AuthAPILoginWithOTPMethodHandler.Argument.otp.rawValue: "", + AuthAPILoginWithOTPMethodHandler.Argument.mfaToken.rawValue: "" + ] + let expectation = self.expectation(description: "Called MFA client provider") + sut.mfaClientProvider = { (account: Account, userAgent: UserAgent, arguments: [String: Any]) -> MFAClient in + XCTAssertEqual(account.clientId, accountDictionary[AccountProperty.clientId]) + XCTAssertEqual(account.domain, accountDictionary[AccountProperty.domain]) + XCTAssertEqual(userAgent.name, userAgentDictionary[UserAgentProperty.name]) + XCTAssertEqual(userAgent.version, userAgentDictionary[UserAgentProperty.version]) + expectation.fulfill() + return SpyMFAClient() + } + sut.handle(FlutterMethodCall(methodName: methodName, arguments: argumentsDictionary)) { _ in } + wait(for: [expectation]) + } + + func testCallsMFAClientProviderForMultifactorChallenge() { + let methodName = AuthAPIHandler.Method.multifactorChallenge.rawValue + let accountDictionary = [AccountProperty.clientId.rawValue: "foo", AccountProperty.domain.rawValue: "bar"] + let userAgentDictionary = [UserAgentProperty.name.rawValue: "baz", UserAgentProperty.version.rawValue: "qux"] + let argumentsDictionary: [String: Any] = [ + Account.key: accountDictionary, + UserAgent.key: userAgentDictionary, + AuthAPIMultifactorChallengeMethodHandler.Argument.mfaToken.rawValue: "", + AuthAPIMultifactorChallengeMethodHandler.Argument.authenticatorId.rawValue: "" + ] + let expectation = self.expectation(description: "Called MFA client provider") + sut.mfaClientProvider = { (account: Account, userAgent: UserAgent, arguments: [String: Any]) -> MFAClient in + XCTAssertEqual(account.clientId, accountDictionary[AccountProperty.clientId]) + XCTAssertEqual(account.domain, accountDictionary[AccountProperty.domain]) + XCTAssertEqual(userAgent.name, userAgentDictionary[UserAgentProperty.name]) + XCTAssertEqual(userAgent.version, userAgentDictionary[UserAgentProperty.version]) + expectation.fulfill() + return SpyMFAClient() + } + sut.handle(FlutterMethodCall(methodName: methodName, arguments: argumentsDictionary)) { _ in } + wait(for: [expectation]) + } + // MARK: AuthAPIMethodHandlerProvider func testCallsMethodHandlerProvider() { @@ -115,9 +163,11 @@ extension AuthAPIHandlerTests { extension AuthAPIHandlerTests { func testReturnsMethodHandlers() { var expectations: [XCTestExpectation] = [] + // `.loginWithOTP` and `.multifactorChallenge` are routed directly to an + // `MFAClient`-backed handler in `handle(_:result:)` and never reach + // `methodHandlerProvider`, so they are intentionally excluded here. var methodHandlers: [AuthAPIHandler.Method: MethodHandler.Type] = [ .loginWithUsernameOrEmail: AuthAPILoginUsernameOrEmailMethodHandler.self, - .loginWithOTP: AuthAPILoginWithOTPMethodHandler.self, .signup: AuthAPISignupMethodHandler.self, .userInfo: AuthAPIUserInfoMethodHandler.self, .renew: AuthAPIRenewMethodHandler.self, @@ -149,7 +199,13 @@ extension AuthAPIHandlerTests { func testCallsMethodHandlers() { var expectations: [XCTestExpectation] = [] - AuthAPIHandler.Method.allCases.forEach { method in + // `.loginWithOTP` and `.multifactorChallenge` are routed directly to an + // `MFAClient`-backed handler in `handle(_:result:)` and never reach + // `methodHandlerProvider`, so they are intentionally excluded here. + let methods = AuthAPIHandler.Method.allCases.filter { + $0 != .loginWithOTP && $0 != .multifactorChallenge + } + methods.forEach { method in let arguments: [String: Any] = arguments() let expectation = self.expectation(description: "\(method.rawValue) handler call") expectations.append(expectation) diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginUsernameOrEmailMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginUsernameOrEmailMethodHandlerTests.swift index 2baa7a6a7..c3c888a63 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginUsernameOrEmailMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginUsernameOrEmailMethodHandlerTests.swift @@ -119,7 +119,7 @@ extension AuthAPILoginUsernameOrEmailMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(), + expiresAt: Date(), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") spy.credentialsResult = .success(credentials) diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginWithOTPMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginWithOTPMethodHandlerTests.swift index 95e2f8702..b3c272caa 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginWithOTPMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginWithOTPMethodHandlerTests.swift @@ -1,16 +1,22 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter fileprivate typealias Argument = AuthAPILoginWithOTPMethodHandler.Argument class AuthAPILoginWithOTPMethodHandlerTests: XCTestCase { - var spy: SpyAuthentication! + var spy: SpyMFAClient! var sut: AuthAPILoginWithOTPMethodHandler! override func setUpWithError() throws { - spy = SpyAuthentication() + spy = SpyMFAClient() sut = AuthAPILoginWithOTPMethodHandler(client: spy) } } @@ -37,7 +43,7 @@ extension AuthAPILoginWithOTPMethodHandlerTests { func testProducesErrorWithInvalidIDToken() { let credentials = Credentials(idToken: "foo") let expectation = self.expectation(description: "ID Token cannot be decoded") - spy.credentialsResult = .success(credentials) + spy.verifyResult = .success(credentials) sut.handle(with: arguments()) { result in assert(result: result, isError: .idTokenDecodingFailed) expectation.fulfill() @@ -53,19 +59,17 @@ extension AuthAPILoginWithOTPMethodHandlerTests { // MARK: otp func testAddsOTP() { - let key = Argument.otp let value = "foo" - sut.handle(with: arguments(withKey: key, value: value)) { _ in } - XCTAssertEqual(spy.arguments[key] as? String, value) + sut.handle(with: arguments(withKey: Argument.otp, value: value)) { _ in } + XCTAssertEqual(spy.verifyOtpArg, value) } // MARK: mfaToken func testAddsMFAToken() { - let key = Argument.mfaToken let value = "foo" - sut.handle(with: arguments(withKey: key, value: value)) { _ in } - XCTAssertEqual(spy.arguments[key] as? String, value) + sut.handle(with: arguments(withKey: Argument.mfaToken, value: value)) { _ in } + XCTAssertTrue(spy.calledVerify) } } @@ -74,7 +78,7 @@ extension AuthAPILoginWithOTPMethodHandlerTests { extension AuthAPILoginWithOTPMethodHandlerTests { func testCallsSDKLoginWithOTPMethod() { sut.handle(with: arguments()) { _ in } - XCTAssertTrue(spy.calledLoginWithOTP) + XCTAssertTrue(spy.calledVerify) } func testProducesCredentials() { @@ -82,10 +86,10 @@ extension AuthAPILoginWithOTPMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(), + expiresAt: Date(), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") - spy.credentialsResult = .success(credentials) + spy.verifyResult = .success(credentials) sut.handle(with: arguments()) { result in assert(result: result, has: CredentialsProperty.allCases) expectation.fulfill() @@ -93,12 +97,15 @@ extension AuthAPILoginWithOTPMethodHandlerTests { wait(for: [expectation]) } - func testProducesAuthenticationError() { - let error = AuthenticationError(info: [:], statusCode: 0) - let expectation = self.expectation(description: "Produced the AuthenticationError \(error)") - spy.credentialsResult = .failure(error) + func testProducesMFAVerifyError() { + let error = MFAVerifyError(info: [:], statusCode: 0) + let expectation = self.expectation(description: "Produced the MFAVerifyError \(error)") + spy.verifyResult = .failure(error) sut.handle(with: arguments()) { result in - assert(result: result, isError: error) + guard let flutterError = result as? FlutterError else { + return XCTFail("The handler did not produce a FlutterError") + } + XCTAssertEqual(flutterError.code, error.code) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift index edf7c3b05..2f95bf0b5 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift @@ -1,16 +1,22 @@ import XCTest +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import Auth0 @testable import auth0_flutter fileprivate typealias Argument = AuthAPIMultifactorChallengeMethodHandler.Argument class AuthAPIMultifactorChallengeMethodHandlerTests: XCTestCase { - var spy: SpyAuthentication! + var spy: SpyMFAClient! var sut: AuthAPIMultifactorChallengeMethodHandler! override func setUpWithError() throws { - spy = SpyAuthentication() + spy = SpyMFAClient() sut = AuthAPIMultifactorChallengeMethodHandler(client: spy) } } @@ -27,77 +33,61 @@ extension AuthAPIMultifactorChallengeMethodHandlerTests { } wait(for: [expectation]) } + + func testProducesErrorWhenAuthenticatorIdIsMissing() { + let key = Argument.authenticatorId + let expectation = self.expectation(description: "authenticatorId is missing") + sut.handle(with: arguments(without: key)) { result in + assert(result: result, isError: .requiredArgumentMissing(key.rawValue)) + expectation.fulfill() + } + wait(for: [expectation]) + } } // MARK: - Arguments extension AuthAPIMultifactorChallengeMethodHandlerTests { - // MARK: mfaToken - - func testAddsMFAToken() { - let key = Argument.mfaToken - let value = "foo" - sut.handle(with: arguments(withKey: key, value: value)) { _ in } - XCTAssertEqual(spy.arguments[key] as? String, value) - } - - // MARK: types - - func testAddsTypes() { - let key = Argument.types - let value = ["foo"] - sut.handle(with: arguments(withKey: key, value: value)) { _ in } - XCTAssertEqual(spy.arguments[key] as? [String], value) - } - - func testDoesNotAddTypesWhenNil() { - let argument = Argument.types - sut.handle(with: arguments(without: argument)) { _ in } - XCTAssertNil(spy.arguments[argument]) - } - // MARK: authenticatorId func testAddsAuthenticatorId() { - let key = Argument.authenticatorId let value = "foo" - sut.handle(with: arguments(withKey: key, value: value)) { _ in } - XCTAssertEqual(spy.arguments[key] as? String, value) - } - - func testDoesNotAddAuthenticatorIdWhenNil() { - let argument = Argument.authenticatorId - sut.handle(with: arguments(without: argument)) { _ in } - XCTAssertNil(spy.arguments[argument]) + sut.handle(with: arguments(withKey: Argument.authenticatorId, value: value)) { _ in } + XCTAssertEqual(spy.challengeAuthenticatorIdArg, value) } } // MARK: - Multifactor Challenge Result extension AuthAPIMultifactorChallengeMethodHandlerTests { - func testCallsSDKMultifactorChallengeMethod() { + func testCallsSDKChallengeMethod() { sut.handle(with: arguments()) { _ in } - XCTAssertTrue(spy.calledMultifactorChallenge) + XCTAssertTrue(spy.calledChallenge) } func testProducesChallenge() { - let challenge = Challenge(challengeType: "foo", oobCode: "bar", bindingMethod: "baz") + let challenge = MFAChallenge(challengeType: "foo", oobCode: "bar", bindingMethod: "baz") let expectation = self.expectation(description: "Produced a challenge") spy.challengeResult = .success(challenge) sut.handle(with: arguments()) { result in - assert(result: result, has: challenge) + guard let dict = result as? [String: Any?] else { + return XCTFail("Did not produce dictionary") + } + XCTAssertEqual(dict["challengeType"] as? String, challenge.challengeType) + XCTAssertEqual(dict["oobCode"] as? String, challenge.oobCode) + XCTAssertEqual(dict["bindingMethod"] as? String, challenge.bindingMethod) expectation.fulfill() } wait(for: [expectation]) } - func testProducesAuthenticationError() { - let error = AuthenticationError(info: [:], statusCode: 0) - let expectation = self.expectation(description: "Produced the AuthenticationError \(error)") + func testProducesMfaChallengeError() { + let error = MfaChallengeError(info: [:], statusCode: 0) + let expectation = self.expectation(description: "Produced the MfaChallengeError \(error)") spy.challengeResult = .failure(error) sut.handle(with: arguments()) { result in - assert(result: result, isError: error) + XCTAssertTrue(result is FlutterError) expectation.fulfill() } wait(for: [expectation]) @@ -110,7 +100,6 @@ extension AuthAPIMultifactorChallengeMethodHandlerTests { override func arguments() -> [String: Any] { return [ Argument.mfaToken.rawValue: "", - Argument.types.rawValue: [], Argument.authenticatorId.rawValue: "" ] } diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIRenewMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIRenewMethodHandlerTests.swift index 6bc0de200..c0184e974 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIRenewMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIRenewMethodHandlerTests.swift @@ -86,7 +86,7 @@ extension AuthAPIRenewMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(), + expiresAt: Date(), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") spy.credentialsResult = .success(credentials) diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISSOExchangeMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISSOExchangeMethodHandlerTests.swift index 19702967c..0441be759 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISSOExchangeMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISSOExchangeMethodHandlerTests.swift @@ -55,7 +55,7 @@ extension AuthAPISSOExchangeMethodHandlerTests { let ssoCredentials = SSOCredentials( sessionTransferToken: "session-token", issuedTokenType: "urn:ietf:params:oauth:token-type:session_transfer", - expiresIn: Date(timeIntervalSinceNow: 60), + expiresAt: Date(timeIntervalSinceNow: 60), idToken: testIdToken, refreshToken: "new-refresh-token" ) diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift index 499571d67..0db8d4772 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift @@ -4,19 +4,19 @@ import Foundation fileprivate let mockCredentials = Credentials() fileprivate let mockChallenge = Challenge(challengeType: "", oobCode: nil, bindingMethod: nil) fileprivate let mockDatabaseUser: DatabaseUser = (email: "", username: nil, verified: true) -fileprivate let mockUserInfo = UserInfo(json: ["sub": ""])! +fileprivate let mockUserInfo = UserProfile(json: ["sub": ""])! fileprivate let mockSSOCredentials = SSOCredentials( sessionTransferToken: "token", issuedTokenType: "type", - expiresIn: Date(), + expiresAt: Date(), idToken: testIdToken, refreshToken: nil ) -class SpyAuthentication: Authentication { +class SpyAuthentication: Authentication, @unchecked Sendable { let clientId = "" let url = mockURL - var telemetry = Telemetry() + var auth0ClientInfo = Auth0ClientInfo() var logger: Logger? var sender: String = "auth0-flutter" var dpop: DPoP? @@ -25,11 +25,9 @@ class SpyAuthentication: Authentication { var ssoCredentialsResult: AuthenticationResult = .success(mockSSOCredentials) var challengeResult: AuthenticationResult = .success(mockChallenge) var databaseUserResult: AuthenticationResult = .success(mockDatabaseUser) - var userInfoResult: AuthenticationResult = .success(mockUserInfo) + var userInfoResult: AuthenticationResult = .success(mockUserInfo) var voidResult: AuthenticationResult = .success(()) var calledLoginWithUsernameOrEmail = false - var calledLoginWithOTP = false - var calledMultifactorChallenge = false var calledSignup = false var calledUserInfo = false var calledRenew = false @@ -60,35 +58,14 @@ class SpyAuthentication: Authentication { password: String, realmOrConnection realm: String, audience: String?, - scope: String) -> Request { + scope: String) -> any TokenRequestable { arguments["usernameOrEmail"] = username arguments["password"] = password arguments["connectionOrRealm"] = realm arguments["audience"] = audience arguments["scope"] = scope calledLoginWithUsernameOrEmail = true - return request(credentialsResult) - } - - func login(withOTP otp: String, mfaToken: String) -> Request { - arguments["otp"] = otp - arguments["mfaToken"] = mfaToken - calledLoginWithOTP = true - return request(credentialsResult) - } - - func login(withRecoveryCode recoveryCode: String, mfaToken: String) -> Request { - return request(credentialsResult) - } - - func multifactorChallenge(mfaToken: String, - types: [String]?, - authenticatorId: String?) -> Request { - arguments["mfaToken"] = mfaToken - arguments["types"] = types - arguments["authenticatorId"] = authenticatorId - calledMultifactorChallenge = true - return request(challengeResult) + return tokenRequest(credentialsResult) } func signup(email: String, @@ -96,7 +73,7 @@ class SpyAuthentication: Authentication { password: String, connection: String, userMetadata: [String: Any]?, - rootAttributes: [String: Any]?) -> Request { + rootAttributes: [String: Any]?) -> any Requestable { arguments["email"] = email arguments["username"] = username arguments["password"] = password @@ -107,14 +84,14 @@ class SpyAuthentication: Authentication { return request(databaseUserResult) } - func resetPassword(email: String, connection: String) -> Request { + func resetPassword(email: String, connection: String) -> any Requestable { arguments["email"] = email arguments["connection"] = connection calledResetPassword = true return request(voidResult) } - func userInfo(withAccessToken accessToken: String, tokenType: String) -> Request { + func userInfo(withAccessToken accessToken: String, tokenType: String) -> any Requestable { arguments["accessToken"] = accessToken arguments["tokenType"] = tokenType calledUserInfo = true @@ -123,21 +100,21 @@ class SpyAuthentication: Authentication { func codeExchange(withCode code: String, codeVerifier: String, - redirectURI: String) -> Request { - return request(credentialsResult) + redirectURI: String) -> any TokenRequestable { + return tokenRequest(credentialsResult) } - func renew(withRefreshToken refreshToken: String, audience: String?, scope: String?) -> Request { + func renew(withRefreshToken refreshToken: String, audience: String?, scope: String?) -> any TokenRequestable { arguments["refreshToken"] = refreshToken arguments["scope"] = scope arguments["audience"] = audience calledRenew = true - return request(credentialsResult) + return tokenRequest(credentialsResult) } - func ssoExchange(withRefreshToken refreshToken: String) -> Request { + func ssoExchange(withRefreshToken refreshToken: String) -> any TokenRequestable { arguments["refreshToken"] = refreshToken - return request(ssoCredentialsResult) + return tokenRequest(ssoCredentialsResult) } var calledCustomTokenExchange = false @@ -147,7 +124,7 @@ class SpyAuthentication: Authentication { audience: String?, scope: String, organization: String?, - parameters: [String: Any]) -> Request { + parameters: [String: Any]) -> any TokenRequestable { arguments["subjectToken"] = subjectToken arguments["subjectTokenType"] = subjectTokenType arguments["audience"] = audience @@ -156,21 +133,21 @@ class SpyAuthentication: Authentication { arguments["actor_token"] = parameters["actor_token"] arguments["actor_token_type"] = parameters["actor_token_type"] calledCustomTokenExchange = true - return request(credentialsResult) + return tokenRequest(credentialsResult) } - func revoke(refreshToken: String) -> Request { + func revoke(refreshToken: String) -> any Requestable { return request(voidResult) } - func jwks() -> Request { + func jwks() -> any Requestable { return request(.success(JWKS(keys: []))) } #if PASSKEYS_PLATFORM @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) func passkeyLoginChallenge(connection: String?, - organization: String?) -> Request { + organization: String?) -> any Requestable { arguments["connection"] = connection arguments["organization"] = organization calledPasskeyLoginChallenge = true @@ -184,13 +161,13 @@ class SpyAuthentication: Authentication { connection: String?, audience: String?, scope: String, - organization: String?) -> Request { + organization: String?) -> any TokenRequestable { arguments["connection"] = connection arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization calledLoginWithPasskey = true - return request(credentialsResult) + return tokenRequest(credentialsResult) } @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) @@ -204,7 +181,7 @@ class SpyAuthentication: Authentication { picture: String?, userMetadata: [String: String]?, connection: String?, - organization: String?) -> Request { + organization: String?) -> any Requestable { arguments["email"] = email arguments["phoneNumber"] = phoneNumber arguments["username"] = username @@ -227,13 +204,13 @@ class SpyAuthentication: Authentication { connection: String?, audience: String?, scope: String, - organization: String?) -> Request { + organization: String?) -> any TokenRequestable { arguments["connection"] = connection arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization calledSignupWithPasskey = true - return request(credentialsResult) + return tokenRequest(credentialsResult) } #endif } @@ -279,12 +256,11 @@ extension SpyAuthentication { #endif private extension SpyAuthentication { - func request(_ result: AuthenticationResult) -> Request { - Request(session: mockURLSession, - url: url, - method: "", - handle: {_, callback in callback(result)}, - logger: nil, - telemetry: telemetry) + func request(_ result: AuthenticationResult) -> any Requestable { + MockRequest(result: result) + } + + func tokenRequest(_ result: AuthenticationResult) -> any TokenRequestable { + MockTokenRequest(result: result) } } diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIUserInfoMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIUserInfoMethodHandlerTests.swift index 6e84611d5..7fd0272d8 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIUserInfoMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIUserInfoMethodHandlerTests.swift @@ -78,7 +78,7 @@ extension AuthAPIUserInfoMethodHandlerTests { UserInfoProperty.customClaims.rawValue: ["foo": "bar"], UserInfoProperty.act.rawValue: ["sub": "actor-sub"] ] - let userInfo = UserInfo(json: data)! + let userInfo = UserProfile(json: data)! let expectation = self.expectation(description: "Produced a user profile") spy.userInfoResult = .success(userInfo) sut.handle(with: arguments()) { result in diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerApiCredentialsMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerApiCredentialsMethodHandlerTests.swift index 9ec3ce7a2..0acf0d12e 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerApiCredentialsMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerApiCredentialsMethodHandlerTests.swift @@ -1,6 +1,12 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter fileprivate typealias Argument = ApiCredentialsMethodHandler.Argument @@ -42,7 +48,7 @@ extension ApiCredentialsMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "scope") let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "API credentials exchange was called") @@ -62,7 +68,7 @@ extension ApiCredentialsMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "a b") let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "Produced API credentials") @@ -84,11 +90,13 @@ extension ApiCredentialsMethodHandlerTests { extension ApiCredentialsMethodHandlerTests { func testProducesCredentialsManagerError() { - let error = CredentialsManagerError.noCredentials - let expectation = self.expectation(description: "Produced the CredentialsManagerError \(error)") + let expectation = self.expectation(description: "Produced a CredentialsManagerError") spyStorage.getEntryReturnValue = nil sut.handle(with: arguments()) { result in - assert(result: result, isError: error) + guard let flutterError = result as? FlutterError else { + return XCTFail("The handler did not produce a FlutterError") + } + XCTAssertTrue(flutterError.message?.contains("SpyCredentialsStorageError") == true) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearApiCredentialsMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearApiCredentialsMethodHandlerTests.swift index 927b2d444..26e33b398 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearApiCredentialsMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearApiCredentialsMethodHandlerTests.swift @@ -1,6 +1,12 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter fileprivate typealias Argument = ClearApiCredentialsMethodHandler.Argument @@ -37,21 +43,21 @@ extension ClearApiCredentialsMethodHandlerTests { XCTAssertTrue(spy.calledDeleteEntry) } - func testProducesTrueOnSuccess() { - let expectation = self.expectation(description: "Produced true") + func testProducesNilOnSuccess() { + let expectation = self.expectation(description: "Produced nil") spy.deleteEntryReturnValue = true sut.handle(with: arguments()) { result in - XCTAssertEqual(result as? Bool, true) + XCTAssertNil(result) expectation.fulfill() } wait(for: [expectation]) } - func testProducesFalseOnFailure() { - let expectation = self.expectation(description: "Produced false") + func testProducesFlutterErrorOnFailure() { + let expectation = self.expectation(description: "Produced a FlutterError") spy.deleteEntryReturnValue = false sut.handle(with: arguments()) { result in - XCTAssertEqual(result as? Bool, false) + XCTAssertTrue(result is FlutterError) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearMethodHandlerTests.swift index e6f99c9d7..bac3fa288 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearMethodHandlerTests.swift @@ -1,6 +1,12 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter class CredentialsManagerClearMethodHandlerTests: XCTestCase { @@ -32,11 +38,11 @@ extension CredentialsManagerClearMethodHandlerTests { wait(for: [expectation]) } - func testProducesFalseOnFailure() { - let expectation = self.expectation(description: "Produced false") + func testProducesFlutterErrorOnFailure() { + let expectation = self.expectation(description: "Produced a FlutterError") spy.deleteEntryReturnValue = false sut.handle(with: arguments()) { result in - XCTAssertEqual(result as? Bool, false) + XCTAssertTrue(result is FlutterError) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerGetMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerGetMethodHandlerTests.swift index 9891c4a9d..9c5ab2c4c 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerGetMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerGetMethodHandlerTests.swift @@ -1,6 +1,12 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter fileprivate typealias Argument = CredentialsManagerGetMethodHandler.Argument @@ -47,10 +53,10 @@ extension CredentialsManagerGetMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: -3600), + expiresAt: Date(timeIntervalSinceNow: -3600), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments(withKey: Argument.scopes, value: value)) { _ in XCTAssertEqual(self.spyAuthentication.arguments["scope"] as? String, value.asSpaceSeparatedString) expectation.fulfill() @@ -63,10 +69,10 @@ extension CredentialsManagerGetMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: -3600), + expiresAt: Date(timeIntervalSinceNow: -3600), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments(withKey: Argument.scopes, value: [])) { _ in XCTAssertNil(self.spyAuthentication.arguments["scope"] as? String) expectation.fulfill() @@ -92,10 +98,10 @@ extension CredentialsManagerGetMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { result in assert(result: result, has: CredentialsProperty.allCases) expectation.fulfill() @@ -104,11 +110,13 @@ extension CredentialsManagerGetMethodHandlerTests { } func testProducesCredentialsManagerError() { - let error = CredentialsManagerError.noCredentials - let expectation = self.expectation(description: "Produced the CredentialsManagerError \(error)") + let expectation = self.expectation(description: "Produced a CredentialsManagerError") spyStorage.getEntryReturnValue = nil sut.handle(with: arguments()) { result in - assert(result: result, isError: error) + guard let flutterError = result as? FlutterError else { + return XCTFail("The handler did not produce a FlutterError") + } + XCTAssertTrue(flutterError.message?.contains("SpyCredentialsStorageError") == true) expectation.fulfill() } wait(for: [expectation], timeout: 5.0) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerHasValidMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerHasValidMethodHandlerTests.swift index ce9b0d6bc..fd1228705 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerHasValidMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerHasValidMethodHandlerTests.swift @@ -40,9 +40,9 @@ extension CredentialsManagerHasValidMethodHandlerTests { } func testProducesTrueWithValidCredentialsAndNoRefreshToken() { - let credentials = Credentials(refreshToken: nil, expiresIn: Date(timeIntervalSinceNow: 3600)) + let credentials = Credentials(refreshToken: nil, expiresAt: Date(timeIntervalSinceNow: 3600)) let expectation = self.expectation(description: "Produced true") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { result in XCTAssertEqual(result as? Bool, true) expectation.fulfill() @@ -51,9 +51,9 @@ extension CredentialsManagerHasValidMethodHandlerTests { } func testProducesTrueWithRefreshTokenAndInvalidCredentials() { - let credentials = Credentials(refreshToken: "foo", expiresIn: Date(timeIntervalSinceNow: -3600)) + let credentials = Credentials(refreshToken: "foo", expiresAt: Date(timeIntervalSinceNow: -3600)) let expectation = self.expectation(description: "Produced true") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { result in XCTAssertEqual(result as? Bool, true) expectation.fulfill() @@ -62,9 +62,9 @@ extension CredentialsManagerHasValidMethodHandlerTests { } func testProducesTrueWithRefreshTokenAndValidCredentials() { - let credentials = Credentials(refreshToken: "foo", expiresIn: Date(timeIntervalSinceNow: 3600)) + let credentials = Credentials(refreshToken: "foo", expiresAt: Date(timeIntervalSinceNow: 3600)) let expectation = self.expectation(description: "Produced true") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { result in XCTAssertEqual(result as? Bool, true) expectation.fulfill() @@ -83,9 +83,9 @@ extension CredentialsManagerHasValidMethodHandlerTests { } func testProducesFalseWithInvalidCredentialsAndNoRefreshToken() { - let credentials = Credentials(refreshToken: nil, expiresIn: Date(timeIntervalSinceNow: -3600)) + let credentials = Credentials(refreshToken: nil, expiresAt: Date(timeIntervalSinceNow: -3600)) let expectation = self.expectation(description: "Produced false") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { result in XCTAssertEqual(result as? Bool, false) expectation.fulfill() diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerRenewMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerRenewMethodHandlerTests.swift index 349d60f42..5fa565501 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerRenewMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerRenewMethodHandlerTests.swift @@ -41,7 +41,7 @@ extension CredentialsManagerRenewMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(), + expiresAt: Date(), scope: "scope") let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "Produced credentials") @@ -58,7 +58,7 @@ extension CredentialsManagerRenewMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "scope") let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "Produced credentials") diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSSOCredentialsMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSSOCredentialsMethodHandlerTests.swift index 96062c72a..ca3b819a7 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSSOCredentialsMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSSOCredentialsMethodHandlerTests.swift @@ -1,6 +1,12 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter fileprivate typealias Argument = SSOCredentialsMethodHandler.Argument @@ -43,10 +49,10 @@ extension SSOCredentialsMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "scope") let expectation = self.expectation(description: "SSO exchange was called") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { _ in XCTAssertEqual(self.spyAuthentication.arguments["refreshToken"] as? String, "refreshToken") expectation.fulfill() @@ -59,10 +65,10 @@ extension SSOCredentialsMethodHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "scope") let expectation = self.expectation(description: "Produced SSO credentials") - _ = credentialsManager.store(credentials: credentials) + try? credentialsManager.store(credentials: credentials) sut.handle(with: arguments()) { result in let expectedKeys: [SSOCredentialsProperty] = [ .sessionTransferToken, .tokenType, .expiresIn, .idToken @@ -78,11 +84,13 @@ extension SSOCredentialsMethodHandlerTests { extension SSOCredentialsMethodHandlerTests { func testProducesCredentialsManagerError() { - let error = CredentialsManagerError.noCredentials - let expectation = self.expectation(description: "Produced the CredentialsManagerError \(error)") + let expectation = self.expectation(description: "Produced a CredentialsManagerError") spyStorage.getEntryReturnValue = nil sut.handle(with: arguments()) { result in - assert(result: result, isError: error) + guard let flutterError = result as? FlutterError else { + return XCTFail("The handler did not produce a FlutterError") + } + XCTAssertTrue(flutterError.message?.contains("SpyCredentialsStorageError") == true) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSaveMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSaveMethodHandlerTests.swift index cd6a9bb0e..59f5baf55 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSaveMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSaveMethodHandlerTests.swift @@ -1,6 +1,12 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter fileprivate typealias Argument = CredentialsManagerSaveMethodHandler.Argument @@ -48,11 +54,11 @@ extension CredentialsManagerSaveMethodHandlerTests { wait(for: [expectation]) } - func testProducesFalseOnFailure() { - let expectation = self.expectation(description: "Produced false") + func testProducesFlutterErrorOnFailure() { + let expectation = self.expectation(description: "Produced a FlutterError") spy.setEntryReturnValue = false sut.handle(with: arguments()) { result in - XCTAssertEqual(result as? Bool, false) + XCTAssertTrue(result is FlutterError) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift index d8cf84755..872e6c174 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift @@ -3,6 +3,8 @@ import Auth0 // MARK: - Auth0.swift Spies +struct SpyCredentialsStorageError: Error {} + class SpyCredentialsStorage: CredentialsStorage { var getEntryReturnValue: Data? var setEntryReturnValue = true @@ -12,19 +14,20 @@ class SpyCredentialsStorage: CredentialsStorage { var calledSetEntry = false var calledDeleteEntry = false - func getEntry(forKey key: String) -> Data? { + func getEntry(forKey key: String) throws -> Data { self.calledGetEntry = true - return self.getEntryReturnValue + guard let value = self.getEntryReturnValue else { throw SpyCredentialsStorageError() } + return value } - func setEntry(_ data: Data, forKey key: String) -> Bool { + func setEntry(_ data: Data, forKey key: String) throws { self.calledSetEntry = true self.getEntryReturnValue = data - return self.setEntryReturnValue + if !self.setEntryReturnValue { throw SpyCredentialsStorageError() } } - func deleteEntry(forKey key: String) -> Bool { + func deleteEntry(forKey key: String) throws { self.calledDeleteEntry = true - return self.deleteEntryReturnValue + if !self.deleteEntryReturnValue { throw SpyCredentialsStorageError() } } } diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerUserInfoMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerUserInfoMethodHandlerTests.swift index 224513281..7701090d1 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerUserInfoMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerUserInfoMethodHandlerTests.swift @@ -1,43 +1,41 @@ import XCTest import Auth0 +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + @testable import auth0_flutter class CredentialsManagerUserInfoMethodHandlerTests: XCTestCase { var spyAuthentication: SpyAuthentication! var spyStorage: SpyCredentialsStorage! + var credentialsManager: CredentialsManager! var sut: CredentialsManagerUserInfoMethodHandler! override func setUpWithError() throws { spyAuthentication = SpyAuthentication() spyStorage = SpyCredentialsStorage() - let credentialsManager = CredentialsManager(authentication: spyAuthentication, storage: spyStorage) + credentialsManager = CredentialsManager(authentication: spyAuthentication, storage: spyStorage) sut = CredentialsManagerUserInfoMethodHandler(credentialsManager: credentialsManager) } } -// MARK: - Method Name - -extension CredentialsManagerUserInfoMethodHandlerTests { - func testHandlesCorrectMethodName() { - XCTAssertTrue(sut.method == .userInfo) - } -} - // MARK: - UserInfo Result extension CredentialsManagerUserInfoMethodHandlerTests { - func testCallsCredentialsManagerUserInfoProperty() { + func testCallsCredentialsManagerUserProfileProperty() { let credentials = Credentials(accessToken: "accessToken", tokenType: "tokenType", - idToken: testIdToken, + idToken: testUserInfoIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "foo bar") - let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) - let expectation = self.expectation(description: "Called userInfo property") - spyStorage.getEntryReturnValue = data - + let expectation = self.expectation(description: "Called userProfile property") + try? credentialsManager.store(credentials: credentials) + sut.handle(with: [:]) { _ in XCTAssertTrue(self.spyStorage.calledGetEntry) expectation.fulfill() @@ -48,23 +46,18 @@ extension CredentialsManagerUserInfoMethodHandlerTests { func testReturnsUserInfoDictionaryWhenUserInfoExists() { let credentials = Credentials(accessToken: "accessToken", tokenType: "tokenType", - idToken: testIdToken, + idToken: testUserInfoIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "foo bar") - let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "Returned user info dictionary") - spyStorage.getEntryReturnValue = data - + try? credentialsManager.store(credentials: credentials) + sut.handle(with: [:]) { result in - guard case .success(let value) = result else { - return XCTFail("Expected success but got error") + guard let userInfo = result as? [String: Any] else { + return XCTFail("Expected dictionary but got \(String(describing: result))") } - - guard let userInfo = value as? [String: Any] else { - return XCTFail("Expected dictionary but got \(type(of: value))") - } - + XCTAssertEqual(userInfo["sub"] as? String, "auth0|user123") XCTAssertEqual(userInfo["name"] as? String, "John Doe") XCTAssertEqual(userInfo["email"] as? String, "john@example.com") @@ -74,41 +67,21 @@ extension CredentialsManagerUserInfoMethodHandlerTests { wait(for: [expectation], timeout: 5.0) } - func testReturnsNilWhenCredentialsHaveNoUserInfo() { - let expectation = self.expectation(description: "Returned nil") - spyStorage.getEntryReturnValue = nil - - sut.handle(with: [:]) { result in - guard case .success(let value) = result else { - return XCTFail("Expected success but got error") - } - - XCTAssertNil(value) - expectation.fulfill() - } - wait(for: [expectation], timeout: 5.0) - } - func testConvertsAllUserInfoFieldsToDictionaryCorrectly() { let credentials = Credentials(accessToken: "accessToken", tokenType: "tokenType", - idToken: testIdToken, + idToken: testUserInfoIdToken, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "foo bar") - let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "Converted all fields correctly") - spyStorage.getEntryReturnValue = data - + try? credentialsManager.store(credentials: credentials) + sut.handle(with: [:]) { result in - guard case .success(let value) = result else { - return XCTFail("Expected success but got error") - } - - guard let userInfo = value as? [String: Any] else { + guard let userInfo = result as? [String: Any] else { return XCTFail("Expected dictionary") } - + XCTAssertNotNil(userInfo["sub"]) XCTAssertNotNil(userInfo["name"]) XCTAssertNotNil(userInfo["email"]) @@ -122,45 +95,35 @@ extension CredentialsManagerUserInfoMethodHandlerTests { func testHandlesUserInfoWithCustomClaims() { let credentials = Credentials(accessToken: "accessToken", tokenType: "tokenType", - idToken: testIdTokenWithCustomClaims, + idToken: testUserInfoIdTokenWithCustomClaims, refreshToken: "refreshToken", - expiresIn: Date(timeIntervalSinceNow: 3600), + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "foo bar") - let data = try? NSKeyedArchiver.archivedData(withRootObject: credentials, requiringSecureCoding: true) let expectation = self.expectation(description: "Handled custom claims") - spyStorage.getEntryReturnValue = data - + try? credentialsManager.store(credentials: credentials) + sut.handle(with: [:]) { result in - guard case .success(let value) = result else { - return XCTFail("Expected success but got error") - } - - guard let userInfo = value as? [String: Any] else { + guard let userInfo = result as? [String: Any] else { return XCTFail("Expected dictionary") } - + XCTAssertNotNil(userInfo["sub"]) - + if let customClaims = userInfo["custom_claims"] as? [String: Any] { XCTAssertNotNil(customClaims) } - + expectation.fulfill() } wait(for: [expectation], timeout: 5.0) } - func testProducesErrorWhenGetFails() { - let expectedError = CredentialsManagerError.noCredentials - let expectation = self.expectation(description: "Produced error") - spyStorage.getEntryError = expectedError - + func testProducesFlutterErrorWhenNoCredentialsAreStored() { + let expectation = self.expectation(description: "Produced a FlutterError") + spyStorage.getEntryReturnValue = nil + sut.handle(with: [:]) { result in - guard case .failure(let error) = result else { - return XCTFail("Expected error but got success") - } - - XCTAssertTrue(error is CredentialsManagerError) + XCTAssertTrue(result is FlutterError) expectation.fulfill() } wait(for: [expectation], timeout: 5.0) @@ -169,6 +132,6 @@ extension CredentialsManagerUserInfoMethodHandlerTests { // MARK: - Test Helpers -private let testIdToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHx1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwibmlja25hbWUiOiJqb2huZG9lIiwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcGljdHVyZS5qcGciLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +private let testUserInfoIdToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHx1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwibmlja25hbWUiOiJqb2huZG9lIiwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcGljdHVyZS5qcGciLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" -private let testIdTokenWithCustomClaims = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHx1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiZGVwYXJ0bWVudCI6ImVuZ2luZWVyaW5nIiwiaWF0IjoxNTE2MjM5MDIyfQ.1234567890" +private let testUserInfoIdTokenWithCustomClaims = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhdXRoMHx1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiZGVwYXJ0bWVudCI6ImVuZ2luZWVyaW5nIiwiaWF0IjoxNTE2MjM5MDIyfQ.1234567890" diff --git a/auth0_flutter/example/ios/Tests/ExtensionsTests.swift b/auth0_flutter/example/ios/Tests/ExtensionsTests.swift index cdc7b6a3e..a8077b075 100644 --- a/auth0_flutter/example/ios/Tests/ExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/ExtensionsTests.swift @@ -58,7 +58,7 @@ extension ExtensionsTests { let credentials = try XCTUnwrap(Credentials(from: values)) XCTAssertEqual(values[CredentialsProperty.accessToken] as? String, credentials.accessToken) XCTAssertEqual(values[CredentialsProperty.idToken] as? String, credentials.idToken) - XCTAssertEqual(values[CredentialsProperty.expiresAt] as? String, credentials.expiresIn.asISO8601String) + XCTAssertEqual(values[CredentialsProperty.expiresAt] as? String, credentials.expiresAt.asISO8601String) XCTAssertEqual(values[CredentialsProperty.tokenType] as? String, credentials.tokenType) XCTAssertNil(credentials.scope) } @@ -116,11 +116,11 @@ extension ExtensionsTests { let credentials = Credentials(accessToken: "accessToken", tokenType: "tokenType", idToken: idToken, - expiresIn: Date()) + expiresAt: Date()) let values = try credentials.asDictionary() XCTAssertEqual(credentials.accessToken, values[CredentialsProperty.accessToken] as? String) XCTAssertEqual(credentials.idToken, values[CredentialsProperty.idToken] as? String) - XCTAssertEqual(credentials.expiresIn.asISO8601String, values[CredentialsProperty.expiresAt] as? String) + XCTAssertEqual(credentials.expiresAt.asISO8601String, values[CredentialsProperty.expiresAt] as? String) XCTAssertEqual(credentials.tokenType, values[CredentialsProperty.tokenType] as? String) XCTAssertTrue([] == values[CredentialsProperty.scopes] as? [String]) XCTAssertTrue([:] == values[CredentialsProperty.userProfile] as? [String: Any]) @@ -131,7 +131,7 @@ extension ExtensionsTests { tokenType: "", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date()) + expiresAt: Date()) let values = try credentials.asDictionary() XCTAssertNotNil(credentials.refreshToken) XCTAssertEqual(credentials.refreshToken, values[CredentialsProperty.refreshToken] as? String) @@ -141,7 +141,7 @@ extension ExtensionsTests { let credentials = Credentials(accessToken: "", tokenType: "", idToken: testIdToken, - expiresIn: Date(), + expiresAt: Date(), scope: "foo bar") let values = try credentials.asDictionary() XCTAssertNotNil(credentials.scope) @@ -150,16 +150,16 @@ extension ExtensionsTests { } func testMapsUserProfileToDictionary() throws { - let credentials = Credentials(accessToken: "", tokenType: "", idToken: testIdToken, expiresIn: Date()) + let credentials = Credentials(accessToken: "", tokenType: "", idToken: testIdToken, expiresAt: Date()) let values = try credentials.asDictionary() let jwt = try decode(jwt: credentials.idToken) - let userProfile = try XCTUnwrap(UserInfo(json: jwt.body)?.asDictionary()) + let userProfile = try XCTUnwrap(UserProfile(json: jwt.body)?.asDictionary()) XCTAssertTrue(userProfile == values[CredentialsProperty.userProfile] as? [String: Any]) } func testFailsToMapCredentialsWithMalformedIdTokenToDictionary() { let idToken = "foo" - let credentials = Credentials(accessToken: "", tokenType: "", idToken: idToken, expiresIn: Date()) + let credentials = Credentials(accessToken: "", tokenType: "", idToken: idToken, expiresAt: Date()) XCTAssertThrowsError(try credentials.asDictionary(), HandlerError.idTokenDecodingFailed.message) } } @@ -168,7 +168,7 @@ extension ExtensionsTests { extension ExtensionsTests { func testMapsRequiredUserInfoProperties() throws { - let userInfo = try XCTUnwrap(UserInfo(json: [UserInfoProperty.sub.rawValue: "foo"])) + let userInfo = try XCTUnwrap(UserProfile(json: [UserInfoProperty.sub.rawValue: "foo"])) let values = userInfo.asDictionary() XCTAssertEqual(userInfo.sub, values[UserInfoProperty.sub] as? String) XCTAssertTrue(values[UserInfoProperty.customClaims] as? [String: Any] == [:]) @@ -179,7 +179,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.name.rawValue: "name" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.name) XCTAssertEqual(userInfo.name, values[UserInfoProperty.name] as? String) @@ -190,7 +190,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.givenName.rawValue: "givenName" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.givenName) XCTAssertEqual(userInfo.givenName, values[UserInfoProperty.givenName] as? String) @@ -201,7 +201,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.familyName.rawValue: "familyName" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.familyName) XCTAssertEqual(userInfo.familyName, values[UserInfoProperty.familyName] as? String) @@ -212,7 +212,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.middleName.rawValue: "middleName" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.middleName) XCTAssertEqual(userInfo.middleName, values[UserInfoProperty.middleName] as? String) @@ -223,7 +223,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.nickname.rawValue: "nickname" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.nickname) XCTAssertEqual(userInfo.nickname, values[UserInfoProperty.nickname] as? String) @@ -234,7 +234,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.preferredUsername.rawValue: "preferredUsername" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.preferredUsername) XCTAssertEqual(userInfo.preferredUsername, values[UserInfoProperty.preferredUsername] as? String) @@ -245,7 +245,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.profile.rawValue: "https://example.com/profile" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.profile) XCTAssertEqual(userInfo.profile?.absoluteString, values[UserInfoProperty.profile] as? String) @@ -256,7 +256,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.picture.rawValue: "https://example.com/picture" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.picture) XCTAssertEqual(userInfo.picture?.absoluteString, values[UserInfoProperty.picture] as? String) @@ -267,7 +267,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.website.rawValue: "https://example.com/website" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.website) XCTAssertEqual(userInfo.website?.absoluteString, values[UserInfoProperty.website] as? String) @@ -278,7 +278,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.email.rawValue: "email" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.email) XCTAssertEqual(userInfo.email, values[UserInfoProperty.email] as? String) @@ -289,7 +289,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.emailVerified.rawValue: true ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.emailVerified) XCTAssertEqual(userInfo.emailVerified, values[UserInfoProperty.emailVerified] as? Bool) @@ -300,7 +300,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.gender.rawValue: "gender" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.gender) XCTAssertEqual(userInfo.gender, values[UserInfoProperty.gender] as? String) @@ -311,7 +311,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.birthdate.rawValue: "birthdate" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.birthdate) XCTAssertEqual(userInfo.birthdate, values[UserInfoProperty.birthdate] as? String) @@ -322,7 +322,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.zoneinfo.rawValue: "America/Los_Angeles" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.zoneinfo) XCTAssertEqual(userInfo.zoneinfo?.identifier, values[UserInfoProperty.zoneinfo] as? String) @@ -333,7 +333,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.locale.rawValue: "en_US" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.locale) XCTAssertEqual(userInfo.locale?.identifier, values[UserInfoProperty.locale] as? String) @@ -344,7 +344,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.phoneNumber.rawValue: "phoneNumber" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.phoneNumber) XCTAssertEqual(userInfo.phoneNumber, values[UserInfoProperty.phoneNumber] as? String) @@ -355,7 +355,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.phoneNumberVerified.rawValue: true ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.phoneNumberVerified) XCTAssertEqual(userInfo.phoneNumberVerified, values[UserInfoProperty.phoneNumberVerified] as? Bool) @@ -366,7 +366,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.address.rawValue: ["foo": "bar"] as [String: String] ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.address) XCTAssertEqual(userInfo.address, values[UserInfoProperty.address] as? [String: String]) @@ -377,7 +377,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.updatedAt.rawValue: "2022-04-15T03:15:51.787Z" ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.updatedAt) XCTAssertEqual(userInfo.updatedAt?.asISO8601String, values[UserInfoProperty.updatedAt] as? String) @@ -388,7 +388,7 @@ extension ExtensionsTests { UserInfoProperty.sub.rawValue: "", UserInfoProperty.customClaims.rawValue: ["foo": "bar"] ] - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertNotNil(userInfo.customClaims) XCTAssertTrue(userInfo.customClaims == values[UserInfoProperty.customClaims] as? [String: Any]) @@ -411,7 +411,7 @@ extension ExtensionsTests { "c_hash": "cHash" ] let data = customClaims.merging(claims) { (first, _) in first } - let userInfo = try XCTUnwrap(UserInfo(json: data)) + let userInfo = try XCTUnwrap(UserProfile(json: data)) let values = userInfo.asDictionary() XCTAssertTrue(values[UserInfoProperty.customClaims] as? [String: Any] == customClaims) } diff --git a/auth0_flutter/example/ios/Tests/Mfa/MfaGetAuthenticatorsMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/Mfa/MfaGetAuthenticatorsMethodHandlerTests.swift index ca5724822..e80a872da 100644 --- a/auth0_flutter/example/ios/Tests/Mfa/MfaGetAuthenticatorsMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/Mfa/MfaGetAuthenticatorsMethodHandlerTests.swift @@ -50,10 +50,19 @@ class MfaGetAuthenticatorsMethodHandlerTests: XCTestCase { func testProducesAuthenticatorListOnSuccess() { let expectation = self.expectation(description: "Produced list") - spy.getAuthenticatorsResult = .success([ - Authenticator(authenticatorType: "oob", oobChannel: "sms", id: "sms|dev_1", - name: "+1******90", active: true, type: "sms") - ]) + let json = """ + { + "authenticator_type": "oob", + "oob_channel": "sms", + "id": "sms|dev_1", + "name": "+1******90", + "active": true, + "type": "sms" + } + """.data(using: .utf8)! + // swiftlint:disable:next force_try + let authenticator = try! JSONDecoder().decode(Authenticator.self, from: json) + spy.getAuthenticatorsResult = .success([authenticator]) sut.handle(with: ["mfaToken": "mfa-token"]) { result in guard let list = result as? [[String: Any?]] else { return XCTFail("Did not produce a list") diff --git a/auth0_flutter/example/ios/Tests/Mfa/MfaSpies.swift b/auth0_flutter/example/ios/Tests/Mfa/MfaSpies.swift index 88116140b..344582cc6 100644 --- a/auth0_flutter/example/ios/Tests/Mfa/MfaSpies.swift +++ b/auth0_flutter/example/ios/Tests/Mfa/MfaSpies.swift @@ -8,9 +8,9 @@ import FlutterMacOS // MARK: - Spy MFAClient -class SpyMFAClient: MFAClient { +class SpyMFAClient: MFAClient, @unchecked Sendable { var dpop: DPoP? - var telemetry = Telemetry() + var auth0ClientInfo = Auth0ClientInfo() var logger: Logger? // MARK: Stubbed results @@ -40,7 +40,7 @@ class SpyMFAClient: MFAClient { var verifyResult: Result = .success( Credentials(accessToken: "access-token", tokenType: "Bearer", idToken: testIdToken, refreshToken: "refresh-token", - expiresIn: Date(timeIntervalSinceNow: 3600), scope: "openid") + expiresAt: Date(timeIntervalSinceNow: 3600), scope: "openid") ) // MARK: Spied args @@ -63,83 +63,59 @@ class SpyMFAClient: MFAClient { var verifyRecoveryCodeArg: String? func getAuthenticators(mfaToken: String, - factorsAllowed: [String]) -> Request<[Authenticator], MfaListAuthenticatorsError> { + factorsAllowed: [String]) -> any Requestable<[Authenticator], MfaListAuthenticatorsError> { calledGetAuthenticators = true getAuthenticatorsMfaTokenArg = mfaToken getAuthenticatorsFactorsArg = factorsAllowed - return request(getAuthenticatorsResult) + return MockRequest(result: getAuthenticatorsResult) } - func enroll(mfaToken: String, phoneNumber: String) -> Request { + func enroll(mfaToken: String, phoneNumber: String) -> any Requestable { calledEnrollPhone = true enrollPhoneNumberArg = phoneNumber - return request(enrollPhoneResult) + return MockRequest(result: enrollPhoneResult) } - func enroll(mfaToken: String, email: String) -> Request { + func enroll(mfaToken: String, email: String) -> any Requestable { calledEnrollEmail = true enrollEmailArg = email - return request(enrollEmailResult) + return MockRequest(result: enrollEmailResult) } - func enroll(mfaToken: String) -> Request { + func enroll(mfaToken: String) -> any Requestable { calledEnrollTotp = true - return request(enrollTotpResult) + return MockRequest(result: enrollTotpResult) } - func enroll(mfaToken: String) -> Request { + func enroll(mfaToken: String) -> any Requestable { calledEnrollPush = true - return request(enrollPushResult) + return MockRequest(result: enrollPushResult) } func challenge(with authenticatorId: String, - mfaToken: String) -> Request { + mfaToken: String) -> any Requestable { calledChallenge = true challengeAuthenticatorIdArg = authenticatorId - return request(challengeResult) + return MockRequest(result: challengeResult) } func verify(oobCode: String, bindingCode: String?, - mfaToken: String) -> Request { + mfaToken: String) -> any TokenRequestable { calledVerify = true verifyOobCodeArg = oobCode verifyBindingCodeArg = bindingCode - return request(verifyResult) + return MockTokenRequest(result: verifyResult) } - func verify(otp: String, mfaToken: String) -> Request { + func verify(otp: String, mfaToken: String) -> any TokenRequestable { calledVerify = true verifyOtpArg = otp - return request(verifyResult) + return MockTokenRequest(result: verifyResult) } - func verify(recoveryCode: String, mfaToken: String) -> Request { + func verify(recoveryCode: String, mfaToken: String) -> any TokenRequestable { calledVerify = true verifyRecoveryCodeArg = recoveryCode - return request(verifyResult) - } -} - -// MARK: - Request Helper - -private extension SpyMFAClient { - func request(_ result: Result) -> Request { - Request(session: mockURLSession, url: mockURL, method: "", - handle: { _, callback in callback(result) }, logger: nil, telemetry: telemetry) - } - - func request(_ result: Result) -> Request { - Request(session: mockURLSession, url: mockURL, method: "", - handle: { _, callback in callback(result) }, logger: nil, telemetry: telemetry) - } - - func request(_ result: Result) -> Request { - Request(session: mockURLSession, url: mockURL, method: "", - handle: { _, callback in callback(result) }, logger: nil, telemetry: telemetry) - } - - func request(_ result: Result) -> Request { - Request(session: mockURLSession, url: mockURL, method: "", - handle: { _, callback in callback(result) }, logger: nil, telemetry: telemetry) + return MockTokenRequest(result: verifyResult) } } diff --git a/auth0_flutter/example/ios/Tests/Mocks.swift b/auth0_flutter/example/ios/Tests/Mocks.swift index 736ef2396..a3074d70c 100644 --- a/auth0_flutter/example/ios/Tests/Mocks.swift +++ b/auth0_flutter/example/ios/Tests/Mocks.swift @@ -40,6 +40,37 @@ class MockURLProtocol: URLProtocol { // MARK: - Auth0.swift Mocks +struct MockRequest: Requestable { + let result: Result + + func parameters(_ extraParameters: [String: Any]) -> any Requestable { self } + func headers(_ extraHeaders: [String: String]) -> any Requestable { self } + func requestValidators(_ extraValidators: [RequestValidator]) -> any Requestable { self } + + func start(_ callback: @escaping @MainActor (Result) -> Void) { + Task { @MainActor in callback(result) } + } +} + +struct MockTokenRequest: TokenRequestable { + let result: Result + + func validateClaims() -> any TokenRequestable { self } + func withLeeway(_ leeway: Int) -> any TokenRequestable { self } + func withIssuer(_ issuer: String) -> any TokenRequestable { self } + func withNonce(_ nonce: String?) -> any TokenRequestable { self } + func withMaxAge(_ maxAge: Int?) -> any TokenRequestable { self } + func withOrganization(_ organization: String?) -> any TokenRequestable { self } + + func parameters(_ extraParameters: [String: Any]) -> any Requestable { self } + func headers(_ extraHeaders: [String: String]) -> any Requestable { self } + func requestValidators(_ extraValidators: [RequestValidator]) -> any Requestable { self } + + func start(_ callback: @escaping @MainActor (Result) -> Void) { + Task { @MainActor in callback(result) } + } +} + struct MockError: Error {} struct MockAuth0Error: Auth0Error { diff --git a/auth0_flutter/example/ios/Tests/MyAccount/MyAccountSpies.swift b/auth0_flutter/example/ios/Tests/MyAccount/MyAccountSpies.swift index c432c3eae..09788b7ff 100644 --- a/auth0_flutter/example/ios/Tests/MyAccount/MyAccountSpies.swift +++ b/auth0_flutter/example/ios/Tests/MyAccount/MyAccountSpies.swift @@ -23,7 +23,7 @@ private func makeAuthMethod(id: String = "test-id", type: String = "phone", class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { var url: URL { mockURL } var token: String { "test-token" } - var telemetry = Telemetry() + var auth0ClientInfo = Auth0ClientInfo() var logger: Logger? var dpop: DPoP? @@ -31,6 +31,20 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { var getAuthMethodResult: Result = .success(makeAuthMethod()) var deleteResult: Result = .success(()) var getFactorsResult: Result<[Factor], MyAccountError> = .success([]) + var enrollPasswordResult: Result = .success( + PasswordEnrollmentChallenge( + authenticationId: "password|test", authenticationSession: "session123", + policy: PasswordPolicy( + complexity: PasswordComplexity(minLength: 8, characterTypes: nil, characterTypeRule: nil, + identicalCharacters: nil, sequentialCharacters: nil, + maxLengthExceeded: nil), + profileData: PasswordProfileData(active: nil, blockedFields: nil), + history: PasswordHistory(active: nil, size: nil), + dictionary: PasswordDictionary(active: nil, default: nil) + ) + ) + ) + var confirmPasswordResult: Result = .success(makeAuthMethod()) var enrollPhoneResult: Result = .success( PhoneEnrollmentChallenge(authenticationId: "phone|test", authenticationSession: "session123") ) @@ -88,7 +102,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { var updateNameArg: String? var updatePreferredArg: PreferredAuthenticationMethod? - func getAuthenticationMethods(type: AuthenticationMethodType?) -> Request<[AuthenticationMethod], MyAccountError> { + func getAuthenticationMethods(type: AuthenticationMethodType?) -> any Requestable<[AuthenticationMethod], MyAccountError> { calledGetAuthMethods = true getAuthMethodsTypeArg = type return request(getAuthMethodsResult) @@ -96,7 +110,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { func updateAuthenticationMethod(by id: String, name: String?, - preferredAuthenticationMethod: PreferredAuthenticationMethod?) -> Request { + preferredAuthenticationMethod: PreferredAuthenticationMethod?) -> any Requestable { calledUpdate = true updateIdArg = id updateNameArg = name @@ -104,54 +118,54 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { return request(updateResult) } - func getAuthenticationMethod(by id: String) -> Request { + func getAuthenticationMethod(by id: String) -> any Requestable { calledGetAuthMethod = true getAuthMethodIdArg = id return request(getAuthMethodResult) } - func deleteAuthenticationMethod(by id: String) -> Request { + func deleteAuthenticationMethod(by id: String) -> any Requestable { calledDelete = true deleteIdArg = id return request(deleteResult) } - func getFactors() -> Request<[Factor], MyAccountError> { + func getFactors() -> any Requestable<[Factor], MyAccountError> { calledGetFactors = true return request(getFactorsResult) } func enrollPhone(phoneNumber: String, - preferredAuthenticationMethod: PreferredAuthenticationMethod?) -> Request { + preferredAuthenticationMethod: PreferredAuthenticationMethod?) -> any Requestable { calledEnrollPhone = true enrollPhoneNumberArg = phoneNumber enrollPhoneMethodArg = preferredAuthenticationMethod return request(enrollPhoneResult) } - func enrollEmail(emailAddress: String) -> Request { + func enrollEmail(emailAddress: String) -> any Requestable { calledEnrollEmail = true enrollEmailArg = emailAddress return request(enrollEmailResult) } - func enrollTOTP() -> Request { + func enrollTOTP() -> any Requestable { calledEnrollTOTP = true return request(enrollTOTPResult) } - func enrollPushNotification() -> Request { + func enrollPushNotification() -> any Requestable { calledEnrollPush = true return request(enrollPushResult) } - func enrollRecoveryCode() -> Request { + func enrollRecoveryCode() -> any Requestable { calledEnrollRecovery = true return request(enrollRecoveryResult) } func confirmPhoneEnrollment(id: String, authSession: String, - otpCode: String) -> Request { + otpCode: String) -> any Requestable { calledConfirm = true confirmIdArg = id confirmAuthSessionArg = authSession @@ -162,7 +176,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) func passkeyEnrollmentChallenge(userIdentityId: String?, - connection: String?) -> Request { + connection: String?) -> any Requestable { calledEnrollPasskeyChallenge = true enrollPasskeyChallengeUserIdentityIdArg = userIdentityId enrollPasskeyChallengeConnectionArg = connection @@ -182,7 +196,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) func enroll(passkey: NewPasskey, - challenge: PasskeyEnrollmentChallenge) -> Request { + challenge: PasskeyEnrollmentChallenge) -> any Requestable { calledEnrollPasskey = true if enrollPasskeyShouldFail { return request(.failure(MyAccountError(info: [:], statusCode: 401))) @@ -208,7 +222,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { } func confirmTOTPEnrollment(id: String, authSession: String, - otpCode: String) -> Request { + otpCode: String) -> any Requestable { calledConfirm = true confirmIdArg = id confirmAuthSessionArg = authSession @@ -218,7 +232,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { } func confirmEmailEnrollment(id: String, authSession: String, - otpCode: String) -> Request { + otpCode: String) -> any Requestable { calledConfirm = true confirmIdArg = id confirmAuthSessionArg = authSession @@ -228,7 +242,7 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { } func confirmPushNotificationEnrollment(id: String, - authSession: String) -> Request { + authSession: String) -> any Requestable { calledConfirm = true confirmIdArg = id confirmAuthSessionArg = authSession @@ -237,13 +251,27 @@ class SpyMyAccountAuthenticationMethods: MyAccountAuthenticationMethods { } func confirmRecoveryCodeEnrollment(id: String, - authSession: String) -> Request { + authSession: String) -> any Requestable { calledConfirm = true confirmIdArg = id confirmAuthSessionArg = authSession confirmEnrollmentFactorType = "recovery-code" return request(confirmResult) } + + func enrollPassword(userIdentityId: String?, + connection: String?) -> any Requestable { + return request(enrollPasswordResult) + } + + func confirmPasswordEnrollment(id: String, authSession: String, + newPassword: String) -> any Requestable { + calledConfirm = true + confirmIdArg = id + confirmAuthSessionArg = authSession + confirmEnrollmentFactorType = "password" + return request(confirmPasswordResult) + } } // MARK: - Spy MyAccount @@ -252,7 +280,7 @@ class SpyMyAccount: MyAccount { static var apiVersion: String { "v1" } var url: URL { mockURL } var token: String { "test-token" } - var telemetry = Telemetry() + var auth0ClientInfo = Auth0ClientInfo() var logger: Logger? var dpop: DPoP? @@ -267,12 +295,7 @@ class SpyMyAccount: MyAccount { // MARK: - Request Helper private extension SpyMyAccountAuthenticationMethods { - func request(_ result: Result) -> Request { - Request(session: mockURLSession, - url: url, - method: "", - handle: { _, callback in callback(result) }, - logger: nil, - telemetry: telemetry) + func request(_ result: Result) -> any Requestable { + MockRequest(result: result) } } diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift index f051792f0..2379a6037 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift @@ -14,10 +14,9 @@ class WebAuthExtensionsTests: XCTestCase { let errors: [String: WebAuthError] = [ "USER_CANCELLED": .userCancelled, "ID_TOKEN_VALIDATION_FAILED": .idTokenValidationFailed, - "INVALID_INVITATION_URL": .invalidInvitationURL, - "NO_AUTHORIZATION_CODE": .noAuthorizationCode, - "NO_BUNDLE_IDENTIFIER": .noBundleIdentifier, - "PKCE_NOT_ALLOWED": .pkceNotAllowed, + "AUTHENTICATION_FAILED": .authenticationFailed, + "CODE_EXCHANGE_FAILED": .codeExchangeFailed, + "CREDENTIALS_MANAGER_ERROR": .credentialsManagerError, "OTHER": .other, "UNKNOWN": .unknown, "TRANSACTION_ACTIVE_ALREADY": .transactionActiveAlready @@ -31,10 +30,9 @@ class WebAuthExtensionsTests: XCTestCase { func testIsRetryableIsFalseForNonNetworkErrors() { let nonRetryableErrors: [WebAuthError] = [ .userCancelled, - .noBundleIdentifier, - .invalidInvitationURL, - .noAuthorizationCode, - .pkceNotAllowed, + .authenticationFailed, + .codeExchangeFailed, + .credentialsManagerError, .idTokenValidationFailed, .transactionActiveAlready, .other diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthLoginMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthLoginMethodHandlerTests.swift index b46693c0f..b85dd573c 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthLoginMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthLoginMethodHandlerTests.swift @@ -248,7 +248,7 @@ extension WebAuthLoginHandlerTests { tokenType: "tokenType", idToken: testIdToken, refreshToken: "refreshToken", - expiresIn: Date(), + expiresAt: Date(), scope: "foo bar") let expectation = self.expectation(description: "Produced credentials") spy.loginResult = .success(credentials) diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthSpies.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthSpies.swift index aec504638..bb985ce70 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthSpies.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthSpies.swift @@ -22,13 +22,14 @@ class SpyMethodHandler: MethodHandler { // MARK: - Auth0.swift Spies -class SpyWebAuth: WebAuth { +class SpyWebAuth: WebAuth, @unchecked Sendable { let clientId = "" let url = mockURL - var telemetry = Telemetry() + var auth0ClientInfo = Auth0ClientInfo() var logger: Logger? var sender: String = "auth0-flutter" var dpop: DPoP? + var credentialsManager: CredentialsManager? var loginResult: WebAuthResult = .success(Credentials()) var logoutResult: WebAuthResult = .success(()) @@ -138,9 +139,18 @@ class SpyWebAuth: WebAuth { return self } - func start(_ callback: @escaping (WebAuthResult) -> Void) { + func useCredentialsManager(_ credentialsManager: CredentialsManager) -> Self { + self.credentialsManager = credentialsManager + return self + } + + func presentationWindow(_ window: Auth0WindowRepresentable) -> Self { + return self + } + + func start(_ callback: @escaping @MainActor @Sendable (WebAuthResult) -> Void) { calledLogin = true - callback(loginResult) + Task { @MainActor in callback(loginResult) } } #if canImport(_Concurrency) @@ -153,9 +163,9 @@ class SpyWebAuth: WebAuth { return loginResult.publisher.eraseToAnyPublisher() } - func clearSession(federated: Bool, callback: @escaping (WebAuthResult) -> Void) { + func logout(federated: Bool, callback: @escaping @MainActor @Sendable (WebAuthResult) -> Void) { calledLogout = true - callback(logoutResult) + Task { @MainActor in callback(logoutResult) } } } diff --git a/auth0_flutter/example/lib/example_app.dart b/auth0_flutter/example/lib/example_app.dart index 24f4aec72..b0668e3dd 100644 --- a/auth0_flutter/example/lib/example_app.dart +++ b/auth0_flutter/example/lib/example_app.dart @@ -135,6 +135,7 @@ class _ExampleAppState extends State { Future apiLogin( final String usernameOrEmail, final String password) async { String output; + bool loggedIn = _isLoggedIn; // Platform messages may fail, so we use a try/catch PlatformException. // We also handle the message potentially returning null. try { @@ -143,6 +144,7 @@ class _ExampleAppState extends State { password: password, connectionOrRealm: 'Username-Password-Authentication'); output = result.accessToken; + loggedIn = true; } on ApiException catch (e) { output = e.toString(); } @@ -153,7 +155,7 @@ class _ExampleAppState extends State { if (!mounted) return; setState(() { - _isLoggedIn = true; + _isLoggedIn = loggedIn; _output = output; }); } diff --git a/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj b/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj index 6be5e4170..7bbb964fc 100644 --- a/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj +++ b/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -22,11 +22,15 @@ /* Begin PBXBuildFile section */ 0103B7EA1132AC95205851B1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42E90A8B7E56466926079940 /* Pods_Runner.framework */; }; + 0C75953F96D8E77E4C7B7EC2 /* MfaGetAuthenticatorsMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6AB04FC0914386E5B1E000 /* MfaGetAuthenticatorsMethodHandlerTests.swift */; }; + 230B751173F29F72609AEAAF /* MfaEnrollPhoneMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8EC4581DA9CDBF5E66D649D /* MfaEnrollPhoneMethodHandlerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 3EFA8C500CFDF41A4421430B /* MfaChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C0DBC4235A122430FFFC3F /* MfaChallengeMethodHandlerTests.swift */; }; + 4646F940A76C9DA727A35D3D /* MfaVerifyMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0132AB790E0F7757F096E84C /* MfaVerifyMethodHandlerTests.swift */; }; 5C1399232B0EB5F3006DCE54 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399052B0EB5F3006DCE54 /* Utilities.swift */; }; 5C1399242B0EB5F3006DCE54 /* ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399062B0EB5F3006DCE54 /* ExtensionsTests.swift */; }; 5C1399252B0EB5F3006DCE54 /* ModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399072B0EB5F3006DCE54 /* ModelsTests.swift */; }; @@ -60,6 +64,7 @@ A09B2E3F1D4C7891BA000011 /* CredentialsManagerSSOCredentialsMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A09B2E3F1D4C7891BA000010 /* CredentialsManagerSSOCredentialsMethodHandlerTests.swift */; }; A0AC1D0F2E0000000000AC12 /* CredentialsManagerApiCredentialsMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0AC1D0F2E0000000000AC11 /* CredentialsManagerApiCredentialsMethodHandlerTests.swift */; }; A0AC1D0F2E0000000000AC14 /* CredentialsManagerClearApiCredentialsMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0AC1D0F2E0000000000AC13 /* CredentialsManagerClearApiCredentialsMethodHandlerTests.swift */; }; + A16E9B5BD4E8E3A7B92138DF /* MfaEnrollPushMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 677FE527B291A2EEE6E0F75E /* MfaEnrollPushMethodHandlerTests.swift */; }; B0CA00020000000000000101 /* MyAccountSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CA00010000000000000101 /* MyAccountSpies.swift */; }; B0CA00020000000000000102 /* MyAccountDeleteAuthMethodMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CA00010000000000000102 /* MyAccountDeleteAuthMethodMethodHandlerTests.swift */; }; B0CA00020000000000000103 /* MyAccountEnrollEmailMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CA00010000000000000103 /* MyAccountEnrollEmailMethodHandlerTests.swift */; }; @@ -76,6 +81,9 @@ B0CA0002000000000000010E /* MyAccountEnrollPasskeyChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CA0001000000000000010E /* MyAccountEnrollPasskeyChallengeMethodHandlerTests.swift */; }; B0CA0002000000000000010F /* MyAccountEnrollPasskeyMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0CA0001000000000000010F /* MyAccountEnrollPasskeyMethodHandlerTests.swift */; }; B1C2D3E42AECB46A00862999 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = B1C2D3E32AECB46A00862999 /* FlutterGeneratedPluginSwiftPackage */; }; + D561774718B03798052F3295 /* MfaEnrollTotpMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BEC4ED803DD1A0DB3CF551 /* MfaEnrollTotpMethodHandlerTests.swift */; }; + DA07934AAABB82E3583DAB40 /* MfaEnrollEmailMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6285C02FB11AC817D7543FBF /* MfaEnrollEmailMethodHandlerTests.swift */; }; + F6BD703AC548F115602A46DF /* MfaSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41D6ADFF4B2604E459CBBC /* MfaSpies.swift */; }; PK00000000000000000101 /* AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PK00000000000000000103 /* AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift */; }; PK00000000000000000105 /* AuthAPIPasskeyExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PK00000000000000000106 /* AuthAPIPasskeyExtensionsTests.swift */; }; PK10000000000000000101 /* AuthAPIPasskeySignupChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = PK10000000000000000103 /* AuthAPIPasskeySignupChallengeMethodHandlerTests.swift */; }; @@ -120,6 +128,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0132AB790E0F7757F096E84C /* MfaVerifyMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaVerifyMethodHandlerTests.swift; sourceTree = ""; }; + 0A6AB04FC0914386E5B1E000 /* MfaGetAuthenticatorsMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaGetAuthenticatorsMethodHandlerTests.swift; sourceTree = ""; }; + 0C41D6ADFF4B2604E459CBBC /* MfaSpies.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaSpies.swift; sourceTree = ""; }; 0D39FAC562F0E6F84339AE58 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 21DBF4BE5C5B1183C69F87D1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 30C90C251A85CFCAF7F218D9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; @@ -169,6 +180,9 @@ 5CD8A5552B1179EE00313321 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5CD8A5612B117A9900313321 /* SmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokeTests.swift; sourceTree = ""; }; 5D0A3A77C308FD33805CA99C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6285C02FB11AC817D7543FBF /* MfaEnrollEmailMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaEnrollEmailMethodHandlerTests.swift; sourceTree = ""; }; + 63BEC4ED803DD1A0DB3CF551 /* MfaEnrollTotpMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaEnrollTotpMethodHandlerTests.swift; sourceTree = ""; }; + 677FE527B291A2EEE6E0F75E /* MfaEnrollPushMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaEnrollPushMethodHandlerTests.swift; sourceTree = ""; }; 6FD903E99CB6C89165AACEDF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 784666492D4C4C64000A1A5F /* FlutterFramework */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterFramework; path = ephemeral/Packages/.packages/FlutterFramework; sourceTree = ""; }; 78DABEA22ED26510000E7860 /* auth0_flutter */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = auth0_flutter; path = ../../../darwin/auth0_flutter; sourceTree = ""; }; @@ -179,6 +193,7 @@ A09B2E3F1D4C7891BA000010 /* CredentialsManagerSSOCredentialsMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsManagerSSOCredentialsMethodHandlerTests.swift; sourceTree = ""; }; A0AC1D0F2E0000000000AC11 /* CredentialsManagerApiCredentialsMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsManagerApiCredentialsMethodHandlerTests.swift; sourceTree = ""; }; A0AC1D0F2E0000000000AC13 /* CredentialsManagerClearApiCredentialsMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsManagerClearApiCredentialsMethodHandlerTests.swift; sourceTree = ""; }; + A8EC4581DA9CDBF5E66D649D /* MfaEnrollPhoneMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaEnrollPhoneMethodHandlerTests.swift; sourceTree = ""; }; B0CA00010000000000000101 /* MyAccountSpies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountSpies.swift; sourceTree = ""; }; B0CA00010000000000000102 /* MyAccountDeleteAuthMethodMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountDeleteAuthMethodMethodHandlerTests.swift; sourceTree = ""; }; B0CA00010000000000000103 /* MyAccountEnrollEmailMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountEnrollEmailMethodHandlerTests.swift; sourceTree = ""; }; @@ -194,6 +209,7 @@ B0CA0001000000000000010D /* MyAccountUpdateAuthMethodMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountUpdateAuthMethodMethodHandlerTests.swift; sourceTree = ""; }; B0CA0001000000000000010E /* MyAccountEnrollPasskeyChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountEnrollPasskeyChallengeMethodHandlerTests.swift; sourceTree = ""; }; B0CA0001000000000000010F /* MyAccountEnrollPasskeyMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAccountEnrollPasskeyMethodHandlerTests.swift; sourceTree = ""; }; + B9C0DBC4235A122430FFFC3F /* MfaChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MfaChallengeMethodHandlerTests.swift; sourceTree = ""; }; FF584AA0512B18AACE2C3C4C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; PK00000000000000000103 /* AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthAPIPasskeyLoginChallengeMethodHandlerTests.swift; sourceTree = ""; }; PK00000000000000000106 /* AuthAPIPasskeyExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthAPIPasskeyExtensionsTests.swift; sourceTree = ""; }; @@ -314,6 +330,7 @@ 5C13991A2B0EB5F3006DCE54 /* Auth0FlutterPluginTests.swift */, 5C13991B2B0EB5F3006DCE54 /* CredentialsManager */, B0CA000300000000000001A1 /* MyAccount */, + 8100C791C3EC5F990BD4210D /* Mfa */, ); name = Tests; path = ../ios/Tests; @@ -377,6 +394,22 @@ path = UITests; sourceTree = ""; }; + 8100C791C3EC5F990BD4210D /* Mfa */ = { + isa = PBXGroup; + children = ( + 0C41D6ADFF4B2604E459CBBC /* MfaSpies.swift */, + 0A6AB04FC0914386E5B1E000 /* MfaGetAuthenticatorsMethodHandlerTests.swift */, + 63BEC4ED803DD1A0DB3CF551 /* MfaEnrollTotpMethodHandlerTests.swift */, + A8EC4581DA9CDBF5E66D649D /* MfaEnrollPhoneMethodHandlerTests.swift */, + 6285C02FB11AC817D7543FBF /* MfaEnrollEmailMethodHandlerTests.swift */, + 677FE527B291A2EEE6E0F75E /* MfaEnrollPushMethodHandlerTests.swift */, + B9C0DBC4235A122430FFFC3F /* MfaChallengeMethodHandlerTests.swift */, + 0132AB790E0F7757F096E84C /* MfaVerifyMethodHandlerTests.swift */, + ); + name = Mfa; + path = Mfa; + sourceTree = ""; + }; 8CE186BA3D2E1C4481596FB8 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -711,6 +744,14 @@ B0CA0002000000000000010D /* MyAccountUpdateAuthMethodMethodHandlerTests.swift in Sources */, B0CA0002000000000000010E /* MyAccountEnrollPasskeyChallengeMethodHandlerTests.swift in Sources */, B0CA0002000000000000010F /* MyAccountEnrollPasskeyMethodHandlerTests.swift in Sources */, + F6BD703AC548F115602A46DF /* MfaSpies.swift in Sources */, + 0C75953F96D8E77E4C7B7EC2 /* MfaGetAuthenticatorsMethodHandlerTests.swift in Sources */, + D561774718B03798052F3295 /* MfaEnrollTotpMethodHandlerTests.swift in Sources */, + 230B751173F29F72609AEAAF /* MfaEnrollPhoneMethodHandlerTests.swift in Sources */, + DA07934AAABB82E3583DAB40 /* MfaEnrollEmailMethodHandlerTests.swift in Sources */, + A16E9B5BD4E8E3A7B92138DF /* MfaEnrollPushMethodHandlerTests.swift in Sources */, + 3EFA8C500CFDF41A4421430B /* MfaChallengeMethodHandlerTests.swift in Sources */, + 4646F940A76C9DA727A35D3D /* MfaVerifyMethodHandlerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/auth0_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/auth0_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 6a183a3ce..000000000 --- a/auth0_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,32 +0,0 @@ -{ - "pins" : [ - { - "identity" : "auth0.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/Auth0.swift", - "state" : { - "revision" : "8fa0ccce3920717843ed4dd95839bf4cbf22c662", - "version" : "2.23.0" - } - }, - { - "identity" : "jwtdecode.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/JWTDecode.swift", - "state" : { - "revision" : "36a5ce735a61c4bc119593f43ce2c027b4ca7392", - "version" : "3.3.0" - } - }, - { - "identity" : "simplekeychain", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/SimpleKeychain", - "state" : { - "revision" : "776c4a6db74d5c6c143974be91c383680d468630", - "version" : "1.3.0" - } - } - ], - "version" : 2 -} diff --git a/auth0_flutter/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/auth0_flutter/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 6a183a3ce..000000000 --- a/auth0_flutter/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,32 +0,0 @@ -{ - "pins" : [ - { - "identity" : "auth0.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/Auth0.swift", - "state" : { - "revision" : "8fa0ccce3920717843ed4dd95839bf4cbf22c662", - "version" : "2.23.0" - } - }, - { - "identity" : "jwtdecode.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/JWTDecode.swift", - "state" : { - "revision" : "36a5ce735a61c4bc119593f43ce2c027b4ca7392", - "version" : "3.3.0" - } - }, - { - "identity" : "simplekeychain", - "kind" : "remoteSourceControl", - "location" : "https://github.com/auth0/SimpleKeychain", - "state" : { - "revision" : "776c4a6db74d5c6c143974be91c383680d468630", - "version" : "1.3.0" - } - } - ], - "version" : 2 -} diff --git a/auth0_flutter/ios/auth0_flutter.podspec b/auth0_flutter/ios/auth0_flutter.podspec index ae5f056cd..f7f1cfce3 100644 --- a/auth0_flutter/ios/auth0_flutter.podspec +++ b/auth0_flutter/ios/auth0_flutter.podspec @@ -19,8 +19,8 @@ Pod::Spec.new do |s| s.osx.deployment_target = '11.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '2.23.0' - s.dependency 'JWTDecode', '3.3.0' + s.dependency 'Auth0', '3.0.2' + s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' # Flutter.framework does not contain a i386 slice. diff --git a/auth0_flutter/macos/auth0_flutter.podspec b/auth0_flutter/macos/auth0_flutter.podspec index ae5f056cd..f7f1cfce3 100644 --- a/auth0_flutter/macos/auth0_flutter.podspec +++ b/auth0_flutter/macos/auth0_flutter.podspec @@ -19,8 +19,8 @@ Pod::Spec.new do |s| s.osx.deployment_target = '11.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '2.23.0' - s.dependency 'JWTDecode', '3.3.0' + s.dependency 'Auth0', '3.0.2' + s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' # Flutter.framework does not contain a i386 slice. diff --git a/auth0_flutter/test/desktop/windows_web_authentication_test.mocks.dart b/auth0_flutter/test/desktop/windows_web_authentication_test.mocks.dart index 2cce2f71f..4fa8c0507 100644 --- a/auth0_flutter/test/desktop/windows_web_authentication_test.mocks.dart +++ b/auth0_flutter/test/desktop/windows_web_authentication_test.mocks.dart @@ -25,6 +25,7 @@ import 'windows_web_authentication_test.dart' as _i3; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeCredentials_0 extends _i1.SmartFake implements _i2.Credentials { _FakeCredentials_0( @@ -44,6 +45,12 @@ class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform { _i1.throwOnMissingStub(this); } + @override + _i4.Stream<_i2.Credentials> get onCredentialsRecovered => (super.noSuchMethod( + Invocation.getter(#onCredentialsRecovered), + returnValue: _i4.Stream<_i2.Credentials>.empty(), + ) as _i4.Stream<_i2.Credentials>); + @override _i4.Future<_i2.Credentials> login( _i5.WebAuthRequest<_i5.WebAuthLoginOptions>? request) => diff --git a/auth0_flutter/test/mobile/authentication_api_test.dart b/auth0_flutter/test/mobile/authentication_api_test.dart index 7098d873c..bb3652904 100644 --- a/auth0_flutter/test/mobile/authentication_api_test.dart +++ b/auth0_flutter/test/mobile/authentication_api_test.dart @@ -234,7 +234,6 @@ void main() { .api .multifactorChallenge( mfaToken: 'test-mfa-token', - types: [ChallengeType.otp, ChallengeType.oob], authenticatorId: 'test-authenticatorId'); final verificationResult = @@ -244,26 +243,10 @@ void main() { expect(verificationResult.account.domain, 'test-domain'); expect(verificationResult.account.clientId, 'test-clientId'); expect(verificationResult.options.mfaToken, 'test-mfa-token'); - expect(verificationResult.options.types, - [ChallengeType.otp, ChallengeType.oob]); expect( verificationResult.options.authenticatorId, 'test-authenticatorId'); expect(result, TestPlatform.multifactorChallengeResult); }); - - test('set parameters to default value when omitted', () async { - when(mockedPlatform.multifactorChallenge(any)).thenAnswer( - (final _) async => TestPlatform.multifactorChallengeResult); - - await Auth0('', '').api.multifactorChallenge(mfaToken: ''); - - final verificationResult = - verify(mockedPlatform.multifactorChallenge(captureAny)) - .captured - .single as ApiRequest; - expect(verificationResult.options.types, isNull); - expect(verificationResult.options.authenticatorId, isNull); - }); }); group('resetPassword', () { diff --git a/auth0_flutter/test/mobile/authentication_api_test.mocks.dart b/auth0_flutter/test/mobile/authentication_api_test.mocks.dart index 911d0cf41..d0cb967b4 100644 --- a/auth0_flutter/test/mobile/authentication_api_test.mocks.dart +++ b/auth0_flutter/test/mobile/authentication_api_test.mocks.dart @@ -68,7 +68,7 @@ class _FakeDatabaseUser_3 extends _i1.SmartFake implements _i2.DatabaseUser { } class _FakeSSOCredentials_4 extends _i1.SmartFake - implements _i3.SSOCredentials { + implements _i2.SSOCredentials { _FakeSSOCredentials_4( Object parent, Invocation parentInvocation, @@ -79,7 +79,7 @@ class _FakeSSOCredentials_4 extends _i1.SmartFake } class _FakePasskeyChallenge_5 extends _i1.SmartFake - implements _i3.PasskeyChallenge { + implements _i2.PasskeyChallenge { _FakePasskeyChallenge_5( Object parent, Invocation parentInvocation, @@ -89,6 +89,17 @@ class _FakePasskeyChallenge_5 extends _i1.SmartFake ); } +class _FakePasswordlessChallenge_6 extends _i1.SmartFake + implements _i2.PasswordlessChallenge { + _FakePasswordlessChallenge_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [TestPlatform]. /// /// See the documentation for Mockito's code generation for more information. @@ -275,21 +286,21 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { ) as _i5.Future<_i2.Credentials>); @override - _i5.Future<_i3.SSOCredentials> ssoExchange( + _i5.Future<_i2.SSOCredentials> ssoExchange( _i3.ApiRequest<_i3.AuthSSOExchangeOptions>? request) => (super.noSuchMethod( Invocation.method( #ssoExchange, [request], ), - returnValue: _i5.Future<_i3.SSOCredentials>.value(_FakeSSOCredentials_4( + returnValue: _i5.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_4( this, Invocation.method( #ssoExchange, [request], ), )), - ) as _i5.Future<_i3.SSOCredentials>); + ) as _i5.Future<_i2.SSOCredentials>); @override _i5.Future resetPassword( @@ -304,7 +315,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { ) as _i5.Future); @override - _i5.Future<_i3.PasskeyChallenge> passkeyLoginChallenge( + _i5.Future<_i2.PasskeyChallenge> passkeyLoginChallenge( _i3.ApiRequest<_i3.AuthPasskeyLoginChallengeOptions>? request) => (super.noSuchMethod( Invocation.method( @@ -312,17 +323,17 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { [request], ), returnValue: - _i5.Future<_i3.PasskeyChallenge>.value(_FakePasskeyChallenge_5( + _i5.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_5( this, Invocation.method( #passkeyLoginChallenge, [request], ), )), - ) as _i5.Future<_i3.PasskeyChallenge>); + ) as _i5.Future<_i2.PasskeyChallenge>); @override - _i5.Future<_i3.PasskeyChallenge> passkeySignupChallenge( + _i5.Future<_i2.PasskeyChallenge> passkeySignupChallenge( _i3.ApiRequest<_i3.AuthPasskeySignupChallengeOptions>? request) => (super.noSuchMethod( Invocation.method( @@ -330,14 +341,14 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { [request], ), returnValue: - _i5.Future<_i3.PasskeyChallenge>.value(_FakePasskeyChallenge_5( + _i5.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_5( this, Invocation.method( #passkeySignupChallenge, [request], ), )), - ) as _i5.Future<_i3.PasskeyChallenge>); + ) as _i5.Future<_i2.PasskeyChallenge>); @override _i5.Future<_i2.Credentials> passkeyCredentialExchange( @@ -355,4 +366,57 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { ), )), ) as _i5.Future<_i2.Credentials>); + + @override + _i5.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithEmail( + _i3.ApiRequest<_i3.AuthPasswordlessChallengeEmailOptions>? request) => + (super.noSuchMethod( + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + returnValue: _i5.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_6( + this, + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + )), + ) as _i5.Future<_i2.PasswordlessChallenge>); + + @override + _i5.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithPhoneNumber( + _i3.ApiRequest<_i3.AuthPasswordlessChallengePhoneOptions>? request) => + (super.noSuchMethod( + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + returnValue: _i5.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_6( + this, + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + )), + ) as _i5.Future<_i2.PasswordlessChallenge>); + + @override + _i5.Future<_i2.Credentials> passwordlessLoginWithOtp( + _i3.ApiRequest<_i3.AuthPasswordlessLoginWithOtpOptions>? request) => + (super.noSuchMethod( + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); } diff --git a/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart b/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart index 5f097268f..ded98d805 100644 --- a/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart +++ b/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart @@ -3,14 +3,14 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'package:auth0_flutter/auth0_flutter.dart' as _i2; import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart' - as _i3; + as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'credentials_manager_test.dart' as _i4; +import 'credentials_manager_test.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -38,7 +38,7 @@ class _FakeCredentials_0 extends _i1.SmartFake implements _i2.Credentials { } class _FakeSSOCredentials_1 extends _i1.SmartFake - implements _i3.SSOCredentials { + implements _i2.SSOCredentials { _FakeSSOCredentials_1( Object parent, Invocation parentInvocation, @@ -49,7 +49,7 @@ class _FakeSSOCredentials_1 extends _i1.SmartFake } class _FakeApiCredentials_2 extends _i1.SmartFake - implements _i3.ApiCredentials { + implements _i2.ApiCredentials { _FakeApiCredentials_2( Object parent, Invocation parentInvocation, @@ -62,137 +62,137 @@ class _FakeApiCredentials_2 extends _i1.SmartFake /// A class which mocks [TestPlatform]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { +class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform { MockTestPlatform() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.Credentials> getCredentials( - _i3.CredentialsManagerRequest<_i3.GetCredentialsOptions>? request) => + _i4.Future<_i2.Credentials> getCredentials( + _i5.CredentialsManagerRequest<_i5.GetCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #getCredentials, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #getCredentials, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.Credentials> renewCredentials( - _i3.CredentialsManagerRequest<_i3.RenewCredentialsOptions>? + _i4.Future<_i2.Credentials> renewCredentials( + _i5.CredentialsManagerRequest<_i5.RenewCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #renewCredentials, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #renewCredentials, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.UserProfile?> user( - _i3.CredentialsManagerRequest<_i3.RequestOptions?>? request) => + _i4.Future<_i2.UserProfile?> user( + _i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) => (super.noSuchMethod( Invocation.method( #user, [request], ), - returnValue: _i5.Future<_i2.UserProfile?>.value(), - ) as _i5.Future<_i2.UserProfile?>); + returnValue: _i4.Future<_i2.UserProfile?>.value(), + ) as _i4.Future<_i2.UserProfile?>); @override - _i5.Future clearCredentials( - _i3.CredentialsManagerRequest<_i3.RequestOptions?>? request) => + _i4.Future clearCredentials( + _i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) => (super.noSuchMethod( Invocation.method( #clearCredentials, [request], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future saveCredentials( - _i3.CredentialsManagerRequest<_i3.SaveCredentialsOptions>? request) => + _i4.Future saveCredentials( + _i5.CredentialsManagerRequest<_i5.SaveCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #saveCredentials, [request], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future hasValidCredentials( - _i3.CredentialsManagerRequest<_i3.HasValidCredentialsOptions>? + _i4.Future hasValidCredentials( + _i5.CredentialsManagerRequest<_i5.HasValidCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #hasValidCredentials, [request], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future<_i3.SSOCredentials> getSSOCredentials( - _i3.CredentialsManagerRequest<_i3.GetSSOCredentialsOptions>? + _i4.Future<_i2.SSOCredentials> getSSOCredentials( + _i5.CredentialsManagerRequest<_i5.GetSSOCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #getSSOCredentials, [request], ), - returnValue: _i5.Future<_i3.SSOCredentials>.value(_FakeSSOCredentials_1( + returnValue: _i4.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_1( this, Invocation.method( #getSSOCredentials, [request], ), )), - ) as _i5.Future<_i3.SSOCredentials>); + ) as _i4.Future<_i2.SSOCredentials>); @override - _i5.Future<_i3.ApiCredentials> getApiCredentials( - _i3.CredentialsManagerRequest<_i3.GetApiCredentialsOptions>? + _i4.Future<_i2.ApiCredentials> getApiCredentials( + _i5.CredentialsManagerRequest<_i5.GetApiCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #getApiCredentials, [request], ), - returnValue: _i5.Future<_i3.ApiCredentials>.value(_FakeApiCredentials_2( + returnValue: _i4.Future<_i2.ApiCredentials>.value(_FakeApiCredentials_2( this, Invocation.method( #getApiCredentials, [request], ), )), - ) as _i5.Future<_i3.ApiCredentials>); + ) as _i4.Future<_i2.ApiCredentials>); @override - _i5.Future clearApiCredentials( - _i3.CredentialsManagerRequest<_i3.ClearApiCredentialsOptions>? + _i4.Future clearApiCredentials( + _i5.CredentialsManagerRequest<_i5.ClearApiCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #clearApiCredentials, [request], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } diff --git a/auth0_flutter/test/mobile/mfa_api_test.mocks.dart b/auth0_flutter/test/mobile/mfa_api_test.mocks.dart index 32dae23b6..0da33480a 100644 --- a/auth0_flutter/test/mobile/mfa_api_test.mocks.dart +++ b/auth0_flutter/test/mobile/mfa_api_test.mocks.dart @@ -1,5 +1,5 @@ -// Mocks generated by Mockito 5.4.5 from annotations -// in auth0_flutter/example/windows/flutter/ephemeral/.plugin_symlinks/auth0_flutter/test/mobile/mfa_api_test.dart. +// Mocks generated by Mockito 5.4.6 from annotations +// in auth0_flutter/test/mobile/mfa_api_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes @@ -25,21 +25,37 @@ import 'mfa_api_test.dart' as _i3; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeMfaEnrollmentChallenge_0 extends _i1.SmartFake implements _i2.MfaEnrollmentChallenge { - _FakeMfaEnrollmentChallenge_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeMfaEnrollmentChallenge_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeMfaChallenge_1 extends _i1.SmartFake implements _i2.MfaChallenge { - _FakeMfaChallenge_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeMfaChallenge_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeCredentials_2 extends _i1.SmartFake implements _i2.Credentials { - _FakeCredentials_2(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeCredentials_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [TestPlatform]. @@ -52,100 +68,119 @@ class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform { @override _i4.Future> getAuthenticators( - _i5.ApiRequest<_i5.MfaGetAuthenticatorsOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaGetAuthenticatorsOptions>? request) => (super.noSuchMethod( - Invocation.method(#getAuthenticators, [request]), - returnValue: _i4.Future>.value( - <_i2.MfaAuthenticator>[], - ), - ) - as _i4.Future>); + Invocation.method( + #getAuthenticators, + [request], + ), + returnValue: _i4.Future>.value( + <_i2.MfaAuthenticator>[]), + ) as _i4.Future>); @override _i4.Future<_i2.MfaEnrollmentChallenge> enrollTotp( - _i5.ApiRequest<_i5.MfaEnrollTotpOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaEnrollTotpOptions>? request) => (super.noSuchMethod( - Invocation.method(#enrollTotp, [request]), - returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( - _FakeMfaEnrollmentChallenge_0( - this, - Invocation.method(#enrollTotp, [request]), - ), - ), - ) - as _i4.Future<_i2.MfaEnrollmentChallenge>); + Invocation.method( + #enrollTotp, + [request], + ), + returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( + _FakeMfaEnrollmentChallenge_0( + this, + Invocation.method( + #enrollTotp, + [request], + ), + )), + ) as _i4.Future<_i2.MfaEnrollmentChallenge>); @override _i4.Future<_i2.MfaEnrollmentChallenge> enrollPhone( - _i5.ApiRequest<_i5.MfaEnrollPhoneOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaEnrollPhoneOptions>? request) => (super.noSuchMethod( - Invocation.method(#enrollPhone, [request]), - returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( - _FakeMfaEnrollmentChallenge_0( - this, - Invocation.method(#enrollPhone, [request]), - ), - ), - ) - as _i4.Future<_i2.MfaEnrollmentChallenge>); + Invocation.method( + #enrollPhone, + [request], + ), + returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( + _FakeMfaEnrollmentChallenge_0( + this, + Invocation.method( + #enrollPhone, + [request], + ), + )), + ) as _i4.Future<_i2.MfaEnrollmentChallenge>); @override _i4.Future<_i2.MfaEnrollmentChallenge> enrollEmail( - _i5.ApiRequest<_i5.MfaEnrollEmailOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaEnrollEmailOptions>? request) => (super.noSuchMethod( - Invocation.method(#enrollEmail, [request]), - returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( - _FakeMfaEnrollmentChallenge_0( - this, - Invocation.method(#enrollEmail, [request]), - ), - ), - ) - as _i4.Future<_i2.MfaEnrollmentChallenge>); + Invocation.method( + #enrollEmail, + [request], + ), + returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( + _FakeMfaEnrollmentChallenge_0( + this, + Invocation.method( + #enrollEmail, + [request], + ), + )), + ) as _i4.Future<_i2.MfaEnrollmentChallenge>); @override _i4.Future<_i2.MfaEnrollmentChallenge> enrollPush( - _i5.ApiRequest<_i5.MfaEnrollPushOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaEnrollPushOptions>? request) => (super.noSuchMethod( - Invocation.method(#enrollPush, [request]), - returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( - _FakeMfaEnrollmentChallenge_0( - this, - Invocation.method(#enrollPush, [request]), - ), - ), - ) - as _i4.Future<_i2.MfaEnrollmentChallenge>); + Invocation.method( + #enrollPush, + [request], + ), + returnValue: _i4.Future<_i2.MfaEnrollmentChallenge>.value( + _FakeMfaEnrollmentChallenge_0( + this, + Invocation.method( + #enrollPush, + [request], + ), + )), + ) as _i4.Future<_i2.MfaEnrollmentChallenge>); @override _i4.Future<_i2.MfaChallenge> challenge( - _i5.ApiRequest<_i5.MfaChallengeOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaChallengeOptions>? request) => (super.noSuchMethod( - Invocation.method(#challenge, [request]), - returnValue: _i4.Future<_i2.MfaChallenge>.value( - _FakeMfaChallenge_1( - this, - Invocation.method(#challenge, [request]), - ), - ), - ) - as _i4.Future<_i2.MfaChallenge>); + Invocation.method( + #challenge, + [request], + ), + returnValue: _i4.Future<_i2.MfaChallenge>.value(_FakeMfaChallenge_1( + this, + Invocation.method( + #challenge, + [request], + ), + )), + ) as _i4.Future<_i2.MfaChallenge>); @override _i4.Future<_i2.Credentials> verify( - _i5.ApiRequest<_i5.MfaVerifyOptions>? request, - ) => + _i5.ApiRequest<_i5.MfaVerifyOptions>? request) => (super.noSuchMethod( - Invocation.method(#verify, [request]), - returnValue: _i4.Future<_i2.Credentials>.value( - _FakeCredentials_2(this, Invocation.method(#verify, [request])), - ), - ) - as _i4.Future<_i2.Credentials>); + Invocation.method( + #verify, + [request], + ), + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_2( + this, + Invocation.method( + #verify, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); } diff --git a/auth0_flutter/test/mobile/passwordless_api_test.mocks.dart b/auth0_flutter/test/mobile/passwordless_api_test.mocks.dart index 2349841a5..55a202d13 100644 --- a/auth0_flutter/test/mobile/passwordless_api_test.mocks.dart +++ b/auth0_flutter/test/mobile/passwordless_api_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in auth0_flutter/test/mobile/passwordless_api_test.dart. // Do not manually edit this file. @@ -25,43 +25,79 @@ import 'passwordless_api_test.dart' as _i4; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeCredentials_0 extends _i1.SmartFake implements _i2.Credentials { - _FakeCredentials_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeCredentials_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeChallenge_1 extends _i1.SmartFake implements _i3.Challenge { - _FakeChallenge_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeChallenge_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeUserProfile_2 extends _i1.SmartFake implements _i2.UserProfile { - _FakeUserProfile_2(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeUserProfile_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeDatabaseUser_3 extends _i1.SmartFake implements _i2.DatabaseUser { - _FakeDatabaseUser_3(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeDatabaseUser_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeSSOCredentials_4 extends _i1.SmartFake implements _i2.SSOCredentials { - _FakeSSOCredentials_4(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeSSOCredentials_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakePasskeyChallenge_5 extends _i1.SmartFake implements _i2.PasskeyChallenge { - _FakePasskeyChallenge_5(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePasskeyChallenge_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakePasswordlessChallenge_6 extends _i1.SmartFake implements _i2.PasswordlessChallenge { - _FakePasswordlessChallenge_6(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePasswordlessChallenge_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [TestPlatform]. @@ -74,264 +110,313 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { @override _i5.Future<_i2.Credentials> login( - _i3.ApiRequest<_i3.AuthLoginOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthLoginOptions>? request) => (super.noSuchMethod( - Invocation.method(#login, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0(this, Invocation.method(#login, [request])), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #login, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #login, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future<_i2.Credentials> loginWithOtp( - _i3.ApiRequest<_i3.AuthLoginWithOtpOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthLoginWithOtpOptions>? request) => (super.noSuchMethod( - Invocation.method(#loginWithOtp, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0( - this, - Invocation.method(#loginWithOtp, [request]), - ), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #loginWithOtp, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #loginWithOtp, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future<_i3.Challenge> multifactorChallenge( - _i3.ApiRequest<_i3.AuthMultifactorChallengeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthMultifactorChallengeOptions>? request) => (super.noSuchMethod( - Invocation.method(#multifactorChallenge, [request]), - returnValue: _i5.Future<_i3.Challenge>.value( - _FakeChallenge_1( - this, - Invocation.method(#multifactorChallenge, [request]), - ), - ), - ) - as _i5.Future<_i3.Challenge>); + Invocation.method( + #multifactorChallenge, + [request], + ), + returnValue: _i5.Future<_i3.Challenge>.value(_FakeChallenge_1( + this, + Invocation.method( + #multifactorChallenge, + [request], + ), + )), + ) as _i5.Future<_i3.Challenge>); @override _i5.Future startPasswordlessWithEmail( - _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request) => (super.noSuchMethod( - Invocation.method(#startPasswordlessWithEmail, [request]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #startPasswordlessWithEmail, + [request], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future<_i2.Credentials> loginWithEmailCode( - _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request) => (super.noSuchMethod( - Invocation.method(#loginWithEmailCode, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0( - this, - Invocation.method(#loginWithEmailCode, [request]), - ), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #loginWithEmailCode, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #loginWithEmailCode, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future startPasswordlessWithPhoneNumber( - _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request) => (super.noSuchMethod( - Invocation.method(#startPasswordlessWithPhoneNumber, [request]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #startPasswordlessWithPhoneNumber, + [request], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future<_i2.Credentials> loginWithSmsCode( - _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request) => (super.noSuchMethod( - Invocation.method(#loginWithSmsCode, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0( - this, - Invocation.method(#loginWithSmsCode, [request]), - ), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #loginWithSmsCode, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #loginWithSmsCode, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future<_i2.UserProfile> userInfo( - _i3.ApiRequest<_i3.AuthUserInfoOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthUserInfoOptions>? request) => (super.noSuchMethod( - Invocation.method(#userInfo, [request]), - returnValue: _i5.Future<_i2.UserProfile>.value( - _FakeUserProfile_2(this, Invocation.method(#userInfo, [request])), - ), - ) - as _i5.Future<_i2.UserProfile>); + Invocation.method( + #userInfo, + [request], + ), + returnValue: _i5.Future<_i2.UserProfile>.value(_FakeUserProfile_2( + this, + Invocation.method( + #userInfo, + [request], + ), + )), + ) as _i5.Future<_i2.UserProfile>); @override _i5.Future<_i2.DatabaseUser> signup( - _i3.ApiRequest<_i3.AuthSignupOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthSignupOptions>? request) => (super.noSuchMethod( - Invocation.method(#signup, [request]), - returnValue: _i5.Future<_i2.DatabaseUser>.value( - _FakeDatabaseUser_3(this, Invocation.method(#signup, [request])), - ), - ) - as _i5.Future<_i2.DatabaseUser>); + Invocation.method( + #signup, + [request], + ), + returnValue: _i5.Future<_i2.DatabaseUser>.value(_FakeDatabaseUser_3( + this, + Invocation.method( + #signup, + [request], + ), + )), + ) as _i5.Future<_i2.DatabaseUser>); @override _i5.Future<_i2.Credentials> renew( - _i3.ApiRequest<_i3.AuthRenewOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthRenewOptions>? request) => (super.noSuchMethod( - Invocation.method(#renew, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0(this, Invocation.method(#renew, [request])), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #renew, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #renew, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future<_i2.Credentials> customTokenExchange( - _i3.ApiRequest<_i3.AuthCustomTokenExchangeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthCustomTokenExchangeOptions>? request) => (super.noSuchMethod( - Invocation.method(#customTokenExchange, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0( - this, - Invocation.method(#customTokenExchange, [request]), - ), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #customTokenExchange, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #customTokenExchange, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future<_i2.SSOCredentials> ssoExchange( - _i3.ApiRequest<_i3.AuthSSOExchangeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthSSOExchangeOptions>? request) => (super.noSuchMethod( - Invocation.method(#ssoExchange, [request]), - returnValue: _i5.Future<_i2.SSOCredentials>.value( - _FakeSSOCredentials_4( - this, - Invocation.method(#ssoExchange, [request]), - ), - ), - ) - as _i5.Future<_i2.SSOCredentials>); + Invocation.method( + #ssoExchange, + [request], + ), + returnValue: _i5.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_4( + this, + Invocation.method( + #ssoExchange, + [request], + ), + )), + ) as _i5.Future<_i2.SSOCredentials>); @override _i5.Future resetPassword( - _i3.ApiRequest<_i3.AuthResetPasswordOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthResetPasswordOptions>? request) => (super.noSuchMethod( - Invocation.method(#resetPassword, [request]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #resetPassword, + [request], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future<_i2.PasskeyChallenge> passkeyLoginChallenge( - _i3.ApiRequest<_i3.AuthPasskeyLoginChallengeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasskeyLoginChallengeOptions>? request) => (super.noSuchMethod( - Invocation.method(#passkeyLoginChallenge, [request]), - returnValue: _i5.Future<_i2.PasskeyChallenge>.value( - _FakePasskeyChallenge_5( - this, - Invocation.method(#passkeyLoginChallenge, [request]), - ), - ), - ) - as _i5.Future<_i2.PasskeyChallenge>); + Invocation.method( + #passkeyLoginChallenge, + [request], + ), + returnValue: + _i5.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_5( + this, + Invocation.method( + #passkeyLoginChallenge, + [request], + ), + )), + ) as _i5.Future<_i2.PasskeyChallenge>); @override _i5.Future<_i2.PasskeyChallenge> passkeySignupChallenge( - _i3.ApiRequest<_i3.AuthPasskeySignupChallengeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasskeySignupChallengeOptions>? request) => (super.noSuchMethod( - Invocation.method(#passkeySignupChallenge, [request]), - returnValue: _i5.Future<_i2.PasskeyChallenge>.value( - _FakePasskeyChallenge_5( - this, - Invocation.method(#passkeySignupChallenge, [request]), - ), - ), - ) - as _i5.Future<_i2.PasskeyChallenge>); + Invocation.method( + #passkeySignupChallenge, + [request], + ), + returnValue: + _i5.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_5( + this, + Invocation.method( + #passkeySignupChallenge, + [request], + ), + )), + ) as _i5.Future<_i2.PasskeyChallenge>); @override _i5.Future<_i2.Credentials> passkeyCredentialExchange( - _i3.ApiRequest<_i3.AuthPasskeyExchangeOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasskeyExchangeOptions>? request) => (super.noSuchMethod( - Invocation.method(#passkeyCredentialExchange, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0( - this, - Invocation.method(#passkeyCredentialExchange, [request]), - ), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #passkeyCredentialExchange, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #passkeyCredentialExchange, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); @override _i5.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithEmail( - _i3.ApiRequest<_i3.AuthPasswordlessChallengeEmailOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasswordlessChallengeEmailOptions>? request) => (super.noSuchMethod( - Invocation.method(#passwordlessChallengeWithEmail, [request]), - returnValue: _i5.Future<_i2.PasswordlessChallenge>.value( - _FakePasswordlessChallenge_6( - this, - Invocation.method(#passwordlessChallengeWithEmail, [request]), - ), - ), - ) - as _i5.Future<_i2.PasswordlessChallenge>); + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + returnValue: _i5.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_6( + this, + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + )), + ) as _i5.Future<_i2.PasswordlessChallenge>); @override _i5.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithPhoneNumber( - _i3.ApiRequest<_i3.AuthPasswordlessChallengePhoneOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasswordlessChallengePhoneOptions>? request) => (super.noSuchMethod( - Invocation.method(#passwordlessChallengeWithPhoneNumber, [request]), - returnValue: _i5.Future<_i2.PasswordlessChallenge>.value( - _FakePasswordlessChallenge_6( - this, - Invocation.method(#passwordlessChallengeWithPhoneNumber, [ - request, - ]), - ), - ), - ) - as _i5.Future<_i2.PasswordlessChallenge>); + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + returnValue: _i5.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_6( + this, + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + )), + ) as _i5.Future<_i2.PasswordlessChallenge>); @override _i5.Future<_i2.Credentials> passwordlessLoginWithOtp( - _i3.ApiRequest<_i3.AuthPasswordlessLoginWithOtpOptions>? request, - ) => + _i3.ApiRequest<_i3.AuthPasswordlessLoginWithOtpOptions>? request) => (super.noSuchMethod( - Invocation.method(#passwordlessLoginWithOtp, [request]), - returnValue: _i5.Future<_i2.Credentials>.value( - _FakeCredentials_0( - this, - Invocation.method(#passwordlessLoginWithOtp, [request]), - ), - ), - ) - as _i5.Future<_i2.Credentials>); + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + )), + ) as _i5.Future<_i2.Credentials>); } diff --git a/auth0_flutter/test/mobile/web_authentication_test.mocks.dart b/auth0_flutter/test/mobile/web_authentication_test.mocks.dart index 9e0bd2601..d2347ad4f 100644 --- a/auth0_flutter/test/mobile/web_authentication_test.mocks.dart +++ b/auth0_flutter/test/mobile/web_authentication_test.mocks.dart @@ -3,14 +3,14 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'package:auth0_flutter/auth0_flutter.dart' as _i2; import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart' - as _i3; + as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'web_authentication_test.dart' as _i4; +import 'web_authentication_test.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -38,7 +38,7 @@ class _FakeCredentials_0 extends _i1.SmartFake implements _i2.Credentials { } class _FakeSSOCredentials_1 extends _i1.SmartFake - implements _i3.SSOCredentials { + implements _i2.SSOCredentials { _FakeSSOCredentials_1( Object parent, Invocation parentInvocation, @@ -48,42 +48,59 @@ class _FakeSSOCredentials_1 extends _i1.SmartFake ); } +class _FakeApiCredentials_2 extends _i1.SmartFake + implements _i2.ApiCredentials { + _FakeApiCredentials_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [TestPlatform]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { +class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform { MockTestPlatform() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.Credentials> login( - _i3.WebAuthRequest<_i3.WebAuthLoginOptions>? request) => + _i4.Stream<_i2.Credentials> get onCredentialsRecovered => (super.noSuchMethod( + Invocation.getter(#onCredentialsRecovered), + returnValue: _i4.Stream<_i2.Credentials>.empty(), + ) as _i4.Stream<_i2.Credentials>); + + @override + _i4.Future<_i2.Credentials> login( + _i5.WebAuthRequest<_i5.WebAuthLoginOptions>? request) => (super.noSuchMethod( Invocation.method( #login, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #login, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future logout( - _i3.WebAuthRequest<_i3.WebAuthLogoutOptions>? request) => + _i4.Future logout( + _i5.WebAuthRequest<_i5.WebAuthLogoutOptions>? request) => (super.noSuchMethod( Invocation.method( #logout, [request], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void cancel() => super.noSuchMethod( @@ -98,108 +115,139 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform { /// A class which mocks [TestCMPlatform]. /// /// See the documentation for Mockito's code generation for more information. -class MockTestCMPlatform extends _i1.Mock implements _i4.TestCMPlatform { +class MockTestCMPlatform extends _i1.Mock implements _i3.TestCMPlatform { MockTestCMPlatform() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.Credentials> getCredentials( - _i3.CredentialsManagerRequest<_i3.GetCredentialsOptions>? request) => + _i4.Future<_i2.Credentials> getCredentials( + _i5.CredentialsManagerRequest<_i5.GetCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #getCredentials, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #getCredentials, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.Credentials> renewCredentials( - _i3.CredentialsManagerRequest<_i3.RenewCredentialsOptions>? + _i4.Future<_i2.Credentials> renewCredentials( + _i5.CredentialsManagerRequest<_i5.RenewCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #renewCredentials, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #renewCredentials, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.UserProfile?> user( - _i3.CredentialsManagerRequest<_i3.RequestOptions?>? request) => + _i4.Future<_i2.UserProfile?> user( + _i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) => (super.noSuchMethod( Invocation.method( #user, [request], ), - returnValue: _i5.Future<_i2.UserProfile?>.value(), - ) as _i5.Future<_i2.UserProfile?>); + returnValue: _i4.Future<_i2.UserProfile?>.value(), + ) as _i4.Future<_i2.UserProfile?>); @override - _i5.Future clearCredentials( - _i3.CredentialsManagerRequest<_i3.RequestOptions?>? request) => + _i4.Future clearCredentials( + _i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) => (super.noSuchMethod( Invocation.method( #clearCredentials, [request], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future saveCredentials( - _i3.CredentialsManagerRequest<_i3.SaveCredentialsOptions>? request) => + _i4.Future saveCredentials( + _i5.CredentialsManagerRequest<_i5.SaveCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #saveCredentials, [request], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future hasValidCredentials( - _i3.CredentialsManagerRequest<_i3.HasValidCredentialsOptions>? + _i4.Future hasValidCredentials( + _i5.CredentialsManagerRequest<_i5.HasValidCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #hasValidCredentials, [request], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future<_i3.SSOCredentials> getSSOCredentials( - _i3.CredentialsManagerRequest<_i3.GetSSOCredentialsOptions>? + _i4.Future<_i2.SSOCredentials> getSSOCredentials( + _i5.CredentialsManagerRequest<_i5.GetSSOCredentialsOptions>? request) => (super.noSuchMethod( Invocation.method( #getSSOCredentials, [request], ), - returnValue: _i5.Future<_i3.SSOCredentials>.value(_FakeSSOCredentials_1( + returnValue: _i4.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_1( this, Invocation.method( #getSSOCredentials, [request], ), )), - ) as _i5.Future<_i3.SSOCredentials>); + ) as _i4.Future<_i2.SSOCredentials>); + + @override + _i4.Future<_i2.ApiCredentials> getApiCredentials( + _i5.CredentialsManagerRequest<_i5.GetApiCredentialsOptions>? + request) => + (super.noSuchMethod( + Invocation.method( + #getApiCredentials, + [request], + ), + returnValue: _i4.Future<_i2.ApiCredentials>.value(_FakeApiCredentials_2( + this, + Invocation.method( + #getApiCredentials, + [request], + ), + )), + ) as _i4.Future<_i2.ApiCredentials>); + + @override + _i4.Future clearApiCredentials( + _i5.CredentialsManagerRequest<_i5.ClearApiCredentialsOptions>? + request) => + (super.noSuchMethod( + Invocation.method( + #clearApiCredentials, + [request], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [CredentialsManager]. @@ -212,7 +260,7 @@ class MockCredentialsManager extends _i1.Mock } @override - _i5.Future<_i2.Credentials> credentials({ + _i4.Future<_i2.Credentials> credentials({ int? minTtl = 0, Set? scopes = const {}, Map? parameters = const {}, @@ -227,7 +275,7 @@ class MockCredentialsManager extends _i1.Mock #parameters: parameters, }, ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #credentials, @@ -239,10 +287,10 @@ class MockCredentialsManager extends _i1.Mock }, ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.Credentials> renewCredentials( + _i4.Future<_i2.Credentials> renewCredentials( {Map? parameters = const {}}) => (super.noSuchMethod( Invocation.method( @@ -250,7 +298,7 @@ class MockCredentialsManager extends _i1.Mock [], {#parameters: parameters}, ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #renewCredentials, @@ -258,49 +306,49 @@ class MockCredentialsManager extends _i1.Mock {#parameters: parameters}, ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.UserProfile?> user() => (super.noSuchMethod( + _i4.Future<_i2.UserProfile?> user() => (super.noSuchMethod( Invocation.method( #user, [], ), - returnValue: _i5.Future<_i2.UserProfile?>.value(), - ) as _i5.Future<_i2.UserProfile?>); + returnValue: _i4.Future<_i2.UserProfile?>.value(), + ) as _i4.Future<_i2.UserProfile?>); @override - _i5.Future storeCredentials(_i2.Credentials? credentials) => + _i4.Future storeCredentials(_i2.Credentials? credentials) => (super.noSuchMethod( Invocation.method( #storeCredentials, [credentials], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future hasValidCredentials({int? minTtl = 0}) => + _i4.Future hasValidCredentials({int? minTtl = 0}) => (super.noSuchMethod( Invocation.method( #hasValidCredentials, [], {#minTtl: minTtl}, ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future clearCredentials() => (super.noSuchMethod( + _i4.Future clearCredentials() => (super.noSuchMethod( Invocation.method( #clearCredentials, [], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future<_i3.SSOCredentials> ssoCredentials({ + _i4.Future<_i2.SSOCredentials> ssoCredentials({ Map? parameters = const {}, Map? headers = const {}, }) => @@ -313,7 +361,7 @@ class MockCredentialsManager extends _i1.Mock #headers: headers, }, ), - returnValue: _i5.Future<_i3.SSOCredentials>.value(_FakeSSOCredentials_1( + returnValue: _i4.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_1( this, Invocation.method( #ssoCredentials, @@ -324,5 +372,59 @@ class MockCredentialsManager extends _i1.Mock }, ), )), - ) as _i5.Future<_i3.SSOCredentials>); + ) as _i4.Future<_i2.SSOCredentials>); + + @override + _i4.Future<_i2.ApiCredentials> getApiCredentials({ + required String? audience, + Set? scope = const {}, + int? minTtl = 0, + Map? parameters = const {}, + Map? headers = const {}, + }) => + (super.noSuchMethod( + Invocation.method( + #getApiCredentials, + [], + { + #audience: audience, + #scope: scope, + #minTtl: minTtl, + #parameters: parameters, + #headers: headers, + }, + ), + returnValue: _i4.Future<_i2.ApiCredentials>.value(_FakeApiCredentials_2( + this, + Invocation.method( + #getApiCredentials, + [], + { + #audience: audience, + #scope: scope, + #minTtl: minTtl, + #parameters: parameters, + #headers: headers, + }, + ), + )), + ) as _i4.Future<_i2.ApiCredentials>); + + @override + _i4.Future clearApiCredentials({ + required String? audience, + String? scope, + }) => + (super.noSuchMethod( + Invocation.method( + #clearApiCredentials, + [], + { + #audience: audience, + #scope: scope, + }, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } diff --git a/auth0_flutter_platform_interface/lib/src/auth/auth_multifactor_challenge_options.dart b/auth0_flutter_platform_interface/lib/src/auth/auth_multifactor_challenge_options.dart index 8b3869bf9..ed60cfb62 100644 --- a/auth0_flutter_platform_interface/lib/src/auth/auth_multifactor_challenge_options.dart +++ b/auth0_flutter_platform_interface/lib/src/auth/auth_multifactor_challenge_options.dart @@ -7,9 +7,7 @@ class AuthMultifactorChallengeOptions implements RequestOptions { final String? authenticatorId; AuthMultifactorChallengeOptions( - {required this.mfaToken, - this.types, - this.authenticatorId}); + {required this.mfaToken, this.types, this.authenticatorId}); @override Map toMap() => { diff --git a/auth0_flutter_platform_interface/test/method_channel_auth0_flutter_auth_test.dart b/auth0_flutter_platform_interface/test/method_channel_auth0_flutter_auth_test.dart index 719aa42c3..89a77a7f5 100644 --- a/auth0_flutter_platform_interface/test/method_channel_auth0_flutter_auth_test.dart +++ b/auth0_flutter_platform_interface/test/method_channel_auth0_flutter_auth_test.dart @@ -623,7 +623,8 @@ void main() { ApiRequest( account: const Account('', ''), userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); + options: AuthMultifactorChallengeOptions( + mfaToken: '', authenticatorId: ''))); expect( verify(mocked.methodCallHandler(captureAny)).captured.single.method, @@ -640,7 +641,6 @@ void main() { userAgent: UserAgent(name: 'test-name', version: 'test-version'), options: AuthMultifactorChallengeOptions( mfaToken: 'test-mfa-token', - types: [ChallengeType.otp, ChallengeType.oob], authenticatorId: 'test-authenticatorId'))); final verificationResult = @@ -652,31 +652,10 @@ void main() { expect(verificationResult.arguments['_userAgent']['version'], 'test-version'); expect(verificationResult.arguments['mfaToken'], 'test-mfa-token'); - expect(verificationResult.arguments['types'], - [ChallengeType.otp.value, ChallengeType.oob.value]); expect(verificationResult.arguments['authenticatorId'], 'test-authenticatorId'); }); - test( - 'correctly assigns default values to all non-required properties when missing', - () async { - when(mocked.methodCallHandler(any)).thenAnswer((final _) async => - MethodCallHandler.multifactorChallengeResultRequired); - - await MethodChannelAuth0FlutterAuth().multifactorChallenge( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: - AuthMultifactorChallengeOptions(mfaToken: 'test-mfa-token'))); - - final verificationResult = - verify(mocked.methodCallHandler(captureAny)).captured.single; - expect(verificationResult.arguments['types'], isNull); - expect(verificationResult.arguments['authenticatorId'], isNull); - }); - test('correctly returns the response from the Method Channel', () async { when(mocked.methodCallHandler(any)).thenAnswer( (final _) async => MethodCallHandler.multifactorChallengeResult); @@ -685,7 +664,8 @@ void main() { ApiRequest( account: const Account('', ''), userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); + options: AuthMultifactorChallengeOptions( + mfaToken: '', authenticatorId: ''))); verify(mocked.methodCallHandler(captureAny)); @@ -704,7 +684,8 @@ void main() { ApiRequest( account: const Account('', ''), userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); + options: AuthMultifactorChallengeOptions( + mfaToken: '', authenticatorId: ''))); verify(mocked.methodCallHandler(captureAny)); @@ -721,7 +702,8 @@ void main() { .multifactorChallenge(ApiRequest( account: const Account('', ''), userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); + options: AuthMultifactorChallengeOptions( + mfaToken: '', authenticatorId: ''))); return result; } @@ -743,7 +725,8 @@ void main() { .multifactorChallenge(ApiRequest( account: const Account('', ''), userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); + options: AuthMultifactorChallengeOptions( + mfaToken: '', authenticatorId: ''))); return result; } From 3d68c9fad66177dc49ca87d39c5cd0ed51addaa7 Mon Sep 17 00:00:00 2001 From: Utkrisht Sahu Date: Fri, 7 Aug 2026 14:51:41 +0530 Subject: [PATCH 02/20] feat: Upgrade Auth0.Android to v4.0.0. (#918) --- auth0_flutter/V3_MIGRATION_GUIDE.md | 113 ++++++++++++++++++ auth0_flutter/android/build.gradle | 18 +-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../CredentialsManagerMethodCallHandler.kt | 5 +- .../api/LoginWithOtpApiRequestHandler.kt | 16 +-- .../MultifactorChallengeApiRequestHandler.kt | 22 ++-- .../api/SSOExchangeApiRequestHandler.kt | 4 +- .../GetSSOCredentialsRequestHandler.kt | 4 +- .../web_auth/LoginWebAuthRequestHandler.kt | 2 +- .../api/LoginWithOtpApiRequestHandlerTest.kt | 39 +++--- ...ltifactorChallengeApiRequestHandlerTest.kt | 67 ++++++++--- .../api/SSOExchangeApiRequestHandlerTest.kt | 7 +- .../GetSSOCredentialsRequestHandlerTest.kt | 7 +- .../mfa/EnrollTotpRequestHandlerTest.kt | 9 +- .../ConfirmEnrollmentRequestHandlerTest.kt | 13 +- .../EnrollEmailRequestHandlerTest.kt | 8 +- .../EnrollPhoneRequestHandlerTest.kt | 8 +- ...tAuthenticationMethodRequestHandlerTest.kt | 15 ++- ...AuthenticationMethodsRequestHandlerTest.kt | 15 ++- ...eAuthenticationMethodRequestHandlerTest.kt | 15 ++- .../my_account/VerifyOtpRequestHandlerTest.kt | 15 ++- .../example/android/app/build.gradle | 12 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- auth0_flutter/example/android/settings.gradle | 4 +- 24 files changed, 312 insertions(+), 110 deletions(-) create mode 100644 auth0_flutter/V3_MIGRATION_GUIDE.md diff --git a/auth0_flutter/V3_MIGRATION_GUIDE.md b/auth0_flutter/V3_MIGRATION_GUIDE.md new file mode 100644 index 000000000..d69b2533a --- /dev/null +++ b/auth0_flutter/V3_MIGRATION_GUIDE.md @@ -0,0 +1,113 @@ +# Migration Guide from auth0_flutter v2 to v3 + +`auth0_flutter` v3 upgrades the wrapped native SDKs to their next major versions +— **Auth0.Android v4** on Android and **Auth0.swift v3** on iOS/macOS. Those +upgrades raise the minimum platform requirements and change some native +behavior that surfaces through the Dart API. + +> **Note** +> This is a living document. It currently covers the **Android** track changes. +> The remaining cross-platform Dart API renames (for example +> `expiresIn` → `expiresAt`, `clearSession` → `logout`, `UserInfo` → +> `UserProfile`) land in later v3 PRs and will be documented here as they merge. + +## Table of Contents + +- [Requirements Changes](#requirements-changes) +- [Behavior Changes](#behavior-changes) + - [`credentialsManager.clearCredentials` now clears all stored data (Android)](#credentialsmanagerclearcredentials-now-clears-all-stored-data-android) + - [`api.multifactorChallenge` requires `authenticatorId` (Android)](#apimultifactorchallenge-requires-authenticatorid-android) +- [Getting Help](#getting-help) + +## Requirements Changes + +Because the underlying native SDKs raised their floors, `auth0_flutter` v3 +raises the Android minimums: + +| Requirement | v2 | v3 | +| --- | --- | --- | +| Android `minSdkVersion` | 21 | **26** (Android 8.0) | +| Android compile/target SDK | 34 | **36** | +| JDK (to build the Android module) | 8 | **17** | +| Kotlin | 1.9.x | **2.0.21** | +| Android Gradle Plugin | 8.4.x | **8.10.1** | +| Gradle | 8.7 | **8.11.1** | + +**Migration:** if your app targets an Android `minSdkVersion` below 26, raise it +to at least 26 in your app's `android/app/build.gradle`. Ensure your build +environment uses JDK 17 (for example, set it in Android Studio under +*Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JDK*, +or via `org.gradle.java.home`). + +There are **no source changes required** to your Dart code for these +requirement bumps. + +## Behavior Changes + +### `credentialsManager.clearCredentials` now clears all stored data (Android) + +**Change:** In Auth0.Android v4, `clearCredentials()` performs a full wipe of +the underlying storage (`Storage.removeAll()`) rather than removing only the +individual credential entries it wrote. + +**Impact:** If your app stored unrelated values in the same +`SharedPreferences` instance that the credentials manager uses (for example, by +supplying a custom `sharedPreferencesName` via +`CredentialsManagerConfiguration` and reusing it elsewhere), calling +`clearCredentials` now removes those values too. + +**Migration:** Do not share the credentials manager's storage with other data. +Keep any app data you need to persist independently of Auth0 credentials in a +separate store. The Dart API is unchanged: + +```dart +// Same call as v2 — behavior on Android is now a full wipe of the store. +await credentialsManager.clearCredentials(); +``` + +This affects Android only. iOS/macOS behavior is unchanged. + +### `api.multifactorChallenge` requires `authenticatorId` (Android) + +**Change:** Auth0.Android v4 removed the inline MFA methods from the +authentication client and routes MFA through a dedicated MFA client whose +`challenge` operation accepts only an `authenticatorId` — there is no +challenge-type filtering. + +**Impact:** On Android, a `multifactorChallenge` call must now include +`authenticatorId`. A call that supplies only `types` and omits +`authenticatorId` — which was accepted in v2 — now fails on Android. Any `types` +value passed is ignored on Android. + +> **Note** +> The Dart signature of `multifactorChallenge` is unchanged in this release +> (`types` and `authenticatorId` remain optional parameters), so this is a +> runtime behavior change on Android rather than a compile-time break. iOS +> already requires `authenticatorId`. A later v3 PR realigns the shared Dart +> API to make `authenticatorId` required and remove `types` across platforms. + +**Migration:** Always pass `authenticatorId` when calling +`multifactorChallenge`, and stop relying on `types`: + +```dart +// ❌ v2 — worked on Android, relied on challenge-type filtering +final challenge = await auth0.api.multifactorChallenge( + mfaToken: mfaToken, + types: [ChallengeType.otp, ChallengeType.oob], +); + +// ✅ v3 — pass the authenticator to challenge +final challenge = await auth0.api.multifactorChallenge( + mfaToken: mfaToken, + authenticatorId: authenticatorId, +); +``` + +If you support one-time passwords and don't need to select a specific factor, +you can skip the challenge request and call `api.loginWithOtp` directly. + +## Getting Help + +If you encounter issues migrating, please open an issue on the +[auth0-flutter repository](https://github.com/auth0/auth0-flutter/issues) with +details of the API you're migrating and the platform affected. diff --git a/auth0_flutter/android/build.gradle b/auth0_flutter/android/build.gradle index 60391eec1..8a2294b89 100644 --- a/auth0_flutter/android/build.gradle +++ b/auth0_flutter/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.9.0' + ext.kotlin_version = '2.0.21' repositories { google() mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:8.4.0" + classpath "com.android.tools.build:gradle:8.10.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -31,18 +31,18 @@ rootProject.allprojects { } android { - compileSdk 34 + compileSdk 36 if (project.android.hasProperty("namespace")) { namespace libApplicationId } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -52,7 +52,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 26 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders = [auth0Domain: "test-domain", auth0Scheme: "test"] } @@ -73,8 +73,8 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'com.auth0.android:auth0:3.21.0' - implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.auth0.android:auth0:4.0.0' + implementation 'com.google.code.gson:gson:2.11.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0" diff --git a/auth0_flutter/android/gradle/wrapper/gradle-wrapper.properties b/auth0_flutter/android/gradle/wrapper/gradle-wrapper.properties index d951fac2b..11581996b 100644 --- a/auth0_flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/auth0_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerMethodCallHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerMethodCallHandler.kt index 8b5deadf6..3c516a20b 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerMethodCallHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerMethodCallHandler.kt @@ -4,7 +4,6 @@ import android.app.Activity import android.content.Context import androidx.annotation.NonNull import androidx.fragment.app.FragmentActivity -import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.storage.AuthenticationLevel import com.auth0.android.authentication.storage.LocalAuthenticationOptions @@ -114,11 +113,11 @@ class CredentialsManagerMethodCallHandler(private val requestHandlers: List { - override fun onFailure(exception: AuthenticationException) { + loginBuilder.start(object : Callback { + override fun onFailure(exception: MfaVerifyException) { result.error( exception.getCode(), exception.getDescription(), - exception.toMap() + exception.toMfaMap() ) } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandler.kt index 60d128684..8648358bc 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandler.kt @@ -1,11 +1,11 @@ package com.auth0.auth0_flutter.request_handlers.api import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.mfa.MfaException.MfaChallengeException import com.auth0.android.callback.Callback import com.auth0.android.result.Challenge import com.auth0.auth0_flutter.request_handlers.MethodCallRequest -import com.auth0.auth0_flutter.toMap +import com.auth0.auth0_flutter.toMfaMap import com.auth0.auth0_flutter.utils.assertHasProperties import io.flutter.plugin.common.MethodChannel @@ -19,22 +19,18 @@ class MultifactorChallengeApiRequestHandler : ApiRequestHandler { request: MethodCallRequest, result: MethodChannel.Result ) { - assertHasProperties(listOf("mfaToken"), request.data) + assertHasProperties(listOf("mfaToken", "authenticatorId"), request.data) - val challengeTypes = request.data["types"] as ArrayList<*>? + val builder = api + .mfaClient(request.data["mfaToken"] as String) + .challenge(request.data["authenticatorId"] as String) - val builder = api.multifactorChallenge( - mfaToken = request.data["mfaToken"] as String, - challengeType = challengeTypes?.joinToString(separator = " "), - authenticatorId = request.data["authenticatorId"] as String? - ) - - builder.start(object : Callback { - override fun onFailure(exception: AuthenticationException) { + builder.start(object : Callback { + override fun onFailure(exception: MfaChallengeException) { result.error( exception.getCode(), exception.getDescription(), - exception.toMap() + exception.toMfaMap() ) } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandler.kt index 491b35a7d..577961959 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandler.kt @@ -43,10 +43,12 @@ class SSOExchangeApiRequestHandler : ApiRequestHandler { } override fun onSuccess(credentials: SSOCredentials) { + val expiresIn = + (credentials.expiresAt.time - System.currentTimeMillis()) / 1000 val map = mutableMapOf( "sessionTransferToken" to credentials.sessionTransferToken, "tokenType" to credentials.issuedTokenType, - "expiresIn" to credentials.expiresIn, + "expiresIn" to expiresIn, "idToken" to credentials.idToken ) credentials.refreshToken?.let { map["refreshToken"] = it } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt index 25aed8597..51962d145 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt @@ -27,10 +27,12 @@ class GetSSOCredentialsRequestHandler : CredentialsManagerRequestHandler { } override fun onSuccess(credentials: SSOCredentials) { + val expiresIn = + (credentials.expiresAt.time - System.currentTimeMillis()) / 1000 val map = mutableMapOf( "sessionTransferToken" to credentials.sessionTransferToken, "tokenType" to credentials.issuedTokenType, - "expiresIn" to credentials.expiresIn, + "expiresIn" to expiresIn, "idToken" to credentials.idToken ) credentials.refreshToken?.let { map["refreshToken"] = it } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt index d07b5900a..762396e19 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt @@ -66,7 +66,7 @@ class LoginWebAuthRequestHandler( } if (args["useDPoP"] == true) { - WebAuthProvider.useDPoP(context) + builder.useDPoP(context) } builder.start(context, object : Callback { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt index 38a6d8312..3ffc08970 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt @@ -2,10 +2,13 @@ package com.auth0.auth0_flutter.request_handlers.api import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.mfa.MfaApiClient +import com.auth0.android.authentication.mfa.MfaException.MfaVerifyException +import com.auth0.android.authentication.mfa.MfaVerificationType import com.auth0.android.callback.Callback -import com.auth0.android.request.AuthenticationRequest +import com.auth0.android.request.Request import com.auth0.android.result.Credentials + import com.auth0.auth0_flutter.JwtTestUtils import com.auth0.auth0_flutter.request_handlers.MethodCallRequest @@ -69,19 +72,21 @@ class LoginWithOtpApiRequestHandlerTest { } @Test - fun `should call loginWithOTp with the correct parameters`() { + fun `should call mfaClient verify with the correct parameters`() { val options = hashMapOf( "otp" to "test-otp", "mfaToken" to "test-mfaToken" ) val handler = LoginWithOtpApiRequestHandler() - val mockLoginBuilder = mock() + val mockLoginBuilder = mock>() + val mockMfaClient = mock() val mockApi = mock() val mockAccount = mock() val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any()) + doReturn(mockMfaClient).`when`(mockApi).mfaClient(any()) + doReturn(mockLoginBuilder).`when`(mockMfaClient).verify(any()) handler.handle( mockApi, @@ -89,7 +94,8 @@ class LoginWithOtpApiRequestHandlerTest { mockResult ) - verify(mockApi).loginWithOTP("test-mfaToken", "test-otp") + verify(mockApi).mfaClient("test-mfaToken") + verify(mockMfaClient).verify(eq(MfaVerificationType.Otp("test-otp"))) verify(mockLoginBuilder).start(any()) } @@ -100,17 +106,20 @@ class LoginWithOtpApiRequestHandlerTest { "mfaToken" to "test-mfaToken" ) val handler = LoginWithOtpApiRequestHandler() - val mockLoginBuilder = mock() + val mockLoginBuilder = mock>() + val mockMfaClient = mock() val mockApi = mock() val mockAccount = mock() val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - val exception = - AuthenticationException(code = "test-code", description = "test-description") + val exception = mock() + whenever(exception.getCode()).thenReturn("test-code") + whenever(exception.getDescription()).thenReturn("test-description") - doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any()) + doReturn(mockMfaClient).`when`(mockApi).mfaClient(any()) + doReturn(mockLoginBuilder).`when`(mockMfaClient).verify(any()) doAnswer { - val ob = it.getArgument>(0) + val ob = it.getArgument>(0) ob.onFailure(exception) }.`when`(mockLoginBuilder).start(any()) @@ -130,7 +139,8 @@ class LoginWithOtpApiRequestHandlerTest { "mfaToken" to "test-mfaToken" ) val handler = LoginWithOtpApiRequestHandler() - val mockLoginBuilder = mock() + val mockLoginBuilder = mock>() + val mockMfaClient = mock() val mockApi = mock() val mockAccount = mock() val mockResult = mock() @@ -138,9 +148,10 @@ class LoginWithOtpApiRequestHandlerTest { val idToken = JwtTestUtils.createJwt(claims = mapOf("name" to "John Doe")) val credentials = Credentials(idToken, "test", "", null, Date(), "scope1 scope2") - whenever(mockApi.loginWithOTP(any(), any())).thenReturn(mockLoginBuilder) + whenever(mockApi.mfaClient(any())).thenReturn(mockMfaClient) + whenever(mockMfaClient.verify(any())).thenReturn(mockLoginBuilder) whenever(mockLoginBuilder.start(any())).thenAnswer { - val callback = it.getArgument>(0) + val callback = it.getArgument>(0) callback.onSuccess(credentials) } diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandlerTest.kt index 7276d7222..cde9ef913 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandlerTest.kt @@ -2,7 +2,8 @@ package com.auth0.auth0_flutter.request_handlers.api import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.mfa.MfaApiClient +import com.auth0.android.authentication.mfa.MfaException.MfaChallengeException import com.auth0.android.callback.Callback import com.auth0.android.request.Request import com.auth0.android.result.Challenge @@ -21,7 +22,7 @@ import org.robolectric.RobolectricTestRunner class MultifactorChallengeApiRequestHandlerTest { @Test fun `should throw when missing mfaToken`() { - val options = hashMapOf() + val options = hashMapOf("authenticatorId" to "test-authenticatorId") val handler = MultifactorChallengeApiRequestHandler() val mockApi = mock() val mockAccount = mock() @@ -39,42 +40,69 @@ class MultifactorChallengeApiRequestHandlerTest { } @Test - fun `should call multifactorChallenge with the correct parameters`() { + fun `should throw when missing authenticatorId`() { + val options = hashMapOf("mfaToken" to "test-mfaToken") + val handler = MultifactorChallengeApiRequestHandler() + val mockApi = mock() + val mockAccount = mock() + val mockResult = mock() + val request = MethodCallRequest(account = mockAccount, options) + + val exception = Assert.assertThrows(IllegalArgumentException::class.java) { + handler.handle(mockApi, request, mockResult) + } + + assertThat( + exception.message, + equalTo("Required property 'authenticatorId' is not provided.") + ) + } + + @Test + fun `should call mfaClient challenge with the correct parameters`() { val options = hashMapOf( "mfaToken" to "test-mfaToken", - "types" to arrayListOf("type-1", "type-2"), "authenticatorId" to "test-authenticatorId" ) val handler = MultifactorChallengeApiRequestHandler() - val mockBuilder = mock>() + val mockBuilder = mock>() + val mockMfaClient = mock() val mockApi = mock() val mockAccount = mock() val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).multifactorChallenge(any(), any(), any()) + doReturn(mockMfaClient).`when`(mockApi).mfaClient(any()) + doReturn(mockBuilder).`when`(mockMfaClient).challenge(any()) handler.handle(mockApi, request, mockResult) - verify(mockApi).multifactorChallenge("test-mfaToken", "type-1 type-2", "test-authenticatorId") + verify(mockApi).mfaClient("test-mfaToken") + verify(mockMfaClient).challenge("test-authenticatorId") verify(mockBuilder).start(any()) } @Test fun `should call result error on failure`() { - val options = hashMapOf("mfaToken" to "test-mfaToken") + val options = hashMapOf( + "mfaToken" to "test-mfaToken", + "authenticatorId" to "test-authenticatorId" + ) val handler = MultifactorChallengeApiRequestHandler() - val mockBuilder = mock>() + val mockBuilder = mock>() + val mockMfaClient = mock() val mockApi = mock() val mockAccount = mock() val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - val exception = - AuthenticationException(code = "test-code", description = "test-description") + val exception = mock() + whenever(exception.getCode()).thenReturn("test-code") + whenever(exception.getDescription()).thenReturn("test-description") - doReturn(mockBuilder).`when`(mockApi).multifactorChallenge(any(), anyOrNull(), anyOrNull()) + doReturn(mockMfaClient).`when`(mockApi).mfaClient(any()) + doReturn(mockBuilder).`when`(mockMfaClient).challenge(any()) doAnswer { - val ob = it.getArgument>(0) + val ob = it.getArgument>(0) ob.onFailure(exception) }.`when`(mockBuilder).start(any()) @@ -85,18 +113,23 @@ class MultifactorChallengeApiRequestHandlerTest { @Test fun `should call result success on success`() { - val options = hashMapOf("mfaToken" to "test-mfaToken") + val options = hashMapOf( + "mfaToken" to "test-mfaToken", + "authenticatorId" to "test-authenticatorId" + ) val handler = MultifactorChallengeApiRequestHandler() - val mockBuilder = mock>() + val mockBuilder = mock>() + val mockMfaClient = mock() val mockApi = mock() val mockAccount = mock() val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) val challenge = Challenge("challengeType", "oobCode", "bindingMethod") - doReturn(mockBuilder).`when`(mockApi).multifactorChallenge(any(), anyOrNull(), anyOrNull()) + doReturn(mockMfaClient).`when`(mockApi).mfaClient(any()) + doReturn(mockBuilder).`when`(mockMfaClient).challenge(any()) doAnswer { - val ob = it.getArgument>(0) + val ob = it.getArgument>(0) ob.onSuccess(challenge) }.`when`(mockBuilder).start(any()) diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandlerTest.kt index d77df1057..0916f09ad 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/SSOExchangeApiRequestHandlerTest.kt @@ -15,6 +15,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.* import org.robolectric.RobolectricTestRunner +import java.util.Date @RunWith(RobolectricTestRunner::class) class SSOExchangeApiRequestHandlerTest { @@ -181,7 +182,7 @@ class SSOExchangeApiRequestHandlerTest { val mockSSOCredentials = mock() whenever(mockSSOCredentials.sessionTransferToken).thenReturn("sso-token") whenever(mockSSOCredentials.issuedTokenType).thenReturn("session_transfer") - whenever(mockSSOCredentials.expiresIn).thenReturn(60) + whenever(mockSSOCredentials.expiresAt).thenReturn(Date(System.currentTimeMillis() + 60_000)) whenever(mockSSOCredentials.idToken).thenReturn("id-token") whenever(mockSSOCredentials.refreshToken).thenReturn("new-refresh-token") @@ -199,7 +200,7 @@ class SSOExchangeApiRequestHandlerTest { val resultMap = captor.firstValue assertThat(resultMap["sessionTransferToken"], equalTo("sso-token")) assertThat(resultMap["tokenType"], equalTo("session_transfer")) - assertThat(resultMap["expiresIn"], equalTo(60)) + assertThat((resultMap["expiresIn"] as Long) in 58L..60L, equalTo(true)) assertThat(resultMap["idToken"], equalTo("id-token")) assertThat(resultMap["refreshToken"], equalTo("new-refresh-token")) } @@ -219,7 +220,7 @@ class SSOExchangeApiRequestHandlerTest { val mockSSOCredentials = mock() whenever(mockSSOCredentials.sessionTransferToken).thenReturn("sso-token") whenever(mockSSOCredentials.issuedTokenType).thenReturn("session_transfer") - whenever(mockSSOCredentials.expiresIn).thenReturn(60) + whenever(mockSSOCredentials.expiresAt).thenReturn(Date(System.currentTimeMillis() + 60_000)) whenever(mockSSOCredentials.idToken).thenReturn("id-token") whenever(mockSSOCredentials.refreshToken).thenReturn(null) diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandlerTest.kt index ac10d9615..0f772d5da 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandlerTest.kt @@ -15,6 +15,7 @@ import org.mockito.ArgumentMatchers.anyMap import org.mockito.Mockito.`when` import org.mockito.kotlin.* import org.robolectric.RobolectricTestRunner +import java.util.Date @RunWith(RobolectricTestRunner::class) class GetSSOCredentialsRequestHandlerTest { @@ -108,7 +109,7 @@ class GetSSOCredentialsRequestHandlerTest { val mockSSOCredentials = mock() `when`(mockSSOCredentials.sessionTransferToken).thenReturn("sso-token") `when`(mockSSOCredentials.issuedTokenType).thenReturn("session_transfer") - `when`(mockSSOCredentials.expiresIn).thenReturn(3600) + `when`(mockSSOCredentials.expiresAt).thenReturn(Date(System.currentTimeMillis() + 3_600_000)) `when`(mockSSOCredentials.idToken).thenReturn("id-token") `when`(mockSSOCredentials.refreshToken).thenReturn(null) @@ -133,8 +134,8 @@ class GetSSOCredentialsRequestHandlerTest { CoreMatchers.equalTo("session_transfer") ) MatcherAssert.assertThat( - resultMap["expiresIn"], - CoreMatchers.equalTo(3600) + (resultMap["expiresIn"] as Long) in 3598L..3600L, + CoreMatchers.equalTo(true) ) MatcherAssert.assertThat( resultMap["idToken"], diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/mfa/EnrollTotpRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/mfa/EnrollTotpRequestHandlerTest.kt index 6902c9991..341e0e19f 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/mfa/EnrollTotpRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/mfa/EnrollTotpRequestHandlerTest.kt @@ -7,6 +7,7 @@ import com.auth0.android.authentication.mfa.MfaException.MfaEnrollmentException import com.auth0.android.callback.Callback import com.auth0.android.request.Request import com.auth0.android.result.EnrollmentChallenge +import com.auth0.android.result.TotpEnrollmentChallenge import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.junit.Test @@ -52,7 +53,13 @@ class EnrollTotpRequestHandlerTest { doAnswer { val callback = it.getArgument>(0) - callback.onSuccess(mock()) + callback.onSuccess( + TotpEnrollmentChallenge( + id = "totp|test123", + authSession = "session123", + barcodeUri = "otpauth://totp/test" + ) + ) }.whenever(mockRequest).start(any()) handler.handle(mockClient, request, mockResult) diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/ConfirmEnrollmentRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/ConfirmEnrollmentRequestHandlerTest.kt index 3480f15fe..162d4abc1 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/ConfirmEnrollmentRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/ConfirmEnrollmentRequestHandlerTest.kt @@ -6,6 +6,7 @@ import com.auth0.android.myaccount.MyAccountAPIClient import com.auth0.android.myaccount.MyAccountException import com.auth0.android.request.Request import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.PushNotificationAuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.hamcrest.CoreMatchers.equalTo @@ -51,10 +52,14 @@ class ConfirmEnrollmentRequestHandlerTest { ) val request = MethodCallRequest(account = mockAccount, options) - val mockMethod = mock() - whenever(mockMethod.id).thenReturn("push|test123") - whenever(mockMethod.type).thenReturn("push") - whenever(mockMethod.createdAt).thenReturn("2026-01-01") + val mockMethod = PushNotificationAuthenticationMethod( + id = "push|test123", + type = "push", + createdAt = "2026-01-01", + usage = emptyList(), + confirmed = true, + name = null + ) whenever(mockClient.verify(any(), any())).thenReturn(mockRequest) doAnswer { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt index dc2c7bc6e..65e50bb65 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt @@ -6,6 +6,7 @@ import com.auth0.android.myaccount.MyAccountAPIClient import com.auth0.android.myaccount.MyAccountException import com.auth0.android.request.Request import com.auth0.android.result.EnrollmentChallenge +import com.auth0.android.result.MfaEnrollmentChallenge import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.junit.Test @@ -43,9 +44,10 @@ class EnrollEmailRequestHandlerTest { val options = hashMapOf("email" to "test@example.com") val request = MethodCallRequest(account = mockAccount, options) - val mockChallenge = mock() - whenever(mockChallenge.id).thenReturn("email|test123") - whenever(mockChallenge.authSession).thenReturn("session123") + val mockChallenge = MfaEnrollmentChallenge( + id = "email|test123", + authSession = "session123" + ) whenever(mockClient.enrollEmail(any())).thenReturn(mockRequest) doAnswer { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt index 0bd649074..166499f30 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt @@ -7,6 +7,7 @@ import com.auth0.android.myaccount.MyAccountException import com.auth0.android.myaccount.PhoneAuthenticationMethodType import com.auth0.android.request.Request import com.auth0.android.result.EnrollmentChallenge +import com.auth0.android.result.MfaEnrollmentChallenge import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.junit.Test @@ -70,9 +71,10 @@ class EnrollPhoneRequestHandlerTest { ) val request = MethodCallRequest(account = mockAccount, options) - val mockChallenge = mock() - whenever(mockChallenge.id).thenReturn("phone|test123") - whenever(mockChallenge.authSession).thenReturn("session123") + val mockChallenge = MfaEnrollmentChallenge( + id = "phone|test123", + authSession = "session123" + ) whenever(mockClient.enrollPhone(any(), any())).thenReturn(mockRequest) doAnswer { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt index 0b4fa2483..e29f6fce5 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt @@ -6,6 +6,7 @@ import com.auth0.android.myaccount.MyAccountAPIClient import com.auth0.android.myaccount.MyAccountException import com.auth0.android.request.Request import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.PhoneAuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.junit.Test @@ -43,10 +44,16 @@ class GetAuthenticationMethodRequestHandlerTest { val options = hashMapOf("id" to "phone|test123") val request = MethodCallRequest(account = mockAccount, options) - val mockMethod = mock() - whenever(mockMethod.id).thenReturn("phone|test123") - whenever(mockMethod.type).thenReturn("phone") - whenever(mockMethod.createdAt).thenReturn("2026-01-01T00:00:00.000Z") + val mockMethod = PhoneAuthenticationMethod( + id = "phone|test123", + type = "phone", + createdAt = "2026-01-01T00:00:00.000Z", + usage = emptyList(), + confirmed = true, + name = null, + phoneNumber = null, + preferredAuthenticationMethod = null + ) whenever(mockClient.getAuthenticationMethodById(any())).thenReturn(mockRequest) doAnswer { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt index 6dc89f730..028773967 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt @@ -7,6 +7,7 @@ import com.auth0.android.myaccount.MyAccountAPIClient import com.auth0.android.myaccount.MyAccountException import com.auth0.android.request.Request import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.PhoneAuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.junit.Test @@ -61,10 +62,16 @@ class GetAuthenticationMethodsRequestHandlerTest { val mockRequest = mock, MyAccountException>>() val request = MethodCallRequest(account = mockAccount, hashMapOf()) - val mockMethod = mock() - whenever(mockMethod.id).thenReturn("test-id") - whenever(mockMethod.type).thenReturn("phone") - whenever(mockMethod.createdAt).thenReturn("2026-01-01T00:00:00.000Z") + val mockMethod = PhoneAuthenticationMethod( + id = "test-id", + type = "phone", + createdAt = "2026-01-01T00:00:00.000Z", + usage = emptyList(), + confirmed = true, + name = null, + phoneNumber = null, + preferredAuthenticationMethod = null + ) whenever(mockClient.getAuthenticationMethods(anyOrNull())).thenReturn(mockRequest) doAnswer { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/UpdateAuthenticationMethodRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/UpdateAuthenticationMethodRequestHandlerTest.kt index aaf097d1b..33faad2fc 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/UpdateAuthenticationMethodRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/UpdateAuthenticationMethodRequestHandlerTest.kt @@ -7,6 +7,7 @@ import com.auth0.android.myaccount.MyAccountException import com.auth0.android.myaccount.PhoneAuthenticationMethodType import com.auth0.android.request.Request import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.PhoneAuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.hamcrest.CoreMatchers.equalTo @@ -78,10 +79,16 @@ class UpdateAuthenticationMethodRequestHandlerTest { ) val request = MethodCallRequest(account = mockAccount, options) - val mockMethod = mock() - whenever(mockMethod.id).thenReturn("phone|test123") - whenever(mockMethod.type).thenReturn("phone") - whenever(mockMethod.createdAt).thenReturn("2026-01-01") + val mockMethod = PhoneAuthenticationMethod( + id = "phone|test123", + type = "phone", + createdAt = "2026-01-01", + usage = emptyList(), + confirmed = true, + name = null, + phoneNumber = null, + preferredAuthenticationMethod = null + ) whenever(mockClient.updateAuthenticationMethodById(any(), anyOrNull(), anyOrNull())) .thenReturn(mockRequest) diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt index 91816abe0..c2514c4a1 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt @@ -6,6 +6,7 @@ import com.auth0.android.myaccount.MyAccountAPIClient import com.auth0.android.myaccount.MyAccountException import com.auth0.android.request.Request import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.PhoneAuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result import org.hamcrest.CoreMatchers.equalTo @@ -53,10 +54,16 @@ class VerifyOtpRequestHandlerTest { ) val request = MethodCallRequest(account = mockAccount, options) - val mockMethod = mock() - whenever(mockMethod.id).thenReturn("phone|test123") - whenever(mockMethod.type).thenReturn("phone") - whenever(mockMethod.createdAt).thenReturn("2026-01-01") + val mockMethod = PhoneAuthenticationMethod( + id = "phone|test123", + type = "phone", + createdAt = "2026-01-01", + usage = emptyList(), + confirmed = true, + name = null, + phoneNumber = null, + preferredAuthenticationMethod = null + ) whenever(mockClient.verifyOtp(any(), any(), any())).thenReturn(mockRequest) doAnswer { diff --git a/auth0_flutter/example/android/app/build.gradle b/auth0_flutter/example/android/app/build.gradle index 040fc730b..55c86b5ce 100644 --- a/auth0_flutter/example/android/app/build.gradle +++ b/auth0_flutter/example/android/app/build.gradle @@ -25,18 +25,18 @@ if (flutterVersionName == null) { } android { - compileSdk 34 + compileSdk 36 if (project.android.hasProperty("namespace")) { namespace exampleAppApplicationId } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } sourceSets { @@ -47,8 +47,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId exampleAppApplicationId - minSdkVersion 24 - targetSdk 35 + minSdkVersion 26 + targetSdk 36 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/auth0_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/auth0_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index d951fac2b..11581996b 100644 --- a/auth0_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/auth0_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/auth0_flutter/example/android/settings.gradle b/auth0_flutter/example/android/settings.gradle index 57bb17329..849bde3ef 100644 --- a/auth0_flutter/example/android/settings.gradle +++ b/auth0_flutter/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.6.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.22" apply false + id "com.android.application" version "8.10.1" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false } include ':app' \ No newline at end of file From 902a942c54aa67d39424d2065d7cb9ad2d836818 Mon Sep 17 00:00:00 2001 From: Utkrisht Sahu Date: Fri, 21 Aug 2026 13:38:51 +0530 Subject: [PATCH 03/20] Chore/sync develop/v3.0 with main branch (#926) Signed-off-by: dependabot[bot] Co-authored-by: nandan_prabhu Co-authored-by: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/setup-android/action.yml | 2 +- .github/dependabot.yml | 13 + .github/workflows/check-darwin-deps.yml | 37 + .github/workflows/check-symlinks.yml | 2 +- .github/workflows/main.yml | 64 +- .github/workflows/publish-af.yml | 2 +- .github/workflows/publish-afpi.yml | 2 +- .github/workflows/rl-scanner.yml | 2 +- AGENTS.md | 3 + CLAUDE.md | 199 ++++++ DEVELOPMENT.md | 17 + appium-test/package-lock.json | 18 +- auth0_flutter/CHANGELOG.md | 10 + auth0_flutter/EXAMPLES.md | 188 ++++- auth0_flutter/README.md | 18 +- auth0_flutter/analysis_options.yaml | 2 +- .../auth0/auth0_flutter/Auth0FlutterPlugin.kt | 13 +- .../auth0_flutter/CredentialsExtensions.kt | 27 + .../GetCredentialsRequestHandler.kt | 14 +- .../RenewCredentialsRequestHandler.kt | 14 +- .../web_auth/LoginWebAuthRequestHandler.kt | 15 +- .../com/auth0/auth0_flutter/JwtTestUtils.kt | 4 +- .../LoginWebAuthRequestHandlerTest.kt | 30 + .../GetCredentialsRequestHandlerTest.kt | 61 ++ .../RenewCredentialsRequestHandlerTest.kt | 34 + .../CredentialsManagerExtensions.swift | 1 + .../Sources/auth0_flutter/Extensions.swift | 6 + .../Sources/auth0_flutter/Properties.swift | 1 + .../ios/Flutter/AppFrameworkInfo.plist | 2 - auth0_flutter/example/ios/Podfile | 3 +- .../CredentialsManagerSpies.swift | 21 +- .../example/ios/Tests/ExtensionsTests.swift | 16 + auth0_flutter/example/ios/Tests/Mocks.swift | 11 +- auth0_flutter/example/lib/example_app.dart | 4 +- .../example/lib/my_account_card.dart | 676 ------------------ auth0_flutter/example/macos/Podfile | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 18 +- auth0_flutter/example/pubspec.yaml | 1 + auth0_flutter/example/web/index.html | 6 +- auth0_flutter/example/windows/runner/main.cpp | 11 +- auth0_flutter/lib/auth0_flutter_web.dart | 151 +++- auth0_flutter/lib/src/version.dart | 2 +- .../src/web/auth0_flutter_plugin_real.dart | 77 +- .../src/web/auth0_flutter_plugin_stub.dart | 21 + .../web/auth0_flutter_web_platform_proxy.dart | 12 + .../web/extensions/credentials_extension.dart | 17 +- .../web/extensions/passkey_extensions.dart | 56 ++ .../extensions/web_exception_extensions.dart | 30 + auth0_flutter/lib/src/web/js_interop.dart | 90 +++ auth0_flutter/pubspec.yaml | 4 +- .../windows_web_authentication_test.dart | 34 +- .../test/mobile/authentication_api_test.dart | 50 +- .../test/mobile/credentials_manager_test.dart | 44 +- auth0_flutter/test/mobile/mfa_api_test.dart | 20 +- .../test/mobile/passwordless_api_test.dart | 14 +- .../test/mobile/web_authentication_test.dart | 158 ++-- .../test/web/auth0_extension_type_mocks.dart | 7 +- .../test/web/auth0_flutter_web_test.dart | 361 ++++++++-- .../web/auth0_flutter_web_test.mocks.dart | 68 ++ .../client_options_extension_test.dart | 2 +- .../credentials_extension_test.dart | 93 +++ .../extensions/passkey_extensions_test.dart | 138 ++++ .../web_exception_extension_test.dart | 4 +- auth0_flutter/test/web/jwt_decode_test.dart | 4 +- .../web/matchers/auth0_exception_matcher.dart | 4 +- auth0_flutter/test/web/mfa_web_test.dart | 24 +- auth0_flutter/windows/CMakeLists.txt | 58 +- auth0_flutter/windows/auth0_api_client.cpp | 18 +- .../windows/auth0_flutter_plugin.cpp | 2 +- .../windows/authentication_api_client.cpp | 16 +- auth0_flutter/windows/authentication_error.h | 35 +- .../windows/id_token_signature_validator.cpp | 120 +++- auth0_flutter/windows/id_token_validator.cpp | 51 +- auth0_flutter/windows/id_token_validator.h | 4 +- auth0_flutter/windows/jwt_util.cpp | 23 +- auth0_flutter/windows/jwt_util.h | 8 +- auth0_flutter/windows/networking.cpp | 40 +- auth0_flutter/windows/networking.h | 8 +- auth0_flutter/windows/oauth_helpers.cpp | 20 +- auth0_flutter/windows/oauth_helpers.h | 6 +- .../login_web_auth_request_handler.cpp | 16 +- .../web_auth/login_web_auth_request_handler.h | 6 +- .../logout_web_auth_request_handler.cpp | 8 +- .../logout_web_auth_request_handler.h | 4 +- .../test/authentication_api_client_test.cpp | 46 +- .../test/authentication_error_test.cpp | 94 +-- .../id_token_signature_validator_test.cpp | 84 ++- .../windows/test/id_token_validator_test.cpp | 326 ++++----- auth0_flutter/windows/test/jwt_util_test.cpp | 32 +- .../login_web_auth_request_handler_test.cpp | 2 +- .../windows/test/oauth_helpers_test.cpp | 12 +- .../windows/test/token_decoder_test.cpp | 166 +++-- .../windows/test/user_identity_test.cpp | 93 ++- auth0_flutter/windows/token_decoder.cpp | 47 +- auth0_flutter/windows/token_decoder.h | 4 +- auth0_flutter/windows/user_identity.cpp | 39 +- auth0_flutter/windows/user_identity.h | 4 +- auth0_flutter/windows/vcpkg.json | 12 +- auth0_flutter/windows/windows_utils.cpp | 2 +- auth0_flutter_platform_interface/CHANGELOG.md | 10 + .../lib/auth0_flutter_platform_interface.dart | 4 + .../lib/src/auth0_flutter_web_platform.dart | 20 + .../credentials_manager_exception.dart | 9 + .../lib/src/credentials.dart | 46 ++ .../lib/src/web/web_exception.dart | 12 + .../web/web_get_token_by_passkey_options.dart | 34 + .../lib/src/web/web_passkey_challenge.dart | 25 + .../web_passkey_login_challenge_options.dart | 10 + .../web_passkey_signup_challenge_options.dart | 28 + auth0_flutter_platform_interface/pubspec.yaml | 2 +- .../test/auth0_flutter_web_platform_test.dart | 42 ++ .../credential_manager_exception_test.dart | 25 + .../test/credentials_test.dart | 82 +++ .../test/web_exception_test.dart | 40 ++ references/code-style.md | 38 + references/commands.md | 89 +++ references/docs-update.md | 17 + references/git-workflow.md | 15 + references/pitfalls.md | 7 + references/testing.md | 39 + scripts/sync-darwin-deps.sh | 65 ++ 121 files changed, 3382 insertions(+), 1738 deletions(-) create mode 100644 .github/workflows/check-darwin-deps.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt delete mode 100644 auth0_flutter/example/lib/my_account_card.dart create mode 100644 auth0_flutter/lib/src/web/extensions/passkey_extensions.dart create mode 100644 auth0_flutter/test/web/extensions/passkey_extensions_test.dart create mode 100644 auth0_flutter_platform_interface/lib/src/web/web_get_token_by_passkey_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/web/web_passkey_challenge.dart create mode 100644 auth0_flutter_platform_interface/lib/src/web/web_passkey_login_challenge_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/web/web_passkey_signup_challenge_options.dart create mode 100644 auth0_flutter_platform_interface/test/web_exception_test.dart create mode 100644 references/code-style.md create mode 100644 references/commands.md create mode 100644 references/docs-update.md create mode 100644 references/git-workflow.md create mode 100644 references/pitfalls.md create mode 100644 references/testing.md create mode 100755 scripts/sync-darwin-deps.sh diff --git a/.github/actions/setup-android/action.yml b/.github/actions/setup-android/action.yml index 875dc037f..949e4d22f 100644 --- a/.github/actions/setup-android/action.yml +++ b/.github/actions/setup-android/action.yml @@ -35,7 +35,7 @@ runs: shell: bash - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@v5.6.0 with: distribution: 'temurin' java-version: ${{ inputs.java }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 83f9cdd46..46882b120 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -47,6 +47,19 @@ updates: - ignore title check - dependencies + - package-ecosystem: swift + directory: auth0_flutter/darwin/auth0_flutter + schedule: + interval: daily + allow: + - dependency-name: 'https://github.com/auth0/Auth0.swift' + ignore: + - dependency-name: '*' + update-types: ['version-update:semver-major'] + labels: + - ignore title check + - dependencies + - package-ecosystem: github-actions directory: / schedule: diff --git a/.github/workflows/check-darwin-deps.yml b/.github/workflows/check-darwin-deps.yml new file mode 100644 index 000000000..ae217d929 --- /dev/null +++ b/.github/workflows/check-darwin-deps.yml @@ -0,0 +1,37 @@ +## This workflow can be removed once we removed podspec from flutter for Auth0.Seift dependency + +name: Darwin dependencies + +on: + pull_request: + types: + - opened + - synchronize + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + check-darwin-deps: + name: Check iOS/macOS podspec dependency versions + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Run check + run: | + scripts/sync-darwin-deps.sh + + if git diff HEAD --quiet --diff-filter=ACDMRT; then + echo $'\nPodspec dependency versions are in sync with Package.swift.' + echo 'All good.' + else + echo $'\nThe podspec dependency versions are out of sync with Package.swift.' + echo "Please run 'scripts/sync-darwin-deps.sh' from the repository root and commit the changes." + exit 1 + fi diff --git a/.github/workflows/check-symlinks.yml b/.github/workflows/check-symlinks.yml index 19a19188c..5f1b2eb5d 100644 --- a/.github/workflows/check-symlinks.yml +++ b/.github/workflows/check-symlinks.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Run check run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ac861393..1f56cf2c2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,7 @@ env: flutter: '3.x' ios-simulator: iPhone 17 java: 17 + vcpkg-commit: 66c0373dc7fca549e5803087b9487edfe3aca0a1 jobs: @@ -25,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Install Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0 @@ -48,7 +49,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Install Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0 @@ -79,7 +80,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Install Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0 @@ -98,7 +99,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Install Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0 @@ -132,7 +133,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up environment uses: ./.github/actions/setup-darwin @@ -182,7 +183,7 @@ jobs: # steps: # - name: Checkout - # uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + # uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # - name: Set up environment # uses: ./.github/actions/setup-darwin @@ -214,7 +215,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up environment uses: ./.github/actions/setup-darwin @@ -253,7 +254,7 @@ jobs: # steps: # - name: Checkout - # uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + # uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # - name: Set up environment # uses: ./.github/actions/setup-darwin @@ -277,7 +278,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up environment uses: ./.github/actions/setup-android @@ -315,7 +316,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Install Flutter uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0 @@ -333,21 +334,32 @@ jobs: uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # pin@v11.6 with: vcpkgDirectory: '${{ github.workspace }}/vcpkg' - vcpkgGitCommitId: '66c0373dc7fca549e5803087b9487edfe3aca0a1' + vcpkgGitCommitId: '${{ env.vcpkg-commit }}' - - name: Cache vcpkg packages and binaries + - name: Cache vcpkg installed tree uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # pin@v6.1.0 with: - path: | - ${{ github.workspace }}/vcpkg/installed - ${{ github.workspace }}/vcpkg-binary-cache - key: vcpkg-${{ runner.os }}-${{ hashFiles('auth0_flutter/windows/vcpkg.json') }} + path: ${{ github.workspace }}/vcpkg/installed + key: vcpkg-installed-${{ runner.os }}-${{ env.vcpkg-commit }}-${{ hashFiles('auth0_flutter/windows/vcpkg.json') }} restore-keys: | - vcpkg-${{ runner.os }}- + vcpkg-installed-${{ runner.os }}-${{ env.vcpkg-commit }}- + + # Keyed by the pinned vcpkg commit (stable across runs) rather + # than github.run_id, so this cache keeps accumulating per-port + # binaries across runs instead of saving a fresh, ever-growing + # entry every run that evicts other jobs' caches once the repo + # is near the per-repo cache limit. + - name: Cache vcpkg binary cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # pin@v6.1.0 + with: + path: ${{ github.workspace }}/vcpkg-binary-cache + key: vcpkg-binary-cache-${{ runner.os }}-${{ env.vcpkg-commit }} + restore-keys: | + vcpkg-binary-cache-${{ runner.os }}- - name: Install vcpkg dependencies run: | - ${{ github.workspace }}\vcpkg\vcpkg install cpprestsdk:x64-windows openssl:x64-windows boost-system:x64-windows boost-date-time:x64-windows boost-regex:x64-windows + ${{ github.workspace }}\vcpkg\vcpkg install --recurse "cpp-httplib[core,openssl]:x64-windows" nlohmann-json:x64-windows openssl:x64-windows shell: cmd env: VCPKG_BINARY_SOURCES: 'clear;files,${{ github.workspace }}/vcpkg-binary-cache,readwrite' @@ -356,10 +368,10 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # pin@v6.1.0 with: path: auth0_flutter/example/build/windows - key: win-example-build-${{ runner.os }}-${{ hashFiles('auth0_flutter/windows/**/*.cpp', 'auth0_flutter/windows/**/*.h', 'auth0_flutter/windows/CMakeLists.txt', 'auth0_flutter/windows/vcpkg.json') }}-${{ hashFiles('auth0_flutter/example/lib/**/*.dart') }} + key: win-example-build-${{ runner.os }}-${{ env.vcpkg-commit }}-${{ hashFiles('auth0_flutter/windows/**/*.cpp', 'auth0_flutter/windows/**/*.h', 'auth0_flutter/windows/CMakeLists.txt', 'auth0_flutter/windows/vcpkg.json') }}-${{ hashFiles('auth0_flutter/example/lib/**/*.dart') }} restore-keys: | - win-example-build-${{ runner.os }}-${{ hashFiles('auth0_flutter/windows/**/*.cpp', 'auth0_flutter/windows/**/*.h', 'auth0_flutter/windows/CMakeLists.txt', 'auth0_flutter/windows/vcpkg.json') }}- - win-example-build-${{ runner.os }}- + win-example-build-${{ runner.os }}-${{ env.vcpkg-commit }}-${{ hashFiles('auth0_flutter/windows/**/*.cpp', 'auth0_flutter/windows/**/*.h', 'auth0_flutter/windows/CMakeLists.txt', 'auth0_flutter/windows/vcpkg.json') }}- + win-example-build-${{ runner.os }}-${{ env.vcpkg-commit }}- - name: Build Windows example app working-directory: auth0_flutter/example @@ -377,15 +389,15 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # pin@v6.1.0 with: path: auth0_flutter/windows/build - key: win-test-build-${{ runner.os }}-${{ hashFiles('auth0_flutter/windows/**/*.cpp', 'auth0_flutter/windows/**/*.h', 'auth0_flutter/windows/CMakeLists.txt', 'auth0_flutter/windows/test/**', 'auth0_flutter/windows/vcpkg.json') }} + key: win-test-build-${{ runner.os }}-${{ env.vcpkg-commit }}-${{ hashFiles('auth0_flutter/windows/**/*.cpp', 'auth0_flutter/windows/**/*.h', 'auth0_flutter/windows/CMakeLists.txt', 'auth0_flutter/windows/test/**', 'auth0_flutter/windows/vcpkg.json') }} restore-keys: | - win-test-build-${{ runner.os }}- + win-test-build-${{ runner.os }}-${{ env.vcpkg-commit }}- - name: Build Windows unit tests working-directory: auth0_flutter/windows run: | cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake -DAUTH0_FLUTTER_ENABLE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug - cmake --build build --config Debug + cmake --build build --config Debug --parallel shell: cmd - name: Run Windows unit tests with coverage @@ -421,7 +433,7 @@ jobs: # steps: # - name: Checkout - # uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + # uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # - name: Gradle cache # uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc @@ -524,7 +536,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Download coverage report for auth0_flutter uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c diff --git a/.github/workflows/publish-af.yml b/.github/workflows/publish-af.yml index 1c5010706..af9eeed9b 100644 --- a/.github/workflows/publish-af.yml +++ b/.github/workflows/publish-af.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Setup Flutter and Dart SDK uses: ./.github/actions/setup-publish diff --git a/.github/workflows/publish-afpi.yml b/.github/workflows/publish-afpi.yml index 732da1bf6..49c4f7dc8 100644 --- a/.github/workflows/publish-afpi.yml +++ b/.github/workflows/publish-afpi.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Setup Flutter and Dart SDK uses: ./.github/actions/setup-publish diff --git a/.github/workflows/rl-scanner.yml b/.github/workflows/rl-scanner.yml index 8043de84e..097e8e82c 100644 --- a/.github/workflows/rl-scanner.yml +++ b/.github/workflows/rl-scanner.yml @@ -28,7 +28,7 @@ jobs: rl-scanner: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..9f5bd5d52 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AI Agent Guidelines for auth0-flutter + +@./CLAUDE.md for all coding guidelines, commands, project structure, code style, testing conventions, and boundaries. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..f7571bd4b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,199 @@ +# AI Agent Guidelines for auth0-flutter + +This document provides context and guidelines for AI coding assistants working with the auth0-flutter codebase. + +## Your Role + +You are a Flutter/Dart SDK engineer working on auth0-flutter, a federated plugin that wraps native Auth0 SDKs (Auth0.swift, Auth0.Android, auth0-spa-js, and a native C++ client on Windows) behind one Dart API. You write small, well-tested, platform-consistent code and keep the two published packages (`auth0_flutter`, `auth0_flutter_platform_interface`) in lockstep. + +--- + +## Working Principles + +Apply these on every task in this repo — they keep changes correct, small, and reviewable. + +- **Think before coding.** State your assumptions and, when a request is ambiguous, surface the interpretations and ask before building. Recommend a simpler approach when you see one. A clarifying question up front beats a wrong implementation. +- **Simplicity first.** Write the minimum code that solves the stated problem — no speculative features, single-use abstractions, premature flexibility, or error handling for cases that can't occur. +- **Surgical changes.** Touch only what the request requires. Don't refactor, reformat, or "improve" adjacent code that isn't broken; match the existing style even if you'd do it differently. Every changed line should trace directly to the request. Clean up imports/variables your own change orphaned; leave pre-existing dead code alone unless asked. +- **Goal-driven execution.** Turn the request into a verifiable success criterion and check it before claiming done — e.g. "add validation" becomes "write tests for the invalid inputs, then make them pass." Don't report success you haven't verified. + +--- + +## Project Overview + +**auth0-flutter** is the Auth0 SDK for Android, iOS, macOS, Windows, and web Flutter apps. + +- **Language:** Dart 3.5.0+ (Flutter 3.24.0+), plus native Kotlin (Android), Swift (iOS/macOS via SPM/CocoaPods), and C++ (Windows) +- **Tech Stack:** Federated Flutter plugin — a Dart-facing package (`auth0_flutter`) and a platform-interface package (`auth0_flutter_platform_interface`) that dispatches to native implementations over `MethodChannel`; web uses `dart:js_interop` over `auth0-spa-js` +- **Package Manager:** `pub` (Dart/Flutter); native deps via CocoaPods/SPM (Apple), Gradle (Android), vcpkg (Windows) +- **Minimum Platform Version:** Android API 21+, iOS 14+, macOS 11+, Windows 10+ +- **Dependencies:** `auth0_flutter_platform_interface`, `plugin_platform_interface`, `web` · native: SimpleKeychain 1.3.0 (Apple) · test: `mockito`, `flutter_test`, `dart_jsonwebtoken` — see `auth0_flutter/pubspec.yaml` and `auth0_flutter_platform_interface/pubspec.yaml` for the full list + +--- + +## Project Structure + +``` +auth0-flutter/ +├── auth0_flutter/ # Published package: public Dart API + native glue +│ ├── lib/ +│ │ ├── auth0_flutter.dart # Mobile/desktop entry point (Auth0 class) +│ │ ├── auth0_flutter_web.dart # Web entry point (Auth0Web class) +│ │ └── src/ +│ │ ├── mobile/ # Auth, MFA, My Account, Passwordless, CredentialsManager, WebAuthentication +│ │ ├── web/ # JS interop with auth0-spa-js +│ │ └── desktop/ # Windows web authentication +│ ├── android/, ios/, macos/, darwin/ # Native platform implementations (Kotlin, Swift) +│ ├── windows/ # Native C++ implementation (no native SDK dependency) +│ ├── example/ # Sample app used for manual verification and native/integration tests +│ └── test/ # Dart unit tests +├── auth0_flutter_platform_interface/ # Published package: platform contract +│ └── lib/src/ # MethodChannel platform implementations, options/models, exceptions +├── scripts/generate-symlinks.sh # Keeps ios/macos/darwin native sources in sync (see Boundaries) +└── .github/workflows/ # CI: analyze, test, native unit tests, publish, security scans +``` + +### Key Files + +| File | Purpose | +|------|---------| +| `auth0_flutter/lib/auth0_flutter.dart` | Public entry point for mobile/desktop (`Auth0` class) | +| `auth0_flutter/lib/auth0_flutter_web.dart` | Public entry point for web (`Auth0Web` class) | +| `auth0_flutter/lib/src/version.dart` | SDK version string sent as part of the Auth0-Client user agent | +| `auth0_flutter_platform_interface/lib/src/user_agent.dart` | `UserAgent` model carried on every native request (see Boundaries) | +| `auth0_flutter_platform_interface/lib/src/request/request.dart` | Base request types that attach `Account` + `UserAgent` to every platform call | +| `auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/` | Canonical Swift source tree; `ios/`/`macos/`/`darwin/Classes` are generated symlinks — never edit directly | + +--- + +## Boundaries + +### ✅ Always Do + +- Run tests before committing. +- Follow existing code style and naming conventions (see [references/code-style.md](references/code-style.md)). +- Add unit tests for new functionality. +- Update `README.md` and `EXAMPLES.md` (and `auth0_flutter/example` where applicable) in the same PR when changing the public API, configuration options, or supported integration patterns. +- Edit Apple native source **only** under `auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/`, then run `scripts/generate-symlinks.sh` from the repo root — `ios/Classes`, `macos/Classes`, and `darwin/Classes` are generated symlinks (enforced by the `check-symlinks` CI workflow and a pre-commit hook after `git config core.hooksPath .githooks`). +- When adding a request path to a new native platform call, thread the existing `UserAgent` (`auth0_flutter_platform_interface/lib/src/user_agent.dart`, `{name: 'auth0-flutter', version}`) through it rather than hand-rolling a new identifier — every `BaseRequest` subtype already carries it into the native SDK's `Auth0-Client` header. +- Keep `auth0_flutter` and `auth0_flutter_platform_interface` versions in sync — both currently track the same version number (see each package's `pubspec.yaml`); a version bump in one is normally accompanied by a matching bump in the other. +- When updating native Apple SDK versions (Auth0.swift, JWTDecode.swift, or SimpleKeychain), update the version in **both** `auth0_flutter/darwin/auth0_flutter.podspec` **and** `auth0_flutter/darwin/auth0_flutter/Package.swift` — they must stay in sync for CocoaPods and SPM consumers. + +### ⚠️ Ask First + +- **Any breaking change — always ask first.** Never make a breaking change on your own initiative; stop and ask the maintainer before writing it. If one is approved, check `auth0_flutter/MIGRATION_GUIDE.md` for the guide covering the last major and match its structure/tone for any new entry. +- Adding new dependencies (Dart, CocoaPods/SPM, Gradle, or vcpkg). +- Modifying public API signatures in `auth0_flutter/lib/` or `auth0_flutter_platform_interface/lib/`. +- Changes to `.github/workflows/`, `.github/actions/`, or `.coderabbit.yaml`. +- Modifying credential storage/security code (`CredentialsManager`, keychain/SharedPreferences handling, DPoP). +- Running native/integration test tiers that need real credentials or emulators — the `test-ios-unit`, `test-macos-unit`, and `test-android-unit` CI jobs need Auth0 tenant `vars` and Xcode/emulator toolchains not present in a normal dev shell; see [references/testing.md](references/testing.md). + +### 🚫 Never Do + +- Commit secrets, API keys, or tokens — `auth0_flutter/example/.env` is gitignored; only `.env.example` is tracked. +- Hand-edit `ios/Classes`, `macos/Classes`, or `darwin/Classes` — they are symlinks generated from `darwin/auth0_flutter/Sources/auth0_flutter/` by `scripts/generate-symlinks.sh`. +- Modify auto-generated files (`*.g.dart`, `*.mocks.dart`) by hand — regenerate with `build_runner` or `mockito` codegen instead. +- Remove or skip failing tests without fixing them. +- Modify vendor/build output directories (`.dart_tool/`, `build/`, `Pods/`, `.build/`, `vcpkg/`). +- Break backward compatibility without asking first (see Ask First) and getting explicit approval. + +--- + +## Security Considerations + +- **Token storage:** Credentials are persisted via the native `CredentialsManager` — SimpleKeychain 1.3.0 on iOS/macOS (`auth0_flutter/darwin/.../CredentialsManager/CredentialsManagerHandler.swift`), the native `SecureCredentialsManager` (Android Keystore-backed encrypted storage) on Android (`auth0_flutter/android/.../request_handlers/credentials_manager/`), and OS-level secure storage on Windows. Never log tokens, and never bypass `CredentialsManager` to persist credentials by hand. +- **PKCE:** Web Authentication flows delegate PKCE handling to the wrapped native SDKs (Auth0.swift, Auth0.Android, auth0-spa-js) and the Windows C++ client — do not implement a parallel authorization-code flow. +- **DPoP:** `useDPoP` is a first-class option on `ApiRequest`/`CredentialsManagerRequest` (`auth0_flutter_platform_interface/lib/src/request/request.dart`) — preserve it when adding new request types that should support DPoP-bound tokens. +- **Secrets in CI:** Tenant credentials (`AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`) are injected via GitHub Actions `vars`, never hardcoded in workflows or `example/.env` (only `.env.example` is committed). +- Never commit secrets, API keys, or tokens. + +--- + +> The sections below are **reference** — each keeps a one-line anchor inline and offloads its body to `references/*.md` behind a linked pointer. + +## Commands + +The default unit-test/analyze commands run without native tooling. See [references/commands.md](references/commands.md) for the full command reference, including native (Android/iOS/macOS/Windows) build and test commands. Read this when you need to run, build, or test something beyond a quick Dart analyze/test. + +```bash +# Analyze (auth0_flutter — from CI) +cd auth0_flutter && flutter analyze --no-fatal-warnings --no-fatal-infos + +# Analyze (auth0_flutter_platform_interface — from CI) +cd auth0_flutter_platform_interface && flutter analyze + +# Unit tests (auth0_flutter — from CI, requires example/.env; copy from .env.example) +cd auth0_flutter && flutter test --tags browser --platform chrome && flutter test --coverage --exclude-tags browser + +# Unit tests (auth0_flutter_platform_interface — from CI) +cd auth0_flutter_platform_interface && flutter test --coverage + +# Format +dart format . +``` + +--- + +## Testing + +- **Framework:** `flutter_test` + `mockito` (Dart); JUnit/Robolectric (Android, via `./gradlew koverXmlReportDebug`); XCTest (iOS/macOS); a custom C++ test binary built via CMake (Windows) +- **Test Location:** `auth0_flutter/test/`, `auth0_flutter_platform_interface/test/` +- **Coverage Tool:** `flutter test --coverage` (lcov) for Dart; JaCoCo/Kover for Android; xcresultparser→Cobertura for iOS; OpenCppCoverage for Windows — all uploaded to Codecov +- **Coverage Threshold:** patch threshold 50%, project-drop threshold 2% (`codecov.yml`) + +The default `flutter test` suites (Dart) run without credentials — they need only `example/.env` copied from `.env.example` for `auth0_flutter`. The native unit-test tiers (`test-ios-unit`, `test-macos-unit`, `test-android-unit` in CI) need a real Auth0 tenant (`AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`) plus Xcode/Android toolchains — treat running these locally as Ask First (see Boundaries). See [references/testing.md](references/testing.md) for conventions (mocking with `mockito`, `.mocks.dart` regeneration, and the disabled Appium smoke-test tier). + +--- + +## Code Style + +### Naming Conventions + +Dart defaults: `UpperCamelCase` for classes/enums, `lowerCamelCase` for members/variables, `lowercase_with_underscores` for files — enforced by `flutter_lints` plus repo-specific rules in `analysis_options.yaml` (identical in both packages, except `avoid_final_parameters`/`prefer_final_parameters` below). + +**CI-enforced rules** (from `analysis_options.yaml`, `flutter analyze` fails otherwise): +- `prefer_single_quotes`, `lines_longer_than_80_chars`, `always_declare_return_types`, `type_annotate_public_apis` +- `prefer_final_locals`, `prefer_final_in_for_each` — plus `avoid_final_parameters` in `auth0_flutter` (newer Dart SDKs reject `final` on non-constructor parameters); `auth0_flutter_platform_interface` still has the deprecated `prefer_final_parameters` pending the same cleanup +- `strict-casts`, `strict-inference`, `strict-raw-types` (analyzer language modes), `unawaited_futures: error`, `missing_required_param: error` + +See [references/code-style.md](references/code-style.md) for good/bad examples and the dominant options/models pattern. + +--- + +## Git Workflow + +- **Branch naming:** no enforced convention detected; recent branches follow `feat/` / `fix/`. +- **Commit messages:** Conventional Commits (`feat:`, `fix:`, `fix(ci):`, `build(deps):`, `docs:`, `chore:`), matching recent `git log` history. +- **PR titles:** must start with `af:` or `afpi:` (enforced by the `pr-title-checker` workflow against `.github/pr-title-checker-config.json`) — `af:` for `auth0_flutter` changes, `afpi:` for `auth0_flutter_platform_interface` changes. +- **PR body:** follow `.github/PULL_REQUEST_TEMPLATE.md` — check off test coverage and documentation, fill in Changes/References/Testing sections. + +See [references/git-workflow.md](references/git-workflow.md) for more detail. + +--- + +## Common Pitfalls + +See [references/pitfalls.md](references/pitfalls.md) for the full list. Highlights: +- Editing `ios/Classes`, `macos/Classes`, or `darwin/Classes` directly — these are generated symlinks; edit `darwin/auth0_flutter/Sources/auth0_flutter/` and re-run `scripts/generate-symlinks.sh`. +- Forgetting `example/.env` (copied from `.env.example`) before running `auth0_flutter` analyze/tests — CI does this via `cp`/`Copy-Item` per platform. +- Web-only code (`dart:js_interop`) must stay behind the `web/` platform split (`auth0_flutter_plugin_real.dart` vs `_stub.dart`) so non-web platforms still compile. + +--- + +## Docs Update Rules + +> Treat documentation as a first-class deliverable. A PR that adds or changes public API, configuration, or integration patterns is **not complete** until the relevant docs are updated in the same PR. + +### Tracked Docs + +| File | Covers | Exists | +|------|--------|--------| +| `README.md` (root) | Package overview, links to package READMEs | present | +| `auth0_flutter/README.md` | Installation, requirements, quickstart links, feature overview | present | +| `auth0_flutter/EXAMPLES.md` | Runnable code samples for every public API area (Web Auth, Credentials Manager, Auth API, MFA, My Account, Passkeys, etc.) | present | +| `auth0_flutter_platform_interface/README.md` | Brief note that this package is the platform contract, not for direct use | present | +| `auth0_flutter/example/` | Standalone runnable sample app exercising the public API | present | + +See [references/docs-update.md](references/docs-update.md) for the full code-to-docs mapping table. + +> When you touch code that maps to a doc above, update that doc **in the same PR** — do not defer. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ed0f5db7b..d5c22ee9f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -10,6 +10,23 @@ The `.githooks` folder contains git hooks specific to this repository. To make s git config core.hooksPath .githooks ``` +## iOS/macOS dependency versions + +The native Auth0 dependencies (`Auth0.swift`, `JWTDecode.swift`, `SimpleKeychain`) are declared in two places for the iOS/macOS SDK: + +- **`auth0_flutter/darwin/auth0_flutter/Package.swift`** — the Swift Package Manager manifest. This is the **single source of truth**, and the only file [Dependabot](.github/dependabot.yml) updates (via the `swift` ecosystem). CocoaPods has no Dependabot support, so bumps land here. +- **The three CocoaPods podspecs** (`auth0_flutter/ios`, `auth0_flutter/macos`, `auth0_flutter/darwin`) — consumed by apps that use CocoaPods rather than SPM. These must be kept in sync with `Package.swift` manually. + +To propagate a version change from `Package.swift` into the podspecs, run **from the repository root**: + +```sh +scripts/sync-darwin-deps.sh +``` + +The [`Darwin dependencies` workflow](.github/workflows/check-darwin-deps.yml) enforces this on every PR: it runs the script and fails if the podspecs are out of sync, so a Dependabot bump of `Auth0.swift` will show a red check until someone runs the script and commits the result. + +> This whole mechanism only exists because the podspecs duplicate the SPM version pin. Once the CocoaPods podspecs are no longer needed, the script and the workflow can both be removed. + ## Running package tests Run the unit tests for both packages using `flutter test` in **/auth0_flutter** and **/auth0_flutter_platform_interface** respectively. diff --git a/appium-test/package-lock.json b/appium-test/package-lock.json index ddcdc62a1..faafc4b0f 100644 --- a/appium-test/package-lock.json +++ b/appium-test/package-lock.json @@ -491,9 +491,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -1609,9 +1609,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "dev": true, "license": "MIT", "engines": { @@ -3066,9 +3066,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "dev": true, "license": "MIT", "engines": { diff --git a/auth0_flutter/CHANGELOG.md b/auth0_flutter/CHANGELOG.md index b73bfe56c..3da374dfe 100644 --- a/auth0_flutter/CHANGELOG.md +++ b/auth0_flutter/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [af-v2.6.0](https://github.com/auth0/auth0-flutter/tree/af-v2.6.0) (2026-07-31) +[Full Changelog](https://github.com/auth0/auth0-flutter/compare/af-v2.5.0...af-v2.6.0) + +**Added** +- feat: add passkeys support for web [\#907](https://github.com/auth0/auth0-flutter/pull/907) ([NandanPrabhu](https://github.com/NandanPrabhu)) +- feat: surface IPSIE session_expiry claim across Android, iOS and web [\#904](https://github.com/auth0/auth0-flutter/pull/904) ([utkrishtsahu](https://github.com/utkrishtsahu)) + +**Fixed** +- fix: Prevent named-pipe squatting in Windows example app via FILE_FLAG_FIRST_PIPE_INSTANCE [\#905](https://github.com/auth0/auth0-flutter/pull/905) ([NandanPrabhu](https://github.com/NandanPrabhu)) + ## [af-v2.5.0](https://github.com/auth0/auth0-flutter/tree/af-v2.5.0) (2026-07-17) [Full Changelog](https://github.com/auth0/auth0-flutter/compare/af-v2.4.0...af-v2.5.0) diff --git a/auth0_flutter/EXAMPLES.md b/auth0_flutter/EXAMPLES.md index 90b00dd43..d30df1cfb 100644 --- a/auth0_flutter/EXAMPLES.md +++ b/auth0_flutter/EXAMPLES.md @@ -38,6 +38,7 @@ - [Sign up with database connection](#sign-up-with-database-connection) - [Log in with passkeys](#log-in-with-passkeys) - [Sign up with passkeys](#sign-up-with-passkeys) + - [Passkeys on the Web](#passkeys-on-the-web) - [Passwordless Login](#passwordless-login) - [Retrieve user information](#retrieve-user-information) - [Renew credentials](#renew-credentials) @@ -503,7 +504,13 @@ cd C:\vcpkg setx VCPKG_ROOT "C:\vcpkg" ``` -The plugin's `vcpkg.json` manifest automatically pulls the required packages (`cpprestsdk`, `openssl`, `boost-system`, `boost-date-time`, `boost-regex`) at build time — no manual `vcpkg install` is needed. +The plugin's `vcpkg.json` manifest (`auth0_flutter/windows/vcpkg.json`) lists the required packages (`cpp-httplib`, `nlohmann-json`, `openssl`), but because it lives in the plugin's own directory rather than your app's `windows/` root, vcpkg's manifest mode won't auto-install them. Install them explicitly into your vcpkg instance: + +```powershell +C:\vcpkg\vcpkg install --recurse "cpp-httplib[core,openssl]:x64-windows" nlohmann-json:x64-windows openssl:x64-windows +``` + +> 💡 **Upgrading from an older auth0_flutter version?** Previous versions depended on `cpprestsdk` and `boost-system`/`boost-date-time`/`boost-regex`. If your vcpkg instance was already set up for those, run the command above to install the new packages (the old ones can be left installed or removed with `vcpkg remove cpprestsdk boost-system boost-date-time boost-regex`). #### 2. Configure your app's CMakeLists.txt @@ -525,7 +532,7 @@ project(your_app LANGUAGES CXX) # ... rest of your CMakeLists.txt ... ``` -> ⚠️ The `CMAKE_TOOLCHAIN_FILE` line **must** appear before `project()`. If it appears after, CMake will have already configured the compiler and vcpkg packages will not be found, resulting in build errors like `Could not find a package configuration file provided by "cpprestsdk"`. +> ⚠️ The `CMAKE_TOOLCHAIN_FILE` line **must** appear before `project()`. If it appears after, CMake will have already configured the compiler and vcpkg packages will not be found, resulting in build errors like `Could not find a package configuration file provided by "httplib"`. #### 3. Register the custom URL scheme (protocol handler) @@ -664,6 +671,7 @@ await auth0.windowsWebAuthentication().logout( - [Check for stored credentials](#check-for-stored-credentials) - [Retrieve stored credentials](#retrieve-stored-credentials) +- [Session expiry from an upstream IdP](#-session-expiry-from-an-upstream-idp) - [Retrieve API credentials for a specific audience (MRRT)](#retrieve-api-credentials-for-a-specific-audience-mrrt) - [Retrieve user profile](#retrieve-user-profile) - [Custom implementations](#custom-implementations) @@ -700,6 +708,51 @@ final credentials = await auth0.credentialsManager.credentials(); > 💡 You do not need to call `credentialsManager.storeCredentials()` afterward. The Credentials Manager automatically persists the renewed credentials. +### 🕒 Session expiry from an upstream IdP + +> ⚠️ **Early Access:** upstream IdP session expiry (the IPSIE [`session_expiry`](https://openid.github.io/ipsie-openid-sl1/draft-openid-ipsie-sl1-profile.html) claim) is an **Early Access** feature. The API surface described here (the `sessionExpiry` field and the `isSessionExpired` error) may change before it is generally available. + +When a user authenticates through an upstream Identity Provider (for example Okta) over an enterprise connection that has **"Use ID Token for Session Expiry"** enabled (`id_token_session_expiry_supported: true`), Auth0 adds a `session_expiry` claim to the ID token. This claim is an absolute point in time (a Unix-seconds timestamp) that represents when the upstream IdP session ends, and it acts as a **hard ceiling** on the local session: the app session can never outlive the upstream IdP session. + +> ⚠️ **The claim must be in seconds, not milliseconds.** If the ceiling is set by a Post-Login Action, remember to divide by 1000 (e.g. `Math.floor(Date.now() / 1000)`). A milliseconds value reads as a timestamp tens of thousands of years in the future, which is treated as **no ceiling** — enforcement silently switches off rather than erroring. + +**Enforcement is transparent — you do not need to write any new code to have the ceiling enforced.** Once the ceiling passes, the underlying platform SDK treats the stored credentials as expired and skips refresh-token renewal, so `credentials()` raises a `CredentialsManagerException`. The ceiling has its own dedicated error, distinct from "no credentials", so branch on `isSessionExpired` to send the user through a fresh login: + +```dart +try { + final credentials = await auth0.credentialsManager.credentials(); + // Use the credentials +} on CredentialsManagerException catch (e) { + if (e.isSessionExpired) { + // The upstream IdP session_expiry ceiling was reached — the local session + // can no longer be renewed. Send the user through the login flow again. + } else if (e.isNoCredentialsFound) { + // No stored credentials (e.g. logged out) — send the user to login. + } +} +``` + +The ceiling is **layered on top of** the access-token expiry and any idle/absolute timeouts — it does not replace them. The session ends at whichever limit is reached first. + +You can also read the value for your own app logic (for example, to show a session countdown) via the new `sessionExpiry` field on `Credentials`: + +```dart +final credentials = await auth0.credentialsManager.credentials(); + +// null when the connection option is not enabled, or the session predates the +// feature. A null value means "no upstream ceiling". +final sessionExpiry = credentials.sessionExpiry; +if (sessionExpiry != null) { + print('Upstream IdP session ends at: $sessionExpiry'); +} +``` + +> ℹ️ Enforcement applies a small negative leeway (about 30 seconds) for clock skew, so the session is treated as expired slightly **before** this exact timestamp. Account for this if you build a countdown off `sessionExpiry`, otherwise the session may end a little earlier than the value shown. + +> ⚠️ **Upgrade note:** once this feature is enabled on your connection, `credentials()` can raise a session-expired error (`isSessionExpired`) for a user who was previously logged in, when the ceiling is reached. If your app assumed `credentials()` always resolves after login, make sure it handles this case (it already does if you catch `CredentialsManagerException` as shown above). +> +> On the web, enforcement is performed by [auth0-spa-js](https://github.com/auth0/auth0-spa-js) and requires **v2.22.0 or later**. + ### Retrieve API credentials for a specific audience (MRRT) If your app needs an access token for a **different API** than the one it logged in with, use `getApiCredentials()`. It exchanges the stored refresh token for an access token scoped to the requested `audience` using a [Multi-Resource Refresh Token (MRRT)](https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token). If valid API credentials for that audience are already cached, they are returned without a network call; otherwise a new token is fetched and cached for next time. @@ -1021,6 +1074,7 @@ final credentials = await auth0Web.credentials(); - [Sign up with database connection](#sign-up-with-database-connection) - [Log in with passkeys](#log-in-with-passkeys) - [Sign up with passkeys](#sign-up-with-passkeys) +- [Passkeys on the Web](#passkeys-on-the-web) - [Passwordless Login](#passwordless-login) - [Passwordless OTP on database connections](#passwordless-otp-on-database-connections) - [Retrieve user information](#retrieve-user-information) @@ -1093,7 +1147,7 @@ final databaseUser = await auth0.api.signup( ### Log in with passkeys -> This feature is available on **iOS 16.6+** and **Android 9+ (API 28)** only. +> This feature is available on **iOS 16.6+**, **Android 9+ (API 28)**, and **Web** (modern browsers with WebAuthn support). See [Passkeys on the Web](#passkeys-on-the-web) below for the web-specific flow. [Passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys) let an existing user log in with a biometric or device PIN instead of a password, using the platform authenticator (Face ID / Touch ID on iOS, the Credential Manager on Android). @@ -1154,7 +1208,7 @@ final credentials = await auth0.api.passkeyCredentialExchange( ### Sign up with passkeys -> This feature is available on **iOS 16.6+** and **Android 9+ (API 28)** only. +> This feature is available on **iOS 16.6+**, **Android 9+ (API 28)**, and **Web** (modern browsers with WebAuthn support). See [Passkeys on the Web](#passkeys-on-the-web) below for the web-specific flow. [Passkeys](https://auth0.com/docs/authenticate/database-connections/passkeys) let users register with a biometric or device PIN instead of a password, using the platform authenticator (Face ID / Touch ID on iOS, the Credential Manager on Android). @@ -1171,10 +1225,17 @@ The SDK exposes **two** methods for passkey signup — `passkeySignupChallenge` You can identify the new user with any combination of `email`, `phoneNumber`, `username`, `name`, `givenName`, `familyName`, `nickname`, and `picture`, depending on how your connection is configured. +> ⚠️ **Only `email` is accepted by default.** A database connection accepts `email` as the signup identifier unless [Flexible Identifiers](https://auth0.com/docs/authenticate/database-connections/flexible-identifiers) is enabled for it, which additionally lets you sign up with `phoneNumber` and/or `username`. Passing an identifier your connection isn't configured for (for example, `phoneNumber` on an email-only connection) fails the signup challenge request with: +> ```json +> { "error": "invalid_request", "error_description": "Invalid user profile attribute: phone_number" } +> ``` +> Enable Flexible Identifiers for the connection in the [Auth0 Dashboard](https://manage.auth0.com/#/connections/database) (**Authentication > Database > [your connection] > Settings**) if you need to sign up with `phoneNumber` or `username`. + ```dart // 1. Request a registration challenge from Auth0. You can identify the new // user with any combination of email, phoneNumber, username, name, -// givenName, familyName, nickname, and picture. +// givenName, familyName, nickname, and picture — see the note above on +// which of these your connection actually accepts. final challenge = await auth0.api.passkeySignupChallenge( email: 'jane.smith@example.com', name: 'Jane Smith', @@ -1219,6 +1280,123 @@ final credentials = await auth0.api.passkeyCredentialExchange( +### Passkeys on the Web + +Passkeys are supported on web via `Auth0Web`, backed by `@auth0/auth0-spa-js`'s passkey API (requires **auth0-spa-js v2.24.0 or later** loaded on your page). The flow is the same three steps as native (challenge → credential manager → exchange), but step 2 uses the browser's built-in [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) (`navigator.credentials.create()`/`.get()`) instead of a native platform authenticator. + +`Auth0Web.getTokenByPasskey`'s `authResponse` accepts the raw credential object returned directly by `navigator.credentials.create()`/`.get()` — no manual serialization needed (unlike native, which takes a JSON string). + +> ⚠️ `navigator.credentials.create()`/`.get()` require a user gesture, so call `passkeySignupChallenge`/`passkeyLoginChallenge` from within a click handler (not, for example, from `onLoad`). + +> ⚠️ **`passkeySignupChallenge` only accepts `email` unless Flexible Identifiers is enabled.** Just like native, the identifier you pass (`email`, `phoneNumber`, `username`) must be one your connection is configured to accept — see the note in [Sign up with passkeys](#sign-up-with-passkeys) above. + +```dart +import 'package:auth0_flutter/auth0_flutter_web.dart'; +import 'package:web/web.dart' as web; + +final auth0Web = Auth0Web('YOUR_AUTH0_DOMAIN', 'YOUR_AUTH0_CLIENT_ID'); + +Future signUpWithPasskey() async { + // 1. Request a signup challenge from Auth0. + final challenge = await auth0Web.passkeySignupChallenge( + email: 'jane.smith@example.com', + name: 'Jane Smith', + givenName: 'Jane', + familyName: 'Smith', + connection: 'Username-Password-Authentication'); + + // 2. Present the browser's WebAuthn UI. `authParamsPublicKey` is already + // decoded and ready to pass directly to navigator.credentials.create(). + final credential = await web.window.navigator.credentials.create( + web.CredentialCreationOptions( + publicKey: challenge.authParamsPublicKey + as web.PublicKeyCredentialCreationOptions)); + + // 3. Exchange the raw credential for Auth0 tokens. + final credentials = await auth0Web.getTokenByPasskey( + authSession: challenge.authSession, + authResponse: credential, + connection: 'Username-Password-Authentication'); +} +``` + +
+ Log in with an existing passkey on the web + +```dart +Future logInWithPasskey() async { + final challenge = await auth0Web.passkeyLoginChallenge( + connection: 'Username-Password-Authentication'); + + final credential = await web.window.navigator.credentials.get( + web.CredentialRequestOptions( + publicKey: challenge.authParamsPublicKey + as web.PublicKeyCredentialRequestOptions)); + + final credentials = await auth0Web.getTokenByPasskey( + authSession: challenge.authSession, + authResponse: credential, + connection: 'Username-Password-Authentication'); +} +``` + +
+ +#### Handling errors on the web + +`passkeySignupChallenge`, `passkeyLoginChallenge`, and `getTokenByPasskey` all throw a [`WebException`](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/WebException-class.html) on failure, decoded from whatever `auth0-spa-js` throws: + +| Source | `WebException.code` | Notes | +| :--- | :--- | :--- | +| Challenge request fails (`passkeySignupChallenge`/`passkeyLoginChallenge`) | `PASSKEY_ERROR` | `details['code']` carries the underlying `auth0-spa-js` code, e.g. `passkey_register_error`, `passkey_challenge_error`, `passkey_not_supported` (WebAuthn unavailable in this browser). | +| `getTokenByPasskey`'s credential-shape check fails | `PASSKEY_ERROR` | `details['code'] == 'passkey_invalid_credential'` — the `authResponse` isn't a valid attestation or assertion response. | +| `getTokenByPasskey`'s token exchange requires MFA | `MFA_REQUIRED` | Same shape as [`credentials()`/`customTokenExchange()`](#-multi-factor-authentication-mfa) — `details['mfaToken']` is ready to pass to `Auth0Web.mfa(mfaToken:)`. | +| `getTokenByPasskey`'s token exchange fails for another reason (e.g. `invalid_grant`, `access_denied`, an expired `authSession`) | `AUTHENTICATION_ERROR` | `details['code']` carries the OAuth error code returned by `/oauth/token`. | + +```dart +try { + final credentials = await auth0Web.getTokenByPasskey( + authSession: challenge.authSession, + authResponse: credential, + connection: 'Username-Password-Authentication'); +} on WebException catch (e) { + if (e.code == 'MFA_REQUIRED') { + final mfa = auth0Web.mfa(mfaToken: e.details['mfaToken'] as String); + // continue the MFA flow — see Multi-Factor Authentication (MFA) below + } else { + print('Passkey error: ${e.code} (${e.details['code']}) — ${e.message}'); + } +} +``` + +> ⚠️ **`navigator.credentials.create()`/`.get()` errors are not `WebException`s.** The WebAuthn ceremony (step 2) is a browser API call your app makes directly — `Auth0Web` has no visibility into it, so its failures surface as a raw [`DOMException`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException), not a `WebException`. Catch it separately and branch on `DOMException.name`, for example: +> ```dart +> import 'package:web/web.dart' as web; +> +> try { +> final credential = await web.window.navigator.credentials.create( +> web.CredentialCreationOptions(publicKey: challenge.authParamsPublicKey +> as web.PublicKeyCredentialCreationOptions)); +> } on web.DOMException catch (e) { +> switch (e.name) { +> case 'NotAllowedError': +> // The user cancelled the prompt, denied permission, or the operation +> // timed out — by far the most common case in practice. +> print('Passkey creation was cancelled.'); +> case 'InvalidStateError': +> // A passkey for this account/authenticator already exists. +> print('A passkey already exists for this account.'); +> case 'SecurityError': +> // The page's origin doesn't match the relying party ID (misconfigured +> // custom domain, or running on an origin other than the one the +> // connection's passkeys are configured for). +> print('Passkey security error: ${e.message}'); +> default: +> print('Passkey error: ${e.name} — ${e.message}'); +> } +> } +> ``` + ### Passwordless Login Passwordless is a two-step authentication flow that requires the **Passwordless OTP** grant to be enabled for your Auth0 application. Check [our documentation](https://auth0.com/docs/get-started/applications/application-grant-types) for more information. diff --git a/auth0_flutter/README.md b/auth0_flutter/README.md index 9ece6e6b9..deebe8efc 100644 --- a/auth0_flutter/README.md +++ b/auth0_flutter/README.md @@ -9,9 +9,9 @@ 📚 Documentation • 🚀 Getting started • 🌐 API reference • 💬 Feedback -## What's New: v2.3.0 +## What's New -**auth0_flutter v2.3.0** adds **Passkeys** support. You can now authenticate users with passkeys via the `webAuthentication()` flow on Android, iOS, and Web. See the [Auth0 passkeys documentation](https://auth0.com/docs/authenticate/database-connections/passkeys) for setup instructions. +**auth0_flutter** supports **Passkeys** on Android, iOS, and Web. On mobile, use `Auth0.api.passkeyLoginChallenge`/`passkeySignupChallenge`/`passkeyCredentialExchange`; on web, use the equivalent `Auth0Web.passkeyLoginChallenge`/`passkeySignupChallenge`/`getTokenByPasskey` (requires `auth0-spa-js` v2.24.0+). See the [Auth0 passkeys documentation](https://auth0.com/docs/authenticate/database-connections/passkeys) for setup instructions. ## Documentation @@ -368,11 +368,15 @@ final auth0Web = Auth0Web('YOUR_AUTH0_DOMAIN', 'YOUR_AUTH0_CLIENT_ID'); Finally, in your `index.html` add the following ` + ``` > ⚠️ If you use [Custom Token Exchange](EXAMPLES.md#custom-token-exchange) with delegation/impersonation (an actor token), you must reference `auth0-spa-js` **2.20.0 or later**. Actor-token support was added to the underlying SPA SDK in [2.20.0](https://github.com/auth0/auth0-spa-js/releases/tag/v2.20.0); older versions silently drop the actor token, so the returned ID token has no `act` claim and `credentials.user.actor` is `null`. +> ⚠️ Enforcement of the [`session_expiry` upstream-IdP session ceiling](EXAMPLES.md#-session-expiry-from-an-upstream-idp) requires `auth0-spa-js` **2.22.0 or later** on the web. Support was added to the underlying SPA SDK in [2.22.0](https://github.com/auth0/auth0-spa-js/releases/tag/v2.22.0). + +> ⚠️ [Passkeys on the web](EXAMPLES.md#passkeys-on-the-web) (`passkeySignupChallenge`/`passkeyLoginChallenge`/`getTokenByPasskey`) require `auth0-spa-js` **2.24.0 or later**. The passkey API was added to the underlying SPA SDK in [2.24.0](https://github.com/auth0/auth0-spa-js/releases/tag/v2.24.0); older versions do not expose it. + ### Logging in #### 📱 Mobile/macOS @@ -586,8 +590,8 @@ void dispose() { - [Check for stored credentials](EXAMPLES.md#check-for-stored-credentials) - check if the user is already logged in when your app starts up. - [Retrieve stored credentials](EXAMPLES.md#retrieve-stored-credentials) - fetch the user's credentials from the storage, automatically renewing them if they have expired. - [Retrieve user information](EXAMPLES.md#retrieve-user-information) - fetch the latest user information from the `/userinfo` endpoint. -- [Log in with passkeys](EXAMPLES.md#log-in-with-passkeys) - authenticate an existing user with a passkey using the platform authenticator (iOS/Android only). -- [Sign up with passkeys](EXAMPLES.md#sign-up-with-passkeys) - register a new user with a passkey using the platform authenticator (iOS/Android only). +- [Log in with passkeys](EXAMPLES.md#log-in-with-passkeys) - authenticate an existing user with a passkey using the platform authenticator (iOS/Android; see [Passkeys on the Web](EXAMPLES.md#passkeys-on-the-web) for the web equivalent). +- [Sign up with passkeys](EXAMPLES.md#sign-up-with-passkeys) - register a new user with a passkey using the platform authenticator (iOS/Android; see [Passkeys on the Web](EXAMPLES.md#passkeys-on-the-web) for the web equivalent). - [Native to Web SSO](EXAMPLES.md#native-to-web-sso) - obtain a session transfer token to authenticate a WebView without re-prompting the user. - [Multi-Factor Authentication (MFA)](EXAMPLES.md#-multi-factor-authentication-mfa) - complete an MFA flow mid-login using an `mfa_token`: list, challenge, enroll, and verify factors. - [Handle Android process death](#android-handle-process-death-during-login) - recover credentials when the OS kills your app during login. @@ -603,6 +607,7 @@ void dispose() { - [Handling credentials on the web](EXAMPLES.md#handling-credentials-on-the-web) - how to check and retrieve credentials on the web platform. - [Multi-Factor Authentication (MFA)](EXAMPLES.md#-multi-factor-authentication-mfa) - complete an MFA flow mid-login using an `mfa_token`. Backed by [auth0-spa-js](https://github.com/auth0/auth0-spa-js) and requires auth0-spa-js v2.21.0+ loaded on your page. +- [Passkeys on the Web](EXAMPLES.md#passkeys-on-the-web) - authenticate with a passkey using the browser's WebAuthn API. Backed by [auth0-spa-js](https://github.com/auth0/auth0-spa-js) and requires auth0-spa-js v2.24.0+ loaded on your page. ## API reference @@ -782,6 +787,9 @@ try { - [credentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/credentials.html) - [mfa](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/mfa.html) - drive a Multi-Factor Authentication flow with an `mfa_token` - [hasValidCredentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/hasValidCredentials.html) +- [passkeySignupChallenge](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/passkeySignupChallenge.html) - request a WebAuthn registration challenge to sign up a new user with a passkey. Requires `auth0-spa-js` v2.24.0+. +- [passkeyLoginChallenge](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/passkeyLoginChallenge.html) - request a WebAuthn assertion challenge to log in an existing user with a passkey. Requires `auth0-spa-js` v2.24.0+. +- [getTokenByPasskey](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/getTokenByPasskey.html) - exchange a passkey credential from `navigator.credentials` for Auth0 tokens. Requires `auth0-spa-js` v2.24.0+. ## Feedback diff --git a/auth0_flutter/analysis_options.yaml b/auth0_flutter/analysis_options.yaml index 5d3fe8b00..45905c592 100644 --- a/auth0_flutter/analysis_options.yaml +++ b/auth0_flutter/analysis_options.yaml @@ -37,6 +37,7 @@ linter: avoid_double_and_int_checks: true avoid_equals_and_hash_code_on_mutable_classes: true avoid_escaping_inner_quotes: true + avoid_final_parameters: true avoid_implementing_value_types: true avoid_js_rounded_ints: true avoid_positional_boolean_parameters: true @@ -63,7 +64,6 @@ linter: prefer_expression_function_bodies: true prefer_final_in_for_each: true prefer_final_locals: true - prefer_final_parameters: true prefer_if_elements_to_conditional_expressions: true prefer_interpolation_to_compose_strings: true prefer_mixin: true diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt index cf4ba7257..09e93c943 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt @@ -82,18 +82,7 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private val processDeathCallback = object : Callback { override fun onSuccess(credentials: Credentials) { - val scopes = credentials.scope?.split(" ") ?: listOf() - val formattedDate = credentials.expiresAt.toInstant().toString() - - val credentialsMap = mapOf( - "accessToken" to credentials.accessToken, - "idToken" to credentials.idToken, - "refreshToken" to credentials.refreshToken, - "userProfile" to credentials.user.toMap(), - "expiresAt" to formattedDate, - "scopes" to scopes, - "tokenType" to credentials.type - ) + val credentialsMap = credentials.toMap() if (dartReady) { webAuthMethodChannel.invokeMethod("webAuth#onLoginResult", credentialsMap) diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt new file mode 100644 index 000000000..f37c3f219 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsExtensions.kt @@ -0,0 +1,27 @@ +package com.auth0.auth0_flutter + +import com.auth0.android.result.Credentials + +/** + * Serializes [Credentials] into the map shape expected by the Dart + * `Credentials.fromMap` factory. + */ +fun Credentials.toMap(): Map { + val scopes = this.scope?.split(" ") ?: listOf() + val formattedDate = this.expiresAt.toInstant().toString() + + return buildMap { + put("accessToken", accessToken) + put("idToken", idToken) + put("refreshToken", refreshToken) + put("userProfile", user.toMap()) + put("expiresAt", formattedDate) + put("scopes", scopes) + put("tokenType", type) + // IPSIE session_expiry ceiling, pinned at login and enforced by the + // native SecureCredentialsManager; omitted when there is no ceiling. + sessionExpiresAt?.let { + put("sessionExpiry", java.util.Date(it * 1000L).toInstant().toString()) + } + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt index 45abd90ae..9437de1ad 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt @@ -36,19 +36,7 @@ class GetCredentialsRequestHandler : CredentialsManagerRequestHandler { } override fun onSuccess(credentials: Credentials) { - val scopes = credentials.scope?.split(" ") ?: listOf() - val formattedDate = credentials.expiresAt.toInstant().toString() - result.success( - mapOf( - "accessToken" to credentials.accessToken, - "idToken" to credentials.idToken, - "refreshToken" to credentials.refreshToken, - "userProfile" to credentials.user.toMap(), - "expiresAt" to formattedDate, - "scopes" to scopes, - "tokenType" to credentials.type - ) - ) + result.success(credentials.toMap()) } }) } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt index a4f1c4db0..72ba4f4eb 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt @@ -28,19 +28,7 @@ class RenewCredentialsRequestHandler : CredentialsManagerRequestHandler { } override fun onSuccess(credentials: Credentials) { - val scopes = credentials.scope?.split(" ") ?: listOf() - val formattedDate = credentials.expiresAt.toInstant().toString() - result.success( - mapOf( - "accessToken" to credentials.accessToken, - "idToken" to credentials.idToken, - "refreshToken" to credentials.refreshToken, - "userProfile" to credentials.user.toMap(), - "expiresAt" to formattedDate, - "scopes" to scopes, - "tokenType" to credentials.type - ) - ) + result.success(credentials.toMap()) } }) } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt index 762396e19..df93470f0 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt @@ -80,20 +80,7 @@ class LoginWebAuthRequestHandler( } override fun onSuccess(credentials: Credentials) { - val scopes = credentials.scope?.split(" ") ?: listOf() - val formattedDate = credentials.expiresAt.toInstant().toString() - - result.success( - mapOf( - "accessToken" to credentials.accessToken, - "idToken" to credentials.idToken, - "refreshToken" to credentials.refreshToken, - "userProfile" to credentials.user.toMap(), - "expiresAt" to formattedDate, - "scopes" to scopes, - "tokenType" to credentials.type - ) - ) + result.success(credentials.toMap()) } }) } diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/JwtTestUtils.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/JwtTestUtils.kt index 4b567ee6b..75adbd046 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/JwtTestUtils.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/JwtTestUtils.kt @@ -8,12 +8,14 @@ class JwtTestUtils { fun createJwt( subject: String = "test-subject", issuer: String = "test-issuer", - claims: Map = mapOf() + claims: Map = mapOf(), + numberClaims: Map = mapOf() ): String { val alg = Algorithm.HMAC256("test") val jwt = JWT.create().withIssuer(issuer).withSubject(subject) claims.entries.forEach { jwt.withClaim(it.key, it.value) } + numberClaims.entries.forEach { jwt.withClaim(it.key, it.value) } return jwt.sign(alg) } diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt index 624dad19b..66d4e51f5 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt @@ -74,6 +74,36 @@ class LoginWebAuthRequestHandlerTest { } } + @Test + fun `handler should include sessionExpiry when the session_expiry claim is present`() { + // 2023-11-02T10:00:00Z == 1698919200 Unix seconds. + val sessionExpirySeconds = 1698919200L + val idToken = JwtTestUtils.createJwt( + numberClaims = mapOf("session_expiry" to sessionExpirySeconds) + ) + val credentials = Credentials(idToken, "test", "", null, Date(), "openid") + + runRequestHandler(credentials = credentials) { result, _ -> + verify(result).success(check { + val map = it as Map<*, *> + assertThat( + map["sessionExpiry"], + equalTo(java.time.Instant.ofEpochSecond(sessionExpirySeconds).toString()) + ) + }) + } + } + + @Test + fun `handler should omit sessionExpiry when the session_expiry claim is absent`() { + runRequestHandler { result, _ -> + verify(result).success(check { + val map = it as Map<*, *> + assertThat(map.containsKey("sessionExpiry"), equalTo(false)) + }) + } + } + @Test fun `handler should request scopes from the SDK when specified`() { val args = hashMapOf( diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandlerTest.kt index 6c24d90ad..53fa612a1 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandlerTest.kt @@ -346,6 +346,67 @@ class GetCredentialsRequestHandlerTest { ) } + @Test + fun `should include sessionExpiry when the session_expiry claim is present`() { + val options = hashMapOf() + val handler = GetCredentialsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockCredentialsManager = mock() + val request = MethodCallRequest(account = mockAccount, options) + // 2023-11-02T10:00:00Z == 1698919200 Unix seconds. + val sessionExpirySeconds = 1698919200L + val idToken = JwtTestUtils.createJwt( + claims = mapOf("name" to "John Doe"), + numberClaims = mapOf("session_expiry" to sessionExpirySeconds) + ) + val credentials = Credentials(idToken, "test", "", null, Date(), "scope1") + + whenever(mockCredentialsManager.getCredentials(isNull(), anyInt(), anyMap(), any())).thenAnswer { + val callback = it.getArgument>(3) + callback.onSuccess(credentials) + } + + handler.handle(mockCredentialsManager, mock(), request, mockResult) + + val captor = argumentCaptor>() + verify(mockResult).success(captor.capture()) + + MatcherAssert.assertThat( + captor.firstValue["sessionExpiry"] as String, + CoreMatchers.equalTo( + java.time.Instant.ofEpochSecond(sessionExpirySeconds).toString() + ) + ) + } + + @Test + fun `should omit sessionExpiry when the session_expiry claim is absent`() { + val options = hashMapOf() + val handler = GetCredentialsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockCredentialsManager = mock() + val request = MethodCallRequest(account = mockAccount, options) + val idToken = JwtTestUtils.createJwt(claims = mapOf("name" to "John Doe")) + val credentials = Credentials(idToken, "test", "", null, Date(), "scope1") + + whenever(mockCredentialsManager.getCredentials(isNull(), anyInt(), anyMap(), any())).thenAnswer { + val callback = it.getArgument>(3) + callback.onSuccess(credentials) + } + + handler.handle(mockCredentialsManager, mock(), request, mockResult) + + val captor = argumentCaptor>() + verify(mockResult).success(captor.capture()) + + MatcherAssert.assertThat( + captor.firstValue.containsKey("sessionExpiry"), + CoreMatchers.equalTo(false) + ) + } + @Test fun `should call result success on success without scopes`() { val options = hashMapOf() diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandlerTest.kt index 5772a1465..c5c087c27 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandlerTest.kt @@ -196,4 +196,38 @@ class RenewCredentialsRequestHandlerTest { CoreMatchers.equalTo("John Doe") ) } + + @Test + fun `should preserve sessionExpiry across a renew`() { + val options = hashMapOf() + val handler = RenewCredentialsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockCredentialsManager = mock() + val request = MethodCallRequest(account = mockAccount, options) + // 2023-11-02T10:00:00Z == 1698919200 Unix seconds. + val sessionExpirySeconds = 1698919200L + val idToken = JwtTestUtils.createJwt( + claims = mapOf("name" to "John Doe"), + numberClaims = mapOf("session_expiry" to sessionExpirySeconds) + ) + val credentials = Credentials(idToken, "accessToken", "Bearer", null, Date(), "scope1") + + whenever(mockCredentialsManager.getCredentials(isNull(), anyInt(), anyMap(), eq(true), any())).thenAnswer { + val callback = it.getArgument>(4) + callback.onSuccess(credentials) + } + + handler.handle(mockCredentialsManager, mock(), request, mockResult) + + val captor = argumentCaptor>() + verify(mockResult).success(captor.capture()) + + MatcherAssert.assertThat( + captor.firstValue["sessionExpiry"] as String, + CoreMatchers.equalTo( + java.time.Instant.ofEpochSecond(sessionExpirySeconds).toString() + ) + ) + } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerExtensions.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerExtensions.swift index a3efc75ba..64cb37937 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerExtensions.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerExtensions.swift @@ -17,6 +17,7 @@ extension FlutterError { case .biometricsFailed: code = "BIOMETRICS_FAILED" case .revokeFailed: code = "REVOKE_FAILED" case .largeMinTTL: code = "LARGE_MIN_TTL" + case .sessionExpired: code = "SESSION_EXPIRED" default: code = "UNKNOWN" } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift index 2d6536b76..d30d5f582 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Extensions.swift @@ -91,6 +91,12 @@ extension Credentials { CredentialsProperty.tokenType.rawValue: tokenType ] data[CredentialsProperty.refreshToken] = refreshToken + // IPSIE session_expiry ceiling. Use the native `sessionExpiresAt` so the + // surfaced value matches what the CredentialsManager enforces and picks + // up its range check; absent => omit. + if let sessionExpiresAt = sessionExpiresAt { + data[CredentialsProperty.sessionExpiry] = sessionExpiresAt.asISO8601String + } return data } } diff --git a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift index d0f605b9a..67bd3c2ce 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift @@ -6,6 +6,7 @@ enum CredentialsProperty: String, CaseIterable { case expiresAt case scopes case tokenType + case sessionExpiry } enum SSOCredentialsProperty: String, CaseIterable { diff --git a/auth0_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/auth0_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 163000d85..391a902b2 100644 --- a/auth0_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/auth0_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 14.0 diff --git a/auth0_flutter/example/ios/Podfile b/auth0_flutter/example/ios/Podfile index 9131006df..6ab3c66a9 100644 --- a/auth0_flutter/example/ios/Podfile +++ b/auth0_flutter/example/ios/Podfile @@ -1,5 +1,4 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '14.0' +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift index 872e6c174..921a80a82 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift @@ -6,7 +6,6 @@ import Auth0 struct SpyCredentialsStorageError: Error {} class SpyCredentialsStorage: CredentialsStorage { - var getEntryReturnValue: Data? var setEntryReturnValue = true var deleteEntryReturnValue = true @@ -14,20 +13,36 @@ class SpyCredentialsStorage: CredentialsStorage { var calledSetEntry = false var calledDeleteEntry = false + private var storage: [String: Data] = [:] + + var getEntryReturnValue: Data? { + get { self.storage["_default"] ?? self.storage.values.first } + set { + if let newValue = newValue { + self.storage["_default"] = newValue + } else { + self.storage.removeAll() + } + } + } + func getEntry(forKey key: String) throws -> Data { self.calledGetEntry = true - guard let value = self.getEntryReturnValue else { throw SpyCredentialsStorageError() } + guard let value = self.storage[key] ?? self.storage["_default"] else { + throw SpyCredentialsStorageError() + } return value } func setEntry(_ data: Data, forKey key: String) throws { self.calledSetEntry = true - self.getEntryReturnValue = data + self.storage[key] = data if !self.setEntryReturnValue { throw SpyCredentialsStorageError() } } func deleteEntry(forKey key: String) throws { self.calledDeleteEntry = true + self.storage.removeValue(forKey: key) if !self.deleteEntryReturnValue { throw SpyCredentialsStorageError() } } } diff --git a/auth0_flutter/example/ios/Tests/ExtensionsTests.swift b/auth0_flutter/example/ios/Tests/ExtensionsTests.swift index a8077b075..e6a0f418a 100644 --- a/auth0_flutter/example/ios/Tests/ExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/ExtensionsTests.swift @@ -162,6 +162,22 @@ extension ExtensionsTests { let credentials = Credentials(accessToken: "", tokenType: "", idToken: idToken, expiresAt: Date()) XCTAssertThrowsError(try credentials.asDictionary(), HandlerError.idTokenDecodingFailed.message) } + + func testMapsSessionExpiryIntoDictionaryWhenClaimPresent() throws { + // `testIdToken` carries a `session_expiry` claim of 4102444800 (2100-01-01). + let credentials = Credentials(accessToken: "", tokenType: "", idToken: testIdToken, expiresAt: Date()) + let values = try credentials.asDictionary() + let expected = Date(timeIntervalSince1970: 4102444800).asISO8601String + XCTAssertEqual(values[CredentialsProperty.sessionExpiry] as? String, expected) + } + + func testOmitsSessionExpiryFromDictionaryWhenClaimAbsent() throws { + // `{}` payload — no `session_expiry` claim. + let idToken = "eyJhbGciOiJIUzI1NiJ9.e30.ZRrHA1JJJW8opsbCGfG_HACGpVUMN_a9IV7pAx_Zmeo" + let credentials = Credentials(accessToken: "", tokenType: "", idToken: idToken, expiresAt: Date()) + let values = try credentials.asDictionary() + XCTAssertNil(values[CredentialsProperty.sessionExpiry]) + } } // MARK: - UserInfo+asDictionary diff --git a/auth0_flutter/example/ios/Tests/Mocks.swift b/auth0_flutter/example/ios/Tests/Mocks.swift index a3074d70c..ab87814c1 100644 --- a/auth0_flutter/example/ios/Tests/Mocks.swift +++ b/auth0_flutter/example/ios/Tests/Mocks.swift @@ -6,9 +6,16 @@ import Flutter import FlutterMacOS #endif +// Includes a future `session_expiry` claim (Unix seconds; 4102444800 = +// 2100-01-01) so the fully-populated credentials fixtures exercise the IPSIE +// session-expiry serialization while keeping the ceiling valid: the native +// CredentialsManager enforces this claim, so a past value would make it treat +// the stored credentials as expired and skip renewal. The value stays below +// the native `(0, 10_000_000_000)` bound, so it is read as a valid seconds +// ceiling rather than rejected as a millisecond value. let testIdToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28iLCJuYW1lIjoiYmFyIiwiZW1haWwiOiJmb29AZXhhbXBsZS5" + "jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcGljdHVyZSIsInVwZGF0ZWRfYXQiOiIyMDI" - + "yLTA0LTE1VDAzOjE1OjUxLjc4N1oifQ.mFq-johzLTFQUAl9pjgQraTM6I8AGfcEcWBg0Ah2vss" + + "yLTA0LTE1VDAzOjE1OjUxLjc4N1oiLCJzZXNzaW9uX2V4cGlyeSI6NDEwMjQ0NDgwMH0.sig" // MARK: - Foundation Mocks @@ -78,7 +85,7 @@ struct MockAuth0Error: Auth0Error { var cause: Error? } -struct MockAuth0APIError: Auth0APIError { +struct MockAuth0APIError: Auth0APIError, @unchecked Sendable { var info: [String: Any] var code: String var statusCode: Int diff --git a/auth0_flutter/example/lib/example_app.dart b/auth0_flutter/example/lib/example_app.dart index b0668e3dd..3484fc463 100644 --- a/auth0_flutter/example/lib/example_app.dart +++ b/auth0_flutter/example/lib/example_app.dart @@ -34,7 +34,7 @@ class _ExampleAppState extends State { Auth0Web(dotenv.env['AUTH0_DOMAIN']!, dotenv.env['AUTH0_CLIENT_ID']!); webAuth = auth0.webAuthentication(scheme: dotenv.env['AUTH0_CUSTOM_SCHEME']); - if (Platform.isWindows) { + if (!kIsWeb && Platform.isWindows) { windowsWebAuth = auth0.windowsWebAuthentication(); } if (kIsWeb) { @@ -272,7 +272,7 @@ class _ExampleAppState extends State { const SizedBox(height: 10), // DPoP button — not shown on Windows // (DPoP is not yet implemented on that platform) - if (!Platform.isWindows) + if (kIsWeb || !Platform.isWindows) ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, diff --git a/auth0_flutter/example/lib/my_account_card.dart b/auth0_flutter/example/lib/my_account_card.dart deleted file mode 100644 index c4282959e..000000000 --- a/auth0_flutter/example/lib/my_account_card.dart +++ /dev/null @@ -1,676 +0,0 @@ -import 'package:auth0_flutter/auth0_flutter.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class MyAccountCard extends StatefulWidget { - final Auth0 auth0; - final String domain; - final String? scheme; - final void Function(String output) onOutput; - - const MyAccountCard({ - required this.auth0, - required this.domain, - required this.onOutput, - this.scheme, - final Key? key, - }) : super(key: key); - - @override - State createState() => _MyAccountCardState(); -} - -class _MyAccountCardState extends State { - String? _accessToken; - MyAccountApi? _myAccount; - bool _isExpanded = false; - bool _useDPoP = false; - - final _phoneController = TextEditingController(text: '+1234567890'); - final _emailController = TextEditingController(text: 'test@example.com'); - - String? _lastEnrollmentId; - String? _lastAuthSession; - String? _lastFactorType; - - final _otpController = TextEditingController(); - - @override - void dispose() { - _phoneController.dispose(); - _emailController.dispose(); - _otpController.dispose(); - super.dispose(); - } - - void _log(final String message) { - debugPrint('[MyAccount] $message'); - widget.onOutput(message); - } - - // Scopes required to manage authentication methods via the My Account API. - static const _myAccountScopes = { - 'read:me:authentication_methods', - 'create:me:authentication_methods', - 'update:me:authentication_methods', - 'delete:me:authentication_methods', - 'read:me:factors', - }; - - Future _loginForMyAccount() async { - try { - // 1. Log in once for the app, requesting offline_access so that a refresh - // token is stored. This is the recommended approach: rather than - // launching a second interactive login against the `/me/` audience, we - // reuse the existing session and exchange the refresh token below. - _log('Logging in (requesting offline_access for a refresh token)...'); - final webAuth = widget.auth0.webAuthentication( - scheme: widget.scheme, - ); - final result = await webAuth.login( - scopes: {'openid', 'profile', 'email', 'offline_access'}, - ); - await widget.auth0.credentialsManager.storeCredentials(result); - - // 2. Exchange the stored refresh token for an access token scoped to the - // My Account API by requesting the `https://{domain}/me/` audience. - // getApiCredentials returns a separate, audience-scoped token and does - // not replace the stored application credentials. - // (Requires Multi-Resource Refresh Tokens enabled on the tenant.) - _log('Exchanging refresh token for a My Account token ' - '(audience: https://${widget.domain}/me/)...'); - final myAccountCredentials = - await widget.auth0.credentialsManager.getApiCredentials( - audience: 'https://${widget.domain}/me/', - scope: _myAccountScopes, - ); - - setState(() { - _accessToken = myAccountCredentials.accessToken; - _myAccount = widget.auth0 - .myAccount(accessToken: _accessToken!, useDPoP: _useDPoP); - }); - - _log('Ready for My Account API calls!\n' - 'Access Token: ${_accessToken!.substring(0, 30)}...'); - } catch (e) { - _log('Login Error: $e'); - } - } - - Future _getAuthMethods() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Calling getAuthenticationMethods()...'); - final methods = await _myAccount!.getAuthenticationMethods(); - if (methods.isEmpty) { - _log('No authentication methods enrolled.'); - return; - } - final output = StringBuffer( - 'Authentication Methods (${methods.length}):\n'); - for (final m in methods) { - output.writeln('---'); - output.writeln(' ID: ${m.id}'); - output.writeln(' Type: ${m.type}'); - if (m.name != null) output.writeln(' Name: ${m.name}'); - if (m.phoneNumber != null) output.writeln(' Phone: ${m.phoneNumber}'); - if (m.email != null) output.writeln(' Email: ${m.email}'); - if (m.createdAt != null) output.writeln(' Created: ${m.createdAt}'); - if (m.lastAuthAt != null) { - output.writeln(' Last Auth: ${m.lastAuthAt}'); - } - } - _log(output.toString()); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _getFactors() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Calling getFactors()...'); - final factors = await _myAccount!.getFactors(); - if (factors.isEmpty) { - _log('No factors available.'); - return; - } - final output = StringBuffer('Available Factors (${factors.length}):\n'); - for (final f in factors) { - output.writeln(' - ${f.type}: usage=${f.usage?.join(", ") ?? "none"}'); - } - _log(output.toString()); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _enrollPhone() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - final phone = _phoneController.text.trim(); - _log('Calling enrollPhone(phone: $phone, type: sms)...'); - final challenge = await _myAccount!.enrollPhone( - phoneNumber: phone, - type: PhoneType.sms, - ); - setState(() { - _lastEnrollmentId = challenge.id; - _lastAuthSession = challenge.authSession; - _lastFactorType = 'phone'; - }); - _log('Enrollment Challenge:\n' - ' ID: ${challenge.id}\n' - ' Auth Session: ${challenge.authSession}\n' - ' Recovery Code: ${challenge.recoveryCode ?? "N/A"}\n' - '\n** Enter OTP received via SMS and tap Verify OTP **'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _enrollEmail() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - final email = _emailController.text.trim(); - _log('Calling enrollEmail(email: $email)...'); - final challenge = await _myAccount!.enrollEmail(email: email); - _log('Enrollment SUCCESS - ' - 'ID: ${challenge.id} | ' - 'AuthSession: ${challenge.authSession}'); - setState(() { - _lastEnrollmentId = challenge.id; - _lastAuthSession = challenge.authSession; - _lastFactorType = 'email'; - }); - _log('** Enter OTP received via Email and tap Verify OTP **'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _enrollTotp() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Calling enrollTotp()...'); - final challenge = await _myAccount!.enrollTotp(); - setState(() { - _lastEnrollmentId = challenge.id; - _lastAuthSession = challenge.authSession; - _lastFactorType = 'totp'; - }); - _log('TOTP Enrollment Challenge:\n' - ' ID: ${challenge.id}\n' - ' Auth Session: ${challenge.authSession}\n' - ' TOTP Secret: ${challenge.totpSecret ?? "N/A"}\n' - ' TOTP URI: ${challenge.totpUri ?? "N/A"}\n' - ' Barcode URI: ${challenge.barcodeUri ?? "N/A"}\n' - ' Recovery Code: ${challenge.recoveryCode ?? "N/A"}\n' - '\n** Add secret to authenticator app, enter code, ' - 'tap Verify OTP **'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _enrollPush() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Calling enrollPush()...'); - final challenge = await _myAccount!.enrollPush(); - setState(() { - _lastEnrollmentId = challenge.id; - _lastAuthSession = challenge.authSession; - _lastFactorType = 'push-notification'; - }); - _log('Push Enrollment Challenge:\n' - ' ID: ${challenge.id}\n' - ' Auth Session: ${challenge.authSession}\n' - ' Barcode URI: ${challenge.barcodeUri ?? "N/A"}\n' - ' Recovery Code: ${challenge.recoveryCode ?? "N/A"}'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _enrollRecoveryCode() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Calling enrollRecoveryCode()...'); - final challenge = await _myAccount!.enrollRecoveryCode(); - setState(() { - _lastEnrollmentId = challenge.id; - _lastAuthSession = challenge.authSession; - _lastFactorType = 'recovery-code'; - }); - _log('Recovery Code Enrollment:\n' - ' ID: ${challenge.id}\n' - ' Auth Session: ${challenge.authSession}\n' - ' Recovery Code: ${challenge.recoveryCode ?? "N/A"}'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _verifyOtp() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - if (_lastEnrollmentId == null || _lastAuthSession == null) { - _log('Error: No pending enrollment. Enroll a factor first.'); - return; - } - final otp = _otpController.text.trim(); - if (otp.isEmpty) { - _log('Error: Enter OTP code.'); - return; - } - try { - _log('Calling verifyOtp(\n' - ' id: $_lastEnrollmentId,\n' - ' authSession: $_lastAuthSession,\n' - ' otp: $otp)...'); - final method = await _myAccount!.verifyOtp( - id: _lastEnrollmentId!, - authSession: _lastAuthSession!, - otp: otp, - factorType: _lastFactorType!, - ); - _log('OTP Verified Successfully! Enrollment complete.\n' - ' Method: ${method.type} (id: ${method.id})'); - setState(() { - _lastEnrollmentId = null; - _lastAuthSession = null; - _lastFactorType = null; - _otpController.clear(); - }); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _confirmEnrollment() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - if (_lastEnrollmentId == null || _lastAuthSession == null) { - _log('Error: No pending enrollment. Enroll push/recovery first.'); - return; - } - try { - _log('Calling confirmEnrollment(\n' - ' id: $_lastEnrollmentId,\n' - ' authSession: $_lastAuthSession)...'); - final method = await _myAccount!.confirmEnrollment( - id: _lastEnrollmentId!, - authSession: _lastAuthSession!, - factorType: _lastFactorType!, - ); - _log('Enrollment confirmed!\n' - ' Method: ${method.type} (id: ${method.id})'); - setState(() { - _lastEnrollmentId = null; - _lastAuthSession = null; - _lastFactorType = null; - }); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _updateLastMethod() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Fetching methods to update the last one...'); - final methods = await _myAccount!.getAuthenticationMethods(); - if (methods.isEmpty) { - _log('No methods to update.'); - return; - } - final target = methods.last; - const newName = 'Updated by example app'; - _log('Updating method ${target.id} (${target.type}) ' - 'with name "$newName"...'); - final updated = await _myAccount!.updateAuthenticationMethod( - id: target.id, - name: newName, - ); - _log('Updated successfully: ${updated.id} ' - '(name: ${updated.name ?? "N/A"})'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - Future _logout() async { - try { - _log('Logging out...'); - final webAuth = widget.auth0.webAuthentication( - scheme: widget.scheme, - ); - await webAuth.logout(); - setState(() { - _accessToken = null; - _myAccount = null; - _lastEnrollmentId = null; - _lastAuthSession = null; - _otpController.clear(); - }); - _log('Logged out successfully.'); - } catch (e) { - _log('Logout Error: $e'); - } - } - - Future _deleteLastMethod() async { - if (_myAccount == null) { - _log('Error: Not logged in. Login first.'); - return; - } - try { - _log('Fetching methods to delete the last one...'); - final methods = await _myAccount!.getAuthenticationMethods(); - if (methods.isEmpty) { - _log('No methods to delete.'); - return; - } - final target = methods.last; - _log('Deleting method: ${target.id} (${target.type})...'); - await _myAccount!.deleteAuthenticationMethod(id: target.id); - _log('Deleted successfully: ${target.id}'); - } on MyAccountException catch (e) { - _log('MyAccountException: [${e.statusCode}] ${e.code} - ${e.message}'); - } catch (e) { - _log('Error: $e'); - } - } - - @override - Widget build(final BuildContext context) { - if (kIsWeb) { - return const SizedBox.shrink(); - } - - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - InkWell( - onTap: () => setState(() => _isExpanded = !_isExpanded), - child: Row( - children: [ - Icon( - _isExpanded - ? Icons.keyboard_arrow_down - : Icons.keyboard_arrow_right, - color: Colors.indigo, - ), - const SizedBox(width: 8), - const Text( - 'My Account API (MFA)', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - ), - const Spacer(), - if (_accessToken != null) - const Icon(Icons.check_circle, - color: Colors.green, size: 20), - ], - ), - ), - if (_isExpanded) ...[ - const Divider(), - if (_accessToken == null) ...[ - const Text( - 'Login with My Account audience to get started:', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - SwitchListTile( - contentPadding: EdgeInsets.zero, - dense: true, - title: const Text('Use DPoP', - style: TextStyle(fontSize: 13)), - subtitle: const Text( - 'Secure requests with sender-constrained tokens', - style: TextStyle(fontSize: 11, color: Colors.grey), - ), - value: _useDPoP, - onChanged: (final value) => - setState(() => _useDPoP = value), - ), - const SizedBox(height: 8), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - foregroundColor: Colors.white, - ), - onPressed: _loginForMyAccount, - icon: const Icon(Icons.login), - label: const Text('Login for My Account'), - ), - ] else ...[ - const Text( - 'Read Operations', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _buildButton('Get Auth Methods', _getAuthMethods, - Icons.list), - _buildButton( - 'Get Factors', _getFactors, Icons.security), - ], - ), - const SizedBox(height: 12), - const Text( - 'Enroll', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - Row( - children: [ - Expanded( - child: TextField( - controller: _phoneController, - decoration: const InputDecoration( - labelText: 'Phone', - isDense: true, - border: OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 8), - _buildSmallButton('Enroll', _enrollPhone), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: TextField( - controller: _emailController, - decoration: const InputDecoration( - labelText: 'Email', - isDense: true, - border: OutlineInputBorder(), - ), - ), - ), - const SizedBox(width: 8), - _buildSmallButton('Enroll', _enrollEmail), - ], - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _buildButton('Enroll TOTP', _enrollTotp, - Icons.qr_code), - _buildButton('Enroll Push', _enrollPush, - Icons.notifications), - _buildButton('Enroll Recovery', _enrollRecoveryCode, - Icons.restore), - ], - ), - if (_lastEnrollmentId != null) ...[ - const SizedBox(height: 12), - const Text( - 'Verify Enrollment', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - Row( - children: [ - Expanded( - child: TextField( - controller: _otpController, - decoration: const InputDecoration( - labelText: 'OTP Code', - isDense: true, - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - ), - ), - const SizedBox(width: 8), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - onPressed: _verifyOtp, - child: const Text('Verify OTP'), - ), - ], - ), - const SizedBox(height: 8), - const Text( - 'For push / recovery code (no OTP):', - style: TextStyle(fontSize: 11, color: Colors.grey), - ), - const SizedBox(height: 6), - _buildButton('Confirm Enrollment', _confirmEnrollment, - Icons.check, color: Colors.green), - ], - const SizedBox(height: 12), - const Text( - 'Manage', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - _buildButton('Update Last Method', _updateLastMethod, - Icons.edit, color: Colors.teal), - const SizedBox(height: 12), - const Text( - 'Delete', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - _buildButton('Delete Last Method', _deleteLastMethod, - Icons.delete, color: Colors.red), - const SizedBox(height: 12), - const Text( - 'Session', - style: TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), - ), - const SizedBox(height: 6), - _buildButton('Logout', _logout, - Icons.logout, color: Colors.orange), - ], - ], - ], - ), - ), - ); - } - - Widget _buildButton( - final String label, final VoidCallback onPressed, final IconData icon, - {final Color color = Colors.indigo}) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: color, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - onPressed: onPressed, - icon: Icon(icon, size: 16), - label: Text(label, style: const TextStyle(fontSize: 12)), - ); - } - - Widget _buildSmallButton(final String label, final VoidCallback onPressed) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - ), - onPressed: onPressed, - child: Text(label, style: const TextStyle(fontSize: 12)), - ); - } -} diff --git a/auth0_flutter/example/macos/Podfile b/auth0_flutter/example/macos/Podfile index a46f7f232..167132a2f 100644 --- a/auth0_flutter/example/macos/Podfile +++ b/auth0_flutter/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '11.0' +platform :osx, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj b/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj index 7bbb964fc..24be3ac6a 100644 --- a/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj +++ b/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -812,7 +812,7 @@ buildSettings = { CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.example.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -827,7 +827,7 @@ buildSettings = { CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.example.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -842,7 +842,7 @@ buildSettings = { CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.example.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -889,7 +889,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -969,7 +969,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -1016,7 +1016,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -1102,7 +1102,7 @@ GCC_WARN_UNDECLARED_SELECTOR = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -1135,7 +1135,7 @@ GCC_WARN_UNDECLARED_SELECTOR = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.sdks.Flutter.RunnerUITests; @@ -1166,7 +1166,7 @@ GCC_WARN_UNDECLARED_SELECTOR = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.auth0.sdks.Flutter.RunnerUITests; diff --git a/auth0_flutter/example/pubspec.yaml b/auth0_flutter/example/pubspec.yaml index 3872ff416..902de6d38 100644 --- a/auth0_flutter/example/pubspec.yaml +++ b/auth0_flutter/example/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: flutter: sdk: flutter flutter_dotenv: ^5.0.2 + web: ^1.1.0 dependency_overrides: auth0_flutter_platform_interface: diff --git a/auth0_flutter/example/web/index.html b/auth0_flutter/example/web/index.html index 11a3bb650..a7d5d25d2 100644 --- a/auth0_flutter/example/web/index.html +++ b/auth0_flutter/example/web/index.html @@ -39,13 +39,15 @@ - + + + + ``` > ⚠️ If you use [Custom Token Exchange](EXAMPLES.md#custom-token-exchange) with delegation/impersonation (an actor token), you must reference `auth0-spa-js` **2.20.0 or later**. Actor-token support was added to the underlying SPA SDK in [2.20.0](https://github.com/auth0/auth0-spa-js/releases/tag/v2.20.0); older versions silently drop the actor token, so the returned ID token has no `act` claim and `credentials.user.actor` is `null`. diff --git a/auth0_flutter/V3_MIGRATION_GUIDE.md b/auth0_flutter/V3_MIGRATION_GUIDE.md index d2e0c0d9e..8ea82c730 100644 --- a/auth0_flutter/V3_MIGRATION_GUIDE.md +++ b/auth0_flutter/V3_MIGRATION_GUIDE.md @@ -31,10 +31,12 @@ from v2 to v3. ## Requirements Changes Because the underlying native SDKs raised their floors, `auth0_flutter` v3 -raises the Android minimums: +raises the Android, iOS, and macOS minimums: | Requirement | v2 | v3 | | --- | --- | --- | +| iOS deployment target | 14.0 | **15.0** | +| macOS deployment target | 11.0 | **12.0** | | Android `minSdkVersion` | 21 | **26** (Android 8.0) | | Android compile/target SDK | 34 | **36** | | JDK (to build the Android module) | 8 | **17** | @@ -42,7 +44,13 @@ raises the Android minimums: | Android Gradle Plugin | 8.4.x | **8.10.1** | | Gradle | 8.7 | **8.11.1** | -**Migration:** if your app targets an Android `minSdkVersion` below 26, raise it +**Migration (iOS/macOS):** if your app's deployment target is below iOS 15.0 or +macOS 12.0, raise it. Update the `platform` line in `ios/Podfile` (for example +`platform :ios, '15.0'`) and `macos/Podfile` (`platform :osx, '12.0'`), and the +deployment target of the Runner target in Xcode. These floors come from +Auth0.swift v3. + +**Migration (Android):** if your app targets an Android `minSdkVersion` below 26, raise it to at least 26 in your app's `android/app/build.gradle`. Ensure your build environment uses JDK 17 (for example, set it in Android Studio under *Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JDK*, diff --git a/auth0_flutter/android/build.gradle b/auth0_flutter/android/build.gradle index a953c8d94..9ddd46963 100644 --- a/auth0_flutter/android/build.gradle +++ b/auth0_flutter/android/build.gradle @@ -85,7 +85,7 @@ plugins.withId("org.jetbrains.kotlin.android") { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'com.auth0.android:auth0:4.1.0' + implementation 'com.auth0.android:auth0:4.2.0' implementation 'com.google.code.gson:gson:2.11.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt index fd6167860..b96f83fc4 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt @@ -206,7 +206,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { val request = MethodCallRequest(account = mockAccount, options) doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() doReturn(mockBuilder).`when`(mockBuilder).setScope(any()) doReturn(mockBuilder).`when`(mockBuilder).setAudience(any()) @@ -218,7 +218,9 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { eq("test-auth-session"), any(), eq("test-connection"), - eq("test-org") + eq("test-org"), + any(), + any() ) verify(mockBuilder).validateClaims() verify(mockBuilder).setScope("openid profile") @@ -241,7 +243,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { val request = MethodCallRequest(account = mockAccount, options) doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() handler.handle(mockApi, request, mockResult) @@ -250,7 +252,9 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { eq("test-auth-session"), any(), anyOrNull(), - anyOrNull() + anyOrNull(), + any(), + any() ) verify(mockBuilder).validateClaims() verify(mockBuilder).start(any()) @@ -272,7 +276,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { AuthenticationException(code = "test-code", description = "test-description") doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() doAnswer { val cb = it.getArgument>(0) @@ -307,7 +311,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { ) doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() doAnswer { val cb = it.getArgument>(0) diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt index 7770165e4..7608ead36 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt @@ -61,7 +61,7 @@ class PasskeySignupChallengeApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull()) + doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull(), anyOrNull()) handler.handle(mockApi, request, mockResult) @@ -77,7 +77,8 @@ class PasskeySignupChallengeApiRequestHandlerTest { this.picture == "https://www.okta.com" }, eq("test-connection"), - eq("test-org") + eq("test-org"), + isNull() ) verify(mockBuilder).start(any()) } @@ -92,7 +93,7 @@ class PasskeySignupChallengeApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull()) + doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { val cb = it.getArgument>(0) cb.onSuccess(challenge()) @@ -121,7 +122,7 @@ class PasskeySignupChallengeApiRequestHandlerTest { val exception = AuthenticationException(code = "test-code", description = "test-description") - doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull()) + doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { val cb = it.getArgument>(0) cb.onFailure(exception) diff --git a/auth0_flutter/darwin/auth0_flutter.podspec b/auth0_flutter/darwin/auth0_flutter.podspec index befd3a9e8..5dfd104c7 100644 --- a/auth0_flutter/darwin/auth0_flutter.podspec +++ b/auth0_flutter/darwin/auth0_flutter.podspec @@ -13,13 +13,13 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'auth0_flutter/Sources/auth0_flutter/**/*.swift' - s.ios.deployment_target = '14.0' + s.ios.deployment_target = '15.0' s.ios.dependency 'Flutter' - s.osx.deployment_target = '11.0' + s.osx.deployment_target = '12.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '3.0.2' + s.dependency 'Auth0', '3.2.0' s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter/darwin/auth0_flutter/Package.swift b/auth0_flutter/darwin/auth0_flutter/Package.swift index ca61ab968..9dfc01048 100644 --- a/auth0_flutter/darwin/auth0_flutter/Package.swift +++ b/auth0_flutter/darwin/auth0_flutter/Package.swift @@ -4,14 +4,14 @@ import PackageDescription let package = Package( name: "auth0_flutter", platforms: [ - .iOS("14.0"), - .macOS("11.0"), + .iOS("15.0"), + .macOS("12.0"), ], products: [ .library(name: "auth0-flutter", targets: ["auth0_flutter"]), ], dependencies: [ - .package(url: "https://github.com/auth0/Auth0.swift", exact: "3.0.2"), + .package(url: "https://github.com/auth0/Auth0.swift", exact: "3.2.0"), .package(url: "https://github.com/auth0/JWTDecode.swift", exact: "4.0.0"), .package(url: "https://github.com/auth0/SimpleKeychain", exact: "1.3.0"), ], diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift index 0db8d4772..39decee88 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift @@ -84,9 +84,10 @@ class SpyAuthentication: Authentication, @unchecked Sendable { return request(databaseUserResult) } - func resetPassword(email: String, connection: String) -> any Requestable { + func resetPassword(email: String, connection: String, organization: String?) -> any Requestable { arguments["email"] = email arguments["connection"] = connection + arguments["organization"] = organization calledResetPassword = true return request(voidResult) } @@ -144,20 +145,12 @@ class SpyAuthentication: Authentication, @unchecked Sendable { return request(.success(JWKS(keys: []))) } - #if PASSKEYS_PLATFORM + // The next two are kept outside `#if PASSKEYS_PLATFORM`: the macOS test target + // does not define that flag, and since Auth0.swift 3.2.0 no protocol extension + // method matches these requirements' signatures, so the spy must always provide them. @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) - func passkeyLoginChallenge(connection: String?, - organization: String?) -> any Requestable { - arguments["connection"] = connection - arguments["organization"] = organization - calledPasskeyLoginChallenge = true - return request(passkeyLoginChallengeResultOverride - ?? SpyAuthentication.passkeyLoginChallengeResult) - } - - @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) - func login(passkey: LoginPasskey, - challenge: PasskeyLoginChallenge, + func login(passkey: SignupPasskey, + challenge: PasskeySignupChallenge, connection: String?, audience: String?, scope: String, @@ -166,7 +159,7 @@ class SpyAuthentication: Authentication, @unchecked Sendable { arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization - calledLoginWithPasskey = true + calledSignupWithPasskey = true return tokenRequest(credentialsResult) } @@ -194,13 +187,28 @@ class SpyAuthentication: Authentication, @unchecked Sendable { arguments["organization"] = organization arguments["userMetadata"] = userMetadata calledPasskeySignupChallenge = true + #if PASSKEYS_PLATFORM return request(passkeySignupChallengeResultOverride ?? SpyAuthentication.passkeySignupChallengeResult) + #else + return request(.failure(AuthenticationError(info: [:], statusCode: 0))) + #endif } + #if PASSKEYS_PLATFORM @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) - func login(passkey: SignupPasskey, - challenge: PasskeySignupChallenge, + func passkeyLoginChallenge(connection: String?, + organization: String?) -> any Requestable { + arguments["connection"] = connection + arguments["organization"] = organization + calledPasskeyLoginChallenge = true + return request(passkeyLoginChallengeResultOverride + ?? SpyAuthentication.passkeyLoginChallengeResult) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func login(passkey: LoginPasskey, + challenge: PasskeyLoginChallenge, connection: String?, audience: String?, scope: String, @@ -209,6 +217,49 @@ class SpyAuthentication: Authentication, @unchecked Sendable { arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization + calledLoginWithPasskey = true + return tokenRequest(credentialsResult) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func passkeySignupChallenge(email: String?, + phoneNumber: String?, + username: String?, + name: String?, + givenName: String?, + familyName: String?, + nickname: String?, + picture: String?, + userMetadata: [String: String]?, + connection: String?, + organization: String?, + deliveryMethod: DeliveryMethod?) -> any Requestable { + return passkeySignupChallenge(email: email, + phoneNumber: phoneNumber, + username: username, + name: name, + givenName: givenName, + familyName: familyName, + nickname: nickname, + picture: picture, + userMetadata: userMetadata, + connection: connection, + organization: organization) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func login(passkey: SignupPasskey, + verificationRequired: [PasskeyVerificationMethod], + authSession: String, + connection: String?, + audience: String?, + scope: String, + organization: String?, + verification: [PasskeyVerificationMethod: String]) -> any TokenRequestable { + arguments["connection"] = connection + arguments["audience"] = audience + arguments["scope"] = scope + arguments["organization"] = organization calledSignupWithPasskey = true return tokenRequest(credentialsResult) } diff --git a/auth0_flutter/example/web/index.html b/auth0_flutter/example/web/index.html index a7d5d25d2..782133b68 100644 --- a/auth0_flutter/example/web/index.html +++ b/auth0_flutter/example/web/index.html @@ -47,7 +47,7 @@ // Create promise that resolves when Auth0 SDK is loaded window.auth0SdkReady = new Promise((resolve, reject) => { const script = document.createElement('script'); - script.src = 'https://unpkg.com/@auth0/auth0-spa-js@2.24.0/dist/auth0-spa-js.production.js'; + script.src = 'https://unpkg.com/@auth0/auth0-spa-js@2.29.0/dist/auth0-spa-js.production.js'; script.onload = () => { if (window.auth0 && window.auth0.Auth0Client) { diff --git a/auth0_flutter/ios/auth0_flutter.podspec b/auth0_flutter/ios/auth0_flutter.podspec index 6c1b397b7..2f5dfc09a 100644 --- a/auth0_flutter/ios/auth0_flutter.podspec +++ b/auth0_flutter/ios/auth0_flutter.podspec @@ -13,13 +13,13 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.ios.deployment_target = '14.0' + s.ios.deployment_target = '15.0' s.ios.dependency 'Flutter' - s.osx.deployment_target = '11.0' + s.osx.deployment_target = '12.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '3.0.2' + s.dependency 'Auth0', '3.2.0' s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter/macos/auth0_flutter.podspec b/auth0_flutter/macos/auth0_flutter.podspec index 6c1b397b7..2f5dfc09a 100644 --- a/auth0_flutter/macos/auth0_flutter.podspec +++ b/auth0_flutter/macos/auth0_flutter.podspec @@ -13,13 +13,13 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.ios.deployment_target = '14.0' + s.ios.deployment_target = '15.0' s.ios.dependency 'Flutter' - s.osx.deployment_target = '11.0' + s.osx.deployment_target = '12.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '3.0.2' + s.dependency 'Auth0', '3.2.0' s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' From 844ff059acd0260de43a1772c345bc6721a77896 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Tue, 27 Oct 2026 00:23:02 +0530 Subject: [PATCH 20/20] fix(android): bump Auth0.Android to 4.2.1 to keep RSA key for PKCS1 migration on padding mismatch Co-Authored-By: Claude --- auth0_flutter/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth0_flutter/android/build.gradle b/auth0_flutter/android/build.gradle index 9ddd46963..00f27a9ad 100644 --- a/auth0_flutter/android/build.gradle +++ b/auth0_flutter/android/build.gradle @@ -85,7 +85,7 @@ plugins.withId("org.jetbrains.kotlin.android") { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'com.auth0.android:auth0:4.2.0' + implementation 'com.auth0.android:auth0:4.2.1' implementation 'com.google.code.gson:gson:2.11.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0'