diff --git a/.github/actions/setup-darwin/action.yml b/.github/actions/setup-darwin/action.yml index cb2c90193..84d523dab 100644 --- a/.github/actions/setup-darwin/action.yml +++ b/.github/actions/setup-darwin/action.yml @@ -79,7 +79,7 @@ runs: echo "::error::FlutterGeneratedPluginSwiftPackage/Package.swift not found at expected path" exit 1 fi - sed -i '' 's/\.iOS(".*")/.iOS("14.0")/g' "$PACKAGE_FILE" + sed -i '' 's/\.iOS(".*")/.iOS("15.0")/g' "$PACKAGE_FILE" shell: bash - name: Fix macOS minimum deployment target for SPM @@ -90,7 +90,7 @@ runs: echo "::error::FlutterGeneratedPluginSwiftPackage/Package.swift not found at expected path" exit 1 fi - sed -i '' 's/\.macOS(".*")/.macOS("11.0")/g' "$PACKAGE_FILE" + sed -i '' 's/\.macOS(".*")/.macOS("12.0")/g' "$PACKAGE_FILE" shell: bash - name: Set .env diff --git a/CLAUDE.md b/CLAUDE.md index f7571bd4b..93963b683 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ Apply these on every task in this repo — they keep changes correct, small, and - **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+ +- **Minimum Platform Version:** Android API 26+, iOS 15+, macOS 12+, 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 --- diff --git a/auth0_flutter/CHANGELOG.md b/auth0_flutter/CHANGELOG.md index c63960926..083831968 100644 --- a/auth0_flutter/CHANGELOG.md +++ b/auth0_flutter/CHANGELOG.md @@ -12,6 +12,28 @@ - fix: bump Auth0.Android to 3.21.2 for biometric failure fix [\#968](https://github.com/auth0/auth0-flutter/pull/968) ([utkrishtsahu](https://github.com/utkrishtsahu)) - fix(android): bump Auth0.Android to 3.21.1 for Keystore2 INCOMPATIBLE_DEVICE fix [\#963](https://github.com/auth0/auth0-flutter/pull/963) ([utkrishtsahu](https://github.com/utkrishtsahu)) +## [af-v3.0.0-beta.0](https://github.com/auth0/auth0-flutter/tree/af-v3.0.0-beta.0) (2026-09-16) +[Full Changelog](https://github.com/auth0/auth0-flutter/compare/af-v2.6.2...af-v3.0.0-beta.0) + +**Breaking Change** +- feat: remove inline MFA methods from the Authentication API. [\#943](https://github.com/auth0/auth0-flutter/pull/943) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: reconcile WebAuthenticationException error codes across Android and iOS [\#935](https://github.com/auth0/auth0-flutter/pull/935) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: change SSOCredentials.expiresIn to expiresAt (Android + iOS) [\#931](https://github.com/auth0/auth0-flutter/pull/931) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: Upgrade Auth0.Android to v4.0.0. [\#918](https://github.com/auth0/auth0-flutter/pull/918) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: Upgrade Auth0.swift to v3.0.2 [\#909](https://github.com/auth0/auth0-flutter/pull/909) ([NandanPrabhu](https://github.com/NandanPrabhu)) + +**Added** +- feat: add nonce to IdTokenValidationConfig for Web Auth login [\#944](https://github.com/auth0/auth0-flutter/pull/944) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: dpop web auth init config [\#938](https://github.com/auth0/auth0-flutter/pull/938) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: add credentialsManager.clearAll() for full credential and key wipe [\#930](https://github.com/auth0/auth0-flutter/pull/930) ([utkrishtsahu](https://github.com/utkrishtsahu)) + +**Changed** +- feat: default credentialsManager minTtl to 60 seconds [\#929](https://github.com/auth0/auth0-flutter/pull/929) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: honor useEphemeralSession on Android web auth [\#927](https://github.com/auth0/auth0-flutter/pull/927) ([utkrishtsahu](https://github.com/utkrishtsahu)) + +**Fixed** +- fix: register Android web auth callbacks with the activity lifecycle [\#928](https://github.com/auth0/auth0-flutter/pull/928) ([utkrishtsahu](https://github.com/utkrishtsahu)) + ## [af-v2.6.2](https://github.com/auth0/auth0-flutter/tree/af-v2.6.2) (2026-09-15) [Full Changelog](https://github.com/auth0/auth0-flutter/compare/af-v2.6.1...af-v2.6.2) diff --git a/auth0_flutter/EXAMPLES.md b/auth0_flutter/EXAMPLES.md index fc5ca744b..74d5e3834 100644 --- a/auth0_flutter/EXAMPLES.md +++ b/auth0_flutter/EXAMPLES.md @@ -11,6 +11,7 @@ - [1. Configure a custom URL scheme](#1-configure-a-custom-url-scheme) - [2. Capture the callback URL](#2-capture-the-callback-url) - [Using Partial Custom Tabs (Android only)](#using-partial-custom-tabs-android-only) + - [Using Trusted Web Activity or Auth Tab (Android only)](#using-trusted-web-activity-or-auth-tab-android-only) - [Errors](#errors) - [Android: Custom schemes](#android-custom-schemes) - [🪟 Windows Web Authentication](#-windows-web-authentication) @@ -262,6 +263,16 @@ final credentials = await auth0.webAuthentication().login(idTokenValidationConfig: config); ``` +You can also supply your own `nonce` to be validated against the `nonce` claim of the returned ID token. When omitted, the SDK generates and validates one automatically. + +```dart +const config = IdTokenValidationConfig(nonce: 'a1b2c3d4'); +final credentials = + await auth0.webAuthentication().login(idTokenValidationConfig: config); +``` + +> ℹ️ `nonce` is not supported on Web, where it is always managed internally by auth0-spa-js. +
@@ -395,6 +406,8 @@ The available options are: | `sideSheetBreakpoint` | `int?` | Screen width threshold (dp) to switch between bottom sheet and side sheet. Defaults to the browser's built-in value (typically 840dp). | | `backgroundInteractionEnabled` | `bool?` | Whether the user can interact with the app behind the partial tab. Defaults to `false`. | | `allowedBrowsers` | `List` | Allowlist of browser packages for Custom Tabs. | +| `useTrustedWebActivity` | `bool` | Launch the browser as a [Trusted Web Activity](#using-trusted-web-activity-or-auth-tab-android-only). Defaults to `false`. | +| `useAuthTab` | `bool` | Launch the browser as an [Auth Tab](#using-trusted-web-activity-or-auth-tab-android-only). Defaults to `false`. | You can also use `customTabsOptions` during logout: @@ -406,6 +419,35 @@ await auth0.webAuthentication().logout( )); ``` +### Using Trusted Web Activity or Auth Tab (Android only) + +Both launch mechanisms are opt-in and are enabled through `CustomTabsOptions`. They apply to `login` and `logout`, and are ignored on iOS, macOS, Windows and web. + +- **Auth Tab** uses the Chrome Custom Tabs Activity Result API instead of inferring the result from the activity lifecycle. This avoids spurious `isUserCancelledException` errors when Chrome is minimized. It requires Chrome 137+; older browsers fall back to a standard Custom Tab. +- **Trusted Web Activity (TWA)** opens the Auth0 domain without browser UI. Your app must be verified for the domain through [Digital Asset Links](https://developer.android.com/training/app-links/verify-android-applinks). + +```dart +// Auth Tab +await auth0.webAuthentication().login( + customTabsOptions: const CustomTabsOptions(useAuthTab: true)); + +// Trusted Web Activity +await auth0.webAuthentication().logout( + customTabsOptions: const CustomTabsOptions(useTrustedWebActivity: true)); +``` + +The two are mutually exclusive. If both are set to `true`, Trusted Web Activity takes precedence and Auth Tab is not used. + +#### Ephemeral sessions + +`useEphemeralSession: true` on `login` is honored by Auth Tab: the tab runs in an isolated session when the browser supports it (Chrome 136+ or a compatible browser), and otherwise falls back to a regular Auth Tab. It has no effect with Trusted Web Activity, because a TWA shares the user's browser profile by design and cannot run in an isolated session. + +```dart +await auth0.webAuthentication().login( + useEphemeralSession: true, + customTabsOptions: const CustomTabsOptions(useAuthTab: true)); +``` + ### Errors
@@ -418,15 +460,21 @@ try { final credentials = await auth0.webAuthentication().login(); // ... } on WebAuthenticationException catch (e) { - if (e.isRetryable) { + if (e.isUserCancelledException) { + // User dismissed the browser — same on Android and iOS + } else if (e.isRetryable) { // Transient error (e.g. network issue) — safe to retry + } else if (e.code == 'dpop_jkt_mismatch') { + // DPoP thumbprint mismatch returned by the Auth0 server (Android + iOS) } else { print(e); } } ``` -The `isRetryable` property indicates whether the error is transient (e.g. a network outage) and the operation can be retried. +- `isUserCancelledException` — `true` when the user dismisses the browser without completing login (Android + iOS). +- `isRetryable` — `true` when the error is transient (e.g. a network outage) and the operation can be retried. +- For server-returned errors (e.g. `dpop_jkt_mismatch`, `access_denied`), `exception.code` contains the raw server error code directly.
@@ -710,6 +758,13 @@ final credentials = await auth0.credentialsManager.credentials(); > 💡 You do not need to call `credentialsManager.storeCredentials()` afterward. The Credentials Manager automatically persists the renewed credentials. +> 💡 By default, `credentials()` (and `getApiCredentials()`) requires the access token to have at least **60 seconds** of remaining lifetime, matching Auth0.Android v4 and Auth0.swift v3. If the token would expire sooner, it is refreshed using the refresh token. Pass `minTtl: 0` to return the stored token regardless of how soon it expires, or a larger value to require more headroom: +> +> ```dart +> // Return the stored token even if it is about to expire. +> final credentials = await auth0.credentialsManager.credentials(minTtl: 0); +> ``` + ### 🕒 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. @@ -792,6 +847,22 @@ await auth0.credentialsManager.clearApiCredentials( > > 💡 Stored API credentials are keyed by **both** audience and scope on every platform, so pass the same `scope` to `clearApiCredentials()` that you used when fetching them. The native APIs do not consistently report whether a matching entry existed, so this method returns `void` rather than a success flag. +### Clear all credentials and encryption keys + +To wipe **everything** the Credentials Manager stores – all credentials, all cached API credentials, and the underlying encryption keys – use `clearAll()`: + +```dart +await auth0.credentialsManager.clearAll(); +``` + +> 💡 `clearAll()` is a more thorough wipe than `clearCredentials()` or `clearApiCredentials()`: +> +> - `clearCredentials()` removes the stored credential entries only. On iOS/macOS it deletes the credentials, DPoP thumbprint, and session-expiry entries; on Android it clears the credentials store. +> - `clearApiCredentials(audience:)` removes only the cached API credentials for a specific audience (and scope), leaving the main credentials and the encryption keys intact. +> - `clearAll()` additionally removes the cryptographic keys used to protect the stored data – on Android the crypto key pair and the DPoP key; on iOS/macOS every entry in the credentials store plus the DPoP key pair. +> +> ⚠️ Because `clearAll()` deletes *all* entries in the underlying store, avoid sharing the Credentials Manager's storage (for example, a custom `sharedPreferencesName` on Android or `storeKey`/`accessGroup` on iOS) with unrelated app data. + ### Retrieve user profile Fetch the user profile associated with the stored credentials. This method returns `null` if no credentials are present in storage. @@ -933,7 +1004,7 @@ try { print('Session Transfer Token: ${ssoCredentials.sessionTransferToken}'); print('Token Type: ${ssoCredentials.tokenType}'); - print('Expires In: ${ssoCredentials.expiresIn} seconds'); + print('Expires At: ${ssoCredentials.expiresAt}'); } on CredentialsManagerException catch (e) { print('Failed to get SSO credentials: ${e.message}'); } @@ -946,7 +1017,7 @@ try { final ssoCredentials = await auth0.api.ssoExchange(refreshToken: refreshToken); print('Session Transfer Token: ${ssoCredentials.sessionTransferToken}'); - print('Expires In: ${ssoCredentials.expiresIn} seconds'); + print('Expires At: ${ssoCredentials.expiresAt}'); } on ApiException catch (e) { print('SSO Exchange failed: ${e.code} - ${e.message}'); } @@ -1513,7 +1584,7 @@ final credentials = await auth0.passwordless.loginWithOtp(
> [!NOTE] -> If the user has MFA configured, `loginWithOtp` fails with an [`ApiException`](#errors-2) whose `isMultifactorRequired` is `true`. Continue the flow using the existing MFA APIs (`auth0.api.multifactorChallenge` / `auth0.api.loginWithOtp`). +> If the user has MFA configured, `loginWithOtp` fails with an [`ApiException`](#errors-2) whose `isMultifactorRequired` is `true`. Continue the flow using the MFA APIs (`auth0.mfa.challenge` / `auth0.mfa.verifyOtp`). To receive DPoP-bound tokens from the token exchange when DPoP is enabled for your client, construct your `Auth0` instance with `useDPoP: true`: @@ -1753,7 +1824,7 @@ try { if (e.isVerificationRequired) { final credentials = await auth0.webAuthentication().login( scopes: scopes, - useEphemeralSession: true, // Otherwise a session cookie will remain (iOS/macOS only) + useEphemeralSession: true, // Otherwise a session cookie will remain (see note below) parameters: { 'connection': connection, 'login_hint': email // So the user doesn't have to type it again @@ -1763,6 +1834,8 @@ try { } ``` +> 💡 `useEphemeralSession` starts a private browser session so no session cookie is persisted. It is honored on iOS/macOS and, as of v3, on Android. On Android it depends on the device browser supporting ephemeral (private) Custom Tabs sessions — if the browser doesn't support it, the login falls back to a normal Custom Tabs session and is **not** ephemeral. + --- [Go up ⤴](#examples) diff --git a/auth0_flutter/README.md b/auth0_flutter/README.md index ededbb85f..e14e7e374 100644 --- a/auth0_flutter/README.md +++ b/auth0_flutter/README.md @@ -11,6 +11,8 @@ ## What's New +> 📖 **Migrating from auth0_flutter v2?** See the [v3 Migration Guide](V3_MIGRATION_GUIDE.md) for the raised platform requirements, changed defaults, and breaking changes. + **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 @@ -28,8 +30,8 @@ | Flutter | Android | iOS | macOS | Windows | | :---------- | :-------------- | :---------------- | :---------------- | :------------------------------- | -| SDK 3.24.0+ | Android API 21+ | iOS 14+ | macOS 11+ | Windows 10+ | -| Dart 3.5.0+ | Java 8+ | Swift 5.9+ | Swift 5.9+ | C++ 17, Visual Studio 2022 | +| SDK 3.24.0+ | Android API 26+ | iOS 15+ | macOS 12+ | Windows 10+ | +| Dart 3.5.0+ | Java 17+ | Swift 5.9+ | Swift 5.9+ | C++ 17, Visual Studio 2022 | | | | Xcode 15.x / 16.x | Xcode 15.x / 16.x | vcpkg (for dependencies) | ### Installation @@ -368,7 +370,7 @@ 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`. @@ -627,8 +629,6 @@ void dispose() { #### API - [login](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/login.html) -- [loginWithOtp](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/loginWithOtp.html) -- [multifactorChallenge](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/multifactorChallenge.html) - [renewCredentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/renewCredentials.html) - [resetPassword](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/resetPassword.html) - [signup](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/signup.html) diff --git a/auth0_flutter/V3_MIGRATION_GUIDE.md b/auth0_flutter/V3_MIGRATION_GUIDE.md new file mode 100644 index 000000000..8ea82c730 --- /dev/null +++ b/auth0_flutter/V3_MIGRATION_GUIDE.md @@ -0,0 +1,350 @@ +# 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, change some default values for +better out-of-the-box behavior, and change some native behavior that surfaces +through the Dart API. This guide documents the changes required when migrating +from v2 to v3. + +--- + +## Table of Contents + +- [**Requirements Changes**](#requirements-changes) +- [**Breaking Changes**](#breaking-changes) + + [Inline MFA methods removed from the Authentication API](#inline-mfa-methods-removed-from-the-authentication-api) + + [`SSOCredentials.expiresIn` is now `expiresAt` (`DateTime`)](#ssocredentialsexpiresin-is-now-expiresat-datetime) + + [`WebAuthenticationException` error codes reconciled (Android + iOS)](#webauthenticationexception-error-codes-reconciled-android--ios) +- [**Default Values Changed**](#default-values-changed) + + [Credentials manager `minTtl` now defaults to 60 seconds](#credentials-manager-minttl-now-defaults-to-60-seconds) +- [**Behavior Changes**](#behavior-changes) + + [`credentialsManager.clearCredentials` now clears all stored data (Android)](#credentialsmanagerclearcredentials-now-clears-all-stored-data-android) + + [Web Auth `useEphemeralSession` is now honored on Android](#web-auth-useephemeralsession-is-now-honored-on-android) + + [Android Web Auth recovers login results across process death](#android-web-auth-recovers-login-results-across-process-death) +- [**New APIs**](#new-apis) + + [`credentialsManager.clearAll()` — full credential and key cleanup](#credentialsmanagerclearall--full-credential-and-key-cleanup) +- [Getting Help](#getting-help) + +--- + +## Requirements Changes + +Because the underlying native SDKs raised their floors, `auth0_flutter` v3 +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** | +| Kotlin | 1.9.x | **2.0.21** | +| Android Gradle Plugin | 8.4.x | **8.10.1** | +| Gradle | 8.7 | **8.11.1** | + +**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*, +or via `org.gradle.java.home`). + +There are **no source changes required** to your Dart code for these +requirement bumps. + +--- + +## Breaking Changes + +### Inline MFA methods removed from the Authentication API + +**Change:** The inline MFA methods `api.loginWithOtp` and +`api.multifactorChallenge` have been removed from the authentication client. +This follows Auth0.Android v4 and Auth0.swift v3, which route MFA through a +dedicated MFA client. Their functionality is covered by the MFA API +(`auth0.mfa()`), which lists authenticators, requests a +challenge, and verifies OTP/OOB/recovery-code factors. The `ChallengeType` +enum, used only by `multifactorChallenge`, has also been removed. + +**Impact:** Code that calls `auth0.api.loginWithOtp` or +`auth0.api.multifactorChallenge` no longer compiles. + +| Removed (`auth0.api`) | Replacement (`auth0.mfa`) | +|-----------------------|---------------------------| +| `loginWithOtp({otp, mfaToken})` | `verifyOtp({otp})` | +| `multifactorChallenge({mfaToken, types, authenticatorId})` | `challenge({authenticatorId})` | + +**Migration:** Obtain an `MfaApi` instance by passing the `mfaToken` from the +MFA-required `ApiException` to `auth0.mfa()`, then drive the challenge/verify +flow through it. The `mfaToken` is supplied once rather than repeated on each +call, and challenges target a specific `authenticatorId`: + +```dart +try { + await auth0.api.login( + usernameOrEmail: email, + password: password, + connectionOrRealm: 'Username-Password-Authentication', + ); +} on ApiException catch (e) { + if (e.isMultifactorRequired && e.mfaToken != null) { + final mfa = auth0.mfa(mfaToken: e.mfaToken!); + + // ❌ v2 — inline challenge + OTP login on the auth client + // await auth0.api.multifactorChallenge(mfaToken: e.mfaToken!, ...); + // final credentials = + // await auth0.api.loginWithOtp(otp: '123456', mfaToken: e.mfaToken!); + + // ✅ v3 — challenge a specific authenticator, then verify the OTP + final authenticators = + await mfa.getAuthenticators(factorsAllowed: ['totp', 'phone']); + await mfa.challenge(authenticatorId: authenticators.first.id); + final credentials = await mfa.verifyOtp(otp: '123456'); + } +} +``` + +For TOTP factors you can skip the challenge request and call `mfa.verifyOtp` +directly; use `mfa.challenge` for out-of-band factors (SMS, email, push). + +### `SSOCredentials.expiresIn` is now `expiresAt` (`DateTime`) + +**Change:** Auth0.Android v4 and Auth0.swift v3 changed the Native to Web SSO +credential expiry from a relative `expiresIn` (seconds) to an absolute +`expiresAt` (date). `SSOCredentials` now exposes `expiresAt` as a UTC `DateTime` +instead of `expiresIn` as an `int`, matching `Credentials.expiresAt`. This +affects both `credentialsManager.ssoCredentials()` and `api.ssoExchange()` on +Android and iOS/macOS. + +**Impact:** Code that reads `ssoCredentials.expiresIn` no longer compiles. + +**Migration:** Read `expiresAt`, and derive the remaining lifetime yourself if +you still need a relative value: + +```dart +final sso = await auth0.credentialsManager.ssoCredentials(); + +// ❌ v2 — relative seconds +// final secondsLeft = sso.expiresIn; + +// ✅ v3 — absolute UTC timestamp +final DateTime expiresAt = sso.expiresAt; +final secondsLeft = expiresAt.difference(DateTime.now().toUtc()).inSeconds; +``` + +### `WebAuthenticationException` error codes reconciled (Android + iOS) + +**Change:** The error codes emitted by `WebAuthenticationException` are now +consistent across Android and iOS, and the Dart class exposes typed boolean +getters for each case. + +#### Android now emits `USER_CANCELLED` on user cancel + +Previously, when a user dismissed the browser on Android, the native SDK +emitted the raw code `a0.authentication_canceled`. iOS already emitted +`USER_CANCELLED`. Both platforms now emit `USER_CANCELLED`. + +**Migration:** Replace any check on the raw Android code: + +```dart +// v2 — required platform-branching +if (e.code == 'USER_CANCELLED' || e.code == 'a0.authentication_canceled') { ... } + +// v3 — single check, or use the typed getter +if (e.isUserCancelledException) { ... } +``` + +#### New typed getters on `WebAuthenticationException` + +| Getter | Code | Platforms | +|--------|------|-----------| +| `isUserCancelledException` | `USER_CANCELLED` | Android + iOS | +| `isAuthenticationFailed` | `AUTHENTICATION_FAILED` | iOS | +| `isCodeExchangeFailed` | `CODE_EXCHANGE_FAILED` | iOS | +| `isIdTokenValidationFailed` | `ID_TOKEN_VALIDATION_FAILED` | iOS | +| `isTransactionActiveAlready` | `TRANSACTION_ACTIVE_ALREADY` | iOS | + +#### Server error codes surface through `authenticationFailed` / `codeExchangeFailed` (iOS) + +Auth0.swift v3 wraps server-returned errors (e.g. `dpop_jkt_mismatch`, +`access_denied`, `invalid_grant`) inside `authenticationFailed` or +`codeExchangeFailed` as the cause. The Flutter SDK now extracts the underlying +server error code and surfaces it directly, so `exception.code` returns the +actual server code rather than the generic wrapper case name. + +**Migration:** If you were matching on `AUTHENTICATION_FAILED` or +`CODE_EXCHANGE_FAILED` and needed the underlying error detail, switch to +checking `exception.code` directly: + +```dart +try { + await auth0.webAuthentication().login(); +} on WebAuthenticationException catch (e) { + if (e.isUserCancelledException) { + // User dismissed the browser — same code on Android and iOS + } else if (e.code == 'dpop_jkt_mismatch') { + // DPoP thumbprint mismatch from the Auth0 server + } else if (e.isAuthenticationFailed) { + // authenticationFailed with no recognized server cause + } +} +``` + +#### Removed iOS error codes (v2 only) + +The following iOS-only v2 error codes are no longer emitted (they mapped to +removed `WebAuthError` cases in Auth0.swift v3): + +| Removed code | Replacement | +|---|---| +| `a0.no_bundle_identifier` | `UNKNOWN` | +| `a0.pkce_not_allowed` | `UNKNOWN` | +| `a0.no_authorization_code` | `UNKNOWN` — code-exchange failures now use `CODE_EXCHANGE_FAILED` | +| `a0.invalid_invitation_url` | `UNKNOWN` | + +**Migration:** Remove any `catch` branches that match these codes. + +--- + +## Default Values Changed + +### Credentials manager `minTtl` now defaults to 60 seconds + +**Change:** To match Auth0.Android v4 and Auth0.swift v3, the credential +retrieval APIs now default `minTtl` to **60** seconds instead of `0`. This +applies to `credentialsManager.credentials()` and +`credentialsManager.getApiCredentials()` on both Android and iOS/macOS. A +returned access token must have at least 60 seconds of remaining lifetime; if it +would expire sooner, it is refreshed (when a refresh token is available) instead +of being returned as-is. + +**Impact:** Calling these methods without an explicit `minTtl` now applies a +60-second floor. Tokens within 60 seconds of expiration trigger a refresh, and +if no refresh token is available the call fails instead of returning the +near-expired token. + +**Migration:** No code change is required to adopt the new default. To keep the +previous behavior, pass `minTtl: 0` explicitly: + +```dart +await auth0.credentialsManager.credentials(minTtl: 0); +await auth0.credentialsManager.getApiCredentials(audience: '...', minTtl: 0); +``` + +`hasValidCredentials` is intentionally left at `minTtl: 0`, matching the native +`hasValid` behavior — it reports whether a credential is currently valid without +applying the 60-second floor. + +**Reason:** A `minTtl` of `0` meant credentials were not renewed until already +expired, which could deliver access tokens that expire immediately after +retrieval and cause subsequent API requests to fail. A 60-second floor keeps the +returned token valid for a reasonable period. + +--- + +## 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. + +**Reason:** This simplifies credential cleanup, ensures no stale data remains in +storage after logout, and aligns Android behavior with the iOS/macOS store. + +### Web Auth `useEphemeralSession` is now honored on Android + +**Change:** In v2 the Web Auth `useEphemeralSession` login option had no effect +on Android (it was accepted but ignored, and documented as iOS/macOS only). In +v3, Android honors it via Auth0.Android v4's ephemeral browsing, matching +iOS/macOS. + +**Impact:** If your Android app already passed `useEphemeralSession: true` +expecting it to be ignored, the login now runs in a private/ephemeral browser +session: no session is shared with or persisted in the system browser, so the +user is not silently signed in from an existing browser session. + +**Migration:** No code change is required. Review your use of +`useEphemeralSession` on Android if you relied on the previous no-op behavior: + +```dart +// v3: on Android this now starts an ephemeral browser session (as on iOS/macOS). +await auth0.webAuthentication().login(useEphemeralSession: true); +``` + +> **Note** +> Ephemeral sessions depend on the device browser supporting them. If the +> browser that handles the login does not support ephemeral browsing, Android +> falls back to a normal Custom Tabs login and the session is not ephemeral. + +### Android Web Auth recovers login results across process death + +**Change:** Android Web Auth now registers its login callback against the +activity lifecycle (via Auth0.Android v4's `WebAuthProvider.registerCallbacks`) +instead of the deprecated global callback. As a result, a login result is +delivered even if the activity is recreated across a configuration change or the +process is killed and restarted while the browser is in the foreground. + +**Impact:** This is an internal improvement. The Dart API is unchanged and no +code change is required. + +--- + +## New APIs + +### `credentialsManager.clearAll()` — full credential and key cleanup + +**Change (additive):** v3 adds `credentialsManager.clearAll()`, which removes all +stored credentials and cached API credentials **and** the underlying encryption +keys — on Android the crypto key pair and the DPoP key, on iOS/macOS every entry +in the credentials store plus the DPoP key pair. It maps to Auth0.Android v4's +`clearAll()` and Auth0.swift v3's `clearAll()`. + +**Impact:** This is a new, optional method, so existing code is unaffected. Use +`clearCredentials()` to remove only the stored credential entries, or +`clearAll()` when you also want to drop the encryption keys (for example a full +sign-out/reset): + +```dart +await auth0.credentialsManager.clearAll(); +``` + +Because `clearAll()` deletes every entry in the underlying store, avoid sharing +that store (a custom `sharedPreferencesName` on Android, or `storeKey` / +`accessGroup` on iOS/macOS) with unrelated app data. + +--- + +## Getting Help + +If you encounter issues during migration: + +- [GitHub Issues](https://github.com/auth0/auth0-flutter/issues) — report bugs + or ask questions, with details of the API you're migrating and the platform + affected. +- [Auth0 Community](https://community.auth0.com/) — community support. diff --git a/auth0_flutter/android/build.gradle b/auth0_flutter/android/build.gradle index f462436e0..00f27a9ad 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" } } @@ -39,14 +39,14 @@ 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 } sourceSets { @@ -56,7 +56,7 @@ android { } defaultConfig { - minSdkVersion 21 + minSdkVersion 26 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders = [auth0Domain: "test-domain", auth0Scheme: "test"] } @@ -78,15 +78,15 @@ android { plugins.withId("org.jetbrains.kotlin.android") { kotlin { compilerOptions { - jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'com.auth0.android:auth0:3.21.3' - implementation 'com.google.code.gson:gson:2.10.1' + 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' testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0" @@ -96,4 +96,4 @@ dependencies { testImplementation 'androidx.test.espresso:espresso-intents:3.5.1' testImplementation 'com.auth0:java-jwt:3.19.1' -} \ No newline at end of file +} 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/Auth0FlutterPlugin.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt index 09e93c943..74656b029 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 @@ -1,6 +1,8 @@ package com.auth0.auth0_flutter import androidx.annotation.NonNull +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback import com.auth0.android.provider.WebAuthProvider @@ -12,6 +14,7 @@ import com.auth0.auth0_flutter.request_handlers.my_account.* import com.auth0.auth0_flutter.request_handlers.web_auth.LoginWebAuthRequestHandler import com.auth0.auth0_flutter.request_handlers.web_auth.LogoutWebAuthRequestHandler import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall @@ -48,6 +51,7 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { SaveCredentialsRequestHandler(), HasValidCredentialsRequestHandler(), ClearCredentialsRequestHandler(), + ClearAllRequestHandler(), GetCredentialsUserInfoRequestHandler() )) private val dpopCallHandler = Auth0FlutterDPoPMethodCallHandler(listOf( @@ -108,6 +112,11 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { } } + private val processDeathLogoutCallback = object : Callback { + override fun onSuccess(result: Void?) {} + override fun onFailure(exception: AuthenticationException) {} + } + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { binding = flutterPluginBinding val messenger = binding.binaryMessenger @@ -123,8 +132,6 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { authCallHandler = Auth0FlutterAuthMethodCallHandler( listOf( LoginApiRequestHandler(), - LoginWithOtpApiRequestHandler(), - MultifactorChallengeApiRequestHandler(), EmailPasswordlessApiRequestHandler(), PhoneNumberPasswordlessApiRequestHandler(), LoginWithEmailCodeApiRequestHandler(), @@ -189,20 +196,30 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { override fun onAttachedToActivity(binding: ActivityPluginBinding) { webAuthCallHandler.activity = binding.activity credentialsManagerCallHandler.activity = binding.activity - WebAuthProvider.addCallback(processDeathCallback) + registerWebAuthCallbacks(binding) } override fun onDetachedFromActivityForConfigChanges() { - WebAuthProvider.removeCallback(processDeathCallback) } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { webAuthCallHandler.activity = binding.activity credentialsManagerCallHandler.activity = binding.activity - WebAuthProvider.addCallback(processDeathCallback) + registerWebAuthCallbacks(binding) } override fun onDetachedFromActivity() { - WebAuthProvider.removeCallback(processDeathCallback) + } + + private fun registerWebAuthCallbacks(binding: ActivityPluginBinding) { + val activityLifecycle = (binding.lifecycle as HiddenLifecycleReference).lifecycle + val lifecycleOwner = object : LifecycleOwner { + override val lifecycle: Lifecycle = activityLifecycle + } + WebAuthProvider.registerCallbacks( + lifecycleOwner, + loginCallback = processDeathCallback, + logoutCallback = processDeathLogoutCallback + ) } } \ No newline at end of file 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("_isRetryable" to isNetworkError) + cause?.let { + details["cause"] = it.toString() + details["causeStackTrace"] = it.stackTraceToString() + } + val code = when { + isCanceled -> "USER_CANCELLED" + else -> getCode() + } + result.error(code, getDescription(), details) +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandler.kt deleted file mode 100644 index 62e226681..000000000 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandler.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.auth0.auth0_flutter.request_handlers.api - -import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.callback.Callback -import com.auth0.android.result.Credentials -import com.auth0.auth0_flutter.request_handlers.MethodCallRequest -import com.auth0.auth0_flutter.toMap -import com.auth0.auth0_flutter.utils.assertHasProperties -import io.flutter.plugin.common.MethodChannel -import java.util.* - -private const val AUTH_LOGIN_OTP_METHOD = "auth#loginOtp" - -class LoginWithOtpApiRequestHandler: ApiRequestHandler { - override val method: String = AUTH_LOGIN_OTP_METHOD - - override fun handle( - api: AuthenticationAPIClient, - request: MethodCallRequest, - result: MethodChannel.Result - ) { - val args = request.data - - assertHasProperties(listOf("mfaToken", "otp"), args) - - val loginBuilder = api - .loginWithOTP( - args["mfaToken"] as String, - args["otp"] as String, - ) - - loginBuilder.start(object : Callback { - override fun onFailure(exception: AuthenticationException) { - result.error( - exception.getCode(), - exception.getDescription(), - exception.toMap() - ) - } - - override fun onSuccess(credentials: Credentials) { - val scope = credentials.scope?.split(" ") ?: listOf() - var 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 scope, - "tokenType" to credentials.type - ) - ) - } - }) - } - -} 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 deleted file mode 100644 index 60d128684..000000000 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandler.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.auth0.auth0_flutter.request_handlers.api - -import com.auth0.android.authentication.AuthenticationAPIClient -import com.auth0.android.authentication.AuthenticationException -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.utils.assertHasProperties -import io.flutter.plugin.common.MethodChannel - -private const val AUTH_MULTIFACTORCHALLENGE_METHOD = "auth#multifactorChallenge" - -class MultifactorChallengeApiRequestHandler : ApiRequestHandler { - override val method: String = AUTH_MULTIFACTORCHALLENGE_METHOD - - override fun handle( - api: AuthenticationAPIClient, - request: MethodCallRequest, - result: MethodChannel.Result - ) { - assertHasProperties(listOf("mfaToken"), request.data) - - val challengeTypes = request.data["types"] as ArrayList<*>? - - 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) { - result.error( - exception.getCode(), - exception.getDescription(), - exception.toMap() - ) - } - - override fun onSuccess(challenge: Challenge) { - result.success( - mapOf( - "challengeType" to challenge.challengeType, - "oobCode" to challenge.oobCode, - "bindingMethod" to challenge.bindingMethod - ) - ) - } - }) - } -} 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..c39c56194 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 @@ -46,7 +46,7 @@ class SSOExchangeApiRequestHandler : ApiRequestHandler { val map = mutableMapOf( "sessionTransferToken" to credentials.sessionTransferToken, "tokenType" to credentials.issuedTokenType, - "expiresIn" to credentials.expiresIn, + "expiresAt" to credentials.expiresAt.toInstant().toString(), "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/ClearAllRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/ClearAllRequestHandler.kt new file mode 100644 index 000000000..8781463ab --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/ClearAllRequestHandler.kt @@ -0,0 +1,24 @@ +package com.auth0.auth0_flutter.request_handlers.credentials_manager + +import android.content.Context +import com.auth0.android.authentication.storage.SecureCredentialsManager +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel + +class ClearAllRequestHandler : CredentialsManagerRequestHandler { + override val method: String = "credentialsManager#clearAll" + + override fun handle( + credentialsManager: SecureCredentialsManager, + context: Context, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + try { + credentialsManager.clearAll() + result.success(null) + } catch (exception: Exception) { + result.error(exception.message ?: "UNKNOWN ERROR", exception.message, null) + } + } +} 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..ce946c4de 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 @@ -30,7 +30,7 @@ class GetSSOCredentialsRequestHandler : CredentialsManagerRequestHandler { val map = mutableMapOf( "sessionTransferToken" to credentials.sessionTransferToken, "tokenType" to credentials.issuedTokenType, - "expiresIn" to credentials.expiresIn, + "expiresAt" to credentials.expiresAt.toInstant().toString(), "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 41955842d..27b0f842a 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 @@ -7,6 +7,7 @@ import com.auth0.android.provider.WebAuthProvider import com.auth0.android.result.Credentials import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import com.auth0.auth0_flutter.toMap +import com.auth0.auth0_flutter.toWebAuthResult import com.auth0.auth0_flutter.utils.buildCustomTabsOptions import io.flutter.plugin.common.MethodChannel import java.util.* @@ -45,6 +46,17 @@ class LoginWebAuthRequestHandler( buildCustomTabsOptions(args)?.let { builder.withCustomTabsOptions(it) } + // The flags are forwarded as-is. TWA and Auth Tab are mutually + // exclusive and Auth0.Android resolves the conflict: if both are set, + // TWA wins. + val customTabsOptions = args["customTabsOptions"] as? Map<*, *> + if (customTabsOptions?.get("useTrustedWebActivity") == true) { + builder.withTrustedWebActivity() + } + if (customTabsOptions?.get("useAuthTab") == true) { + builder.withAuthTab() + } + if (args["leeway"] is Int) { builder.withIdTokenVerificationLeeway(args["leeway"] as Int) } @@ -57,6 +69,10 @@ class LoginWebAuthRequestHandler( builder.withIdTokenVerificationIssuer(args["issuer"] as String) } + if (args["nonce"] is String) { + builder.withNonce(args["nonce"] as String) + } + if (args["scheme"] is String) { builder.withScheme(args["scheme"] as String) } @@ -65,18 +81,17 @@ class LoginWebAuthRequestHandler( builder.withParameters(args["parameters"] as Map) } + if (args["useEphemeralSession"] == true) { + builder.withEphemeralBrowsing() + } + if (args["useDPoP"] == true) { - WebAuthProvider.useDPoP(context) + builder.useDPoP(context) } builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - val details = mutableMapOf("_isRetryable" to exception.isNetworkError) - exception.cause?.let { - details["cause"] = it.toString() - details["causeStackTrace"] = it.stackTraceToString() - } - result.error(exception.getCode(), exception.getDescription(), details) + exception.toWebAuthResult(result) } override fun onSuccess(credentials: Credentials) { diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt index c92fff691..92f2733bf 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt @@ -5,6 +5,7 @@ import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback import com.auth0.android.provider.WebAuthProvider import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toWebAuthResult import com.auth0.auth0_flutter.utils.buildCustomTabsOptions import io.flutter.plugin.common.MethodChannel @@ -34,14 +35,20 @@ class LogoutWebAuthRequestHandler(private val builderResolver: (MethodCallReques buildCustomTabsOptions(args)?.let { builder.withCustomTabsOptions(it) } + // The flags are forwarded as-is. TWA and Auth Tab are mutually + // exclusive and Auth0.Android resolves the conflict: if both are set, + // TWA wins. + val customTabsOptions = args["customTabsOptions"] as? Map<*, *> + if (customTabsOptions?.get("useTrustedWebActivity") == true) { + builder.withTrustedWebActivity() + } + if (customTabsOptions?.get("useAuthTab") == true) { + builder.withAuthTab() + } + builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - val details = mutableMapOf("_isRetryable" to exception.isNetworkError) - exception.cause?.let { - details["cause"] = it.toString() - details["causeStackTrace"] = it.stackTraceToString() - } - result.error(exception.getCode(), exception.getDescription(), details) + exception.toWebAuthResult(result) } override fun onSuccess(res: Void?) { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt index fe225affa..7b9a9af6b 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt @@ -2,8 +2,10 @@ package com.auth0.auth0_flutter import android.app.Activity import android.content.Context +import androidx.lifecycle.Lifecycle import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference import io.flutter.plugin.common.MethodChannel import org.junit.Test import org.junit.runner.RunWith @@ -95,6 +97,8 @@ class Auth0FlutterPluginTest { val mockActivityBindings = mock() val mockActivity = mock() `when`(mockActivityBindings.activity).thenReturn(mockActivity) + `when`(mockActivityBindings.lifecycle) + .thenReturn(HiddenLifecycleReference(mock())) plugin.onAttachedToActivity(mockActivityBindings) 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 66d4e51f5..6428c20dc 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 @@ -69,6 +69,7 @@ class LoginWebAuthRequestHandlerTest { verify(builder, never()).withIdTokenVerificationLeeway(any()) verify(builder, never()).withMaxAge(any()) verify(builder, never()).withIdTokenVerificationIssuer(any()) + verify(builder, never()).withNonce(any()) verify(builder, never()).withScheme(any()) verify(builder, never()).withParameters(any()) } @@ -275,6 +276,26 @@ class LoginWebAuthRequestHandlerTest { } } + @Test + fun `handler should set the nonce on the SDK when specified`() { + val args = hashMapOf( + "nonce" to "test-nonce" + ) + + runRequestHandler(args) { _, builder -> + verify(builder).withNonce("test-nonce") + } + } + + @Test + fun `handler should not set the nonce on the SDK when not specified`() { + val args = hashMapOf() + + runRequestHandler(args) { _, builder -> + verify(builder, never()).withNonce(anyOrNull()) + } + } + @Test fun `handler should set the scheme on the SDK when specified`() { val args = hashMapOf( @@ -317,6 +338,28 @@ class LoginWebAuthRequestHandlerTest { } } + @Test + fun `handler should enable ephemeral browsing when useEphemeralSession is true`() { + val args = hashMapOf( + "useEphemeralSession" to true + ) + + runRequestHandler(args) { _, builder -> + verify(builder).withEphemeralBrowsing() + } + } + + @Test + fun `handler should not enable ephemeral browsing when useEphemeralSession is false or absent`() { + runRequestHandler(hashMapOf("useEphemeralSession" to false)) { _, builder -> + verify(builder, never()).withEphemeralBrowsing() + } + + runRequestHandler(hashMapOf()) { _, builder -> + verify(builder, never()).withEphemeralBrowsing() + } + } + @Test fun `returns the error when the builder fails`() { val builder = mock() @@ -338,6 +381,28 @@ class LoginWebAuthRequestHandlerTest { verify(mockResult).error(eq("code"), eq("description"), eq(mapOf("_isRetryable" to false))) } + @Test + fun `returns USER_CANCELLED when the user cancels`() { + val builder = mock() + val mockResult = mock() + val exception = mock() + whenever(exception.isCanceled).thenReturn(true) + whenever(exception.getDescription()).thenReturn("User cancelled") + whenever(exception.isNetworkError).thenReturn(false) + + doAnswer { invocation -> + val cb = invocation.getArgument>(1) + cb.onFailure(exception) + }.`when`(builder).start(any(), any()) + + val handler = LoginWebAuthRequestHandler { _ -> builder } + val mockAccount = mock() + val mockRequest = MethodCallRequest(mockAccount, hashMapOf()) + handler.handle(mock(), mockRequest, mockResult) + + verify(mockResult).error(eq("USER_CANCELLED"), any(), any()) + } + @Test fun `returns cause and causeStackTrace in error details when cause is present`() { val builder = mock() @@ -492,4 +557,69 @@ class LoginWebAuthRequestHandlerTest { } } + @Test + fun `handler calls withTrustedWebActivity when useTrustedWebActivity is true`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useTrustedWebActivity" to true + ) + ) + + runRequestHandler(args) { _, builder -> + verify(builder).withTrustedWebActivity() + verify(builder, never()).withAuthTab() + } + } + + @Test + fun `handler calls withAuthTab when useAuthTab is true`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useAuthTab" to true + ) + ) + + runRequestHandler(args) { _, builder -> + verify(builder).withAuthTab() + verify(builder, never()).withTrustedWebActivity() + } + } + + @Test + fun `handler calls both withTrustedWebActivity and withAuthTab when both flags are true`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useTrustedWebActivity" to true, + "useAuthTab" to true + ) + ) + + runRequestHandler(args) { _, builder -> + verify(builder).withTrustedWebActivity() + verify(builder).withAuthTab() + } + } + + @Test + fun `handler does not call either when both flags are false`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useTrustedWebActivity" to false, + "useAuthTab" to false + ) + ) + + runRequestHandler(args) { _, builder -> + verify(builder, never()).withTrustedWebActivity() + verify(builder, never()).withAuthTab() + } + } + + @Test + fun `handler does not call withTrustedWebActivity or withAuthTab when customTabsOptions is absent`() { + runRequestHandler(hashMapOf()) { _, builder -> + verify(builder, never()).withTrustedWebActivity() + verify(builder, never()).withAuthTab() + } + } } diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt index eca4d4fe9..a5b9aad37 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt @@ -270,4 +270,70 @@ class LogoutWebAuthRequestHandlerTest { verify(builder).withCustomTabsOptions(any()) } } + + @Test + fun `handler calls withTrustedWebActivity when useTrustedWebActivity is true`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useTrustedWebActivity" to true + ) + ) + + runHandler(args) { _, builder -> + verify(builder).withTrustedWebActivity() + verify(builder, never()).withAuthTab() + } + } + + @Test + fun `handler calls withAuthTab when useAuthTab is true`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useAuthTab" to true + ) + ) + + runHandler(args) { _, builder -> + verify(builder).withAuthTab() + verify(builder, never()).withTrustedWebActivity() + } + } + + @Test + fun `handler calls both withTrustedWebActivity and withAuthTab when both flags are true`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useTrustedWebActivity" to true, + "useAuthTab" to true + ) + ) + + runHandler(args) { _, builder -> + verify(builder).withTrustedWebActivity() + verify(builder).withAuthTab() + } + } + + @Test + fun `handler does not call either when both flags are false`() { + val args = hashMapOf( + "customTabsOptions" to mapOf( + "useTrustedWebActivity" to false, + "useAuthTab" to false + ) + ) + + runHandler(args) { _, builder -> + verify(builder, never()).withTrustedWebActivity() + verify(builder, never()).withAuthTab() + } + } + + @Test + fun `handler does not call withTrustedWebActivity or withAuthTab when customTabsOptions is absent`() { + runHandler(hashMapOf()) { _, builder -> + verify(builder, never()).withTrustedWebActivity() + verify(builder, never()).withAuthTab() + } + } } 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 deleted file mode 100644 index 38a6d8312..000000000 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/LoginWithOtpApiRequestHandlerTest.kt +++ /dev/null @@ -1,167 +0,0 @@ -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.callback.Callback -import com.auth0.android.request.AuthenticationRequest -import com.auth0.android.result.Credentials -import com.auth0.auth0_flutter.JwtTestUtils - -import com.auth0.auth0_flutter.request_handlers.MethodCallRequest -import io.flutter.plugin.common.MethodChannel.Result -import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.* -import org.robolectric.RobolectricTestRunner -import java.text.SimpleDateFormat -import java.util.* - -@RunWith(RobolectricTestRunner::class) -class LoginWithOtpApiRequestHandlerTest { - @Test - fun `should throw when missing otp`() { - val options = hashMapOf("mfaToken" to "test-mfaToken") - val handler = LoginWithOtpApiRequestHandler() - 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 'otp' is not provided.") - ) - } - - @Test - fun `should throw when missing mfaToken`() { - val options = hashMapOf("otp" to "test-otp") - val handler = LoginWithOtpApiRequestHandler() - 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 'mfaToken' is not provided.") - ) - } - - @Test - fun `should call loginWithOTp with the correct parameters`() { - val options = hashMapOf( - "otp" to "test-otp", - "mfaToken" to "test-mfaToken" - ) - val handler = LoginWithOtpApiRequestHandler() - val mockLoginBuilder = mock() - val mockApi = mock() - val mockAccount = mock() - val mockResult = mock() - val request = MethodCallRequest(account = mockAccount, options) - - doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any()) - - handler.handle( - mockApi, - request, - mockResult - ) - - verify(mockApi).loginWithOTP("test-mfaToken", "test-otp") - verify(mockLoginBuilder).start(any()) - } - - @Test - fun `should call result error on failure`() { - val options = hashMapOf( - "otp" to "test-otp", - "mfaToken" to "test-mfaToken" - ) - val handler = LoginWithOtpApiRequestHandler() - val mockLoginBuilder = 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") - - doReturn(mockLoginBuilder).`when`(mockApi).loginWithOTP(any(), any()) - doAnswer { - val ob = it.getArgument>(0) - ob.onFailure(exception) - }.`when`(mockLoginBuilder).start(any()) - - handler.handle( - mockApi, - request, - mockResult - ) - - verify(mockResult).error(eq("test-code"), eq("test-description"), any()) - } - - @Test - fun `should call result success on success`() { - val options = hashMapOf( - "otp" to "test-otp", - "mfaToken" to "test-mfaToken" - ) - val handler = LoginWithOtpApiRequestHandler() - val mockLoginBuilder = mock() - val mockApi = mock() - val mockAccount = mock() - val mockResult = 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 scope2") - - whenever(mockApi.loginWithOTP(any(), any())).thenReturn(mockLoginBuilder) - whenever(mockLoginBuilder.start(any())).thenAnswer { - val callback = it.getArgument>(0) - callback.onSuccess(credentials) - } - - handler.handle( - mockApi, - request, - mockResult - ) - - val captor = argumentCaptor>() - verify(mockResult).success(captor.capture()) - - val formattedDate = credentials.expiresAt.toInstant().toString() - - val resultMap = captor.firstValue - - assertThat(resultMap["accessToken"], equalTo(credentials.accessToken)) - assertThat(resultMap["idToken"], equalTo(credentials.idToken)) - assertThat(resultMap["refreshToken"], equalTo(credentials.refreshToken)) - assertThat(resultMap["expiresAt"] as String, equalTo(formattedDate)) - assertThat(resultMap["scopes"], equalTo(listOf("scope1", "scope2"))) - assertThat((resultMap["userProfile"] as Map<*, *>)["name"], equalTo("John Doe")) - } -} 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 deleted file mode 100644 index 7276d7222..000000000 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/MultifactorChallengeApiRequestHandlerTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -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.callback.Callback -import com.auth0.android.request.Request -import com.auth0.android.result.Challenge - -import com.auth0.auth0_flutter.request_handlers.MethodCallRequest -import io.flutter.plugin.common.MethodChannel.Result -import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.* -import org.robolectric.RobolectricTestRunner - -@RunWith(RobolectricTestRunner::class) -class MultifactorChallengeApiRequestHandlerTest { - @Test - fun `should throw when missing mfaToken`() { - val options = hashMapOf() - 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 'mfaToken' is not provided.") - ) - } - - @Test - fun `should call multifactorChallenge 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 mockApi = mock() - val mockAccount = mock() - val mockResult = mock() - val request = MethodCallRequest(account = mockAccount, options) - - doReturn(mockBuilder).`when`(mockApi).multifactorChallenge(any(), any(), any()) - - handler.handle(mockApi, request, mockResult) - - verify(mockApi).multifactorChallenge("test-mfaToken", "type-1 type-2", "test-authenticatorId") - verify(mockBuilder).start(any()) - } - - @Test - fun `should call result error on failure`() { - val options = hashMapOf("mfaToken" to "test-mfaToken") - val handler = MultifactorChallengeApiRequestHandler() - val mockBuilder = 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") - - doReturn(mockBuilder).`when`(mockApi).multifactorChallenge(any(), anyOrNull(), anyOrNull()) - doAnswer { - val ob = it.getArgument>(0) - ob.onFailure(exception) - }.`when`(mockBuilder).start(any()) - - handler.handle(mockApi, request, mockResult) - - verify(mockResult).error(eq("test-code"), eq("test-description"), any()) - } - - @Test - fun `should call result success on success`() { - val options = hashMapOf("mfaToken" to "test-mfaToken") - val handler = MultifactorChallengeApiRequestHandler() - val mockBuilder = 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()) - doAnswer { - val ob = it.getArgument>(0) - ob.onSuccess(challenge) - }.`when`(mockBuilder).start(any()) - - handler.handle(mockApi, request, mockResult) - - val captor = argumentCaptor<() -> Map>() - verify(mockResult).success(captor.capture()) - - assertThat( - (captor.firstValue as Map<*, *>)["challengeType"], - equalTo(challenge.challengeType) - ) - assertThat( - (captor.firstValue as Map<*, *>)["oobCode"], - equalTo(challenge.oobCode) - ) - assertThat( - (captor.firstValue as Map<*, *>)["bindingMethod"], - equalTo(challenge.bindingMethod) - ) - } -} 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/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/ResetPasswordApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/ResetPasswordApiRequestHandlerTest.kt index 0b69f6568..3e06f83b9 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/ResetPasswordApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/ResetPasswordApiRequestHandlerTest.kt @@ -81,7 +81,7 @@ class ResetPasswordApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any()) + doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any(), anyOrNull()) handler.handle( mockApi, @@ -89,7 +89,7 @@ class ResetPasswordApiRequestHandlerTest { mockResult ) - verify(mockApi).resetPassword("test-email", "test-connection") + verify(mockApi).resetPassword("test-email", "test-connection", null) verify(mockBuilder).start(any()) } @@ -107,7 +107,7 @@ class ResetPasswordApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any()) + doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any(), anyOrNull()) doReturn(mockBuilder).`when`(mockBuilder).addParameters(any()) handler.handle( @@ -137,7 +137,7 @@ class ResetPasswordApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any()) + doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any(), anyOrNull()) doReturn(mockBuilder).`when`(mockBuilder).addParameters(any()) handler.handle( @@ -164,7 +164,7 @@ class ResetPasswordApiRequestHandlerTest { val exception = AuthenticationException(code = "test-code", description = "test-description") - doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any()) + doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any(), anyOrNull()) doReturn(mockBuilder).`when`(mockBuilder).addParameters(any()) doAnswer { val ob = it.getArgument>(0) @@ -193,7 +193,7 @@ class ResetPasswordApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any()) + doReturn(mockBuilder).`when`(mockApi).resetPassword(any(), any(), anyOrNull()) doReturn(mockBuilder).`when`(mockBuilder).addParameters(any()) doAnswer { val ob = it.getArgument>(0) 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..27def2346 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,8 @@ class SSOExchangeApiRequestHandlerTest { val mockSSOCredentials = mock() whenever(mockSSOCredentials.sessionTransferToken).thenReturn("sso-token") whenever(mockSSOCredentials.issuedTokenType).thenReturn("session_transfer") - whenever(mockSSOCredentials.expiresIn).thenReturn(60) + val expiresAt = Date(1730499395000L) + whenever(mockSSOCredentials.expiresAt).thenReturn(expiresAt) whenever(mockSSOCredentials.idToken).thenReturn("id-token") whenever(mockSSOCredentials.refreshToken).thenReturn("new-refresh-token") @@ -199,7 +201,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["expiresAt"], equalTo(expiresAt.toInstant().toString())) assertThat(resultMap["idToken"], equalTo("id-token")) assertThat(resultMap["refreshToken"], equalTo("new-refresh-token")) } @@ -219,7 +221,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/ClearAllRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/ClearAllRequestHandlerTest.kt new file mode 100644 index 000000000..d8f318baf --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/ClearAllRequestHandlerTest.kt @@ -0,0 +1,73 @@ +package com.auth0.auth0_flutter.request_handlers.credentials_manager + +import com.auth0.android.Auth0 +import com.auth0.android.authentication.storage.SecureCredentialsManager +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ClearAllRequestHandlerTest { + + @Test + fun `should call clearAll with the correct parameters`() { + val handler = ClearAllRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockCredentialsManager = mock() + val request = MethodCallRequest(account = mockAccount, mock()) + + handler.handle( + mockCredentialsManager, + mock(), + request, + mockResult + ) + + verify(mockCredentialsManager).clearAll() + } + + @Test + fun `should call result success with null because the native API returns void`() { + val handler = ClearAllRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockCredentialsManager = mock() + val request = MethodCallRequest(account = mockAccount, mock()) + + handler.handle( + mockCredentialsManager, + mock(), + request, + mockResult + ) + + verify(mockResult).success(isNull()) + } + + @Test + fun `should call result error when clearAll throws`() { + val handler = ClearAllRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockCredentialsManager = mock() + val request = MethodCallRequest(account = mockAccount, mock()) + + doThrow(RuntimeException("keystore error")) + .whenever(mockCredentialsManager).clearAll() + + handler.handle( + mockCredentialsManager, + mock(), + request, + mockResult + ) + + verify(mockResult).error(eq("keystore error"), eq("keystore error"), isNull()) + verify(mockResult, never()).success(any()) + } + +} 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..54ce4adff 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,8 @@ class GetSSOCredentialsRequestHandlerTest { val mockSSOCredentials = mock() `when`(mockSSOCredentials.sessionTransferToken).thenReturn("sso-token") `when`(mockSSOCredentials.issuedTokenType).thenReturn("session_transfer") - `when`(mockSSOCredentials.expiresIn).thenReturn(3600) + val expiresAt = Date(1730499395000L) + `when`(mockSSOCredentials.expiresAt).thenReturn(expiresAt) `when`(mockSSOCredentials.idToken).thenReturn("id-token") `when`(mockSSOCredentials.refreshToken).thenReturn(null) @@ -133,8 +135,8 @@ class GetSSOCredentialsRequestHandlerTest { CoreMatchers.equalTo("session_transfer") ) MatcherAssert.assertThat( - resultMap["expiresIn"], - CoreMatchers.equalTo(3600) + resultMap["expiresAt"], + CoreMatchers.equalTo(expiresAt.toInstant().toString()) ) 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/darwin/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift b/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift deleted file mode 120000 index e586bafb3..000000000 --- a/auth0_flutter/darwin/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift +++ /dev/null @@ -1 +0,0 @@ -../../auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift b/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift deleted file mode 120000 index 8ec0d5f74..000000000 --- a/auth0_flutter/darwin/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift +++ /dev/null @@ -1 +0,0 @@ -../../auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift new file mode 120000 index 000000000..3d21314e8 --- /dev/null +++ b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift @@ -0,0 +1 @@ +../../auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/darwin/auth0_flutter.podspec b/auth0_flutter/darwin/auth0_flutter.podspec index ba5603e64..5dfd104c7 100644 --- a/auth0_flutter/darwin/auth0_flutter.podspec +++ b/auth0_flutter/darwin/auth0_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'auth0_flutter' - s.version = '2.7.0' + s.version = '3.0.0-beta.0' s.summary = 'Auth0 SDK for Flutter' s.description = 'Auth0 SDK for Flutter Android, iOS, macOS, Windows, and web apps.' s.homepage = 'https://auth0.com' @@ -13,14 +13,14 @@ 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', '2.24.1' - s.dependency 'JWTDecode', '3.3.0' + s.dependency 'Auth0', '3.2.0' + 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 994468fb4..9dfc01048 100644 --- a/auth0_flutter/darwin/auth0_flutter/Package.swift +++ b/auth0_flutter/darwin/auth0_flutter/Package.swift @@ -4,15 +4,15 @@ 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: "2.24.1"), - .package(url: "https://github.com/auth0/JWTDecode.swift", exact: "3.3.0"), + .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"), ], 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..95d27bb7a 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 @@ -16,8 +16,6 @@ typealias AuthAPIMethodHandlerProvider = (_ method: AuthAPIHandler.Method, _ cli public class AuthAPIHandler: NSObject, FlutterPlugin { enum Method: String, CaseIterable { case loginWithUsernameOrEmail = "auth#login" - case loginWithOTP = "auth#loginOtp" - case multifactorChallenge = "auth#multifactorChallenge" case signup = "auth#signUp" case userInfo = "auth#userInfo" case renew = "auth#renew" @@ -67,8 +65,6 @@ public class AuthAPIHandler: NSObject, FlutterPlugin { 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) 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 deleted file mode 100644 index e1a36cf8f..000000000 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Auth0 - -#if os(iOS) -import Flutter -#else -import FlutterMacOS -#endif - -struct AuthAPILoginWithOTPMethodHandler: MethodHandler { - enum Argument: String { - case otp - case mfaToken - } - - let client: Authentication - - func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { - guard let otp = arguments[Argument.otp] as? String else { - return callback(FlutterError(from: .requiredArgumentMissing(Argument.otp.rawValue))) - } - guard let mfaToken = arguments[Argument.mfaToken] as? String else { - return callback(FlutterError(from: .requiredArgumentMissing(Argument.mfaToken.rawValue))) - } - - client - .login(withOTP: otp, mfaToken: mfaToken) - .start { - switch $0 { - case let .success(credentials): callback(result(from: credentials)) - case let .failure(error): callback(FlutterError(from: error)) - } - } - } -} 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 deleted file mode 100644 index a409a4670..000000000 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Auth0 - -#if os(iOS) -import Flutter -#else -import FlutterMacOS -#endif - -enum ChallengeProperty: String { - case challengeType - case oobCode - case bindingMethod -} - -fileprivate extension MethodHandler { - func result(from challenge: Challenge) -> Any? { - var data: [String: Any] = [ChallengeProperty.challengeType.rawValue: challenge.challengeType] - data[ChallengeProperty.oobCode] = challenge.oobCode - data[ChallengeProperty.bindingMethod] = challenge.bindingMethod - return data - } -} - -struct AuthAPIMultifactorChallengeMethodHandler: MethodHandler { - enum Argument: String { - case mfaToken - case types - case authenticatorId - } - - let client: Authentication - - 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 - - client - .multifactorChallenge(mfaToken: mfaToken, types: types, authenticatorId: authenticatorId) - .start { - switch $0 { - case let .success(challenge): callback(result(from: challenge)) - case let .failure(error): callback(FlutterError(from: error)) - } - - } - } -} 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..d2b2ec37b 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) + "expiresAt": ssoCredentials.expiresAt.asISO8601String ] 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/CredentialsManagerClearAllMethodHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift new file mode 100644 index 000000000..a86ac0cf2 --- /dev/null +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift @@ -0,0 +1,20 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct CredentialsManagerClearAllMethodHandler: MethodHandler { + let credentialsManager: CredentialsManager + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + do { + try self.credentialsManager.clearAll() + 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/CredentialsManagerHandler.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerHandler.swift index 908097dfe..60f97be50 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerHandler.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/CredentialsManager/CredentialsManagerHandler.swift @@ -27,6 +27,7 @@ public class CredentialsManagerHandler: NSObject, FlutterPlugin { case sso = "credentialsManager#getSSOCredentials" case apiCredentials = "credentialsManager#getApiCredentials" case clearApiCredentials = "credentialsManager#clearApiCredentials" + case clearAll = "credentialsManager#clearAll" } private struct ManagerCacheKey: Equatable { @@ -139,6 +140,7 @@ public class CredentialsManagerHandler: NSObject, FlutterPlugin { case .sso: return SSOCredentialsMethodHandler(credentialsManager: credentialsManager) case .apiCredentials: return ApiCredentialsMethodHandler(credentialsManager: credentialsManager) case .clearApiCredentials: return ClearApiCredentialsMethodHandler(credentialsManager: credentialsManager) + case .clearAll: return CredentialsManagerClearAllMethodHandler(credentialsManager: credentialsManager) } } 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..85cb32a2a 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) + "expiresAt": ssoCredentials.expiresAt.asISO8601String ] 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 47b2c7935..d30d5f582 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 @@ -106,13 +106,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/Properties.swift b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift index 67bd3c2ce..cd0d220ea 100644 --- a/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift +++ b/auth0_flutter/darwin/auth0_flutter/Sources/auth0_flutter/Properties.swift @@ -12,7 +12,7 @@ enum CredentialsProperty: String, CaseIterable { enum SSOCredentialsProperty: String, CaseIterable { case sessionTransferToken case tokenType - case expiresIn + case expiresAt case idToken case refreshToken } 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..63b95bde5 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 @@ -8,16 +8,18 @@ import FlutterMacOS extension FlutterError { convenience init(from webAuthError: WebAuthError) { + func causeCode(fallback: String) -> String { + (webAuthError.cause as? AuthenticationError)?.code ?? fallback + } 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 = causeCode(fallback: "AUTHENTICATION_FAILED") + case WebAuthError.codeExchangeFailed: code = causeCode(fallback: "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" + case WebAuthError.other: code = causeCode(fallback: "OTHER") default: code = "UNKNOWN" } var details = webAuthError.details 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..b81287696 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 { @@ -25,6 +25,7 @@ struct WebAuthLoginMethodHandler: MethodHandler { case useDPoP case issuer case maxAge + case nonce #if os(iOS) case safariViewController #endif @@ -35,7 +36,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 } @@ -100,10 +103,18 @@ struct WebAuthLoginMethodHandler: MethodHandler { webAuth = webAuth.maxAge(maxAge) } + if let nonce = arguments[Argument.nonce] as? String { + webAuth = webAuth.nonce(nonce) + } + #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/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 diff --git a/auth0_flutter/example/ios/Podfile b/auth0_flutter/example/ios/Podfile index cf07ade05..6ab3c66a9 100644 --- a/auth0_flutter/example/ios/Podfile +++ b/auth0_flutter/example/ios/Podfile @@ -38,5 +38,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 eac5f0e7c..bbd3c369a 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,7 +37,6 @@ 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 */; }; @@ -109,10 +109,10 @@ /* 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 = ""; }; 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 +145,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 +186,7 @@ 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 = ""; }; 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 +265,6 @@ 5C335E4627FE68A500EDDE3A /* AuthAPI */ = { isa = PBXGroup; children = ( - 2D82163728F9A89300467FD1 /* AuthAPILoginWithOTPMethodHandlerTests.swift */, 5C335E4727FE68BC00EDDE3A /* AuthAPISpies.swift */, 5C59DA6427FFCF0600365CDB /* AuthAPIHandlerTests.swift */, 5C59DA6627FFE75600365CDB /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift */, @@ -278,7 +277,6 @@ PK10000000000000000003 /* AuthAPIPasskeySignupChallengeMethodHandlerTests.swift */, PK20000000000000000002 /* AuthAPIPasskeyCredentialExchangeMethodHandlerTests.swift */, 5C59DA8A2809386B00365CDB /* AuthAPIExtensionsTests.swift */, - 5CA2853929C14EBA008A06B8 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */, ); path = AuthAPI; sourceTree = ""; @@ -296,6 +294,7 @@ A09B2E3F1D4C7891BA000001 /* CredentialsManagerSSOCredentialsMethodHandlerTests.swift */, A0AC1D0F2E0000000000AC01 /* CredentialsManagerApiCredentialsMethodHandlerTests.swift */, A0AC1D0F2E0000000000AC03 /* CredentialsManagerClearApiCredentialsMethodHandlerTests.swift */, + 0B3D36CB4A98386C0AF7BC50 /* CredentialsManagerUserInfoMethodHandlerTests.swift */, ); path = CredentialsManager; sourceTree = ""; @@ -371,6 +370,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + DDC7BE2C01B563724E04C1E9 /* SceneDelegate.swift */, ); path = Runner; sourceTree = ""; @@ -643,8 +643,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 +692,7 @@ B0CB00020000000000000006 /* MfaEnrollPushMethodHandlerTests.swift in Sources */, B0CB00020000000000000007 /* MfaChallengeMethodHandlerTests.swift in Sources */, B0CB00020000000000000008 /* MfaVerifyMethodHandlerTests.swift in Sources */, + 51B33CFEAF2B34075EB0D5BE /* CredentialsManagerUserInfoMethodHandlerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -711,6 +710,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 0AA9CE2720467CE301D1C118 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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..15e2b0500 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIHandlerTests.swift @@ -117,7 +117,6 @@ extension AuthAPIHandlerTests { var expectations: [XCTestExpectation] = [] var methodHandlers: [AuthAPIHandler.Method: MethodHandler.Type] = [ .loginWithUsernameOrEmail: AuthAPILoginUsernameOrEmailMethodHandler.self, - .loginWithOTP: AuthAPILoginWithOTPMethodHandler.self, .signup: AuthAPISignupMethodHandler.self, .userInfo: AuthAPIUserInfoMethodHandler.self, .renew: AuthAPIRenewMethodHandler.self, @@ -149,7 +148,8 @@ extension AuthAPIHandlerTests { func testCallsMethodHandlers() { var expectations: [XCTestExpectation] = [] - AuthAPIHandler.Method.allCases.forEach { method in + let methods = AuthAPIHandler.Method.allCases + 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 deleted file mode 100644 index 95e2f8702..000000000 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPILoginWithOTPMethodHandlerTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -import XCTest -import Auth0 - -@testable import auth0_flutter - -fileprivate typealias Argument = AuthAPILoginWithOTPMethodHandler.Argument - -class AuthAPILoginWithOTPMethodHandlerTests: XCTestCase { - var spy: SpyAuthentication! - var sut: AuthAPILoginWithOTPMethodHandler! - - override func setUpWithError() throws { - spy = SpyAuthentication() - sut = AuthAPILoginWithOTPMethodHandler(client: spy) - } -} - -// MARK: - Required Arguments Error - -extension AuthAPILoginWithOTPMethodHandlerTests { - func testProducesErrorWhenRequiredArgumentsAreMissing() { - let keys: [Argument] = [.otp, .mfaToken] - let expectations = keys.map { expectation(description: "\($0.rawValue) is missing") } - for (argument, currentExpectation) in zip(keys, expectations) { - sut.handle(with: arguments(without: argument)) { result in - assert(result: result, isError: .requiredArgumentMissing(argument.rawValue)) - currentExpectation.fulfill() - } - } - wait(for: expectations) - } -} - -// MARK: - ID Token Decoding Failed Error - -extension AuthAPILoginWithOTPMethodHandlerTests { - func testProducesErrorWithInvalidIDToken() { - let credentials = Credentials(idToken: "foo") - let expectation = self.expectation(description: "ID Token cannot be decoded") - spy.credentialsResult = .success(credentials) - sut.handle(with: arguments()) { result in - assert(result: result, isError: .idTokenDecodingFailed) - expectation.fulfill() - } - wait(for: [expectation]) - } -} - -// MARK: - Arguments - -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) - } - - // 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: - Login Result - -extension AuthAPILoginWithOTPMethodHandlerTests { - func testCallsSDKLoginWithOTPMethod() { - sut.handle(with: arguments()) { _ in } - XCTAssertTrue(spy.calledLoginWithOTP) - } - - func testProducesCredentials() { - let credentials = Credentials(accessToken: "accessToken", - tokenType: "tokenType", - idToken: testIdToken, - refreshToken: "refreshToken", - expiresIn: Date(), - scope: "foo bar") - let expectation = self.expectation(description: "Produced credentials") - spy.credentialsResult = .success(credentials) - sut.handle(with: arguments()) { result in - assert(result: result, has: CredentialsProperty.allCases) - expectation.fulfill() - } - wait(for: [expectation]) - } - - func testProducesAuthenticationError() { - let error = AuthenticationError(info: [:], statusCode: 0) - let expectation = self.expectation(description: "Produced the AuthenticationError \(error)") - spy.credentialsResult = .failure(error) - sut.handle(with: arguments()) { result in - assert(result: result, isError: error) - expectation.fulfill() - } - wait(for: [expectation]) - } -} - -// MARK: - Helpers - -extension AuthAPILoginWithOTPMethodHandlerTests { - override func arguments() -> [String: Any] { - return [ - Argument.otp.rawValue: "", - Argument.mfaToken.rawValue: "" - ] - } -} diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift deleted file mode 100644 index edf7c3b05..000000000 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPIMultifactorChallengeMethodHandlerTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -import XCTest - -@testable import Auth0 -@testable import auth0_flutter - -fileprivate typealias Argument = AuthAPIMultifactorChallengeMethodHandler.Argument - -class AuthAPIMultifactorChallengeMethodHandlerTests: XCTestCase { - var spy: SpyAuthentication! - var sut: AuthAPIMultifactorChallengeMethodHandler! - - override func setUpWithError() throws { - spy = SpyAuthentication() - sut = AuthAPIMultifactorChallengeMethodHandler(client: spy) - } -} - -// MARK: - Required Argument Error - -extension AuthAPIMultifactorChallengeMethodHandlerTests { - func testProducesErrorWhenMFATokenIsMissing() { - let key = Argument.mfaToken - let expectation = self.expectation(description: "mfaToken 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]) - } -} - -// MARK: - Multifactor Challenge Result - -extension AuthAPIMultifactorChallengeMethodHandlerTests { - func testCallsSDKMultifactorChallengeMethod() { - sut.handle(with: arguments()) { _ in } - XCTAssertTrue(spy.calledMultifactorChallenge) - } - - func testProducesChallenge() { - let challenge = Challenge(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) - expectation.fulfill() - } - wait(for: [expectation]) - } - - func testProducesAuthenticationError() { - let error = AuthenticationError(info: [:], statusCode: 0) - let expectation = self.expectation(description: "Produced the AuthenticationError \(error)") - spy.challengeResult = .failure(error) - sut.handle(with: arguments()) { result in - assert(result: result, isError: error) - expectation.fulfill() - } - wait(for: [expectation]) - } -} - -// MARK: - Helpers - -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..b774ad0fe 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISSOExchangeMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISSOExchangeMethodHandlerTests.swift @@ -43,7 +43,7 @@ extension AuthAPISSOExchangeMethodHandlerTests { let expectation = self.expectation(description: "Produced SSO credentials") sut.handle(with: arguments()) { result in let expectedKeys: [SSOCredentialsProperty] = [ - .sessionTransferToken, .tokenType, .expiresIn, .idToken + .sessionTransferToken, .tokenType, .expiresAt, .idToken ] assert(result: result, has: expectedKeys) expectation.fulfill() @@ -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..39decee88 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,15 @@ class SpyAuthentication: Authentication { return request(databaseUserResult) } - func resetPassword(email: String, connection: String) -> Request { + func resetPassword(email: String, connection: String, organization: String?) -> any Requestable { arguments["email"] = email arguments["connection"] = connection + arguments["organization"] = organization 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 +101,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 +125,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,41 +134,33 @@ 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 + // 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?) -> Request { - 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, - organization: String?) -> Request { + organization: String?) -> any TokenRequestable { arguments["connection"] = connection arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization - calledLoginWithPasskey = true - return request(credentialsResult) + calledSignupWithPasskey = true + return tokenRequest(credentialsResult) } @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) @@ -204,7 +174,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 @@ -217,23 +187,81 @@ class SpyAuthentication: Authentication { 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 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, + organization: String?) -> any TokenRequestable { + arguments["connection"] = connection + 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, - challenge: PasskeySignupChallenge, + verificationRequired: [PasskeyVerificationMethod], + authSession: String, connection: String?, audience: String?, scope: String, - organization: String?) -> Request { + organization: String?, + verification: [PasskeyVerificationMethod: 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 +307,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/CredentialsManagerClearAllMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearAllMethodHandlerTests.swift new file mode 100644 index 000000000..674e038a9 --- /dev/null +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerClearAllMethodHandlerTests.swift @@ -0,0 +1,50 @@ +import XCTest +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +@testable import auth0_flutter + +final class CredentialsManagerClearAllMethodHandlerTests: XCTestCase { + var spy: SpyCredentialsStorage! + var sut: CredentialsManagerClearAllMethodHandler! + + override func setUpWithError() throws { + spy = SpyCredentialsStorage() + let credentialsManager = CredentialsManager(authentication: SpyAuthentication(), storage: spy) + sut = CredentialsManagerClearAllMethodHandler(credentialsManager: credentialsManager) + } +} + +// MARK: - Clear All Result + +extension CredentialsManagerClearAllMethodHandlerTests { + func testCallsSDKClearAllMethod() { + sut.handle(with: arguments()) { _ in } + XCTAssertTrue(spy.calledDeleteAllEntries) + } + + func testProducesNilOnSuccess() { + let expectation = self.expectation(description: "Produced nil") + spy.deleteAllEntriesReturnValue = true + sut.handle(with: arguments()) { result in + XCTAssertNil(result) + expectation.fulfill() + } + wait(for: [expectation]) + } + + func testProducesFlutterErrorOnFailure() { + let expectation = self.expectation(description: "Produced a FlutterError") + spy.deleteAllEntriesReturnValue = false + sut.handle(with: arguments()) { result in + XCTAssertTrue(result is FlutterError) + 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..6e10fec47 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,13 +65,13 @@ 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 + .sessionTransferToken, .tokenType, .expiresAt, .idToken ] assert(result: result, has: expectedKeys) expectation.fulfill() @@ -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 0678bfa88..5ee64c203 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerSpies.swift @@ -3,22 +3,20 @@ import Auth0 // MARK: - Auth0.swift Spies +struct SpyCredentialsStorageError: Error {} + class SpyCredentialsStorage: CredentialsStorage { var setEntryReturnValue = true var deleteEntryReturnValue = true + var deleteAllEntriesReturnValue = true var calledGetEntry = false var calledSetEntry = false var calledDeleteEntry = false + var calledDeleteAllEntries = false - // Backing store keyed by entry name so the credentials blob and the pinned - // `session_expiry` value (which the manager stores under separate keys) can - // coexist. A single-slot spy would let the second write clobber the first. private var storage: [String: Data] = [:] - /// Convenience for tests that set/read the credentials blob without caring - /// about the key, preserving the previous single-value spy API. Setting - /// `nil` clears all entries (used to simulate "no credentials stored"). var getEntryReturnValue: Data? { get { self.storage["_default"] ?? self.storage.values.first } set { @@ -30,22 +28,29 @@ class SpyCredentialsStorage: CredentialsStorage { } } - func getEntry(forKey key: String) -> Data? { + func getEntry(forKey key: String) throws -> Data { self.calledGetEntry = true - // Fall back to the `_default` slot for tests that seed a value via - // `getEntryReturnValue` without going through `setEntry`. - return self.storage[key] ?? self.storage["_default"] + guard let value = self.storage[key] ?? self.storage["_default"] 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.storage[key] = data - return self.setEntryReturnValue + if !self.setEntryReturnValue { throw SpyCredentialsStorageError() } } - func deleteEntry(forKey key: String) -> Bool { + func deleteEntry(forKey key: String) throws { self.calledDeleteEntry = true self.storage.removeValue(forKey: key) - return self.deleteEntryReturnValue + if !self.deleteEntryReturnValue { throw SpyCredentialsStorageError() } + } + + func deleteAllEntries() throws { + self.calledDeleteAllEntries = true + self.storage.removeAll() + if !self.deleteAllEntriesReturnValue { 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 94cd06cf8..e6a0f418a 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,22 +150,22 @@ 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) } func testMapsSessionExpiryIntoDictionaryWhenClaimPresent() throws { // `testIdToken` carries a `session_expiry` claim of 4102444800 (2100-01-01). - let credentials = Credentials(accessToken: "", tokenType: "", idToken: testIdToken, expiresIn: Date()) + 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) @@ -174,7 +174,7 @@ extension ExtensionsTests { func testOmitsSessionExpiryFromDictionaryWhenClaimAbsent() throws { // `{}` payload — no `session_expiry` claim. let idToken = "eyJhbGciOiJIUzI1NiJ9.e30.ZRrHA1JJJW8opsbCGfG_HACGpVUMN_a9IV7pAx_Zmeo" - let credentials = Credentials(accessToken: "", tokenType: "", idToken: idToken, expiresIn: Date()) + let credentials = Credentials(accessToken: "", tokenType: "", idToken: idToken, expiresAt: Date()) let values = try credentials.asDictionary() XCTAssertNil(values[CredentialsProperty.sessionExpiry]) } @@ -184,7 +184,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] == [:]) @@ -195,7 +195,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) @@ -206,7 +206,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) @@ -217,7 +217,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) @@ -228,7 +228,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) @@ -239,7 +239,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) @@ -250,7 +250,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) @@ -261,7 +261,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) @@ -272,7 +272,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) @@ -283,7 +283,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) @@ -294,7 +294,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) @@ -305,7 +305,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) @@ -316,7 +316,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) @@ -327,7 +327,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) @@ -338,7 +338,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) @@ -349,7 +349,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) @@ -360,7 +360,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) @@ -371,7 +371,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) @@ -382,7 +382,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]) @@ -393,7 +393,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) @@ -404,7 +404,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]) @@ -427,7 +427,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 d6f111624..ab87814c1 100644 --- a/auth0_flutter/example/ios/Tests/Mocks.swift +++ b/auth0_flutter/example/ios/Tests/Mocks.swift @@ -47,6 +47,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 { @@ -54,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/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/Utilities.swift b/auth0_flutter/example/ios/Tests/Utilities.swift index 2828969c5..fb9c48099 100644 --- a/auth0_flutter/example/ios/Tests/Utilities.swift +++ b/auth0_flutter/example/ios/Tests/Utilities.swift @@ -71,16 +71,6 @@ func assert(result: Any?, has keys: [K]) where K.RawValue = XCTAssertEqual(result.keys.sorted(), keys.map(\.rawValue).sorted()) } -func assert(result: Any?, has challenge: Challenge) { - guard let result = result as? [String: Any] else { - return XCTFail("The handler did not produce a dictionary") - } - - XCTAssertEqual(result[ChallengeProperty.challengeType] as? String, challenge.challengeType) - XCTAssertEqual(result[ChallengeProperty.oobCode] as? String, challenge.oobCode) - XCTAssertEqual(result[ChallengeProperty.bindingMethod] as? String, challenge.bindingMethod) -} - func assert(result: Any?, has databaseUser: DatabaseUser) { guard let result = result as? [String: Any] else { return XCTFail("The handler did not produce a dictionary") diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift index f051792f0..606b635da 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 @@ -28,13 +27,17 @@ class WebAuthExtensionsTests: XCTestCase { } } + func testAuthenticationFailedFallsBackWhenNoCause() { + let flutterError = FlutterError(from: WebAuthError.authenticationFailed) + XCTAssertEqual(flutterError.code, "AUTHENTICATION_FAILED") + } + 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..c88a5ce3b 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthLoginMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthLoginMethodHandlerTests.swift @@ -197,6 +197,18 @@ extension WebAuthLoginHandlerTests { XCTAssertNil(spy.maxAgeValue) } + + func testAddsNonce() { + let value = "foo" + sut.handle(with: arguments(withKey: Argument.nonce, value: value)) { _ in } + XCTAssertEqual(spy.nonceValue, value) + } + + func testDoesNotAddNonceWhenNil() { + sut.handle(with: arguments(without: Argument.nonce)) { _ in } + XCTAssertNil(spy.nonceValue) + } + #if os(iOS) // MARK: safariViewController @@ -248,7 +260,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) @@ -286,6 +298,7 @@ extension WebAuthLoginHandlerTests { Argument.leeway.rawValue: 1, Argument.issuer.rawValue: "", Argument.maxAge.rawValue: 1, + Argument.nonce.rawValue: "", ] #if os(iOS) diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthSpies.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthSpies.swift index aec504638..50b4ba2ae 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(()) @@ -42,6 +43,7 @@ class SpyWebAuth: WebAuth { private(set) var issuerValue: String? private(set) var leewayValue: Int? private(set) var maxAgeValue: Int? + private(set) var nonceValue: String? private(set) var useHTTPSValue: Bool? private(set) var useEmphemeralSessionValue: Bool? private(set) var invitationURLValue: URL? @@ -83,6 +85,7 @@ class SpyWebAuth: WebAuth { } func nonce(_ nonce: String) -> Self { + self.nonceValue = nonce return self } @@ -138,9 +141,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 +165,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 450f56769..87438060a 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; }); } @@ -192,12 +194,17 @@ class _ExampleAppState extends State { 'Expires At: ${credentials.expiresAt}'; } else { // Mobile (Android/iOS): Use WebAuth with DPoP - final webAuthDPoP = auth0.webAuthentication( + final auth0DPoP = Auth0( + dotenv.env['AUTH0_DOMAIN']!, + dotenv.env['AUTH0_CLIENT_ID']!, + useDPoP: true, // Enable DPoP for mobile + ); + + final webAuthDPoP = auth0DPoP.webAuthentication( scheme: dotenv.env['AUTH0_CUSTOM_SCHEME'], ); final result = await webAuthDPoP.login( - useDPoP: true, // Enable DPoP for mobile useHTTPS: true, audience: dotenv.env['AUTH0_AUDIENCE'], ); @@ -230,7 +237,7 @@ class _ExampleAppState extends State { output = 'SSO Credentials:\n\n' 'Session Transfer Token: ${token.substring(0, 20)}...\n' 'Token Type: ${ssoCredentials.tokenType}\n' - 'Expires In: ${ssoCredentials.expiresIn}s\n' + 'Expires At: ${ssoCredentials.expiresAt.toIso8601String()}\n' 'ID Token: ****\n' 'Refresh Token: ' '${ssoCredentials.refreshToken != null ? '****' : 'N/A'}'; diff --git a/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj b/auth0_flutter/example/macos/Runner.xcodeproj/project.pbxproj index 2bc255fc6..d130487ce 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 */; }; @@ -37,10 +41,8 @@ 5C13992A2B0EB5F3006DCE54 /* WebAuthHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13990D2B0EB5F3006DCE54 /* WebAuthHandlerTests.swift */; }; 5C13992B2B0EB5F3006DCE54 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13990E2B0EB5F3006DCE54 /* Mocks.swift */; }; 5C13992C2B0EB5F3006DCE54 /* AuthAPIExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399102B0EB5F3006DCE54 /* AuthAPIExtensionsTests.swift */; }; - 5C13992D2B0EB5F3006DCE54 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399112B0EB5F3006DCE54 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */; }; 5C13992E2B0EB5F3006DCE54 /* AuthAPISignupMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399122B0EB5F3006DCE54 /* AuthAPISignupMethodHandlerTests.swift */; }; 5C13992F2B0EB5F3006DCE54 /* AuthAPIRenewMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399132B0EB5F3006DCE54 /* AuthAPIRenewMethodHandlerTests.swift */; }; - 5C1399302B0EB5F3006DCE54 /* AuthAPILoginWithOTPMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399142B0EB5F3006DCE54 /* AuthAPILoginWithOTPMethodHandlerTests.swift */; }; 5C1399312B0EB5F3006DCE54 /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399152B0EB5F3006DCE54 /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift */; }; 5C1399322B0EB5F3006DCE54 /* AuthAPIHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399162B0EB5F3006DCE54 /* AuthAPIHandlerTests.swift */; }; 5C1399332B0EB5F3006DCE54 /* AuthAPIResetPasswordMethodHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1399172B0EB5F3006DCE54 /* AuthAPIResetPasswordMethodHandlerTests.swift */; }; @@ -60,6 +62,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 +79,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 +126,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 = ""; }; @@ -149,10 +158,8 @@ 5C13990D2B0EB5F3006DCE54 /* WebAuthHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebAuthHandlerTests.swift; sourceTree = ""; }; 5C13990E2B0EB5F3006DCE54 /* Mocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = ""; }; 5C1399102B0EB5F3006DCE54 /* AuthAPIExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIExtensionsTests.swift; sourceTree = ""; }; - 5C1399112B0EB5F3006DCE54 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIMultifactorChallengeMethodHandlerTests.swift; sourceTree = ""; }; 5C1399122B0EB5F3006DCE54 /* AuthAPISignupMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPISignupMethodHandlerTests.swift; sourceTree = ""; }; 5C1399132B0EB5F3006DCE54 /* AuthAPIRenewMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIRenewMethodHandlerTests.swift; sourceTree = ""; }; - 5C1399142B0EB5F3006DCE54 /* AuthAPILoginWithOTPMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPILoginWithOTPMethodHandlerTests.swift; sourceTree = ""; }; 5C1399152B0EB5F3006DCE54 /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPILoginUsernameOrEmailMethodHandlerTests.swift; sourceTree = ""; }; 5C1399162B0EB5F3006DCE54 /* AuthAPIHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIHandlerTests.swift; sourceTree = ""; }; 5C1399172B0EB5F3006DCE54 /* AuthAPIResetPasswordMethodHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAPIResetPasswordMethodHandlerTests.swift; sourceTree = ""; }; @@ -169,6 +176,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 +189,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 +205,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 +326,7 @@ 5C13991A2B0EB5F3006DCE54 /* Auth0FlutterPluginTests.swift */, 5C13991B2B0EB5F3006DCE54 /* CredentialsManager */, B0CA000300000000000001A1 /* MyAccount */, + 8100C791C3EC5F990BD4210D /* Mfa */, ); name = Tests; path = ../ios/Tests; @@ -335,10 +348,8 @@ isa = PBXGroup; children = ( 5C1399102B0EB5F3006DCE54 /* AuthAPIExtensionsTests.swift */, - 5C1399112B0EB5F3006DCE54 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift */, 5C1399122B0EB5F3006DCE54 /* AuthAPISignupMethodHandlerTests.swift */, 5C1399132B0EB5F3006DCE54 /* AuthAPIRenewMethodHandlerTests.swift */, - 5C1399142B0EB5F3006DCE54 /* AuthAPILoginWithOTPMethodHandlerTests.swift */, 5C1399152B0EB5F3006DCE54 /* AuthAPILoginUsernameOrEmailMethodHandlerTests.swift */, 5C1399162B0EB5F3006DCE54 /* AuthAPIHandlerTests.swift */, 5C1399172B0EB5F3006DCE54 /* AuthAPIResetPasswordMethodHandlerTests.swift */, @@ -377,6 +388,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 = ( @@ -673,11 +700,9 @@ 5C13992A2B0EB5F3006DCE54 /* WebAuthHandlerTests.swift in Sources */, 5C1399282B0EB5F3006DCE54 /* WebAuthLogoutMethodHandlerTests.swift in Sources */, 5C1399272B0EB5F3006DCE54 /* WebAuthExtensionsTests.swift in Sources */, - 5C13992D2B0EB5F3006DCE54 /* AuthAPIMultifactorChallengeMethodHandlerTests.swift in Sources */, 5C1399262B0EB5F3006DCE54 /* WebAuthSpies.swift in Sources */, 5C1399322B0EB5F3006DCE54 /* AuthAPIHandlerTests.swift in Sources */, 5C1399362B0EB5F3006DCE54 /* Auth0FlutterPluginTests.swift in Sources */, - 5C1399302B0EB5F3006DCE54 /* AuthAPILoginWithOTPMethodHandlerTests.swift in Sources */, 5C1399372B0EB5F3006DCE54 /* CredentialsManagerHasValidMethodHandlerTests.swift in Sources */, 5C13993B2B0EB5F3006DCE54 /* CredentialsManagerClearMethodHandlerTests.swift in Sources */, 5C13992F2B0EB5F3006DCE54 /* AuthAPIRenewMethodHandlerTests.swift in Sources */, @@ -711,6 +736,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/example/web/index.html b/auth0_flutter/example/web/index.html index 1fe289279..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.1/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/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift b/auth0_flutter/ios/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift deleted file mode 120000 index 854e8df59..000000000 --- a/auth0_flutter/ios/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift +++ /dev/null @@ -1 +0,0 @@ -../../../darwin/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift b/auth0_flutter/ios/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift deleted file mode 120000 index b4f6ace64..000000000 --- a/auth0_flutter/ios/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift +++ /dev/null @@ -1 +0,0 @@ -../../../darwin/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift b/auth0_flutter/ios/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift new file mode 120000 index 000000000..87a4cfe54 --- /dev/null +++ b/auth0_flutter/ios/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/auth0_flutter.podspec b/auth0_flutter/ios/auth0_flutter.podspec index c02a00aee..2f5dfc09a 100644 --- a/auth0_flutter/ios/auth0_flutter.podspec +++ b/auth0_flutter/ios/auth0_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'auth0_flutter' - s.version = '2.7.0' + s.version = '3.0.0-beta.0' s.summary = 'Auth0 SDK for Flutter' s.description = 'Auth0 SDK for Flutter Android, iOS, macOS, Windows, and web apps.' s.homepage = 'https://auth0.com' @@ -13,14 +13,14 @@ 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', '2.24.1' - s.dependency 'JWTDecode', '3.3.0' + s.dependency 'Auth0', '3.2.0' + 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/lib/auth0_flutter.dart b/auth0_flutter/lib/auth0_flutter.dart index 88abde514..fbf43dd4b 100644 --- a/auth0_flutter/lib/auth0_flutter.dart +++ b/auth0_flutter/lib/auth0_flutter.dart @@ -15,7 +15,6 @@ export 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interfac AuthenticationMethod, AuthenticationMethodType, ApiException, - ChallengeType, Credentials, CredentialsManagerException, DatabaseUser, @@ -164,7 +163,8 @@ class Auth0 { WebAuthentication webAuthentication( {final String? scheme, final bool useCredentialsManager = true}) => WebAuthentication(_account, _userAgent, scheme, - useCredentialsManager ? credentialsManager : null); + useCredentialsManager ? credentialsManager : null, + useDPoP: _useDPoP); /// Creates a class of [WindowsWebAuthentication], the Windows-specific /// class for interacting with the [Auth0 Universal Login page](https://auth0.com/docs/authenticate/login/auth0-universal-login). diff --git a/auth0_flutter/lib/src/mobile/authentication_api.dart b/auth0_flutter/lib/src/mobile/authentication_api.dart index fec9670ff..c54d17449 100644 --- a/auth0_flutter/lib/src/mobile/authentication_api.dart +++ b/auth0_flutter/lib/src/mobile/authentication_api.dart @@ -84,71 +84,6 @@ class AuthenticationApi { parameters: parameters, ))); - /// Authenticates the user using a [mfaToken] and an [otp], after [login] - /// returned with an [ApiException] with [ApiException.isMultifactorRequired] - /// set to `true`. - /// If successful, it returns a set of tokens, as well as the user's profile - /// (constructed from ID token claims). - /// - /// - /// ## Endpoint - /// https://auth0.com/docs/api/authentication#verify-with-one-time-password-otp- - /// - /// ## Usage example - /// - /// ```dart - /// final result = await auth0.api.loginWithOtp({ - /// otp: '123456', - /// mfaToken: 'received_mfa_token' - /// }); - /// ``` - Future loginWithOtp({ - required final String otp, - required final String mfaToken, - }) => - Auth0FlutterAuthPlatform.instance - .loginWithOtp(_createApiRequest(AuthLoginWithOtpOptions( - otp: otp, - mfaToken: mfaToken, - ))); - - /// Requests a challenge for multi-factor authentication (MFA) based on the - /// challenge types supported by the app and user. - /// - /// The `type` is how the user will get the challenge and prove possession. - /// Excluding this parameter means that your app accepts all supported - /// challenge types. - /// - /// Supported challenge types include: - /// - `otp`: for one-time password (OTP). - /// - `oob`: for SMS/voice messages or out-of-band (OOB). - /// - /// **Important**: If OTP is supported by the user and you don't want to - /// request a different factor, you can skip the challenge request and call - /// [loginWithOtp] directly. - /// - /// ## Endpoint - /// https://auth0.com/docs/api/authentication#challenge-request - /// - /// ## Usage example - /// - /// ```dart - /// final result = await auth0.api.multifactorChallenge({ - /// mfaToken: 'received_mfa_token', - /// types: [ChallengeType.otp, ChallengeType.oob], - /// authenticatorId: 'authenticator_id' - /// }); - /// ``` - Future multifactorChallenge( - {required final String mfaToken, - final List? types, - final String? authenticatorId}) => - Auth0FlutterAuthPlatform.instance.multifactorChallenge(_createApiRequest( - AuthMultifactorChallengeOptions( - mfaToken: mfaToken, - types: types, - authenticatorId: authenticatorId))); - /// Start a passwordless flow with an [Email](https://auth0.com/docs/api/authentication#get-code-or-link). /// /// Your Application must have the **Passwordless OTP** Grant Type enabled. diff --git a/auth0_flutter/lib/src/mobile/credentials_manager.dart b/auth0_flutter/lib/src/mobile/credentials_manager.dart index 9f0a126ed..cc369d485 100644 --- a/auth0_flutter/lib/src/mobile/credentials_manager.dart +++ b/auth0_flutter/lib/src/mobile/credentials_manager.dart @@ -4,7 +4,7 @@ import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interfac /// CredentialManager. abstract class CredentialsManager { Future credentials({ - final int minTtl = 0, + final int minTtl = 60, final Set scopes = const {}, final Map parameters = const {}, }); @@ -68,7 +68,7 @@ abstract class CredentialsManager { Future getApiCredentials({ required final String audience, final Set scope = const {}, - final int minTtl = 0, + final int minTtl = 60, final Map parameters = const {}, final Map headers = const {}, }); @@ -84,6 +84,15 @@ abstract class CredentialsManager { required final String audience, final String? scope, }); + + /// Removes all stored credentials and API credentials, including the + /// underlying encryption keys. + /// + /// This is a more thorough wipe than [clearCredentials]: in addition to the + /// stored credential entries it also removes the cryptographic keys used to + /// protect them — on Android the crypto key pair and the DPoP key; on + /// iOS/macOS every entry in the credentials store plus the DPoP key pair. + Future clearAll(); } /// Default [CredentialsManager] implementation that passes calls to @@ -115,7 +124,7 @@ class DefaultCredentialsManager extends CredentialsManager { /// request to refresh expired credentials. @override Future credentials({ - final int minTtl = 0, + final int minTtl = 60, final Set scopes = const {}, final Map parameters = const {}, }) => @@ -197,7 +206,7 @@ class DefaultCredentialsManager extends CredentialsManager { Future getApiCredentials({ required final String audience, final Set scope = const {}, - final int minTtl = 0, + final int minTtl = 60, final Map parameters = const {}, final Map headers = const {}, }) => @@ -221,6 +230,14 @@ class DefaultCredentialsManager extends CredentialsManager { CredentialsManagerPlatform.instance.clearApiCredentials(_createApiRequest( ClearApiCredentialsOptions(audience: audience, scope: scope))); + /// Removes all stored credentials and API credentials, including the + /// underlying encryption keys. + /// + /// See [CredentialsManager.clearAll] for full documentation. + @override + Future clearAll() => + CredentialsManagerPlatform.instance.clearAll(_createApiRequest(null)); + CredentialsManagerRequest _createApiRequest( final TOptions? options) => diff --git a/auth0_flutter/lib/src/mobile/passwordless_api.dart b/auth0_flutter/lib/src/mobile/passwordless_api.dart index b7f0cc2d2..8bc5fc4df 100644 --- a/auth0_flutter/lib/src/mobile/passwordless_api.dart +++ b/auth0_flutter/lib/src/mobile/passwordless_api.dart @@ -118,8 +118,7 @@ class Passwordless { /// /// If the user has MFA configured, this call fails with an [ApiException] for /// which [ApiException.isMultifactorRequired] is `true`; continue the flow - /// using the existing MFA APIs (`auth0.api.multifactorChallenge` / - /// `auth0.api.loginWithOtp`). + /// using the MFA APIs (`auth0.mfa.challenge` / `auth0.mfa.verifyOtp`). /// /// * [scopes] the scopes to request. When left empty, the native SDK's /// default scope is applied. When you provide scopes, include `openid` if you diff --git a/auth0_flutter/lib/src/mobile/web_authentication.dart b/auth0_flutter/lib/src/mobile/web_authentication.dart index 0ddd5d343..dd3bf557f 100644 --- a/auth0_flutter/lib/src/mobile/web_authentication.dart +++ b/auth0_flutter/lib/src/mobile/web_authentication.dart @@ -24,9 +24,12 @@ class WebAuthentication { final UserAgent _userAgent; final String? _scheme; final CredentialsManager? _credentialsManager; + final bool _useDPoP; WebAuthentication( - this._account, this._userAgent, this._scheme, this._credentialsManager); + this._account, this._userAgent, this._scheme, this._credentialsManager, + {bool useDPoP = false}) + : _useDPoP = useDPoP; /// Redirects the user to the [Auth0 Universal Login page](https://auth0.com/docs/authenticate/login/auth0-universal-login) for authentication. If successful, it returns /// a set of tokens, as well as the user's profile (constructed from ID token @@ -74,8 +77,9 @@ class WebAuthentication { /// See [CustomTabsOptions] for available settings. /// * (android only): [allowedBrowsers] Defines an allowlist of browser /// packages. **Deprecated**: use [customTabsOptions] instead. - /// * [useDPoP] enables DPoP for enhanced token security. - /// See README for details. Defaults to `false`. + /// + /// DPoP-bound tokens are obtained when `useDPoP` is set to `true` on the + /// [Auth0] constructor. Future login( {final String? audience, final Set scopes = const { @@ -95,8 +99,7 @@ class WebAuthentication { final Map parameters = const {}, final IdTokenValidationConfig idTokenValidationConfig = const IdTokenValidationConfig(), - final SafariViewController? safariViewController, - final bool useDPoP = false}) async { + final SafariViewController? safariViewController}) async { final credentials = await Auth0FlutterWebAuthPlatform.instance.login( _createWebAuthRequest(WebAuthLoginOptions( audience: audience, @@ -113,7 +116,7 @@ class WebAuthentication { customTabsOptions: customTabsOptions, // ignore: deprecated_member_use_from_same_package allowedBrowsers: allowedBrowsers, - useDPoP: useDPoP))); + useDPoP: _useDPoP))); await _credentialsManager?.storeCredentials(credentials); diff --git a/auth0_flutter/lib/src/version.dart b/auth0_flutter/lib/src/version.dart index 22a36dfaf..6057e9340 100644 --- a/auth0_flutter/lib/src/version.dart +++ b/auth0_flutter/lib/src/version.dart @@ -1 +1 @@ -const String version = '2.7.0'; +const String version = '3.0.0-beta.0'; diff --git a/auth0_flutter/macos/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift b/auth0_flutter/macos/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift deleted file mode 120000 index 854e8df59..000000000 --- a/auth0_flutter/macos/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift +++ /dev/null @@ -1 +0,0 @@ -../../../darwin/Classes/AuthAPI/AuthAPILoginWithOTPMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift b/auth0_flutter/macos/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift deleted file mode 120000 index b4f6ace64..000000000 --- a/auth0_flutter/macos/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift +++ /dev/null @@ -1 +0,0 @@ -../../../darwin/Classes/AuthAPI/AuthAPIMultifactorChallengeMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift b/auth0_flutter/macos/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift new file mode 120000 index 000000000..87a4cfe54 --- /dev/null +++ b/auth0_flutter/macos/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/CredentialsManager/CredentialsManagerClearAllMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/auth0_flutter.podspec b/auth0_flutter/macos/auth0_flutter.podspec index c02a00aee..2f5dfc09a 100644 --- a/auth0_flutter/macos/auth0_flutter.podspec +++ b/auth0_flutter/macos/auth0_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'auth0_flutter' - s.version = '2.7.0' + s.version = '3.0.0-beta.0' s.summary = 'Auth0 SDK for Flutter' s.description = 'Auth0 SDK for Flutter Android, iOS, macOS, Windows, and web apps.' s.homepage = 'https://auth0.com' @@ -13,14 +13,14 @@ 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', '2.24.1' - s.dependency 'JWTDecode', '3.3.0' + s.dependency 'Auth0', '3.2.0' + 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/pubspec.yaml b/auth0_flutter/pubspec.yaml index a4b9cb22c..8d592e006 100644 --- a/auth0_flutter/pubspec.yaml +++ b/auth0_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: auth0_flutter description: Auth0 SDK for Flutter. Easily integrate Auth0 into Android, iOS, macOS, Windows, and web Flutter apps. -version: 2.7.0 +version: 3.0.0-beta.0 homepage: https://github.com/auth0/auth0-flutter environment: @@ -8,7 +8,7 @@ environment: flutter: ">=3.24.0" dependencies: - auth0_flutter_platform_interface: ^2.7.0 + auth0_flutter_platform_interface: ^3.0.0-beta.0 flutter: sdk: flutter flutter_web_plugins: 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..43eea5f6a 100644 --- a/auth0_flutter/test/desktop/windows_web_authentication_test.mocks.dart +++ b/auth0_flutter/test/desktop/windows_web_authentication_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.6 from annotations +// Mocks generated by Mockito from annotations // in auth0_flutter/test/desktop/windows_web_authentication_test.dart. // Do not manually edit this file. @@ -18,6 +18,7 @@ import 'windows_web_authentication_test.dart' as _i3; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: must_be_immutable @@ -25,6 +26,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 +46,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 e68def2bc..82a2d9284 100644 --- a/auth0_flutter/test/mobile/authentication_api_test.dart +++ b/auth0_flutter/test/mobile/authentication_api_test.dart @@ -27,9 +27,6 @@ class TestPlatform extends Mock 'tokenType': 'Bearer' }); - static Challenge multifactorChallengeResult = - Challenge(type: ChallengeType.oob, oobCode: 'oobCode'); - static Credentials renewResult = Credentials.fromMap({ 'accessToken': 'accessToken', 'idToken': 'idToken', @@ -40,10 +37,10 @@ class TestPlatform extends Mock 'tokenType': 'Bearer' }); - static const SSOCredentials ssoExchangeResult = SSOCredentials( + static final SSOCredentials ssoExchangeResult = SSOCredentials( sessionTransferToken: 'sso-token', tokenType: 'session_transfer', - expiresIn: 60, + expiresAt: DateTime.utc(2024, 11, 1, 22, 16, 35), idToken: 'id-token', refreshToken: 'new-refresh-token', ); @@ -205,67 +202,6 @@ void main() { }); }); - group('loginWithOtp', () { - test('passes through properties to the platform', () async { - when(mockedPlatform.loginWithOtp(any)) - .thenAnswer((_) async => TestPlatform.loginResult); - - final result = await Auth0('test-domain', 'test-clientId') - .api - .loginWithOtp(otp: 'test-otp', mfaToken: 'test-mfa-token'); - - final verificationResult = verify(mockedPlatform.loginWithOtp(captureAny)) - .captured - .single as ApiRequest; - expect(verificationResult.account.domain, 'test-domain'); - expect(verificationResult.account.clientId, 'test-clientId'); - expect(verificationResult.options.mfaToken, 'test-mfa-token'); - expect(verificationResult.options.otp, 'test-otp'); - expect(result, TestPlatform.loginResult); - }); - }); - - group('multifactorChallenge', () { - test('passes through properties to the platform', () async { - when(mockedPlatform.multifactorChallenge(any)).thenAnswer( - (_) async => TestPlatform.multifactorChallengeResult); - - final result = await Auth0('test-domain', 'test-clientId') - .api - .multifactorChallenge( - mfaToken: 'test-mfa-token', - types: [ChallengeType.otp, ChallengeType.oob], - authenticatorId: 'test-authenticatorId'); - - final verificationResult = - verify(mockedPlatform.multifactorChallenge(captureAny)) - .captured - .single as ApiRequest; - 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( - (_) 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', () { test('passes through properties to the platform', () async { when(mockedPlatform.resetPassword(any)).thenAnswer((_) async => {}); diff --git a/auth0_flutter/test/mobile/authentication_api_test.mocks.dart b/auth0_flutter/test/mobile/authentication_api_test.mocks.dart index 911d0cf41..a6a1f7107 100644 --- a/auth0_flutter/test/mobile/authentication_api_test.mocks.dart +++ b/auth0_flutter/test/mobile/authentication_api_test.mocks.dart @@ -1,16 +1,16 @@ -// Mocks generated by Mockito 5.4.6 from annotations +// Mocks generated by Mockito from annotations // in auth0_flutter/test/mobile/authentication_api_test.dart. // 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 'authentication_api_test.dart' as _i4; +import 'authentication_api_test.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -18,6 +18,7 @@ import 'authentication_api_test.dart' as _i4; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: must_be_immutable @@ -37,8 +38,8 @@ class _FakeCredentials_0 extends _i1.SmartFake implements _i2.Credentials { ); } -class _FakeChallenge_1 extends _i1.SmartFake implements _i3.Challenge { - _FakeChallenge_1( +class _FakeUserProfile_1 extends _i1.SmartFake implements _i2.UserProfile { + _FakeUserProfile_1( Object parent, Invocation parentInvocation, ) : super( @@ -47,8 +48,8 @@ class _FakeChallenge_1 extends _i1.SmartFake implements _i3.Challenge { ); } -class _FakeUserProfile_2 extends _i1.SmartFake implements _i2.UserProfile { - _FakeUserProfile_2( +class _FakeDatabaseUser_2 extends _i1.SmartFake implements _i2.DatabaseUser { + _FakeDatabaseUser_2( Object parent, Invocation parentInvocation, ) : super( @@ -57,8 +58,9 @@ class _FakeUserProfile_2 extends _i1.SmartFake implements _i2.UserProfile { ); } -class _FakeDatabaseUser_3 extends _i1.SmartFake implements _i2.DatabaseUser { - _FakeDatabaseUser_3( +class _FakeSSOCredentials_3 extends _i1.SmartFake + implements _i2.SSOCredentials { + _FakeSSOCredentials_3( Object parent, Invocation parentInvocation, ) : super( @@ -67,9 +69,9 @@ class _FakeDatabaseUser_3 extends _i1.SmartFake implements _i2.DatabaseUser { ); } -class _FakeSSOCredentials_4 extends _i1.SmartFake - implements _i3.SSOCredentials { - _FakeSSOCredentials_4( +class _FakePasskeyChallenge_4 extends _i1.SmartFake + implements _i2.PasskeyChallenge { + _FakePasskeyChallenge_4( Object parent, Invocation parentInvocation, ) : super( @@ -78,9 +80,9 @@ class _FakeSSOCredentials_4 extends _i1.SmartFake ); } -class _FakePasskeyChallenge_5 extends _i1.SmartFake - implements _i3.PasskeyChallenge { - _FakePasskeyChallenge_5( +class _FakePasswordlessChallenge_5 extends _i1.SmartFake + implements _i2.PasswordlessChallenge { + _FakePasswordlessChallenge_5( Object parent, Invocation parentInvocation, ) : super( @@ -92,267 +94,286 @@ class _FakePasskeyChallenge_5 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> login( - _i3.ApiRequest<_i3.AuthLoginOptions>? request) => + _i4.Future<_i2.Credentials> login( + _i5.ApiRequest<_i5.AuthLoginOptions>? 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<_i2.Credentials> loginWithOtp( - _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>); - - @override - _i5.Future<_i3.Challenge> multifactorChallenge( - _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>); - - @override - _i5.Future startPasswordlessWithEmail( - _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request) => + _i4.Future startPasswordlessWithEmail( + _i5.ApiRequest<_i5.AuthPasswordlessLoginOptions>? request) => (super.noSuchMethod( Invocation.method( #startPasswordlessWithEmail, [request], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future<_i2.Credentials> loginWithEmailCode( - _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request) => + _i4.Future<_i2.Credentials> loginWithEmailCode( + _i5.ApiRequest<_i5.AuthLoginWithCodeOptions>? request) => (super.noSuchMethod( Invocation.method( #loginWithEmailCode, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #loginWithEmailCode, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future startPasswordlessWithPhoneNumber( - _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request) => + _i4.Future startPasswordlessWithPhoneNumber( + _i5.ApiRequest<_i5.AuthPasswordlessLoginOptions>? request) => (super.noSuchMethod( Invocation.method( #startPasswordlessWithPhoneNumber, [request], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future<_i2.Credentials> loginWithSmsCode( - _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request) => + _i4.Future<_i2.Credentials> loginWithSmsCode( + _i5.ApiRequest<_i5.AuthLoginWithCodeOptions>? request) => (super.noSuchMethod( Invocation.method( #loginWithSmsCode, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #loginWithSmsCode, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.UserProfile> userInfo( - _i3.ApiRequest<_i3.AuthUserInfoOptions>? request) => + _i4.Future<_i2.UserProfile> userInfo( + _i5.ApiRequest<_i5.AuthUserInfoOptions>? request) => (super.noSuchMethod( Invocation.method( #userInfo, [request], ), - returnValue: _i5.Future<_i2.UserProfile>.value(_FakeUserProfile_2( + returnValue: _i4.Future<_i2.UserProfile>.value(_FakeUserProfile_1( this, Invocation.method( #userInfo, [request], ), )), - ) as _i5.Future<_i2.UserProfile>); + ) as _i4.Future<_i2.UserProfile>); @override - _i5.Future<_i2.DatabaseUser> signup( - _i3.ApiRequest<_i3.AuthSignupOptions>? request) => + _i4.Future<_i2.DatabaseUser> signup( + _i5.ApiRequest<_i5.AuthSignupOptions>? request) => (super.noSuchMethod( Invocation.method( #signup, [request], ), - returnValue: _i5.Future<_i2.DatabaseUser>.value(_FakeDatabaseUser_3( + returnValue: _i4.Future<_i2.DatabaseUser>.value(_FakeDatabaseUser_2( this, Invocation.method( #signup, [request], ), )), - ) as _i5.Future<_i2.DatabaseUser>); + ) as _i4.Future<_i2.DatabaseUser>); @override - _i5.Future<_i2.Credentials> renew( - _i3.ApiRequest<_i3.AuthRenewOptions>? request) => + _i4.Future<_i2.Credentials> renew( + _i5.ApiRequest<_i5.AuthRenewOptions>? request) => (super.noSuchMethod( Invocation.method( #renew, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #renew, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.Credentials> customTokenExchange( - _i3.ApiRequest<_i3.AuthCustomTokenExchangeOptions>? request) => + _i4.Future<_i2.Credentials> customTokenExchange( + _i5.ApiRequest<_i5.AuthCustomTokenExchangeOptions>? request) => (super.noSuchMethod( Invocation.method( #customTokenExchange, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #customTokenExchange, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i3.SSOCredentials> ssoExchange( - _i3.ApiRequest<_i3.AuthSSOExchangeOptions>? request) => + _i4.Future<_i2.SSOCredentials> ssoExchange( + _i5.ApiRequest<_i5.AuthSSOExchangeOptions>? request) => (super.noSuchMethod( Invocation.method( #ssoExchange, [request], ), - returnValue: _i5.Future<_i3.SSOCredentials>.value(_FakeSSOCredentials_4( + returnValue: _i4.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_3( this, Invocation.method( #ssoExchange, [request], ), )), - ) as _i5.Future<_i3.SSOCredentials>); + ) as _i4.Future<_i2.SSOCredentials>); @override - _i5.Future resetPassword( - _i3.ApiRequest<_i3.AuthResetPasswordOptions>? request) => + _i4.Future resetPassword( + _i5.ApiRequest<_i5.AuthResetPasswordOptions>? request) => (super.noSuchMethod( Invocation.method( #resetPassword, [request], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future<_i3.PasskeyChallenge> passkeyLoginChallenge( - _i3.ApiRequest<_i3.AuthPasskeyLoginChallengeOptions>? request) => + _i4.Future<_i2.PasskeyChallenge> passkeyLoginChallenge( + _i5.ApiRequest<_i5.AuthPasskeyLoginChallengeOptions>? request) => (super.noSuchMethod( Invocation.method( #passkeyLoginChallenge, [request], ), returnValue: - _i5.Future<_i3.PasskeyChallenge>.value(_FakePasskeyChallenge_5( + _i4.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_4( this, Invocation.method( #passkeyLoginChallenge, [request], ), )), - ) as _i5.Future<_i3.PasskeyChallenge>); + ) as _i4.Future<_i2.PasskeyChallenge>); @override - _i5.Future<_i3.PasskeyChallenge> passkeySignupChallenge( - _i3.ApiRequest<_i3.AuthPasskeySignupChallengeOptions>? request) => + _i4.Future<_i2.PasskeyChallenge> passkeySignupChallenge( + _i5.ApiRequest<_i5.AuthPasskeySignupChallengeOptions>? request) => (super.noSuchMethod( Invocation.method( #passkeySignupChallenge, [request], ), returnValue: - _i5.Future<_i3.PasskeyChallenge>.value(_FakePasskeyChallenge_5( + _i4.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_4( this, Invocation.method( #passkeySignupChallenge, [request], ), )), - ) as _i5.Future<_i3.PasskeyChallenge>); + ) as _i4.Future<_i2.PasskeyChallenge>); @override - _i5.Future<_i2.Credentials> passkeyCredentialExchange( - _i3.ApiRequest<_i3.AuthPasskeyExchangeOptions>? request) => + _i4.Future<_i2.Credentials> passkeyCredentialExchange( + _i5.ApiRequest<_i5.AuthPasskeyExchangeOptions>? request) => (super.noSuchMethod( Invocation.method( #passkeyCredentialExchange, [request], ), - returnValue: _i5.Future<_i2.Credentials>.value(_FakeCredentials_0( + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( this, Invocation.method( #passkeyCredentialExchange, [request], ), )), - ) as _i5.Future<_i2.Credentials>); + ) as _i4.Future<_i2.Credentials>); + + @override + _i4.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithEmail( + _i5.ApiRequest<_i5.AuthPasswordlessChallengeEmailOptions>? request) => + (super.noSuchMethod( + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + returnValue: _i4.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_5( + this, + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + )), + ) as _i4.Future<_i2.PasswordlessChallenge>); + + @override + _i4.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithPhoneNumber( + _i5.ApiRequest<_i5.AuthPasswordlessChallengePhoneOptions>? request) => + (super.noSuchMethod( + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + returnValue: _i4.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_5( + this, + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + )), + ) as _i4.Future<_i2.PasswordlessChallenge>); + + @override + _i4.Future<_i2.Credentials> passwordlessLoginWithOtp( + _i5.ApiRequest<_i5.AuthPasswordlessLoginWithOtpOptions>? request) => + (super.noSuchMethod( + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); } diff --git a/auth0_flutter/test/mobile/credentials_manager_test.dart b/auth0_flutter/test/mobile/credentials_manager_test.dart index ccb9c189f..b35e5848b 100644 --- a/auth0_flutter/test/mobile/credentials_manager_test.dart +++ b/auth0_flutter/test/mobile/credentials_manager_test.dart @@ -33,10 +33,10 @@ class TestPlatform extends Mock 'tokenType': 'Bearer' }); - static const SSOCredentials ssoResult = SSOCredentials( + static final SSOCredentials ssoResult = SSOCredentials( sessionTransferToken: 'ssoToken', tokenType: 'session_transfer', - expiresIn: 60, + expiresAt: DateTime.utc(2024, 11, 1, 22, 16, 35), idToken: 'idToken', refreshToken: 'refreshToken', ); @@ -90,7 +90,7 @@ void main() { final verificationResult = verify(mockedPlatform.getCredentials(captureAny)).captured.single as CredentialsManagerRequest; - expect(verificationResult.options?.minTtl, 0); + expect(verificationResult.options?.minTtl, 60); // ignore: inference_failure_on_collection_literal expect(verificationResult.options?.scopes, isEmpty); expect(verificationResult.options?.parameters, isEmpty); @@ -191,6 +191,20 @@ void main() { }); }); + group('clearAll', () { + test('calls the platform', () async { + when(mockedPlatform.clearAll(any)).thenAnswer((_) async {}); + + await DefaultCredentialsManager(account, userAgent).clearAll(); + + final verificationResult = + verify(mockedPlatform.clearAll(captureAny)).captured.single + as CredentialsManagerRequest; + expect(verificationResult.account.domain, 'test-domain'); + expect(verificationResult.account.clientId, 'test-clientId'); + }); + }); + group('renewCredentials', () { test('renew credentials return new set of credentials', () async { when(mockedPlatform.renewCredentials(any)) @@ -341,7 +355,7 @@ void main() { expect(result.sessionTransferToken, TestPlatform.ssoResult.sessionTransferToken); expect(result.tokenType, TestPlatform.ssoResult.tokenType); - expect(result.expiresIn, TestPlatform.ssoResult.expiresIn); + expect(result.expiresAt, TestPlatform.ssoResult.expiresAt); expect(result.idToken, TestPlatform.ssoResult.idToken); expect(result.refreshToken, TestPlatform.ssoResult.refreshToken); }); @@ -385,7 +399,7 @@ void main() { expect(verificationResult.options?.audience, 'test-audience'); // ignore: inference_failure_on_collection_literal expect(verificationResult.options?.scopes, isEmpty); - expect(verificationResult.options?.minTtl, 0); + expect(verificationResult.options?.minTtl, 60); // ignore: inference_failure_on_collection_literal expect(verificationResult.options?.parameters, isEmpty); // ignore: inference_failure_on_collection_literal diff --git a/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart b/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart index 5f097268f..468dde810 100644 --- a/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart +++ b/auth0_flutter/test/mobile/credentials_manager_test.mocks.dart @@ -1,16 +1,16 @@ -// Mocks generated by Mockito 5.4.6 from annotations +// Mocks generated by Mockito from annotations // in auth0_flutter/test/mobile/credentials_manager_test.dart. // 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 @@ -18,6 +18,7 @@ import 'credentials_manager_test.dart' as _i4; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: must_be_immutable @@ -38,7 +39,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 +50,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 +63,149 @@ 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); + + @override + _i4.Future clearAll( + _i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) => + (super.noSuchMethod( + Invocation.method( + #clearAll, + [request], + ), + 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..36abd25c2 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 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 @@ -18,6 +18,7 @@ import 'mfa_api_test.dart' as _i3; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: must_be_immutable @@ -25,21 +26,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 +69,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..8e1ad57c3 100644 --- a/auth0_flutter/test/mobile/passwordless_api_test.mocks.dart +++ b/auth0_flutter/test/mobile/passwordless_api_test.mocks.dart @@ -1,16 +1,16 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito from annotations // in auth0_flutter/test/mobile/passwordless_api_test.dart. // 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 'passwordless_api_test.dart' as _i4; +import 'passwordless_api_test.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -18,6 +18,7 @@ import 'passwordless_api_test.dart' as _i4; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: must_be_immutable @@ -25,313 +26,354 @@ 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); +class _FakeUserProfile_1 extends _i1.SmartFake implements _i2.UserProfile { + _FakeUserProfile_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); +class _FakeDatabaseUser_2 extends _i1.SmartFake implements _i2.DatabaseUser { + _FakeDatabaseUser_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); -} - -class _FakeSSOCredentials_4 extends _i1.SmartFake +class _FakeSSOCredentials_3 extends _i1.SmartFake implements _i2.SSOCredentials { - _FakeSSOCredentials_4(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeSSOCredentials_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } -class _FakePasskeyChallenge_5 extends _i1.SmartFake +class _FakePasskeyChallenge_4 extends _i1.SmartFake implements _i2.PasskeyChallenge { - _FakePasskeyChallenge_5(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePasskeyChallenge_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } -class _FakePasswordlessChallenge_6 extends _i1.SmartFake +class _FakePasswordlessChallenge_5 extends _i1.SmartFake implements _i2.PasswordlessChallenge { - _FakePasswordlessChallenge_6(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakePasswordlessChallenge_5( + 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.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>); - - @override - _i5.Future<_i2.Credentials> loginWithOtp( - _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>); - - @override - _i5.Future<_i3.Challenge> multifactorChallenge( - _i3.ApiRequest<_i3.AuthMultifactorChallengeOptions>? request, - ) => + _i4.Future<_i2.Credentials> login( + _i5.ApiRequest<_i5.AuthLoginOptions>? 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( + #login, + [request], + ), + returnValue: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #login, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future startPasswordlessWithEmail( - _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request, - ) => + _i4.Future startPasswordlessWithEmail( + _i5.ApiRequest<_i5.AuthPasswordlessLoginOptions>? request) => (super.noSuchMethod( - Invocation.method(#startPasswordlessWithEmail, [request]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #startPasswordlessWithEmail, + [request], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future<_i2.Credentials> loginWithEmailCode( - _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request, - ) => + _i4.Future<_i2.Credentials> loginWithEmailCode( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #loginWithEmailCode, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future startPasswordlessWithPhoneNumber( - _i3.ApiRequest<_i3.AuthPasswordlessLoginOptions>? request, - ) => + _i4.Future startPasswordlessWithPhoneNumber( + _i5.ApiRequest<_i5.AuthPasswordlessLoginOptions>? request) => (super.noSuchMethod( - Invocation.method(#startPasswordlessWithPhoneNumber, [request]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #startPasswordlessWithPhoneNumber, + [request], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future<_i2.Credentials> loginWithSmsCode( - _i3.ApiRequest<_i3.AuthLoginWithCodeOptions>? request, - ) => + _i4.Future<_i2.Credentials> loginWithSmsCode( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #loginWithSmsCode, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.UserProfile> userInfo( - _i3.ApiRequest<_i3.AuthUserInfoOptions>? request, - ) => + _i4.Future<_i2.UserProfile> userInfo( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.UserProfile>.value(_FakeUserProfile_1( + this, + Invocation.method( + #userInfo, + [request], + ), + )), + ) as _i4.Future<_i2.UserProfile>); @override - _i5.Future<_i2.DatabaseUser> signup( - _i3.ApiRequest<_i3.AuthSignupOptions>? request, - ) => + _i4.Future<_i2.DatabaseUser> signup( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.DatabaseUser>.value(_FakeDatabaseUser_2( + this, + Invocation.method( + #signup, + [request], + ), + )), + ) as _i4.Future<_i2.DatabaseUser>); @override - _i5.Future<_i2.Credentials> renew( - _i3.ApiRequest<_i3.AuthRenewOptions>? request, - ) => + _i4.Future<_i2.Credentials> renew( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #renew, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.Credentials> customTokenExchange( - _i3.ApiRequest<_i3.AuthCustomTokenExchangeOptions>? request, - ) => + _i4.Future<_i2.Credentials> customTokenExchange( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #customTokenExchange, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.SSOCredentials> ssoExchange( - _i3.ApiRequest<_i3.AuthSSOExchangeOptions>? request, - ) => + _i4.Future<_i2.SSOCredentials> ssoExchange( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.SSOCredentials>.value(_FakeSSOCredentials_3( + this, + Invocation.method( + #ssoExchange, + [request], + ), + )), + ) as _i4.Future<_i2.SSOCredentials>); @override - _i5.Future resetPassword( - _i3.ApiRequest<_i3.AuthResetPasswordOptions>? request, - ) => + _i4.Future resetPassword( + _i5.ApiRequest<_i5.AuthResetPasswordOptions>? request) => (super.noSuchMethod( - Invocation.method(#resetPassword, [request]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #resetPassword, + [request], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future<_i2.PasskeyChallenge> passkeyLoginChallenge( - _i3.ApiRequest<_i3.AuthPasskeyLoginChallengeOptions>? request, - ) => + _i4.Future<_i2.PasskeyChallenge> passkeyLoginChallenge( + _i5.ApiRequest<_i5.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: + _i4.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_4( + this, + Invocation.method( + #passkeyLoginChallenge, + [request], + ), + )), + ) as _i4.Future<_i2.PasskeyChallenge>); @override - _i5.Future<_i2.PasskeyChallenge> passkeySignupChallenge( - _i3.ApiRequest<_i3.AuthPasskeySignupChallengeOptions>? request, - ) => + _i4.Future<_i2.PasskeyChallenge> passkeySignupChallenge( + _i5.ApiRequest<_i5.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: + _i4.Future<_i2.PasskeyChallenge>.value(_FakePasskeyChallenge_4( + this, + Invocation.method( + #passkeySignupChallenge, + [request], + ), + )), + ) as _i4.Future<_i2.PasskeyChallenge>); @override - _i5.Future<_i2.Credentials> passkeyCredentialExchange( - _i3.ApiRequest<_i3.AuthPasskeyExchangeOptions>? request, - ) => + _i4.Future<_i2.Credentials> passkeyCredentialExchange( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #passkeyCredentialExchange, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); @override - _i5.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithEmail( - _i3.ApiRequest<_i3.AuthPasswordlessChallengeEmailOptions>? request, - ) => + _i4.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithEmail( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_5( + this, + Invocation.method( + #passwordlessChallengeWithEmail, + [request], + ), + )), + ) as _i4.Future<_i2.PasswordlessChallenge>); @override - _i5.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithPhoneNumber( - _i3.ApiRequest<_i3.AuthPasswordlessChallengePhoneOptions>? request, - ) => + _i4.Future<_i2.PasswordlessChallenge> passwordlessChallengeWithPhoneNumber( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.PasswordlessChallenge>.value( + _FakePasswordlessChallenge_5( + this, + Invocation.method( + #passwordlessChallengeWithPhoneNumber, + [request], + ), + )), + ) as _i4.Future<_i2.PasswordlessChallenge>); @override - _i5.Future<_i2.Credentials> passwordlessLoginWithOtp( - _i3.ApiRequest<_i3.AuthPasswordlessLoginWithOtpOptions>? request, - ) => + _i4.Future<_i2.Credentials> passwordlessLoginWithOtp( + _i5.ApiRequest<_i5.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: _i4.Future<_i2.Credentials>.value(_FakeCredentials_0( + this, + Invocation.method( + #passwordlessLoginWithOtp, + [request], + ), + )), + ) as _i4.Future<_i2.Credentials>); } diff --git a/auth0_flutter/test/mobile/web_authentication_test.dart b/auth0_flutter/test/mobile/web_authentication_test.dart index aaed91558..f92a3ec57 100644 --- a/auth0_flutter/test/mobile/web_authentication_test.dart +++ b/auth0_flutter/test/mobile/web_authentication_test.dart @@ -409,6 +409,43 @@ void main() { expect(verificationResult.options.customTabsOptions, isNull); }); + test('passes useTrustedWebActivity and useAuthTab to the platform', + () async { + when(mockedPlatform.login(any)) + .thenAnswer((_) async => TestPlatform.loginResult); + when(mockedCMPlatform.saveCredentials(any)) + .thenAnswer((_) async => true); + + await Auth0('test-domain', 'test-clientId').webAuthentication().login( + customTabsOptions: const CustomTabsOptions( + useTrustedWebActivity: true, useAuthTab: true)); + + final verificationResult = verify(mockedPlatform.login(captureAny)) + .captured + .single as WebAuthRequest; + final options = verificationResult.options.customTabsOptions!; + expect(options.useTrustedWebActivity, true); + expect(options.useAuthTab, true); + }); + + test('useTrustedWebActivity and useAuthTab default to false', () async { + when(mockedPlatform.login(any)) + .thenAnswer((_) async => TestPlatform.loginResult); + when(mockedCMPlatform.saveCredentials(any)) + .thenAnswer((_) async => true); + + await Auth0('test-domain', 'test-clientId') + .webAuthentication() + .login(customTabsOptions: const CustomTabsOptions()); + + final verificationResult = verify(mockedPlatform.login(captureAny)) + .captured + .single as WebAuthRequest; + final options = verificationResult.options.customTabsOptions!; + expect(options.useTrustedWebActivity, false); + expect(options.useAuthTab, false); + }); + test('passes customTabsOptions with side sheet options', () async { when(mockedPlatform.login(any)) .thenAnswer((_) async => TestPlatform.loginResult); @@ -484,9 +521,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication() - .login(useDPoP: true); + .login(); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -533,11 +570,12 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId').webAuthentication().login( - useDPoP: true, - audience: 'test-audience', - scopes: {'openid', 'profile', 'email'}, - organizationId: 'org_123'); + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login( + audience: 'test-audience', + scopes: {'openid', 'profile', 'email'}, + organizationId: 'org_123'); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -567,9 +605,10 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - final result = await Auth0('test-domain', 'test-clientId') - .webAuthentication() - .login(useDPoP: true); + final result = + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login(); final verificationResult = verify(mockedCMPlatform.saveCredentials(captureAny)).captured.single @@ -587,9 +626,9 @@ void main() { when(mockedPlatform.login(any)) .thenAnswer((_) async => TestPlatform.loginResult); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication(useCredentialsManager: false) - .login(useDPoP: true); + .login(); verifyNever(mockedCMPlatform.saveCredentials(any)); }); @@ -600,9 +639,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication() - .login(useDPoP: true, redirectUrl: 'https://example.com/callback'); + .login(redirectUrl: 'https://example.com/callback'); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -619,9 +658,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication() - .login(useDPoP: true, useHTTPS: true); + .login(useHTTPS: true); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -637,9 +676,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication() - .login(useDPoP: true, useEphemeralSession: true); + .login(useEphemeralSession: true); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -655,9 +694,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication() - .login(useDPoP: true, parameters: {'custom_param': 'custom_value'}); + .login(parameters: {'custom_param': 'custom_value'}); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -674,9 +713,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId').webAuthentication().login( - useDPoP: true, - invitationUrl: 'https://example.com/invite?ticket=abc123'); + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login(invitationUrl: 'https://example.com/invite?ticket=abc123'); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -694,11 +733,12 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId').webAuthentication().login( - useDPoP: true, - safariViewController: const SafariViewController( - presentationStyle: - SafariViewControllerPresentationStyle.fullScreen)); + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login( + safariViewController: const SafariViewController( + presentationStyle: + SafariViewControllerPresentationStyle.fullScreen)); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -718,9 +758,11 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId').webAuthentication().login( - useDPoP: true, - allowedBrowsers: ['com.android.chrome', 'org.mozilla.firefox']); + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login( + // ignore: deprecated_member_use_from_same_package + allowedBrowsers: ['com.android.chrome', 'org.mozilla.firefox']); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -737,9 +779,10 @@ void main() { final mockCm = MockCredentialsManager(); when(mockCm.storeCredentials(any)).thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId', credentialsManager: mockCm) + await Auth0('test-domain', 'test-clientId', + credentialsManager: mockCm, useDPoP: true) .webAuthentication() - .login(useDPoP: true); + .login(); verifyNever(mockedCMPlatform.saveCredentials(any)); @@ -775,9 +818,10 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - final result = await Auth0('test-domain', 'test-clientId') - .webAuthentication() - .login(useDPoP: true); + final result = + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login(); expect(result.accessToken, 'dpop-token'); expect(result.tokenType, 'DPoP'); @@ -790,16 +834,16 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - final result = await Auth0('test-domain', 'test-clientId') - .webAuthentication() - .login( - useDPoP: true, - audience: 'https://api.example.com', - scopes: {'openid', 'profile', 'email', 'offline_access'}, - organizationId: 'org_123', - redirectUrl: 'myapp://callback', - useHTTPS: true, - parameters: {'connection': 'google-oauth2'}); + final result = + await Auth0('test-domain', 'test-clientId', useDPoP: true) + .webAuthentication() + .login( + audience: 'https://api.example.com', + scopes: {'openid', 'profile', 'email', 'offline_access'}, + organizationId: 'org_123', + redirectUrl: 'myapp://callback', + useHTTPS: true, + parameters: {'connection': 'google-oauth2'}); final verificationResult = verify(mockedPlatform.login(captureAny)) .captured @@ -834,9 +878,9 @@ void main() { when(mockedCMPlatform.saveCredentials(any)) .thenAnswer((_) async => true); - await Auth0('test-domain', 'test-clientId') + await Auth0('test-domain', 'test-clientId', useDPoP: true) .webAuthentication() - .login(useDPoP: true); + .login(); final savedCredentials = verify(mockedCMPlatform.saveCredentials(captureAny)).captured.single diff --git a/auth0_flutter/test/mobile/web_authentication_test.mocks.dart b/auth0_flutter/test/mobile/web_authentication_test.mocks.dart index 9e0bd2601..9aee10ff8 100644 --- a/auth0_flutter/test/mobile/web_authentication_test.mocks.dart +++ b/auth0_flutter/test/mobile/web_authentication_test.mocks.dart @@ -1,16 +1,16 @@ -// Mocks generated by Mockito 5.4.6 from annotations +// Mocks generated by Mockito from annotations // in auth0_flutter/test/mobile/web_authentication_test.dart. // 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 @@ -18,6 +18,7 @@ import 'web_authentication_test.dart' as _i4; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: must_be_immutable @@ -38,7 +39,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 +49,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 +116,151 @@ 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); + + @override + _i4.Future clearAll( + _i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) => + (super.noSuchMethod( + Invocation.method( + #clearAll, + [request], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [CredentialsManager]. @@ -212,8 +273,8 @@ class MockCredentialsManager extends _i1.Mock } @override - _i5.Future<_i2.Credentials> credentials({ - int? minTtl = 0, + _i4.Future<_i2.Credentials> credentials({ + int? minTtl = 60, Set? scopes = const {}, Map? parameters = const {}, }) => @@ -227,7 +288,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 +300,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 +311,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 +319,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 +374,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 +385,69 @@ 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 = 60, + 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); + + @override + _i4.Future clearAll() => (super.noSuchMethod( + Invocation.method( + #clearAll, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } diff --git a/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart b/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart index 1b4d0b311..ce2c27e50 100644 --- a/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart +++ b/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart @@ -1,16 +1,16 @@ -// Mocks generated by Mockito 5.4.4 from annotations -// in auth0_flutter/test/web/auth0_flutter_web_test.dart. +// Mocks generated by Mockito from annotations +// in auth0_flutter/example/windows/flutter/ephemeral/.plugin_symlinks/auth0_flutter/test/web/auth0_flutter_web_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; -import 'dart:js_interop'; +import 'dart:async' as _i5; +import 'dart:js_interop' as _i6; import 'package:auth0_flutter/src/web/auth0_flutter_web_platform_proxy.dart' - as _i3; -import 'package:auth0_flutter/src/web/js_interop.dart' as _i2; + as _i2; +import 'package:auth0_flutter/src/web/js_interop.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'auth0_extension_type_mocks.dart'; +import 'package:mockito/src/dummies.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -18,94 +18,49 @@ import 'auth0_extension_type_mocks.dart'; // ignore_for_file: comment_references // ignore_for_file: deprecated_member_use // ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: experimental_member_use // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class - -@JSExport() -class _FakeAuth0Client_0 extends _i1.SmartFake implements Auth0ClientImpl { - _FakeAuth0Client_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -@JSExport() -class _FakeWebCredentials_1 extends _i1.SmartFake - implements WebCredentialsImpl { - _FakeWebCredentials_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -@JSExport() -class _FakeRedirectLoginResult_2 extends _i1.SmartFake - implements RedirectLoginResultImpl { - _FakeRedirectLoginResult_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -@JSExport() -class _FakePasskeyChallengeResponse_3 extends _i1.SmartFake - implements PasskeyChallengeResponseImpl { - _FakePasskeyChallengeResponse_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} +// ignore_for_file: invalid_use_of_internal_member /// A class which mocks [Auth0FlutterWebClientProxy]. /// /// See the documentation for Mockito's code generation for more information. class MockAuth0FlutterWebClientProxy extends _i1.Mock - implements _i3.Auth0FlutterWebClientProxy { + implements _i2.Auth0FlutterWebClientProxy { MockAuth0FlutterWebClientProxy() { _i1.throwOnMissingStub(this); } @override - _i2.Auth0Client get client => (super.noSuchMethod( + _i3.Auth0Client get client => (super.noSuchMethod( Invocation.getter(#client), - returnValue: - createJSInteropWrapper<_FakeAuth0Client_0>(_FakeAuth0Client_0( + returnValue: _i4.dummyValue<_i3.Auth0Client>( this, Invocation.getter(#client), - )) as _i2.Auth0Client, - ) as _i2.Auth0Client); + ), + ) as _i3.Auth0Client); @override - _i4.Future loginWithRedirect(_i2.RedirectLoginOptions? options) => + _i5.Future loginWithRedirect(_i3.RedirectLoginOptions? options) => (super.noSuchMethod( Invocation.method( #loginWithRedirect, [options], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future loginWithPopup([ - _i2.PopupLoginOptions? options, - _i2.PopupConfigOptions? config, + _i5.Future loginWithPopup([ + _i3.PopupLoginOptions? options, + _i3.PopupConfigOptions? config, ]) => (super.noSuchMethod( Invocation.method( @@ -115,146 +70,214 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock config, ], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future checkSession() => (super.noSuchMethod( + _i5.Future checkSession() => (super.noSuchMethod( Invocation.method( #checkSession, [], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i4.Future<_i2.WebCredentials> getTokenSilently( - [_i2.GetTokenSilentlyOptions? options]) => + _i5.Future<_i3.WebCredentials> getTokenSilently( + [_i3.GetTokenSilentlyOptions? options]) => (super.noSuchMethod( Invocation.method( #getTokenSilently, [options], ), - returnValue: _i4.Future<_i2.WebCredentials>.value( - createJSInteropWrapper<_FakeWebCredentials_1>(_FakeWebCredentials_1( + returnValue: _i5.Future<_i3.WebCredentials>.value( + _i4.dummyValue<_i3.WebCredentials>( this, Invocation.method( #getTokenSilently, [options], ), - )) as _i2.WebCredentials), - ) as _i4.Future<_i2.WebCredentials>); + )), + ) as _i5.Future<_i3.WebCredentials>); @override - _i4.Future<_i2.WebCredentials> exchangeToken( - _i2.ExchangeTokenOptions? options) => + _i5.Future<_i3.WebCredentials> exchangeToken( + _i3.ExchangeTokenOptions? options) => (super.noSuchMethod( Invocation.method( #exchangeToken, [options], ), - returnValue: _i4.Future<_i2.WebCredentials>.value( - createJSInteropWrapper<_FakeWebCredentials_1>(_FakeWebCredentials_1( + returnValue: _i5.Future<_i3.WebCredentials>.value( + _i4.dummyValue<_i3.WebCredentials>( this, Invocation.method( #exchangeToken, [options], ), - )) as _i2.WebCredentials), - ) as _i4.Future<_i2.WebCredentials>); + )), + ) as _i5.Future<_i3.WebCredentials>); @override - _i4.Future<_i2.RedirectLoginResult> handleRedirectCallback([String? url]) => + _i5.Future<_i3.RedirectLoginResult> handleRedirectCallback([String? url]) => (super.noSuchMethod( Invocation.method( #handleRedirectCallback, [url], ), - returnValue: _i4.Future<_i2.RedirectLoginResult>.value( - createJSInteropWrapper<_FakeRedirectLoginResult_2>( - _FakeRedirectLoginResult_2( + returnValue: _i5.Future<_i3.RedirectLoginResult>.value( + _i4.dummyValue<_i3.RedirectLoginResult>( this, Invocation.method( #handleRedirectCallback, [url], ), - )) as _i2.RedirectLoginResult), - ) as _i4.Future<_i2.RedirectLoginResult>); + )), + ) as _i5.Future<_i3.RedirectLoginResult>); @override - _i4.Future isAuthenticated() => (super.noSuchMethod( + _i5.Future isAuthenticated() => (super.noSuchMethod( Invocation.method( #isAuthenticated, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future logout(_i2.LogoutOptions? options) => (super.noSuchMethod( + _i5.Future logout(_i3.LogoutOptions? options) => (super.noSuchMethod( Invocation.method( #logout, [options], ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i6.JSArray<_i3.MfaAuthenticatorJS>> mfaGetAuthenticators( + String? mfaToken) => + (super.noSuchMethod( + Invocation.method( + #mfaGetAuthenticators, + [mfaToken], + ), + returnValue: _i5.Future<_i6.JSArray<_i3.MfaAuthenticatorJS>>.value( + _i4.dummyValue<_i6.JSArray<_i3.MfaAuthenticatorJS>>( + this, + Invocation.method( + #mfaGetAuthenticators, + [mfaToken], + ), + )), + ) as _i5.Future<_i6.JSArray<_i3.MfaAuthenticatorJS>>); + + @override + _i5.Future<_i3.MfaEnrollmentResponse> mfaEnroll( + _i3.MfaEnrollParams? params) => + (super.noSuchMethod( + Invocation.method( + #mfaEnroll, + [params], + ), + returnValue: _i5.Future<_i3.MfaEnrollmentResponse>.value( + _i4.dummyValue<_i3.MfaEnrollmentResponse>( + this, + Invocation.method( + #mfaEnroll, + [params], + ), + )), + ) as _i5.Future<_i3.MfaEnrollmentResponse>); + + @override + _i5.Future<_i3.MfaChallengeResponse> mfaChallenge( + _i3.MfaChallengeParams? params) => + (super.noSuchMethod( + Invocation.method( + #mfaChallenge, + [params], + ), + returnValue: _i5.Future<_i3.MfaChallengeResponse>.value( + _i4.dummyValue<_i3.MfaChallengeResponse>( + this, + Invocation.method( + #mfaChallenge, + [params], + ), + )), + ) as _i5.Future<_i3.MfaChallengeResponse>); + + @override + _i5.Future<_i3.WebCredentials> mfaVerify(_i3.MfaVerifyParams? params) => + (super.noSuchMethod( + Invocation.method( + #mfaVerify, + [params], + ), + returnValue: _i5.Future<_i3.WebCredentials>.value( + _i4.dummyValue<_i3.WebCredentials>( + this, + Invocation.method( + #mfaVerify, + [params], + ), + )), + ) as _i5.Future<_i3.WebCredentials>); @override - _i4.Future<_i2.PasskeyChallengeResponse> passkeyGetSignupChallenge( - _i2.PasskeySignupChallengeParams? params) => + _i5.Future<_i3.PasskeyChallengeResponse> passkeyGetSignupChallenge( + _i3.PasskeySignupChallengeParams? params) => (super.noSuchMethod( Invocation.method( #passkeyGetSignupChallenge, [params], ), - returnValue: _i4.Future<_i2.PasskeyChallengeResponse>.value( - createJSInteropWrapper<_FakePasskeyChallengeResponse_3>( - _FakePasskeyChallengeResponse_3( + returnValue: _i5.Future<_i3.PasskeyChallengeResponse>.value( + _i4.dummyValue<_i3.PasskeyChallengeResponse>( this, Invocation.method( #passkeyGetSignupChallenge, [params], ), - )) as _i2.PasskeyChallengeResponse), - ) as _i4.Future<_i2.PasskeyChallengeResponse>); + )), + ) as _i5.Future<_i3.PasskeyChallengeResponse>); @override - _i4.Future<_i2.PasskeyChallengeResponse> passkeyGetLoginChallenge( - [_i2.PasskeyLoginChallengeParams? params]) => + _i5.Future<_i3.PasskeyChallengeResponse> passkeyGetLoginChallenge( + [_i3.PasskeyLoginChallengeParams? params]) => (super.noSuchMethod( Invocation.method( #passkeyGetLoginChallenge, [params], ), - returnValue: _i4.Future<_i2.PasskeyChallengeResponse>.value( - createJSInteropWrapper<_FakePasskeyChallengeResponse_3>( - _FakePasskeyChallengeResponse_3( + returnValue: _i5.Future<_i3.PasskeyChallengeResponse>.value( + _i4.dummyValue<_i3.PasskeyChallengeResponse>( this, Invocation.method( #passkeyGetLoginChallenge, [params], ), - )) as _i2.PasskeyChallengeResponse), - ) as _i4.Future<_i2.PasskeyChallengeResponse>); + )), + ) as _i5.Future<_i3.PasskeyChallengeResponse>); @override - _i4.Future<_i2.WebCredentials> passkeyGetTokenWithPasskey( - _i2.PasskeyGetTokenParams? params) => + _i5.Future<_i3.WebCredentials> passkeyGetTokenWithPasskey( + _i3.PasskeyGetTokenParams? params) => (super.noSuchMethod( Invocation.method( #passkeyGetTokenWithPasskey, [params], ), - returnValue: _i4.Future<_i2.WebCredentials>.value( - createJSInteropWrapper<_FakeWebCredentials_1>(_FakeWebCredentials_1( + returnValue: _i5.Future<_i3.WebCredentials>.value( + _i4.dummyValue<_i3.WebCredentials>( this, Invocation.method( #passkeyGetTokenWithPasskey, [params], ), - )) as _i2.WebCredentials), - ) as _i4.Future<_i2.WebCredentials>); + )), + ) as _i5.Future<_i3.WebCredentials>); } diff --git a/auth0_flutter/windows/request_handlers/web_auth/login_web_auth_request_handler.cpp b/auth0_flutter/windows/request_handlers/web_auth/login_web_auth_request_handler.cpp index d84b574c5..334ab2948 100644 --- a/auth0_flutter/windows/request_handlers/web_auth/login_web_auth_request_handler.cpp +++ b/auth0_flutter/windows/request_handlers/web_auth/login_web_auth_request_handler.cpp @@ -285,6 +285,15 @@ namespace auth0_flutter } } + if (auto nonceIt = arguments->find(flutter::EncodableValue("nonce")); + nonceIt != arguments->end()) + { + if (auto s = std::get_if(&nonceIt->second); s && !s->empty()) + { + nonce = *s; + } + } + std::map queryParams; if (parametersMap) { diff --git a/auth0_flutter/windows/test/login_web_auth_request_handler_test.cpp b/auth0_flutter/windows/test/login_web_auth_request_handler_test.cpp index a6d3dbfbe..4054e2d1e 100644 --- a/auth0_flutter/windows/test/login_web_auth_request_handler_test.cpp +++ b/auth0_flutter/windows/test/login_web_auth_request_handler_test.cpp @@ -272,6 +272,23 @@ TEST(LoginHandlerTest, SilentlyIgnoresUseDPoPTrue) << "useDPoP:true must not cause a bad_args error on Windows"; } +TEST(LoginHandlerTest, AcceptsTopLevelNonce) +{ + LoginWebAuthRequestHandler handler; + + auto args = MinimalArgs(); + args[flutter::EncodableValue("nonce")] = + flutter::EncodableValue(std::string("test-nonce")); + + auto state = Invoke(args); + + if (state->kind == CapturingMethodResult::Kind::Error) + { + EXPECT_NE(state->errorCode, "bad_args") + << "A top-level nonce must not cause a bad_args error"; + } +} + TEST(LoginHandlerTest, DISABLED_AcceptsValidScopesListTopLevel) { LoginWebAuthRequestHandler handler; diff --git a/auth0_flutter_platform_interface/CHANGELOG.md b/auth0_flutter_platform_interface/CHANGELOG.md index fcef307bb..54f782c17 100644 --- a/auth0_flutter_platform_interface/CHANGELOG.md +++ b/auth0_flutter_platform_interface/CHANGELOG.md @@ -12,6 +12,28 @@ - fix: bump Auth0.Android to 3.21.2 for biometric failure fix [\#968](https://github.com/auth0/auth0-flutter/pull/968) ([utkrishtsahu](https://github.com/utkrishtsahu)) - fix(android): bump Auth0.Android to 3.21.1 for Keystore2 INCOMPATIBLE_DEVICE fix [\#963](https://github.com/auth0/auth0-flutter/pull/963) ([utkrishtsahu](https://github.com/utkrishtsahu)) +## [afpi-v3.0.0-beta.0](https://github.com/auth0/auth0-flutter/tree/afpi-v3.0.0-beta.0) (2026-09-16) +[Full Changelog](https://github.com/auth0/auth0-flutter/compare/afpi-v2.6.2...afpi-v3.0.0-beta.0) + +**Breaking Change** +- feat: remove inline MFA methods from the Authentication API. [\#943](https://github.com/auth0/auth0-flutter/pull/943) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: reconcile WebAuthenticationException error codes across Android and iOS [\#935](https://github.com/auth0/auth0-flutter/pull/935) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: change SSOCredentials.expiresIn to expiresAt (Android + iOS) [\#931](https://github.com/auth0/auth0-flutter/pull/931) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: Upgrade Auth0.Android to v4.0.0. [\#918](https://github.com/auth0/auth0-flutter/pull/918) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: Upgrade Auth0.swift to v3.0.2 [\#909](https://github.com/auth0/auth0-flutter/pull/909) ([NandanPrabhu](https://github.com/NandanPrabhu)) + +**Added** +- feat: add nonce to IdTokenValidationConfig for Web Auth login [\#944](https://github.com/auth0/auth0-flutter/pull/944) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: dpop web auth init config [\#938](https://github.com/auth0/auth0-flutter/pull/938) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: add credentialsManager.clearAll() for full credential and key wipe [\#930](https://github.com/auth0/auth0-flutter/pull/930) ([utkrishtsahu](https://github.com/utkrishtsahu)) + +**Changed** +- feat: default credentialsManager minTtl to 60 seconds [\#929](https://github.com/auth0/auth0-flutter/pull/929) ([utkrishtsahu](https://github.com/utkrishtsahu)) +- feat: honor useEphemeralSession on Android web auth [\#927](https://github.com/auth0/auth0-flutter/pull/927) ([utkrishtsahu](https://github.com/utkrishtsahu)) + +**Fixed** +- fix: register Android web auth callbacks with the activity lifecycle [\#928](https://github.com/auth0/auth0-flutter/pull/928) ([utkrishtsahu](https://github.com/utkrishtsahu)) + ## [afpi-v2.6.2](https://github.com/auth0/auth0-flutter/tree/afpi-v2.6.2) (2026-09-11) [Full Changelog](https://github.com/auth0/auth0-flutter/compare/afpi-v2.6.1...afpi-v2.6.2) diff --git a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart index 878bf7a51..8e548269f 100644 --- a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart +++ b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart @@ -6,8 +6,6 @@ export 'src/auth/auth_custom_token_exchange_options.dart'; export 'src/auth/auth_dpop_headers_options.dart'; export 'src/auth/auth_login_code_options.dart'; export 'src/auth/auth_login_options.dart'; -export 'src/auth/auth_login_with_otp_options.dart'; -export 'src/auth/auth_multifactor_challenge_options.dart'; export 'src/auth/auth_passkey_exchange_options.dart'; export 'src/auth/auth_passkey_login_challenge_options.dart'; export 'src/auth/auth_passkey_signup_challenge_options.dart'; @@ -21,8 +19,6 @@ export 'src/auth/auth_reset_password_options.dart'; export 'src/auth/auth_signup_options.dart'; export 'src/auth/auth_sso_exchange_options.dart'; export 'src/auth/auth_user_info_options.dart'; -export 'src/auth/challenge.dart'; -export 'src/auth/challenge_type.dart'; export 'src/auth/delivery_method.dart'; export 'src/auth/dpop_headers.dart'; export 'src/auth/empty_request_options.dart'; diff --git a/auth0_flutter_platform_interface/lib/src/auth/auth_login_with_otp_options.dart b/auth0_flutter_platform_interface/lib/src/auth/auth_login_with_otp_options.dart deleted file mode 100644 index 264a281e0..000000000 --- a/auth0_flutter_platform_interface/lib/src/auth/auth_login_with_otp_options.dart +++ /dev/null @@ -1,11 +0,0 @@ -import '../request/request_options.dart'; - -class AuthLoginWithOtpOptions implements RequestOptions { - final String otp; - final String mfaToken; - - AuthLoginWithOtpOptions({required this.otp, required this.mfaToken}); - - @override - Map toMap() => {'otp': otp, 'mfaToken': mfaToken}; -} 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 deleted file mode 100644 index 8b3869bf9..000000000 --- a/auth0_flutter_platform_interface/lib/src/auth/auth_multifactor_challenge_options.dart +++ /dev/null @@ -1,20 +0,0 @@ -import '../request/request_options.dart'; -import './challenge_type.dart'; - -class AuthMultifactorChallengeOptions implements RequestOptions { - final String mfaToken; - final List? types; - final String? authenticatorId; - - AuthMultifactorChallengeOptions( - {required this.mfaToken, - this.types, - this.authenticatorId}); - - @override - Map toMap() => { - 'mfaToken': mfaToken, - 'types': types?.map((final e) => e.value).toList(), - 'authenticatorId': authenticatorId - }; -} diff --git a/auth0_flutter_platform_interface/lib/src/auth/challenge.dart b/auth0_flutter_platform_interface/lib/src/auth/challenge.dart deleted file mode 100644 index dbef05c72..000000000 --- a/auth0_flutter_platform_interface/lib/src/auth/challenge.dart +++ /dev/null @@ -1,24 +0,0 @@ -import '../../auth0_flutter_platform_interface.dart'; - -/// A multi-factor challenge. -class Challenge { - /// How the user will get the challenge and prove possession. - final ChallengeType type; - - /// Out-of-Band (OOB) code. - final String? oobCode; - - /// When the challenge response includes a `prompt` binding method, your app - /// needs to prompt the user for the `binding_code` and send it as part of the - /// request. - final String? bindingMethod; - - Challenge({required this.type, this.oobCode, this.bindingMethod}); - - factory Challenge.fromMap(final Map result) => Challenge( - type: - ChallengeType.fromString(result['challengeType'] as String), - oobCode: result['oobCode'] as String?, - bindingMethod: result['bindingMethod'] as String?, - ); -} diff --git a/auth0_flutter_platform_interface/lib/src/auth/challenge_type.dart b/auth0_flutter_platform_interface/lib/src/auth/challenge_type.dart deleted file mode 100644 index d10260b2d..000000000 --- a/auth0_flutter_platform_interface/lib/src/auth/challenge_type.dart +++ /dev/null @@ -1,28 +0,0 @@ -const String _otp = 'otp'; -const String _oob = 'oob'; - -/// How the user will get the multi-factor authentication challenge and prove -/// possession. -enum ChallengeType { - /// One-time password (OTP). - otp(_otp), - - /// SMS/voice messages or out-of-band (OOB). - oob(_oob); - - const ChallengeType(this.value); - - factory ChallengeType.fromString(final String value) { - switch (value) { - case _otp: - return ChallengeType.otp; - case _oob: - return ChallengeType.oob; - default: - throw ArgumentError('Unexpected challenge_type value: $value'); - } - } - - /// String value for the current enum case. - final String value; -} diff --git a/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart b/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart index 0c31c2354..d54b3a2af 100644 --- a/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart +++ b/auth0_flutter_platform_interface/lib/src/auth0_flutter_auth_platform.dart @@ -4,8 +4,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'auth/auth_custom_token_exchange_options.dart'; import 'auth/auth_login_code_options.dart'; import 'auth/auth_login_options.dart'; -import 'auth/auth_login_with_otp_options.dart'; -import 'auth/auth_multifactor_challenge_options.dart'; import 'auth/auth_passkey_exchange_options.dart'; import 'auth/auth_passkey_login_challenge_options.dart'; import 'auth/auth_passkey_signup_challenge_options.dart'; @@ -18,7 +16,6 @@ import 'auth/auth_reset_password_options.dart'; import 'auth/auth_signup_options.dart'; import 'auth/auth_sso_exchange_options.dart'; import 'auth/auth_user_info_options.dart'; -import 'auth/challenge.dart'; import 'auth/passkey_challenge.dart'; import 'auth/passwordless_challenge.dart'; import 'credentials.dart'; @@ -43,16 +40,6 @@ abstract class Auth0FlutterAuthPlatform extends PlatformInterface { throw UnimplementedError('authLogin() has not been implemented'); } - Future loginWithOtp( - final ApiRequest request) { - throw UnimplementedError('authLoginWithOtp() has not been implemented'); - } - - Future multifactorChallenge( - final ApiRequest request) { - throw UnimplementedError('multifactorChallenge() has not been implemented'); - } - Future startPasswordlessWithEmail( final ApiRequest request) { throw UnimplementedError('startPasswordlessWithEmail() has not been ' diff --git a/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_platform.dart b/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_platform.dart index 32c400cbf..3b5bf578d 100644 --- a/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_platform.dart +++ b/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_platform.dart @@ -95,4 +95,12 @@ abstract class CredentialsManagerPlatform extends PlatformInterface { ) { throw UnimplementedError('clearApiCredentials() has not been implemented'); } + + /// Removes all credentials and API credentials from the native storage, + /// including the underlying encryption keys. + /// + /// See `CredentialsManager.clearAll` for full documentation. + Future clearAll(final CredentialsManagerRequest request) { + throw UnimplementedError('clearAll() has not been implemented'); + } } diff --git a/auth0_flutter_platform_interface/lib/src/credentials-manager/method_channel_credentials_manager.dart b/auth0_flutter_platform_interface/lib/src/credentials-manager/method_channel_credentials_manager.dart index c2be0a222..654cf78ec 100644 --- a/auth0_flutter_platform_interface/lib/src/credentials-manager/method_channel_credentials_manager.dart +++ b/auth0_flutter_platform_interface/lib/src/credentials-manager/method_channel_credentials_manager.dart @@ -22,6 +22,8 @@ const String credentialsManagerGetApiCredentialsMethod = 'credentialsManager#getApiCredentials'; const String credentialsManagerClearApiCredentialsMethod = 'credentialsManager#clearApiCredentials'; +const String credentialsManagerClearAllMethod = + 'credentialsManager#clearAll'; /// Method Channel implementation to communicate with the Native /// CredentialsManager @@ -148,6 +150,19 @@ class MethodChannelCredentialsManager extends CredentialsManagerPlatform { throwOnNull: false); } + /// Removes all credentials and API credentials from the native storage, + /// including the underlying encryption keys. + /// + /// Uses the [MethodChannel] to communicate with the native platforms. + /// See `CredentialsManager.clearAll` for full documentation. + @override + Future clearAll(final CredentialsManagerRequest request) async { + await _invokeRequest( + method: credentialsManagerClearAllMethod, + request: request, + throwOnNull: false); + } + Future _invokeRequest({ required final String method, required final CredentialsManagerRequest request, diff --git a/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_api_credentials_options.dart b/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_api_credentials_options.dart index 7daed2b24..aae182880 100644 --- a/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_api_credentials_options.dart +++ b/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_api_credentials_options.dart @@ -25,7 +25,7 @@ class GetApiCredentialsOptions implements RequestOptions { GetApiCredentialsOptions({ required this.audience, this.scopes = const {}, - this.minTtl = 0, + this.minTtl = 60, this.parameters = const {}, this.headers = const {}, }); diff --git a/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_credentials_options.dart b/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_credentials_options.dart index 740b84163..9e995b849 100644 --- a/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_credentials_options.dart +++ b/auth0_flutter_platform_interface/lib/src/credentials-manager/options/get_credentials_options.dart @@ -9,7 +9,7 @@ class GetCredentialsOptions implements RequestOptions { final Map parameters; GetCredentialsOptions({ - this.minTtl = 0, + this.minTtl = 60, this.scopes = const {}, this.parameters = const {}, }); diff --git a/auth0_flutter_platform_interface/lib/src/id_token_validation_config.dart b/auth0_flutter_platform_interface/lib/src/id_token_validation_config.dart index 8f4a830b2..73c24c970 100644 --- a/auth0_flutter_platform_interface/lib/src/id_token_validation_config.dart +++ b/auth0_flutter_platform_interface/lib/src/id_token_validation_config.dart @@ -19,5 +19,16 @@ class IdTokenValidationConfig { /// Defaults to `0`. final int? maxAge; - const IdTokenValidationConfig({this.leeway, this.issuer, this.maxAge}); + /// A one-time random value used to mitigate replay attacks. When provided, it + /// must match the `nonce` claim in the returned ID token. + /// + /// When omitted, the underlying SDK generates and validates a nonce + /// automatically. + /// + /// Not supported on web, where the nonce is always managed internally by + /// auth0-spa-js. + final String? nonce; + + const IdTokenValidationConfig( + {this.leeway, this.issuer, this.maxAge, this.nonce}); } diff --git a/auth0_flutter_platform_interface/lib/src/login_options.dart b/auth0_flutter_platform_interface/lib/src/login_options.dart index 70b8f3b70..5a16c069d 100644 --- a/auth0_flutter_platform_interface/lib/src/login_options.dart +++ b/auth0_flutter_platform_interface/lib/src/login_options.dart @@ -27,6 +27,7 @@ class LoginOptions implements RequestOptions { 'leeway': idTokenValidationConfig?.leeway, 'maxAge': idTokenValidationConfig?.maxAge, 'issuer': idTokenValidationConfig?.issuer, + 'nonce': idTokenValidationConfig?.nonce, 'audience': audience, 'scopes': scopes.toList(), 'redirectUrl': redirectUrl, diff --git a/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart b/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart index f84dc63a9..ac3604bd9 100644 --- a/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart +++ b/auth0_flutter_platform_interface/lib/src/method_channel_auth0_flutter_auth.dart @@ -4,8 +4,6 @@ import 'auth/api_exception.dart'; import 'auth/auth_custom_token_exchange_options.dart'; import 'auth/auth_login_code_options.dart'; import 'auth/auth_login_options.dart'; -import 'auth/auth_login_with_otp_options.dart'; -import 'auth/auth_multifactor_challenge_options.dart'; import 'auth/auth_passkey_exchange_options.dart'; import 'auth/auth_passkey_login_challenge_options.dart'; import 'auth/auth_passkey_signup_challenge_options.dart'; @@ -18,7 +16,6 @@ import 'auth/auth_reset_password_options.dart'; import 'auth/auth_signup_options.dart'; import 'auth/auth_sso_exchange_options.dart'; import 'auth/auth_user_info_options.dart'; -import 'auth/challenge.dart'; import 'auth/passkey_challenge.dart'; import 'auth/passwordless_challenge.dart'; import 'auth0_flutter_auth_platform.dart'; @@ -32,8 +29,6 @@ import 'user_profile.dart'; const MethodChannel _channel = MethodChannel('auth0.com/auth0_flutter/auth'); const String authLoginMethod = 'auth#login'; -const String authLoginWithOtpMethod = 'auth#loginOtp'; -const String authMultifactorChallengeMethod = 'auth#multifactorChallenge'; const String authStartPasswordlessWithEmailMethod = 'auth#passwordlessWithEmail'; const String authStartPasswordlessWithPhoneNumberMethod = @@ -67,24 +62,6 @@ class MethodChannelAuth0FlutterAuth extends Auth0FlutterAuthPlatform { return Credentials.fromMap(result); } - @override - Future loginWithOtp( - final ApiRequest request) async { - final Map result = - await invokeRequest(method: authLoginWithOtpMethod, request: request); - - return Credentials.fromMap(result); - } - - @override - Future multifactorChallenge( - final ApiRequest request) async { - final Map result = await invokeRequest( - method: authMultifactorChallengeMethod, request: request); - - return Challenge.fromMap(result); - } - @override Future startPasswordlessWithEmail( final ApiRequest request) async { diff --git a/auth0_flutter_platform_interface/lib/src/sso_credentials.dart b/auth0_flutter_platform_interface/lib/src/sso_credentials.dart index 6f1be1464..e567a7a97 100644 --- a/auth0_flutter_platform_interface/lib/src/sso_credentials.dart +++ b/auth0_flutter_platform_interface/lib/src/sso_credentials.dart @@ -29,8 +29,8 @@ class SSOCredentials { /// The token type, typically `"session_transfer"`. final String tokenType; - /// The number of seconds until the [sessionTransferToken] expires. - final int expiresIn; + /// The date and time, in UTC, at which the [sessionTransferToken] expires. + final DateTime expiresAt; /// A JSON web token containing user identity information. final String idToken; @@ -44,7 +44,7 @@ class SSOCredentials { const SSOCredentials({ required this.sessionTransferToken, required this.tokenType, - required this.expiresIn, + required this.expiresAt, required this.idToken, this.refreshToken, }); @@ -54,8 +54,16 @@ class SSOCredentials { SSOCredentials( sessionTransferToken: result['sessionTransferToken'] as String, tokenType: result['tokenType'] as String, - expiresIn: result['expiresIn'] as int, + expiresAt: DateTime.parse(result['expiresAt'] as String).toUtc(), idToken: result['idToken'] as String, refreshToken: result['refreshToken'] as String?, ); + + Map toMap() => { + 'sessionTransferToken': sessionTransferToken, + 'tokenType': tokenType, + 'expiresAt': expiresAt.toUtc().toIso8601String(), + 'idToken': idToken, + 'refreshToken': refreshToken, + }; } diff --git a/auth0_flutter_platform_interface/lib/src/web-auth/custom_tabs_options.dart b/auth0_flutter_platform_interface/lib/src/web-auth/custom_tabs_options.dart index 817cb710d..66b511d29 100644 --- a/auth0_flutter_platform_interface/lib/src/web-auth/custom_tabs_options.dart +++ b/auth0_flutter_platform_interface/lib/src/web-auth/custom_tabs_options.dart @@ -42,6 +42,21 @@ class CustomTabsOptions { /// When no allowed browser is installed, an error is returned. final List allowedBrowsers; + /// Whether to launch the browser as a Trusted Web Activity (TWA). + /// + /// Defaults to `false`. TWA requires the app to be verified for the Auth0 + /// domain through Digital Asset Links. Takes precedence over [useAuthTab] + /// when both are `true`. + final bool useTrustedWebActivity; + + /// Whether to launch the browser as an Auth Tab. + /// + /// Auth Tab uses the Chrome Custom Tabs Activity Result API, which avoids + /// spurious user-cancelled errors caused by Chrome being minimized. It + /// requires Chrome 137+; older browsers fall back to a standard Custom Tab. + /// Defaults to `false`. Ignored when [useTrustedWebActivity] is `true`. + final bool useAuthTab; + const CustomTabsOptions({ this.initialHeight, this.resizable, @@ -50,6 +65,8 @@ class CustomTabsOptions { this.sideSheetBreakpoint, this.backgroundInteractionEnabled, this.allowedBrowsers = const [], + this.useTrustedWebActivity = false, + this.useAuthTab = false, }); Map toMap() => { @@ -60,5 +77,7 @@ class CustomTabsOptions { 'sideSheetBreakpoint': sideSheetBreakpoint, 'backgroundInteractionEnabled': backgroundInteractionEnabled, 'allowedBrowsers': allowedBrowsers, + 'useTrustedWebActivity': useTrustedWebActivity, + 'useAuthTab': useAuthTab, }; } diff --git a/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart b/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart index 8e3d0b401..193e09900 100644 --- a/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart +++ b/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart @@ -15,8 +15,15 @@ class WebAuthenticationException extends Auth0Exception { WebAuthenticationException.fromPlatformException(final PlatformException e) : this(e.code, e.messageString, e.detailsMap); - bool get isUserCancelledException => - code == 'USER_CANCELLED' || code == 'a0.authentication_canceled'; + bool get isUserCancelledException => code == 'USER_CANCELLED'; + + bool get isAuthenticationFailed => code == 'AUTHENTICATION_FAILED'; + + bool get isCodeExchangeFailed => code == 'CODE_EXCHANGE_FAILED'; + + bool get isIdTokenValidationFailed => code == 'ID_TOKEN_VALIDATION_FAILED'; + + bool get isTransactionActiveAlready => code == 'TRANSACTION_ACTIVE_ALREADY'; bool get isRetryable => details.getBooleanOrFalse('_isRetryable'); } diff --git a/auth0_flutter_platform_interface/pubspec.yaml b/auth0_flutter_platform_interface/pubspec.yaml index c1de45af5..1e9fa98ea 100644 --- a/auth0_flutter_platform_interface/pubspec.yaml +++ b/auth0_flutter_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: auth0_flutter_platform_interface description: A common platform interface for the auth0_flutter federated plugin. -version: 2.7.0 +version: 3.0.0-beta.0 homepage: https://github.com/auth0/auth0-flutter diff --git a/auth0_flutter_platform_interface/test/api_credentials_test.dart b/auth0_flutter_platform_interface/test/api_credentials_test.dart index f2841b071..9deedb6cf 100644 --- a/auth0_flutter_platform_interface/test/api_credentials_test.dart +++ b/auth0_flutter_platform_interface/test/api_credentials_test.dart @@ -70,7 +70,7 @@ void main() { expect(map['audience'], 'test-audience'); expect(map['scopes'], isEmpty); - expect(map['minTtl'], 0); + expect(map['minTtl'], 60); expect(map['parameters'], isEmpty); expect(map['headers'], isEmpty); }); diff --git a/auth0_flutter_platform_interface/test/custom_tabs_options_test.dart b/auth0_flutter_platform_interface/test/custom_tabs_options_test.dart new file mode 100644 index 000000000..bcb13d6e6 --- /dev/null +++ b/auth0_flutter_platform_interface/test/custom_tabs_options_test.dart @@ -0,0 +1,35 @@ +import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CustomTabsOptions', () { + test('toMap includes useTrustedWebActivity and useAuthTab', () { + const options = CustomTabsOptions( + useTrustedWebActivity: true, + useAuthTab: true, + ); + + final map = options.toMap(); + + expect(map['useTrustedWebActivity'], true); + expect(map['useAuthTab'], true); + }); + + test('useTrustedWebActivity and useAuthTab default to false', () { + final map = const CustomTabsOptions().toMap(); + + expect(map['useTrustedWebActivity'], false); + expect(map['useAuthTab'], false); + }); + + test('is passed through WebAuthLoginOptions and WebAuthLogoutOptions', () { + const tabs = CustomTabsOptions(useAuthTab: true); + + final login = WebAuthLoginOptions(customTabsOptions: tabs).toMap(); + final logout = WebAuthLogoutOptions(customTabsOptions: tabs).toMap(); + + expect((login['customTabsOptions'] as Map)['useAuthTab'], true); + expect((logout['customTabsOptions'] as Map)['useAuthTab'], true); + }); + }); +} 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..33e793b31 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 @@ -23,16 +23,6 @@ class MethodCallHandler { 'refreshToken': 'refreshToken' }; - static const Map multifactorChallengeResultRequired = { - 'challengeType': 'oob' - }; - - static const Map multifactorChallengeResult = { - ...multifactorChallengeResultRequired, - 'oobCode': 'oobCode', - 'bindingMethod': 'bindingMethod' - }; - static const Map signupResultRequired = { 'email': 'test-email', 'emailVerified': true @@ -56,7 +46,7 @@ class MethodCallHandler { static const Map ssoExchangeResult = { 'sessionTransferToken': 'sso-token', 'tokenType': 'session_transfer', - 'expiresIn': 60, + 'expiresAt': '2024-11-01T22:16:35.000Z', 'idToken': 'id-token', 'refreshToken': 'new-refresh-token' }; @@ -492,266 +482,6 @@ void main() { }); }); - group('loginWithOtp', () { - test('calls the correct MethodChannel method', () async { - when(mocked.methodCallHandler(any)) - .thenAnswer((final _) async => MethodCallHandler.loginResult); - - await MethodChannelAuth0FlutterAuth().loginWithOtp( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: 'test-name', version: 'test-version'), - options: AuthLoginWithOtpOptions( - otp: 'test-otp', mfaToken: 'test-mfa-token')), - ); - - expect( - verify(mocked.methodCallHandler(captureAny)).captured.single.method, - 'auth#loginOtp'); - }); - - test('correctly maps all properties', () async { - when(mocked.methodCallHandler(any)) - .thenAnswer((final _) async => MethodCallHandler.loginResult); - - await MethodChannelAuth0FlutterAuth().loginWithOtp( - ApiRequest( - account: const Account('test-domain', 'test-clientId'), - userAgent: UserAgent(name: 'test-name', version: 'test-version'), - options: AuthLoginWithOtpOptions( - otp: 'test-otp', mfaToken: 'test-mfa-token')), - ); - - final verificationResult = - verify(mocked.methodCallHandler(captureAny)).captured.single; - expect(verificationResult.arguments['_account']['domain'], 'test-domain'); - expect(verificationResult.arguments['_account']['clientId'], - 'test-clientId'); - expect(verificationResult.arguments['_userAgent']['name'], 'test-name'); - expect(verificationResult.arguments['_userAgent']['version'], - 'test-version'); - expect(verificationResult.arguments['otp'], 'test-otp'); - expect(verificationResult.arguments['mfaToken'], 'test-mfa-token'); - }); - - test('correctly returns the response from the Method Channel', () async { - when(mocked.methodCallHandler(any)) - .thenAnswer((final _) async => MethodCallHandler.loginResult); - - final result = await MethodChannelAuth0FlutterAuth().loginWithOtp( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: 'test-name', version: 'test-version'), - options: AuthLoginWithOtpOptions( - mfaToken: 'test-mfa-token', otp: 'test-otp')), - ); - - verify(mocked.methodCallHandler(captureAny)); - - expect(result.accessToken, MethodCallHandler.loginResult['accessToken']); - expect(result.idToken, MethodCallHandler.loginResult['idToken']); - expect(result.expiresAt, - DateTime.parse(MethodCallHandler.loginResult['expiresAt'] as String)); - expect(result.scopes, MethodCallHandler.loginResult['scopes']); - expect( - result.refreshToken, MethodCallHandler.loginResult['refreshToken']); - expect(result.user.name, - MethodCallHandler.loginResult['userProfile']['name']); - }); - - test( - 'correctly returns the response from the Method Channel when properties missing', - () async { - when(mocked.methodCallHandler(any)) - .thenAnswer((final _) async => MethodCallHandler.loginResultRequired); - - final result = await MethodChannelAuth0FlutterAuth().loginWithOtp( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: AuthLoginWithOtpOptions(otp: '', mfaToken: ''))); - - verify(mocked.methodCallHandler(captureAny)); - - expect(result.refreshToken, isNull); - }); - - test('throws an ApiException when method channel returns null', () async { - when(mocked.methodCallHandler(any)).thenAnswer((final _) async => null); - - Future actual() async { - final result = await MethodChannelAuth0FlutterAuth().loginWithOtp( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: 'test-name', version: 'test-version'), - options: AuthLoginWithOtpOptions(otp: '', mfaToken: '')), - ); - - return result; - } - - await expectLater(actual, throwsA(isA())); - }); - - test( - 'throws an ApiException when method channel throws a PlatformException', - () async { - when(mocked.methodCallHandler(any)) - .thenThrow(PlatformException(code: '123')); - - Future actual() async { - final result = await MethodChannelAuth0FlutterAuth().loginWithOtp( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: 'test-name', version: 'test-version'), - options: AuthLoginWithOtpOptions(otp: '', mfaToken: '')), - ); - - return result; - } - - await expectLater(actual, throwsA(isA())); - }); - }); - - group('multifactorChallenge', () { - test('calls the correct MethodChannel method', () async { - when(mocked.methodCallHandler(any)).thenAnswer((final _) async => - MethodCallHandler.multifactorChallengeResultRequired); - - await MethodChannelAuth0FlutterAuth().multifactorChallenge( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); - - expect( - verify(mocked.methodCallHandler(captureAny)).captured.single.method, - 'auth#multifactorChallenge'); - }); - - test('correctly maps all properties', () async { - when(mocked.methodCallHandler(any)).thenAnswer( - (final _) async => MethodCallHandler.multifactorChallengeResult); - - await MethodChannelAuth0FlutterAuth().multifactorChallenge( - ApiRequest( - account: const Account('test-domain', 'test-clientId'), - userAgent: UserAgent(name: 'test-name', version: 'test-version'), - options: AuthMultifactorChallengeOptions( - mfaToken: 'test-mfa-token', - types: [ChallengeType.otp, ChallengeType.oob], - authenticatorId: 'test-authenticatorId'))); - - final verificationResult = - verify(mocked.methodCallHandler(captureAny)).captured.single; - expect(verificationResult.arguments['_account']['domain'], 'test-domain'); - expect(verificationResult.arguments['_account']['clientId'], - 'test-clientId'); - expect(verificationResult.arguments['_userAgent']['name'], 'test-name'); - 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); - - final result = await MethodChannelAuth0FlutterAuth().multifactorChallenge( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); - - verify(mocked.methodCallHandler(captureAny)); - - expect(result.type, ChallengeType.oob); - expect(result.oobCode, 'oobCode'); - expect(result.bindingMethod, 'bindingMethod'); - }); - - test( - 'correctly returns the response from the Method Channel when properties missing', - () async { - when(mocked.methodCallHandler(any)).thenAnswer((final _) async => - MethodCallHandler.multifactorChallengeResultRequired); - - final result = await MethodChannelAuth0FlutterAuth().multifactorChallenge( - ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); - - verify(mocked.methodCallHandler(captureAny)); - - expect(result.type, ChallengeType.oob); - expect(result.oobCode, isNull); - expect(result.bindingMethod, isNull); - }); - - test('throws an ApiException when method channel returns null', () async { - when(mocked.methodCallHandler(any)).thenAnswer((final _) async => null); - - Future actual() async { - final result = await MethodChannelAuth0FlutterAuth() - .multifactorChallenge(ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); - - return result; - } - - expectLater( - actual, - throwsA(predicate((final e) => - e is ApiException && e.message == 'Channel returned null.'))); - }); - - test( - 'throws an ApiException when method channel throws a PlatformException', - () async { - when(mocked.methodCallHandler(any)) - .thenThrow(PlatformException(code: '123')); - - Future actual() async { - final result = await MethodChannelAuth0FlutterAuth() - .multifactorChallenge(ApiRequest( - account: const Account('', ''), - userAgent: UserAgent(name: '', version: ''), - options: AuthMultifactorChallengeOptions(mfaToken: ''))); - - return result; - } - - await expectLater(actual, throwsA(isA())); - }); - }); - group('resetPassword', () { test('calls the correct MethodChannel method', () async { when(mocked.methodCallHandler(any)).thenAnswer((final _) async => null); @@ -1345,7 +1075,10 @@ void main() { expect( result.tokenType, MethodCallHandler.ssoExchangeResult['tokenType']); expect( - result.expiresIn, MethodCallHandler.ssoExchangeResult['expiresIn']); + result.expiresAt, + DateTime.parse( + MethodCallHandler.ssoExchangeResult['expiresAt'] as String) + .toUtc()); expect(result.idToken, MethodCallHandler.ssoExchangeResult['idToken']); expect(result.refreshToken, MethodCallHandler.ssoExchangeResult['refreshToken']); diff --git a/auth0_flutter_platform_interface/test/method_channel_credentials_manager_test.dart b/auth0_flutter_platform_interface/test/method_channel_credentials_manager_test.dart index 16f9a3d2f..19c1bf026 100644 --- a/auth0_flutter_platform_interface/test/method_channel_credentials_manager_test.dart +++ b/auth0_flutter_platform_interface/test/method_channel_credentials_manager_test.dart @@ -21,7 +21,7 @@ class MethodCallHandler { static const Map ssoCredentials = { 'sessionTransferToken': 'ssoToken', 'tokenType': 'session_transfer', - 'expiresIn': 60, + 'expiresAt': '2024-11-01T22:16:35.000Z', 'idToken': 'idToken', 'refreshToken': 'refreshToken', }; @@ -114,7 +114,7 @@ void main() { final verificationResult = verify(mocked.methodCallHandler(captureAny)).captured.single; - expect(verificationResult.arguments['minTtl'], 0); + expect(verificationResult.arguments['minTtl'], 60); expect(verificationResult.arguments['scopes'], isEmpty); expect(verificationResult.arguments['parameters'], isEmpty); }); @@ -952,7 +952,11 @@ void main() { expect(result.sessionTransferToken, MethodCallHandler.ssoCredentials['sessionTransferToken']); expect(result.tokenType, MethodCallHandler.ssoCredentials['tokenType']); - expect(result.expiresIn, MethodCallHandler.ssoCredentials['expiresIn']); + expect( + result.expiresAt, + DateTime.parse( + MethodCallHandler.ssoCredentials['expiresAt'] as String) + .toUtc()); expect(result.idToken, MethodCallHandler.ssoCredentials['idToken']); expect(result.refreshToken, MethodCallHandler.ssoCredentials['refreshToken']); @@ -962,7 +966,7 @@ void main() { when(mocked.methodCallHandler(any)).thenAnswer((final _) async => { 'sessionTransferToken': 'ssoToken', 'tokenType': 'session_transfer', - 'expiresIn': 30, + 'expiresAt': '2024-11-01T22:16:35.000Z', 'idToken': 'id-token', }); @@ -1090,7 +1094,7 @@ void main() { final verificationResult = verify(mocked.methodCallHandler(captureAny)).captured.single; expect(verificationResult.arguments['scopes'], isEmpty); - expect(verificationResult.arguments['minTtl'], 0); + expect(verificationResult.arguments['minTtl'], 60); expect(verificationResult.arguments['parameters'], isEmpty); expect(verificationResult.arguments['headers'], isEmpty); }); @@ -1235,4 +1239,54 @@ void main() { await expectLater(actual, throwsA(isA())); }); }); + + group('clearAll', () { + test('calls the correct MethodChannel method', () async { + when(mocked.methodCallHandler(any)).thenAnswer((final _) async => null); + + await MethodChannelCredentialsManager().clearAll( + CredentialsManagerRequest( + account: const Account('test-domain', 'test-clientId'), + userAgent: + UserAgent(name: 'test-name', version: 'test-version'))); + + expect( + verify(mocked.methodCallHandler(captureAny)).captured.single.method, + 'credentialsManager#clearAll'); + }); + + test('correctly maps all properties', () async { + when(mocked.methodCallHandler(any)).thenAnswer((final _) async => null); + + await MethodChannelCredentialsManager().clearAll( + CredentialsManagerRequest( + account: const Account('test-domain', 'test-clientId'), + userAgent: + UserAgent(name: 'test-name', version: 'test-version'))); + + final verificationResult = + verify(mocked.methodCallHandler(captureAny)).captured.single; + expect(verificationResult.arguments['_account']['domain'], 'test-domain'); + expect(verificationResult.arguments['_account']['clientId'], + 'test-clientId'); + expect(verificationResult.arguments['_userAgent']['name'], 'test-name'); + expect(verificationResult.arguments['_userAgent']['version'], + 'test-version'); + }); + + test( + 'throws a CredentialsManagerException when method channel throws a PlatformException', + () async { + when(mocked.methodCallHandler(any)) + .thenThrow(PlatformException(code: '123')); + + Future actual() async => + MethodChannelCredentialsManager().clearAll(CredentialsManagerRequest( + account: const Account('', ''), + userAgent: + UserAgent(name: 'test-name', version: 'test-version'))); + + await expectLater(actual, throwsA(isA())); + }); + }); } diff --git a/auth0_flutter_platform_interface/test/sso_credentials_test.dart b/auth0_flutter_platform_interface/test/sso_credentials_test.dart new file mode 100644 index 000000000..69eeb0b0c --- /dev/null +++ b/auth0_flutter_platform_interface/test/sso_credentials_test.dart @@ -0,0 +1,72 @@ +import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SSOCredentials.fromMap', () { + test('parses all properties, converting expiresAt to a UTC DateTime', () { + final sut = SSOCredentials.fromMap(const { + 'sessionTransferToken': 'ssoToken', + 'tokenType': 'session_transfer', + 'expiresAt': '2024-11-01T22:16:35.000Z', + 'idToken': 'idToken', + 'refreshToken': 'refreshToken', + }); + + expect(sut.sessionTransferToken, 'ssoToken'); + expect(sut.tokenType, 'session_transfer'); + expect(sut.expiresAt, DateTime.utc(2024, 11, 1, 22, 16, 35)); + expect(sut.expiresAt.isUtc, isTrue); + expect(sut.idToken, 'idToken'); + expect(sut.refreshToken, 'refreshToken'); + }); + + test('normalizes a non-UTC expiresAt to UTC', () { + final sut = SSOCredentials.fromMap(const { + 'sessionTransferToken': 'ssoToken', + 'tokenType': 'session_transfer', + 'expiresAt': '2024-11-01T22:16:35.000+02:00', + 'idToken': 'idToken', + }); + + expect(sut.expiresAt.isUtc, isTrue); + expect(sut.expiresAt, DateTime.utc(2024, 11, 1, 20, 16, 35)); + expect(sut.refreshToken, isNull); + }); + }); + + group('SSOCredentials.toMap', () { + test('emits expiresAt as an ISO-8601 UTC string', () { + final map = SSOCredentials( + sessionTransferToken: 'ssoToken', + tokenType: 'session_transfer', + expiresAt: DateTime.utc(2024, 11, 1, 22, 16, 35), + idToken: 'idToken', + refreshToken: 'refreshToken', + ).toMap(); + + expect(map['sessionTransferToken'], 'ssoToken'); + expect(map['tokenType'], 'session_transfer'); + expect(map['expiresAt'], '2024-11-01T22:16:35.000Z'); + expect(map['idToken'], 'idToken'); + expect(map['refreshToken'], 'refreshToken'); + }); + }); + + test('fromMap/toMap round-trip preserves values', () { + final original = SSOCredentials( + sessionTransferToken: 'ssoToken', + tokenType: 'session_transfer', + expiresAt: DateTime.utc(2024, 11, 1, 22, 16, 35), + idToken: 'idToken', + refreshToken: 'refreshToken', + ); + + final roundTripped = SSOCredentials.fromMap(original.toMap()); + + expect(roundTripped.sessionTransferToken, original.sessionTransferToken); + expect(roundTripped.tokenType, original.tokenType); + expect(roundTripped.expiresAt, original.expiresAt); + expect(roundTripped.idToken, original.idToken); + expect(roundTripped.refreshToken, original.refreshToken); + }); +} diff --git a/auth0_flutter_platform_interface/test/web_auth_login_options_test.dart b/auth0_flutter_platform_interface/test/web_auth_login_options_test.dart index d8bf18bb7..fe52cf691 100644 --- a/auth0_flutter_platform_interface/test/web_auth_login_options_test.dart +++ b/auth0_flutter_platform_interface/test/web_auth_login_options_test.dart @@ -6,8 +6,8 @@ void main() { test('toMap should include all fields correctly', () { const safariViewController = SafariViewController( presentationStyle: SafariViewControllerPresentationStyle.automatic); - const idTokenValidationConfig = - IdTokenValidationConfig(leeway: 0, maxAge: 0, issuer: 'issuer'); + const idTokenValidationConfig = IdTokenValidationConfig( + leeway: 0, maxAge: 0, issuer: 'issuer', nonce: 'test-nonce'); final options = WebAuthLoginOptions( audience: 'https://myapi.com', idTokenValidationConfig: idTokenValidationConfig, @@ -30,6 +30,7 @@ void main() { expect(map['leeway'], idTokenValidationConfig.leeway); expect(map['issuer'], idTokenValidationConfig.issuer); expect(map['maxAge'], idTokenValidationConfig.maxAge); + expect(map['nonce'], idTokenValidationConfig.nonce); expect(map['organizationId'], 'org_123'); expect(map['invitationUrl'], 'https://invite.com'); expect(map['redirectUrl'], 'com.app://login'); diff --git a/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart b/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart index d81fb7973..443561c35 100644 --- a/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart +++ b/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart @@ -51,5 +51,63 @@ void main() { expect(exception.isRetryable, false); }); + + test('isUserCancelledException returns true for USER_CANCELLED', () { + final exception = WebAuthenticationException.fromPlatformException( + PlatformException( + code: 'USER_CANCELLED', + message: '', + details: {})); + expect(exception.isUserCancelledException, true); + }); + + test('isUserCancelledException returns false for other codes', () { + final exception = WebAuthenticationException.fromPlatformException( + PlatformException( + code: 'a0.authentication_canceled', + message: '', + details: {})); + expect(exception.isUserCancelledException, false); + }); + + test('isAuthenticationFailed returns true for AUTHENTICATION_FAILED', () { + final exception = WebAuthenticationException.fromPlatformException( + PlatformException( + code: 'AUTHENTICATION_FAILED', + message: '', + details: {})); + expect(exception.isAuthenticationFailed, true); + }); + + test('isCodeExchangeFailed returns true for CODE_EXCHANGE_FAILED', () { + final exception = WebAuthenticationException.fromPlatformException( + PlatformException( + code: 'CODE_EXCHANGE_FAILED', + message: '', + details: {})); + expect(exception.isCodeExchangeFailed, true); + }); + + test( + 'isIdTokenValidationFailed returns true for ' + 'ID_TOKEN_VALIDATION_FAILED', () { + final exception = WebAuthenticationException.fromPlatformException( + PlatformException( + code: 'ID_TOKEN_VALIDATION_FAILED', + message: '', + details: {})); + expect(exception.isIdTokenValidationFailed, true); + }); + + test( + 'isTransactionActiveAlready returns true for ' + 'TRANSACTION_ACTIVE_ALREADY', () { + final exception = WebAuthenticationException.fromPlatformException( + PlatformException( + code: 'TRANSACTION_ACTIVE_ALREADY', + message: '', + details: {})); + expect(exception.isTransactionActiveAlready, true); + }); }); }