Skip to content

🐛 [firebase_auth] MFA resolveSignIn not working with named Firebase app instance on iOS #10275

@koenmuilwijk

Description

@koenmuilwijk

Bug report

It seems e.resolver.resolveSignIn does not work with a named Firebase app on iOS. Same code works perfectly fine on Android. Running the same code with the [DEFAULT] name works fine on both platforms.

The signin itself works fine (e.g. the correct email and username are returned from the resolveSignin method), however, global auth.currentUser state doesn't get updated and authStateChanges listeners are not updated.

Steps to reproduce

Steps to reproduce the behavior:

  1. Register a Firebase app with the name defined:
firebaseApp = await Firebase.initializeApp(
    name: "foo",
    options: DefaultFirebaseOptions.currentPlatform,
  );
  1. Get FirebaseAuth instance for this specific instance: final auth = FirebaseAuth.instanceFor(app: firebaseApp!);
  2. Implement MFA as described in the documentation
  3. Trigger login flow and observe that the print("/// Hi ${result.user?.email} 👋. CurrentUser: ${_auth.currentUser?.email}."); statement prints 'CurrentUser: null' and that none of the listeners are triggered.
  4. Now change the assignment to firebaseApp to be the default named instance (is outcommented in the code) and notice that it will now all work as expected.

Expected behavior

After the call to resolveSigning the auth.currentUser needs to get updated and listeners to authStateChanges need to be notified.

Sample project

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

FirebaseApp? firebaseApp;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Note: This line will make the sample work
  /*firebaseApp = await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );*/
  // Note: This is not working
  firebaseApp = await Firebase.initializeApp(
    name: "foo",
    options: DefaultFirebaseOptions.currentPlatform,
  );

  FirebaseAuth.instance
      .authStateChanges()
      .listen((e) => {print("/// auth state on [DEFAULT] changed: ${e?.uid}")});
  final auth = FirebaseAuth.instanceFor(app: firebaseApp!);
  auth
      .authStateChanges()
      .listen((e) => {print("/// auth state on foo changed: ${e?.uid}")});

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  Future<void> _incrementCounter() async {
    final _auth = FirebaseAuth.instanceFor(app: firebaseApp!);

    print("/// Test login flow on ${_auth.app.name}");
    _auth.userChanges().listen((user) {
      print("/// Firebase user state changed to ${user != null}}");
    });

    // sign out first for proper test
    await _auth.signOut();

    try {
      await _auth.signInWithEmailAndPassword(
        email: "[email protected]",
        password: "secret",
      );
    } on FirebaseAuthMultiFactorException catch (e) {
      print("/// MFA needed.");
      final firstHint = e.resolver.hints.first;
      if (firstHint is! PhoneMultiFactorInfo) {
        return;
      }
      await FirebaseAuth.instance.verifyPhoneNumber(
        multiFactorSession: e.resolver.session,
        multiFactorInfo: firstHint,
        verificationCompleted: (e) {
          print("verificationCompleted: ${e.toString()}.");
        },
        verificationFailed: (e) {
          print("verificationFailed error: ${e.toString()}.");
        },
        codeSent: (String verificationId, int? resendToken) async {
          final smsCode = "123456";

          if (smsCode != null) {
            // Create a PhoneAuthCredential with the code
            final credential = PhoneAuthProvider.credential(
              verificationId: verificationId,
              smsCode: smsCode,
            );

            try {
              final result = await e.resolver.resolveSignIn(
                PhoneMultiFactorGenerator.getAssertion(
                  credential,
                ),
              );
              if (result.user != null) {
                print("/// Hi ${result.user?.email} 👋. CurrentUser: ${_auth.currentUser?.email}.");
              }
            } on FirebaseAuthException catch (e) {
              print("resolveSignIn error: ${e.toString()}.");
            }
          }
        },
        codeAutoRetrievalTimeout: (_) {},
      );
    } catch (e) {
      print("MFA error: ${e.toString()}.");
    }
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
         title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}


Additional context

As noted, the same code works perfectly fine on Android. It is not working on iOS though (neither simulator or real device). Changing FirebaseAuth to use the default instance will make it work on iOS as well but this is not an option for me.

Be sure to enable MFA on the user account used for testing for the MFA code to be triggered.


Flutter doctor

Run flutter doctor and paste the output below:

Click To Expand
[✓] Flutter (Channel stable, 3.3.10, on macOS 13.0.1 22A400 darwin-arm, locale en-NL)
[✗] Android toolchain - develop for Android devices
    ✗ Unable to locate Android SDK.
[✓] Xcode - develop for iOS and macOS (Xcode 14.1)
[✗] Chrome - develop for the web (Cannot find Chrome executable at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[!] Android Studio (not installed)
[✓] VS Code (version 1.73.1)
[✓] Connected device (2 available)
    ! Error: To use <NAME> iPhone for development, enable Developer Mode in Settings → Privacy & Security.  (code 6)
[✓] HTTP Host Availability

Flutter dependencies

Run flutter pub deps -- --style=compact and paste the output below:

Click To Expand
Dart SDK 2.18.6
Flutter SDK 3.3.10
authtest 1.0.0+1

dependencies:
- cupertino_icons 1.0.5
- firebase_auth 4.2.5 [firebase_auth_platform_interface firebase_auth_web firebase_core firebase_core_platform_interface flutter meta]
- firebase_core 2.4.1 [firebase_core_platform_interface firebase_core_web flutter meta]
- flutter 0.0.0 [characters collection material_color_utilities meta vector_math sky_engine]

dev dependencies:
- flutter_lints 2.0.1 [lints]
- flutter_test 0.0.0 [flutter test_api path fake_async clock stack_trace vector_math async boolean_selector characters collection matcher material_color_utilities meta source_span stream_channel string_scanner term_glyph]

transitive dependencies:
- _flutterfire_internals 1.0.12 [collection firebase_core firebase_core_platform_interface flutter meta]
- async 2.9.0 [collection meta]
- boolean_selector 2.1.0 [source_span string_scanner]
- characters 1.2.1
- clock 1.1.1
- collection 1.16.0
- fake_async 1.3.1 [clock collection]
- firebase_auth_platform_interface 6.11.7 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface]
- firebase_auth_web 5.2.4 [firebase_auth_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins http_parser intl js meta]
- firebase_core_platform_interface 4.5.2 [collection flutter flutter_test meta plugin_platform_interface]
- firebase_core_web 2.1.0 [firebase_core_platform_interface flutter flutter_web_plugins js meta]
- flutter_web_plugins 0.0.0 [flutter js characters collection material_color_utilities meta vector_math]
- http_parser 4.0.2 [collection source_span string_scanner typed_data]
- intl 0.17.0 [clock path]
- js 0.6.4
- lints 2.0.1
- matcher 0.12.12 [stack_trace]
- material_color_utilities 0.1.5
- meta 1.8.0
- path 1.8.2
- plugin_platform_interface 2.1.3 [meta]
- sky_engine 0.0.99
- source_span 1.9.0 [collection path term_glyph]
- stack_trace 1.10.0 [path]
- stream_channel 2.1.0 [async]
- string_scanner 1.1.1 [source_span]
- term_glyph 1.2.1
- test_api 0.4.12 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph matcher]
- typed_data 1.3.1 [collection]
- vector_math 2.1.2

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions