diff --git a/docs/auth/start.md b/docs/auth/start.md index bac3d5eeaa52..75da1e797839 100644 --- a/docs/auth/start.md +++ b/docs/auth/start.md @@ -223,6 +223,35 @@ final auth = FirebaseAuth.instanceFor(app: Firebase.app(), persistence: Persiste await auth.setPersistence(Persistence.LOCAL); ``` +## Share authentication state between Apple apps + +On Apple platforms, you can share authentication state between apps in the same +developer account by storing it in a shared Keychain access group. First, enable +the Keychain Sharing capability for each app and add the same access group. +Then configure Firebase Auth with the fully qualified access group: + +```dart +await FirebaseAuth.instance.setSettings( + userAccessGroup: 'TEAMID.com.example.group1', +); +``` + +Changing from the default Keychain to a shared access group signs out an +existing user unless the user is migrated. To preserve the current session, +including an anonymous session, opt into migration when setting the access +group. `migrateCurrentUser: true` requires a non-null `userAccessGroup`: + +```dart +await FirebaseAuth.instance.setSettings( + userAccessGroup: 'TEAMID.com.example.group1', + migrateCurrentUser: true, +); +``` + +Migration can replace a user already stored in the destination access group. +For more information about configuring access groups, see +[Enabling cross-app authentication with shared Apple Keychain](https://firebase.google.com/docs/auth/ios/single-sign-on). + ## Next Steps Explore the guides on signing in and signing up users with the supported diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java index a122221c601c..c93e692ee09c 100755 --- a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java @@ -570,7 +570,10 @@ public void setLanguageCode( public void setSettings( @NonNull GeneratedAndroidFirebaseAuth.AuthPigeonFirebaseApp app, @NonNull GeneratedAndroidFirebaseAuth.InternalFirebaseAuthSettings settings, - @NonNull GeneratedAndroidFirebaseAuth.VoidResult result) { + @NonNull + GeneratedAndroidFirebaseAuth.NullableResult< + GeneratedAndroidFirebaseAuth.InternalUserDetails> + result) { try { FirebaseAuth firebaseAuth = getAuthFromPigeon(app); @@ -591,7 +594,8 @@ public void setSettings( settings.getPhoneNumber(), settings.getSmsCode()); } - result.success(); + // Apple-only migration; Android has nothing to return here. + result.success(null); } catch (Exception e) { result.error(e); } diff --git a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java index 5aaa168b70e8..8489fedca077 100644 --- a/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java +++ b/packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/GeneratedAndroidFirebaseAuth.java @@ -2128,6 +2128,19 @@ public void setUserAccessGroup(@Nullable String setterArg) { this.userAccessGroup = setterArg; } + private @NonNull Boolean migrateCurrentUser; + + public @NonNull Boolean getMigrateCurrentUser() { + return migrateCurrentUser; + } + + public void setMigrateCurrentUser(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"migrateCurrentUser\" is null."); + } + this.migrateCurrentUser = setterArg; + } + private @Nullable String phoneNumber; public @Nullable String getPhoneNumber() { @@ -2173,6 +2186,7 @@ public boolean equals(Object o) { return pigeonDeepEquals( appVerificationDisabledForTesting, that.appVerificationDisabledForTesting) && pigeonDeepEquals(userAccessGroup, that.userAccessGroup) + && pigeonDeepEquals(migrateCurrentUser, that.migrateCurrentUser) && pigeonDeepEquals(phoneNumber, that.phoneNumber) && pigeonDeepEquals(smsCode, that.smsCode) && pigeonDeepEquals(forceRecaptchaFlow, that.forceRecaptchaFlow); @@ -2185,6 +2199,7 @@ public int hashCode() { getClass(), appVerificationDisabledForTesting, userAccessGroup, + migrateCurrentUser, phoneNumber, smsCode, forceRecaptchaFlow @@ -2210,6 +2225,14 @@ public static final class Builder { return this; } + private @Nullable Boolean migrateCurrentUser; + + @CanIgnoreReturnValue + public @NonNull Builder setMigrateCurrentUser(@NonNull Boolean setterArg) { + this.migrateCurrentUser = setterArg; + return this; + } + private @Nullable String phoneNumber; @CanIgnoreReturnValue @@ -2238,6 +2261,7 @@ public static final class Builder { InternalFirebaseAuthSettings pigeonReturn = new InternalFirebaseAuthSettings(); pigeonReturn.setAppVerificationDisabledForTesting(appVerificationDisabledForTesting); pigeonReturn.setUserAccessGroup(userAccessGroup); + pigeonReturn.setMigrateCurrentUser(migrateCurrentUser); pigeonReturn.setPhoneNumber(phoneNumber); pigeonReturn.setSmsCode(smsCode); pigeonReturn.setForceRecaptchaFlow(forceRecaptchaFlow); @@ -2247,9 +2271,10 @@ public static final class Builder { @NonNull public ArrayList toList() { - ArrayList toListResult = new ArrayList<>(5); + ArrayList toListResult = new ArrayList<>(6); toListResult.add(appVerificationDisabledForTesting); toListResult.add(userAccessGroup); + toListResult.add(migrateCurrentUser); toListResult.add(phoneNumber); toListResult.add(smsCode); toListResult.add(forceRecaptchaFlow); @@ -2264,11 +2289,13 @@ public ArrayList toList() { (Boolean) appVerificationDisabledForTesting); Object userAccessGroup = pigeonVar_list.get(1); pigeonResult.setUserAccessGroup((String) userAccessGroup); - Object phoneNumber = pigeonVar_list.get(2); + Object migrateCurrentUser = pigeonVar_list.get(2); + pigeonResult.setMigrateCurrentUser((Boolean) migrateCurrentUser); + Object phoneNumber = pigeonVar_list.get(3); pigeonResult.setPhoneNumber((String) phoneNumber); - Object smsCode = pigeonVar_list.get(3); + Object smsCode = pigeonVar_list.get(4); pigeonResult.setSmsCode((String) smsCode); - Object forceRecaptchaFlow = pigeonVar_list.get(4); + Object forceRecaptchaFlow = pigeonVar_list.get(5); pigeonResult.setForceRecaptchaFlow((Boolean) forceRecaptchaFlow); return pigeonResult; } @@ -3345,7 +3372,7 @@ void setLanguageCode( void setSettings( @NonNull AuthPigeonFirebaseApp app, @NonNull InternalFirebaseAuthSettings settings, - @NonNull VoidResult result); + @NonNull NullableResult result); void verifyPasswordResetCode( @NonNull AuthPigeonFirebaseApp app, @NonNull String code, @NonNull Result result); @@ -3999,10 +4026,10 @@ public void error(Throwable error) { AuthPigeonFirebaseApp appArg = (AuthPigeonFirebaseApp) args.get(0); InternalFirebaseAuthSettings settingsArg = (InternalFirebaseAuthSettings) args.get(1); - VoidResult resultCallback = - new VoidResult() { - public void success() { - wrapped.add(0, null); + NullableResult resultCallback = + new NullableResult() { + public void success(InternalUserDetails result) { + wrapped.add(0, result); reply.reply(wrapped); } diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m index 29140b911e9b..2eedbe71b1f1 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/FLTFirebaseAuthPlugin.m @@ -1531,8 +1531,10 @@ - (void)setLanguageCodeApp:(nonnull AuthPigeonFirebaseApp *)app - (void)setSettingsApp:(nonnull AuthPigeonFirebaseApp *)app settings:(nonnull InternalFirebaseAuthSettings *)settings - completion:(nonnull void (^)(FlutterError *_Nullable))completion { + completion:(nonnull void (^)(InternalUserDetails *_Nullable, + FlutterError *_Nullable))completion { FIRAuth *auth = [self getFIRAuthFromAppNameFromPigeon:app]; + FIRUser *userToMigrate = settings.migrateCurrentUser ? auth.currentUser : nil; if (settings.userAccessGroup != nil) { BOOL useUserAccessGroupSuccessful; @@ -1540,7 +1542,7 @@ - (void)setSettingsApp:(nonnull AuthPigeonFirebaseApp *)app useUserAccessGroupSuccessful = [auth useUserAccessGroup:settings.userAccessGroup error:&useUserAccessGroupErrorPtr]; if (!useUserAccessGroupSuccessful) { - completion([FLTFirebaseAuthPlugin convertToFlutterError:useUserAccessGroupErrorPtr]); + completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:useUserAccessGroupErrorPtr]); return; } } @@ -1554,7 +1556,22 @@ - (void)setSettingsApp:(nonnull AuthPigeonFirebaseApp *)app @"on MacOS."); #endif - completion(nil); + if (userToMigrate != nil) { + [auth + updateCurrentUser:userToMigrate + completion:^(NSError *_Nullable error) { + if (error != nil) { + completion(nil, [FLTFirebaseAuthPlugin convertToFlutterError:error]); + return; + } + FIRUser *migratedUser = auth.currentUser; + completion( + migratedUser != nil ? [PigeonParser getPigeonDetails:migratedUser] : nil, nil); + }]; + return; + } + + completion(nil, nil); } - (void)signInAnonymouslyApp:(nonnull AuthPigeonFirebaseApp *)app diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m index 82ae8cfcccc7..061891794224 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/firebase_auth_messages.g.m @@ -969,12 +969,14 @@ - (NSUInteger)hash { @implementation InternalFirebaseAuthSettings + (instancetype)makeWithAppVerificationDisabledForTesting:(BOOL)appVerificationDisabledForTesting userAccessGroup:(nullable NSString *)userAccessGroup + migrateCurrentUser:(BOOL)migrateCurrentUser phoneNumber:(nullable NSString *)phoneNumber smsCode:(nullable NSString *)smsCode forceRecaptchaFlow:(nullable NSNumber *)forceRecaptchaFlow { InternalFirebaseAuthSettings *pigeonResult = [[InternalFirebaseAuthSettings alloc] init]; pigeonResult.appVerificationDisabledForTesting = appVerificationDisabledForTesting; pigeonResult.userAccessGroup = userAccessGroup; + pigeonResult.migrateCurrentUser = migrateCurrentUser; pigeonResult.phoneNumber = phoneNumber; pigeonResult.smsCode = smsCode; pigeonResult.forceRecaptchaFlow = forceRecaptchaFlow; @@ -984,9 +986,10 @@ + (InternalFirebaseAuthSettings *)fromList:(NSArray *)list { InternalFirebaseAuthSettings *pigeonResult = [[InternalFirebaseAuthSettings alloc] init]; pigeonResult.appVerificationDisabledForTesting = [GetNullableObjectAtIndex(list, 0) boolValue]; pigeonResult.userAccessGroup = GetNullableObjectAtIndex(list, 1); - pigeonResult.phoneNumber = GetNullableObjectAtIndex(list, 2); - pigeonResult.smsCode = GetNullableObjectAtIndex(list, 3); - pigeonResult.forceRecaptchaFlow = GetNullableObjectAtIndex(list, 4); + pigeonResult.migrateCurrentUser = [GetNullableObjectAtIndex(list, 2) boolValue]; + pigeonResult.phoneNumber = GetNullableObjectAtIndex(list, 3); + pigeonResult.smsCode = GetNullableObjectAtIndex(list, 4); + pigeonResult.forceRecaptchaFlow = GetNullableObjectAtIndex(list, 5); return pigeonResult; } + (nullable InternalFirebaseAuthSettings *)nullableFromList:(NSArray *)list { @@ -996,6 +999,7 @@ + (nullable InternalFirebaseAuthSettings *)nullableFromList:(NSArray *)list return @[ @(self.appVerificationDisabledForTesting), self.userAccessGroup ?: [NSNull null], + @(self.migrateCurrentUser), self.phoneNumber ?: [NSNull null], self.smsCode ?: [NSNull null], self.forceRecaptchaFlow ?: [NSNull null], @@ -1011,6 +1015,7 @@ - (BOOL)isEqual:(id)object { InternalFirebaseAuthSettings *other = (InternalFirebaseAuthSettings *)object; return self.appVerificationDisabledForTesting == other.appVerificationDisabledForTesting && FLTPigeonDeepEquals(self.userAccessGroup, other.userAccessGroup) && + self.migrateCurrentUser == other.migrateCurrentUser && FLTPigeonDeepEquals(self.phoneNumber, other.phoneNumber) && FLTPigeonDeepEquals(self.smsCode, other.smsCode) && FLTPigeonDeepEquals(self.forceRecaptchaFlow, other.forceRecaptchaFlow); @@ -1020,6 +1025,7 @@ - (NSUInteger)hash { NSUInteger result = [self class].hash; result = result * 31 + @(self.appVerificationDisabledForTesting).hash; result = result * 31 + FLTPigeonDeepHash(self.userAccessGroup); + result = result * 31 + @(self.migrateCurrentUser).hash; result = result * 31 + FLTPigeonDeepHash(self.phoneNumber); result = result * 31 + FLTPigeonDeepHash(self.smsCode); result = result * 31 + FLTPigeonDeepHash(self.forceRecaptchaFlow); @@ -2036,11 +2042,12 @@ void SetUpFirebaseAuthHostApiWithSuffix(id binaryMesseng NSArray *args = message; AuthPigeonFirebaseApp *arg_app = GetNullableObjectAtIndex(args, 0); InternalFirebaseAuthSettings *arg_settings = GetNullableObjectAtIndex(args, 1); - [api setSettingsApp:arg_app - settings:arg_settings - completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api + setSettingsApp:arg_app + settings:arg_settings + completion:^(InternalUserDetails *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; } else { [channel setMessageHandler:nil]; diff --git a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/firebase_auth_messages.g.h b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/firebase_auth_messages.g.h index f83da7e34a3b..529c628670f3 100644 --- a/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/firebase_auth_messages.g.h +++ b/packages/firebase_auth/firebase_auth/ios/firebase_auth/Sources/firebase_auth/include/Public/firebase_auth_messages.g.h @@ -230,11 +230,13 @@ typedef NS_ENUM(NSUInteger, ActionCodeInfoOperation) { - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithAppVerificationDisabledForTesting:(BOOL)appVerificationDisabledForTesting userAccessGroup:(nullable NSString *)userAccessGroup + migrateCurrentUser:(BOOL)migrateCurrentUser phoneNumber:(nullable NSString *)phoneNumber smsCode:(nullable NSString *)smsCode forceRecaptchaFlow:(nullable NSNumber *)forceRecaptchaFlow; @property(nonatomic, assign) BOOL appVerificationDisabledForTesting; @property(nonatomic, copy, nullable) NSString *userAccessGroup; +@property(nonatomic, assign) BOOL migrateCurrentUser; @property(nonatomic, copy, nullable) NSString *phoneNumber; @property(nonatomic, copy, nullable) NSString *smsCode; @property(nonatomic, strong, nullable) NSNumber *forceRecaptchaFlow; @@ -388,7 +390,8 @@ NSObject *nullGetFirebaseAuthMessagesCodec(void); completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; - (void)setSettingsApp:(AuthPigeonFirebaseApp *)app settings:(InternalFirebaseAuthSettings *)settings - completion:(void (^)(FlutterError *_Nullable))completion; + completion: + (void (^)(InternalUserDetails *_Nullable, FlutterError *_Nullable))completion; - (void)verifyPasswordResetCodeApp:(AuthPigeonFirebaseApp *)app code:(NSString *)code completion: diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 3c07d4b4707a..19c93b8ae189 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -397,9 +397,15 @@ class FirebaseAuth extends FirebasePlugin implements FirebaseService { /// Key Sharing capabilities must be enabled for your app via XCode (Project /// settings > Capabilities). To learn more, visit the /// [Apple documentation](https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps). + /// + /// [migrateCurrentUser] This setting only applies to iOS and macOS platforms. + /// When `true`, the currently signed-in user is migrated to + /// [userAccessGroup]. [userAccessGroup] must be non-null when this is + /// `true`. This may replace a user already stored in that access group. Future setSettings({ bool appVerificationDisabledForTesting = false, String? userAccessGroup, + bool migrateCurrentUser = false, String? phoneNumber, String? smsCode, bool? forceRecaptchaFlow, @@ -407,6 +413,7 @@ class FirebaseAuth extends FirebasePlugin implements FirebaseService { return _delegate.setSettings( appVerificationDisabledForTesting: appVerificationDisabledForTesting, userAccessGroup: userAccessGroup, + migrateCurrentUser: migrateCurrentUser, phoneNumber: phoneNumber, smsCode: smsCode, forceRecaptchaFlow: forceRecaptchaFlow, diff --git a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart index 5e570b0b5b82..865fc1b112d7 100644 --- a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart @@ -506,6 +506,7 @@ void main() { smsCode: any, forceRecaptchaFlow: any, userAccessGroup: any, + migrateCurrentUser: true, )).thenAnswer((i) async {}); String phoneNumber = '123456'; @@ -513,6 +514,7 @@ void main() { bool forceRecaptchaFlow = true; bool appVerificationDisabledForTesting = true; String userAccessGroup = 'group-id'; + bool migrateCurrentUser = true; await auth.setSettings( appVerificationDisabledForTesting: appVerificationDisabledForTesting, @@ -520,6 +522,7 @@ void main() { smsCode: smsCode, forceRecaptchaFlow: forceRecaptchaFlow, userAccessGroup: userAccessGroup, + migrateCurrentUser: migrateCurrentUser, ); verify( @@ -530,6 +533,7 @@ void main() { smsCode: smsCode, forceRecaptchaFlow: forceRecaptchaFlow, userAccessGroup: userAccessGroup, + migrateCurrentUser: migrateCurrentUser, ), ); }); @@ -1122,6 +1126,7 @@ class MockFirebaseAuth extends Mock Future setSettings({ bool? appVerificationDisabledForTesting, String? userAccessGroup, + bool migrateCurrentUser = false, String? phoneNumber, String? smsCode, bool? forceRecaptchaFlow, @@ -1130,6 +1135,7 @@ class MockFirebaseAuth extends Mock Invocation.method(#setSettings, [ appVerificationDisabledForTesting, userAccessGroup, + migrateCurrentUser, phoneNumber, smsCode, forceRecaptchaFlow, diff --git a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp index dfc130f8ef23..7322e7a1879b 100644 --- a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp +++ b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp @@ -921,7 +921,8 @@ void FirebaseAuthPlugin::SetLanguageCode( void FirebaseAuthPlugin::SetSettings( const AuthPigeonFirebaseApp& app, const InternalFirebaseAuthSettings& settings, - std::function reply)> result) { + std::function> reply)> + result) { result(FlutterError("unimplemented", "SetSettings is not available on this platform yet.", nullptr)); diff --git a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h index 575e201ff355..040b42a4cf3b 100644 --- a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h +++ b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.h @@ -129,7 +129,8 @@ class FirebaseAuthPlugin : public flutter::Plugin, virtual void SetSettings( const AuthPigeonFirebaseApp& app, const InternalFirebaseAuthSettings& settings, - std::function reply)> result) override; + std::function> reply)> + result) override; virtual void VerifyPasswordResetCode( const AuthPigeonFirebaseApp& app, const std::string& code, std::function reply)> result) override; diff --git a/packages/firebase_auth/firebase_auth/windows/messages.g.cpp b/packages/firebase_auth/firebase_auth/windows/messages.g.cpp index ea2200e617b2..3940e6497251 100644 --- a/packages/firebase_auth/firebase_auth/windows/messages.g.cpp +++ b/packages/firebase_auth/firebase_auth/windows/messages.g.cpp @@ -1821,19 +1821,22 @@ size_t PigeonInternalDeepHash(const InternalActionCodeSettings& v) { // InternalFirebaseAuthSettings InternalFirebaseAuthSettings::InternalFirebaseAuthSettings( - bool app_verification_disabled_for_testing) + bool app_verification_disabled_for_testing, bool migrate_current_user) : app_verification_disabled_for_testing_( - app_verification_disabled_for_testing) {} + app_verification_disabled_for_testing), + migrate_current_user_(migrate_current_user) {} InternalFirebaseAuthSettings::InternalFirebaseAuthSettings( bool app_verification_disabled_for_testing, - const std::string* user_access_group, const std::string* phone_number, - const std::string* sms_code, const bool* force_recaptcha_flow) + const std::string* user_access_group, bool migrate_current_user, + const std::string* phone_number, const std::string* sms_code, + const bool* force_recaptcha_flow) : app_verification_disabled_for_testing_( app_verification_disabled_for_testing), user_access_group_(user_access_group ? std::optional(*user_access_group) : std::nullopt), + migrate_current_user_(migrate_current_user), phone_number_(phone_number ? std::optional(*phone_number) : std::nullopt), sms_code_(sms_code ? std::optional(*sms_code) @@ -1867,6 +1870,14 @@ void InternalFirebaseAuthSettings::set_user_access_group( user_access_group_ = value_arg; } +bool InternalFirebaseAuthSettings::migrate_current_user() const { + return migrate_current_user_; +} + +void InternalFirebaseAuthSettings::set_migrate_current_user(bool value_arg) { + migrate_current_user_ = value_arg; +} + const std::string* InternalFirebaseAuthSettings::phone_number() const { return phone_number_ ? &(*phone_number_) : nullptr; } @@ -1911,10 +1922,11 @@ void InternalFirebaseAuthSettings::set_force_recaptcha_flow(bool value_arg) { EncodableList InternalFirebaseAuthSettings::ToEncodableList() const { EncodableList list; - list.reserve(5); + list.reserve(6); list.push_back(EncodableValue(app_verification_disabled_for_testing_)); list.push_back(user_access_group_ ? EncodableValue(*user_access_group_) : EncodableValue()); + list.push_back(EncodableValue(migrate_current_user_)); list.push_back(phone_number_ ? EncodableValue(*phone_number_) : EncodableValue()); list.push_back(sms_code_ ? EncodableValue(*sms_code_) : EncodableValue()); @@ -1925,21 +1937,22 @@ EncodableList InternalFirebaseAuthSettings::ToEncodableList() const { InternalFirebaseAuthSettings InternalFirebaseAuthSettings::FromEncodableList( const EncodableList& list) { - InternalFirebaseAuthSettings decoded(std::get(list[0])); + InternalFirebaseAuthSettings decoded(std::get(list[0]), + std::get(list[2])); auto& encodable_user_access_group = list[1]; if (!encodable_user_access_group.IsNull()) { decoded.set_user_access_group( std::get(encodable_user_access_group)); } - auto& encodable_phone_number = list[2]; + auto& encodable_phone_number = list[3]; if (!encodable_phone_number.IsNull()) { decoded.set_phone_number(std::get(encodable_phone_number)); } - auto& encodable_sms_code = list[3]; + auto& encodable_sms_code = list[4]; if (!encodable_sms_code.IsNull()) { decoded.set_sms_code(std::get(encodable_sms_code)); } - auto& encodable_force_recaptcha_flow = list[4]; + auto& encodable_force_recaptcha_flow = list[5]; if (!encodable_force_recaptcha_flow.IsNull()) { decoded.set_force_recaptcha_flow( std::get(encodable_force_recaptcha_flow)); @@ -1954,6 +1967,8 @@ bool InternalFirebaseAuthSettings::operator==( other.app_verification_disabled_for_testing_) && PigeonInternalDeepEquals(user_access_group_, other.user_access_group_) && + PigeonInternalDeepEquals(migrate_current_user_, + other.migrate_current_user_) && PigeonInternalDeepEquals(phone_number_, other.phone_number_) && PigeonInternalDeepEquals(sms_code_, other.sms_code_) && PigeonInternalDeepEquals(force_recaptcha_flow_, @@ -1970,6 +1985,7 @@ size_t InternalFirebaseAuthSettings::Hash() const { result = result * 31 + PigeonInternalDeepHash(app_verification_disabled_for_testing_); result = result * 31 + PigeonInternalDeepHash(user_access_group_); + result = result * 31 + PigeonInternalDeepHash(migrate_current_user_); result = result * 31 + PigeonInternalDeepHash(phone_number_); result = result * 31 + PigeonInternalDeepHash(sms_code_); result = result * 31 + PigeonInternalDeepHash(force_recaptcha_flow_); @@ -3912,16 +3928,24 @@ void FirebaseAuthHostApi::SetUp(::flutter::BinaryMessenger* binary_messenger, const auto& settings_arg = std::any_cast( std::get(encodable_settings_arg)); - api->SetSettings(app_arg, settings_arg, - [reply](std::optional&& output) { - if (output.has_value()) { - reply(WrapError(output.value())); - return; - } - EncodableList wrapped; - wrapped.push_back(EncodableValue()); - reply(EncodableValue(std::move(wrapped))); - }); + api->SetSettings( + app_arg, settings_arg, + [reply]( + ErrorOr>&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + auto output_optional = std::move(output).TakeValue(); + if (output_optional) { + wrapped.push_back(CustomEncodableValue( + std::move(output_optional).value())); + } else { + wrapped.push_back(EncodableValue()); + } + reply(EncodableValue(std::move(wrapped))); + }); } catch (const std::exception& exception) { reply(WrapError(exception.what())); } diff --git a/packages/firebase_auth/firebase_auth/windows/messages.g.h b/packages/firebase_auth/firebase_auth/windows/messages.g.h index e1e1af028fbc..6d6258c44ece 100644 --- a/packages/firebase_auth/firebase_auth/windows/messages.g.h +++ b/packages/firebase_auth/firebase_auth/windows/messages.g.h @@ -812,13 +812,14 @@ class InternalFirebaseAuthSettings { public: // Constructs an object setting all non-nullable fields. explicit InternalFirebaseAuthSettings( - bool app_verification_disabled_for_testing); + bool app_verification_disabled_for_testing, bool migrate_current_user); // Constructs an object setting all fields. explicit InternalFirebaseAuthSettings( bool app_verification_disabled_for_testing, - const std::string* user_access_group, const std::string* phone_number, - const std::string* sms_code, const bool* force_recaptcha_flow); + const std::string* user_access_group, bool migrate_current_user, + const std::string* phone_number, const std::string* sms_code, + const bool* force_recaptcha_flow); bool app_verification_disabled_for_testing() const; void set_app_verification_disabled_for_testing(bool value_arg); @@ -827,6 +828,9 @@ class InternalFirebaseAuthSettings { void set_user_access_group(const std::string_view* value_arg); void set_user_access_group(std::string_view value_arg); + bool migrate_current_user() const; + void set_migrate_current_user(bool value_arg); + const std::string* phone_number() const; void set_phone_number(const std::string_view* value_arg); void set_phone_number(std::string_view value_arg); @@ -863,6 +867,7 @@ class InternalFirebaseAuthSettings { friend class PigeonInternalCodecSerializer; bool app_verification_disabled_for_testing_; std::optional user_access_group_; + bool migrate_current_user_; std::optional phone_number_; std::optional sms_code_; std::optional force_recaptcha_flow_; @@ -1263,7 +1268,8 @@ class FirebaseAuthHostApi { virtual void SetSettings( const AuthPigeonFirebaseApp& app, const InternalFirebaseAuthSettings& settings, - std::function reply)> result) = 0; + std::function> reply)> + result) = 0; virtual void VerifyPasswordResetCode( const AuthPigeonFirebaseApp& app, const std::string& code, std::function reply)> result) = 0; diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart index c4ee96da1389..26bd42059954 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart @@ -595,10 +595,17 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { Future setSettings({ bool appVerificationDisabledForTesting = false, String? userAccessGroup, + bool migrateCurrentUser = false, String? phoneNumber, String? smsCode, bool? forceRecaptchaFlow, }) async { + if (migrateCurrentUser && userAccessGroup == null) { + throw ArgumentError( + 'The [userAccessGroup] must be set when [migrateCurrentUser] is true.', + ); + } + if (phoneNumber != null && smsCode == null || phoneNumber == null && smsCode != null) { throw ArgumentError( @@ -607,16 +614,33 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform { } try { - await _api.setSettings( + final InternalUserDetails? migratedUser = await _api.setSettings( pigeonDefault, InternalFirebaseAuthSettings( appVerificationDisabledForTesting: appVerificationDisabledForTesting, userAccessGroup: userAccessGroup, + migrateCurrentUser: migrateCurrentUser, phoneNumber: phoneNumber, smsCode: smsCode, forceRecaptchaFlow: forceRecaptchaFlow, )); + + // Native migration completes before this Future resolves, but auth-state + // events are delivered asynchronously. Assign [currentUser] from the + // returned user (same pattern as sign-in APIs) so callers don't observe a + // stale or transiently-null cache. + if (migratedUser != null) { + MethodChannelMultiFactor? multiFactorInstance = + _multiFactorInstances[app.name]; + if (multiFactorInstance == null) { + multiFactorInstance = MethodChannelMultiFactor(this); + _multiFactorInstances[app.name] = multiFactorInstance; + } + final user = MethodChannelUser(this, multiFactorInstance, migratedUser); + currentUser = user; + sendAuthChangesEvent(app.name, user); + } } catch (e, stack) { convertPlatformException(e, stack); } diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart index 60844c34575c..ba1a51d6eadd 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -919,6 +919,7 @@ class InternalFirebaseAuthSettings { InternalFirebaseAuthSettings({ required this.appVerificationDisabledForTesting, this.userAccessGroup, + required this.migrateCurrentUser, this.phoneNumber, this.smsCode, this.forceRecaptchaFlow, @@ -928,6 +929,8 @@ class InternalFirebaseAuthSettings { String? userAccessGroup; + bool migrateCurrentUser; + String? phoneNumber; String? smsCode; @@ -938,6 +941,7 @@ class InternalFirebaseAuthSettings { return [ appVerificationDisabledForTesting, userAccessGroup, + migrateCurrentUser, phoneNumber, smsCode, forceRecaptchaFlow, @@ -953,9 +957,10 @@ class InternalFirebaseAuthSettings { return InternalFirebaseAuthSettings( appVerificationDisabledForTesting: result[0]! as bool, userAccessGroup: result[1] as String?, - phoneNumber: result[2] as String?, - smsCode: result[3] as String?, - forceRecaptchaFlow: result[4] as bool?, + migrateCurrentUser: result[2]! as bool, + phoneNumber: result[3] as String?, + smsCode: result[4] as String?, + forceRecaptchaFlow: result[5] as bool?, ); } @@ -972,6 +977,7 @@ class InternalFirebaseAuthSettings { return _deepEquals(appVerificationDisabledForTesting, other.appVerificationDisabledForTesting) && _deepEquals(userAccessGroup, other.userAccessGroup) && + _deepEquals(migrateCurrentUser, other.migrateCurrentUser) && _deepEquals(phoneNumber, other.phoneNumber) && _deepEquals(smsCode, other.smsCode) && _deepEquals(forceRecaptchaFlow, other.forceRecaptchaFlow); @@ -1816,7 +1822,7 @@ class FirebaseAuthHostApi { return pigeonVar_replyValue! as String; } - Future setSettings( + Future setSettings( AuthPigeonFirebaseApp app, InternalFirebaseAuthSettings settings) async { final pigeonVar_channelName = 'dev.flutter.pigeon.firebase_auth_platform_interface.FirebaseAuthHostApi.setSettings$pigeonVar_messageChannelSuffix'; @@ -1829,11 +1835,12 @@ class FirebaseAuthHostApi { pigeonVar_channel.send([app, settings]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow( + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( pigeonVar_replyList, pigeonVar_channelName, isNullValid: true, ); + return pigeonVar_replyValue as InternalUserDetails?; } Future verifyPasswordResetCode( diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart index 60377db69238..01f21264c11b 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart @@ -396,9 +396,15 @@ abstract class FirebaseAuthPlatform extends PlatformInterface { /// Key Sharing capabilities must be enabled for your app via XCode (Project /// settings > Capabilities). To learn more, visit the /// [Apple documentation](https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps). + /// + /// [migrateCurrentUser] This setting only applies to iOS and macOS platforms. + /// When `true`, the currently signed-in user is migrated to + /// [userAccessGroup]. [userAccessGroup] must be non-null when this is + /// `true`. This may replace a user already stored in that access group. Future setSettings({ bool appVerificationDisabledForTesting = false, String? userAccessGroup, + bool migrateCurrentUser = false, String? phoneNumber, String? smsCode, bool? forceRecaptchaFlow, diff --git a/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart b/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart index 943fa1693b87..56228d5c6661 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/pigeons/messages.dart @@ -242,6 +242,7 @@ class InternalFirebaseAuthSettings { const InternalFirebaseAuthSettings({ required this.appVerificationDisabledForTesting, required this.userAccessGroup, + required this.migrateCurrentUser, required this.phoneNumber, required this.smsCode, required this.forceRecaptchaFlow, @@ -249,6 +250,7 @@ class InternalFirebaseAuthSettings { final bool appVerificationDisabledForTesting; final String? userAccessGroup; + final bool migrateCurrentUser; final String? phoneNumber; final String? smsCode; final bool? forceRecaptchaFlow; @@ -397,8 +399,11 @@ abstract class FirebaseAuthHostApi { String? languageCode, ); + /// Applies auth settings. When [InternalFirebaseAuthSettings.migrateCurrentUser] + /// is true and a user was migrated, returns that user so Dart can reconcile + /// [currentUser] before auth-state events arrive. Otherwise returns null. @async - void setSettings( + InternalUserDetails? setSettings( AuthPigeonFirebaseApp app, InternalFirebaseAuthSettings settings, ); diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart index 5dcc22207592..ce936b11a74c 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/pigeon/test_api.dart @@ -188,7 +188,7 @@ abstract class TestFirebaseAuthHostApi { Future setLanguageCode( AuthPigeonFirebaseApp app, String? languageCode); - Future setSettings( + Future setSettings( AuthPigeonFirebaseApp app, InternalFirebaseAuthSettings settings); Future verifyPasswordResetCode( @@ -753,8 +753,9 @@ abstract class TestFirebaseAuthHostApi { final InternalFirebaseAuthSettings arg_settings = args[1]! as InternalFirebaseAuthSettings; try { - await api.setSettings(arg_app, arg_settings); - return wrapResponse(empty: true); + final InternalUserDetails? output = + await api.setSettings(arg_app, arg_settings); + return [output]; } on PlatformException catch (e) { return wrapResponse(error: e); } catch (e) { diff --git a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart index 7234322bd51e..860e6c5cce0d 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart @@ -393,6 +393,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { Future setSettings({ bool? appVerificationDisabledForTesting, String? userAccessGroup, + bool migrateCurrentUser = false, String? phoneNumber, String? smsCode, bool? forceRecaptchaFlow, diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/websocket_transport_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/websocket_transport_test.mocks.dart index f73d415a9c22..bdfabe96506d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/network/websocket_transport_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/websocket_transport_test.mocks.dart @@ -379,6 +379,7 @@ class MockFirebaseAuth extends _i1.Mock implements _i4.FirebaseAuth { _i5.Future setSettings({ bool? appVerificationDisabledForTesting = false, String? userAccessGroup, + bool migrateCurrentUser = false, String? phoneNumber, String? smsCode, bool? forceRecaptchaFlow, @@ -391,6 +392,7 @@ class MockFirebaseAuth extends _i1.Mock implements _i4.FirebaseAuth { #appVerificationDisabledForTesting: appVerificationDisabledForTesting, #userAccessGroup: userAccessGroup, + #migrateCurrentUser: migrateCurrentUser, #phoneNumber: phoneNumber, #smsCode: smsCode, #forceRecaptchaFlow: forceRecaptchaFlow, diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index 17c1448a273b..f7aed5d30336 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -1030,6 +1030,37 @@ void main() { ); group('setSettings()', () { + test( + 'migrates the current user when changing access groups', + () async { + final auth = FirebaseAuth.instance; + + await auth.signOut(); + final credential = await auth.signInAnonymously(); + final uid = credential.user!.uid; + + try { + await auth.setSettings( + userAccessGroup: 'YYX2P3XVJ7.io.flutter.plugins.firebase.tests', + migrateCurrentUser: true, + ); + + // Prefer the reconciled Dart cache from setSettings, then prove + // the native session survived with a token round-trip. + expect(auth.currentUser, isNotNull); + expect(auth.currentUser!.uid, uid); + expect(auth.currentUser!.isAnonymous, isTrue); + final token = await auth.currentUser!.getIdToken(); + expect(token, isNotEmpty); + } finally { + // Leave later tests without this anonymous session. Access-group + // resets are not supported via null today (pre-existing). + await auth.signOut(); + } + }, + skip: kIsWeb || defaultTargetPlatform != TargetPlatform.iOS, + ); + test( 'throws argument error if phoneNumber & smsCode have not been set simultaneously', () async { diff --git a/tests/ios/Runner/Runner.entitlements b/tests/ios/Runner/Runner.entitlements index d7ab8334dfa2..e21cc550c0d9 100644 --- a/tests/ios/Runner/Runner.entitlements +++ b/tests/ios/Runner/Runner.entitlements @@ -6,5 +6,9 @@ applinks:flutterfiretests.page.link + keychain-access-groups + + $(AppIdentifierPrefix)io.flutter.plugins.firebase.tests +