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/README.md b/auth0_flutter/README.md index 1a95de24c..ae3d456eb 100644 --- a/auth0_flutter/README.md +++ b/auth0_flutter/README.md @@ -30,7 +30,7 @@ | Flutter | Android | iOS | macOS | Windows | | :---------- | :-------------- | :---------------- | :---------------- | :------------------------------- | -| SDK 3.24.0+ | Android API 26+ | iOS 14+ | macOS 11+ | Windows 10+ | +| 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) | @@ -370,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`. diff --git a/auth0_flutter/V3_MIGRATION_GUIDE.md b/auth0_flutter/V3_MIGRATION_GUIDE.md index d2e0c0d9e..8ea82c730 100644 --- a/auth0_flutter/V3_MIGRATION_GUIDE.md +++ b/auth0_flutter/V3_MIGRATION_GUIDE.md @@ -31,10 +31,12 @@ from v2 to v3. ## Requirements Changes Because the underlying native SDKs raised their floors, `auth0_flutter` v3 -raises the Android minimums: +raises the Android, iOS, and macOS minimums: | Requirement | v2 | v3 | | --- | --- | --- | +| iOS deployment target | 14.0 | **15.0** | +| macOS deployment target | 11.0 | **12.0** | | Android `minSdkVersion` | 21 | **26** (Android 8.0) | | Android compile/target SDK | 34 | **36** | | JDK (to build the Android module) | 8 | **17** | @@ -42,7 +44,13 @@ raises the Android minimums: | Android Gradle Plugin | 8.4.x | **8.10.1** | | Gradle | 8.7 | **8.11.1** | -**Migration:** if your app targets an Android `minSdkVersion` below 26, raise it +**Migration (iOS/macOS):** if your app's deployment target is below iOS 15.0 or +macOS 12.0, raise it. Update the `platform` line in `ios/Podfile` (for example +`platform :ios, '15.0'`) and `macos/Podfile` (`platform :osx, '12.0'`), and the +deployment target of the Runner target in Xcode. These floors come from +Auth0.swift v3. + +**Migration (Android):** if your app targets an Android `minSdkVersion` below 26, raise it to at least 26 in your app's `android/app/build.gradle`. Ensure your build environment uses JDK 17 (for example, set it in Android Studio under *Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JDK*, diff --git a/auth0_flutter/android/build.gradle b/auth0_flutter/android/build.gradle index a953c8d94..9ddd46963 100644 --- a/auth0_flutter/android/build.gradle +++ b/auth0_flutter/android/build.gradle @@ -85,7 +85,7 @@ plugins.withId("org.jetbrains.kotlin.android") { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'com.auth0.android:auth0:4.1.0' + implementation 'com.auth0.android:auth0:4.2.0' implementation 'com.google.code.gson:gson:2.11.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt index fd6167860..b96f83fc4 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeyCredentialExchangeApiRequestHandlerTest.kt @@ -206,7 +206,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { val request = MethodCallRequest(account = mockAccount, options) doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() doReturn(mockBuilder).`when`(mockBuilder).setScope(any()) doReturn(mockBuilder).`when`(mockBuilder).setAudience(any()) @@ -218,7 +218,9 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { eq("test-auth-session"), any(), eq("test-connection"), - eq("test-org") + eq("test-org"), + any(), + any() ) verify(mockBuilder).validateClaims() verify(mockBuilder).setScope("openid profile") @@ -241,7 +243,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { val request = MethodCallRequest(account = mockAccount, options) doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() handler.handle(mockApi, request, mockResult) @@ -250,7 +252,9 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { eq("test-auth-session"), any(), anyOrNull(), - anyOrNull() + anyOrNull(), + any(), + any() ) verify(mockBuilder).validateClaims() verify(mockBuilder).start(any()) @@ -272,7 +276,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { AuthenticationException(code = "test-code", description = "test-description") doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() doAnswer { val cb = it.getArgument>(0) @@ -307,7 +311,7 @@ class PasskeyCredentialExchangeApiRequestHandlerTest { ) doReturn(mockBuilder).`when`(mockApi) - .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull()) + .signinWithPasskey(any(), any(), anyOrNull(), anyOrNull(), any(), any()) doReturn(mockBuilder).`when`(mockBuilder).validateClaims() doAnswer { val cb = it.getArgument>(0) diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt index 7770165e4..7608ead36 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/api/PasskeySignupChallengeApiRequestHandlerTest.kt @@ -61,7 +61,7 @@ class PasskeySignupChallengeApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull()) + doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull(), anyOrNull()) handler.handle(mockApi, request, mockResult) @@ -77,7 +77,8 @@ class PasskeySignupChallengeApiRequestHandlerTest { this.picture == "https://www.okta.com" }, eq("test-connection"), - eq("test-org") + eq("test-org"), + isNull() ) verify(mockBuilder).start(any()) } @@ -92,7 +93,7 @@ class PasskeySignupChallengeApiRequestHandlerTest { val mockResult = mock() val request = MethodCallRequest(account = mockAccount, options) - doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull()) + doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { val cb = it.getArgument>(0) cb.onSuccess(challenge()) @@ -121,7 +122,7 @@ class PasskeySignupChallengeApiRequestHandlerTest { val exception = AuthenticationException(code = "test-code", description = "test-description") - doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull()) + doReturn(mockBuilder).`when`(mockApi).signupWithPasskey(any(), anyOrNull(), anyOrNull(), anyOrNull()) doAnswer { val cb = it.getArgument>(0) cb.onFailure(exception) diff --git a/auth0_flutter/darwin/auth0_flutter.podspec b/auth0_flutter/darwin/auth0_flutter.podspec index befd3a9e8..5dfd104c7 100644 --- a/auth0_flutter/darwin/auth0_flutter.podspec +++ b/auth0_flutter/darwin/auth0_flutter.podspec @@ -13,13 +13,13 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'auth0_flutter/Sources/auth0_flutter/**/*.swift' - s.ios.deployment_target = '14.0' + s.ios.deployment_target = '15.0' s.ios.dependency 'Flutter' - s.osx.deployment_target = '11.0' + s.osx.deployment_target = '12.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '3.0.2' + s.dependency 'Auth0', '3.2.0' s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter/darwin/auth0_flutter/Package.swift b/auth0_flutter/darwin/auth0_flutter/Package.swift index ca61ab968..9dfc01048 100644 --- a/auth0_flutter/darwin/auth0_flutter/Package.swift +++ b/auth0_flutter/darwin/auth0_flutter/Package.swift @@ -4,14 +4,14 @@ import PackageDescription let package = Package( name: "auth0_flutter", platforms: [ - .iOS("14.0"), - .macOS("11.0"), + .iOS("15.0"), + .macOS("12.0"), ], products: [ .library(name: "auth0-flutter", targets: ["auth0_flutter"]), ], dependencies: [ - .package(url: "https://github.com/auth0/Auth0.swift", exact: "3.0.2"), + .package(url: "https://github.com/auth0/Auth0.swift", exact: "3.2.0"), .package(url: "https://github.com/auth0/JWTDecode.swift", exact: "4.0.0"), .package(url: "https://github.com/auth0/SimpleKeychain", exact: "1.3.0"), ], diff --git a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift index 0db8d4772..39decee88 100644 --- a/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift +++ b/auth0_flutter/example/ios/Tests/AuthAPI/AuthAPISpies.swift @@ -84,9 +84,10 @@ class SpyAuthentication: Authentication, @unchecked Sendable { return request(databaseUserResult) } - func resetPassword(email: String, connection: String) -> any Requestable { + func resetPassword(email: String, connection: String, organization: String?) -> any Requestable { arguments["email"] = email arguments["connection"] = connection + arguments["organization"] = organization calledResetPassword = true return request(voidResult) } @@ -144,20 +145,12 @@ class SpyAuthentication: Authentication, @unchecked Sendable { return request(.success(JWKS(keys: []))) } - #if PASSKEYS_PLATFORM + // The next two are kept outside `#if PASSKEYS_PLATFORM`: the macOS test target + // does not define that flag, and since Auth0.swift 3.2.0 no protocol extension + // method matches these requirements' signatures, so the spy must always provide them. @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) - func passkeyLoginChallenge(connection: String?, - organization: String?) -> any Requestable { - arguments["connection"] = connection - arguments["organization"] = organization - calledPasskeyLoginChallenge = true - return request(passkeyLoginChallengeResultOverride - ?? SpyAuthentication.passkeyLoginChallengeResult) - } - - @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) - func login(passkey: LoginPasskey, - challenge: PasskeyLoginChallenge, + func login(passkey: SignupPasskey, + challenge: PasskeySignupChallenge, connection: String?, audience: String?, scope: String, @@ -166,7 +159,7 @@ class SpyAuthentication: Authentication, @unchecked Sendable { arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization - calledLoginWithPasskey = true + calledSignupWithPasskey = true return tokenRequest(credentialsResult) } @@ -194,13 +187,28 @@ class SpyAuthentication: Authentication, @unchecked Sendable { arguments["organization"] = organization arguments["userMetadata"] = userMetadata calledPasskeySignupChallenge = true + #if PASSKEYS_PLATFORM return request(passkeySignupChallengeResultOverride ?? SpyAuthentication.passkeySignupChallengeResult) + #else + return request(.failure(AuthenticationError(info: [:], statusCode: 0))) + #endif } + #if PASSKEYS_PLATFORM @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) - func login(passkey: SignupPasskey, - challenge: PasskeySignupChallenge, + func passkeyLoginChallenge(connection: String?, + organization: String?) -> any Requestable { + arguments["connection"] = connection + arguments["organization"] = organization + calledPasskeyLoginChallenge = true + return request(passkeyLoginChallengeResultOverride + ?? SpyAuthentication.passkeyLoginChallengeResult) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func login(passkey: LoginPasskey, + challenge: PasskeyLoginChallenge, connection: String?, audience: String?, scope: String, @@ -209,6 +217,49 @@ class SpyAuthentication: Authentication, @unchecked Sendable { arguments["audience"] = audience arguments["scope"] = scope arguments["organization"] = organization + calledLoginWithPasskey = true + return tokenRequest(credentialsResult) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func passkeySignupChallenge(email: String?, + phoneNumber: String?, + username: String?, + name: String?, + givenName: String?, + familyName: String?, + nickname: String?, + picture: String?, + userMetadata: [String: String]?, + connection: String?, + organization: String?, + deliveryMethod: DeliveryMethod?) -> any Requestable { + return passkeySignupChallenge(email: email, + phoneNumber: phoneNumber, + username: username, + name: name, + givenName: givenName, + familyName: familyName, + nickname: nickname, + picture: picture, + userMetadata: userMetadata, + connection: connection, + organization: organization) + } + + @available(iOS 16.6, macOS 13.5, visionOS 1.0, *) + func login(passkey: SignupPasskey, + verificationRequired: [PasskeyVerificationMethod], + authSession: String, + connection: String?, + audience: String?, + scope: String, + organization: String?, + verification: [PasskeyVerificationMethod: String]) -> any TokenRequestable { + arguments["connection"] = connection + arguments["audience"] = audience + arguments["scope"] = scope + arguments["organization"] = organization calledSignupWithPasskey = true return tokenRequest(credentialsResult) } diff --git a/auth0_flutter/example/web/index.html b/auth0_flutter/example/web/index.html index a7d5d25d2..782133b68 100644 --- a/auth0_flutter/example/web/index.html +++ b/auth0_flutter/example/web/index.html @@ -47,7 +47,7 @@ // Create promise that resolves when Auth0 SDK is loaded window.auth0SdkReady = new Promise((resolve, reject) => { const script = document.createElement('script'); - script.src = 'https://unpkg.com/@auth0/auth0-spa-js@2.24.0/dist/auth0-spa-js.production.js'; + script.src = 'https://unpkg.com/@auth0/auth0-spa-js@2.29.0/dist/auth0-spa-js.production.js'; script.onload = () => { if (window.auth0 && window.auth0.Auth0Client) { diff --git a/auth0_flutter/ios/auth0_flutter.podspec b/auth0_flutter/ios/auth0_flutter.podspec index 6c1b397b7..2f5dfc09a 100644 --- a/auth0_flutter/ios/auth0_flutter.podspec +++ b/auth0_flutter/ios/auth0_flutter.podspec @@ -13,13 +13,13 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.ios.deployment_target = '14.0' + s.ios.deployment_target = '15.0' s.ios.dependency 'Flutter' - s.osx.deployment_target = '11.0' + s.osx.deployment_target = '12.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '3.0.2' + s.dependency 'Auth0', '3.2.0' s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter/macos/auth0_flutter.podspec b/auth0_flutter/macos/auth0_flutter.podspec index 6c1b397b7..2f5dfc09a 100644 --- a/auth0_flutter/macos/auth0_flutter.podspec +++ b/auth0_flutter/macos/auth0_flutter.podspec @@ -13,13 +13,13 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.ios.deployment_target = '14.0' + s.ios.deployment_target = '15.0' s.ios.dependency 'Flutter' - s.osx.deployment_target = '11.0' + s.osx.deployment_target = '12.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '3.0.2' + s.dependency 'Auth0', '3.2.0' s.dependency 'JWTDecode', '4.0.0' s.dependency 'SimpleKeychain', '1.3.0'