Skip to content

[firebase_auth]: credential property is always null for FirebaseAuthException 'email-already-in-use' on iOS #13268

Open
@michaelowolf

Description

@michaelowolf

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

Auth

Which platforms are affected?

iOS

Description

When calling auth.currentUser?.linkWithProvider on iOS, if the email the user signs in with is already in use with another provider, the resulting FirebaseAuthException ('email-already-in-use') will always have a credential property that is set to null.

As far as I understand (and as per the docs), 'email-already-in-use' should always be thrown with a credential property, so that we can recover from this by signing in using the credential via auth.signInWithCredential.

This works as expected on Android and web.

Reproducing the issue

Prerequisites

  • Accounts with two separate providers, e.g. email and Google, using the same email address.
  • One of the above accounts existing in Firebase (e.g. has signed in before), one not (i.e. has not signed in before)
  • The "Link accounts that use the same email" setting enabled

Code sample

// main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:my_app/app/app.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(); // Include Firebase options if needed

  runApp(const App());
}

// app.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

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

  static final AuthProvider googleProvider = GoogleAuthProvider();

  @override
  Widget build(BuildContext context) {
    final auth = FirebaseAuth.instance;

    return MaterialApp(
      home: StreamBuilder<User?>(
        stream: auth.userChanges(),
        builder: (context, userSnapshot) =>
            userSnapshot.connectionState == ConnectionState.waiting
                ? const SizedBox.shrink()
                : Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      if (userSnapshot.hasData)
                        if (userSnapshot.data!.isAnonymous) ...[
                          OutlinedButton(
                            onPressed: () => _link(googleProvider, 'Google'),
                            child: const Text('Link Google'),
                          ),
                        ] else
                          OutlinedButton(
                            onPressed: auth.signOut,
                            child: const Text('Sign Out'),
                          )
                      else
                        OutlinedButton(
                          onPressed: _signInAnonymously,
                          child: const Text('Sign In Anonymously'),
                        ),
                      if (userSnapshot.hasData)
                        userSnapshot.data!.isAnonymous
                            ? const Text('User: Anonymous')
                            : Text('User: ${userSnapshot.data!.email}')
                      else
                        const Center(child: Text('No user signed in')),
                    ],
                  ),
      ),
    );
  }

  Future<void> _link(AuthProvider provider, String name) async {
    final auth = FirebaseAuth.instance;

    print('Linking $name...');
    try {
      if (kIsWeb) {
        await auth.currentUser?.linkWithPopup(provider);
      } else {
        await auth.currentUser?.linkWithProvider(provider);
      }

      print('Linked $name!');
    } catch (error) {
      if (error is FirebaseAuthException &&
          (error.code == 'email-already-in-use' ||
              error.code == 'credential-already-in-use')) {
        print('Failed to link $name: ${error.code}'
            ' (credential: ${error.credential})');
        print('Signing in to $name instead...');
        await auth.signInWithCredential(error.credential!);
        print('Signed in to $name!');
      } else {
        print('Failed to link $name: $error');
        rethrow;
      }
    }
  }

  Future<void> _signInAnonymously() async {
    print('Signing in anonymously...');
    await FirebaseAuth.instance.signInAnonymously();
    print('Signed in anonymously!');
  }

  Future<void> _signOut() async {
    print('Signing out...');
    await FirebaseAuth.instance.signOut();
    print('Signed out!');
  }
}

Steps

  1. Create an email and password user in Firebase auth
  2. Run the above app on iOS
  3. Sign in anonymously
  4. Link Google using the Google account with the same email
  5. Notice that the credential in the resulting 'email-already-in-use' error is null

Firebase Core version

7.3.0

Flutter Version

7.24.1

Relevant Log Output

When the reproduction code is ran on iOS:

flutter: Signing in anonymously...
flutter: Signed in anonymously!
flutter: Linking Google...
flutter: Failed to link Google: email-already-in-use (credential: null)
flutter: Signing in to Google instead...
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Null check operator used on a null value
#0      App._link (package:my_app/app/view/app.dart:73:57)
app.dart:73
<asynchronous suspension>

When the reproduction code is ran on Android:

W/System  (11258): Ignoring header X-Firebase-Locale because its value was null.
D/FirebaseAuth(11258): Notifying id token listeners about user ( [...] ).
D/FirebaseAuth(11258): Notifying auth state listeners about user ( [...] ).
I/flutter (11258): Signed in anonymously!
I/flutter (11258): Linking Google...
W/System  (11258): Ignoring header X-Firebase-Locale because its value was null.
I/GenericIdpActivity(11258): Opening IDP Sign In link in a browser window.
I/TRuntime.CctTransportBackend(11258): Making request to: https://firebaselogging-pa.googleapis.com/v1/firelog/legacy/batchlog
I/TRuntime.CctTransportBackend(11258): Status Code: 200
W/System  (11258): Ignoring header X-Firebase-Locale because its value was null.
I/flutter (11258): Failed to link Google: email-already-in-use (credential: AuthCredential(providerId: google.com, signInMethod: google.com, token: [...], accessToken: [...]))
I/flutter (11258): Signing in to Google instead...
W/System  (11258): Ignoring header X-Firebase-Locale because its value was null.
W/System  (11258): Ignoring header X-Firebase-Locale because its value was null.
D/FirebaseAuth(11258): Notifying id token listeners about user ( [...] ).
D/FirebaseAuth(11258): Notifying auth state listeners about user ( [...] ).
I/flutter (11258): Signed in to Google!

Flutter dependencies

Expand Flutter dependencies snippet
Dart SDK 3.5.1
Flutter SDK 3.24.1

dependencies:
- bloc 8.1.4 [meta]
- crypto 3.0.3 [typed_data]
- firebase_auth 5.1.4 [firebase_auth_platform_interface firebase_auth_web firebase_core firebase_core_platform_interface flutter meta]
- firebase_core 3.3.0 [firebase_core_platform_interface firebase_core_web flutter meta]
- firebase_crashlytics 4.0.4 [firebase_core firebase_core_platform_interface firebase_crashlytics_platform_interface flutter stack_trace]
- flutter 0.0.0 [characters collection material_color_utilities meta vector_math sky_engine]
- flutter_bloc 8.1.6 [bloc flutter provider]
- flutter_localizations 0.0.0 [flutter intl characters clock collection material_color_utilities meta path vector_math]
- flutter_staggered_grid_view 0.7.0 [flutter]
- flutter_svg 2.0.10+1 [flutter http vector_graphics vector_graphics_codec vector_graphics_compiler]
- fluttertoast 8.2.8 [flutter flutter_web_plugins web]
- get_it 7.7.0 [async collection meta]
- go_router 14.2.3 [collection flutter flutter_web_plugins logging meta]
- google_sign_in 6.2.1 [flutter google_sign_in_android google_sign_in_ios google_sign_in_platform_interface google_sign_in_web]
- intl 0.19.0 [clock meta path]
- mock_exceptions 0.8.2 [matcher]
- package_info_plus 8.0.2 [ffi flutter flutter_web_plugins http meta path package_info_plus_platform_interface web win32 clock]
- pointer_interceptor 0.10.1+1 [flutter flutter_web_plugins pointer_interceptor_ios pointer_interceptor_platform_interface pointer_interceptor_web]
- url_launcher 6.3.0 [flutter url_launcher_android url_launcher_ios url_launcher_linux url_launcher_macos url_launcher_platform_interface url_launcher_web url_launcher_windows]
- webview_flutter 4.8.0 [flutter webview_flutter_android webview_flutter_platform_interface webview_flutter_wkwebview]
- webview_flutter_web 0.2.2+4 [flutter flutter_web_plugins webview_flutter_platform_interface]

dev dependencies:
- bloc_test 9.1.7 [bloc diff_match_patch meta mocktail test]
- firebase_auth_mocks 0.14.0 [flutter firebase_auth firebase_core meta equatable dart_jsonwebtoken uuid firebase_auth_platform_interface mock_exceptions]
- flutter_test 0.0.0 [flutter test_api matcher path fake_async clock stack_trace vector_math leak_tracker_flutter_testing async boolean_selector characters collection leak_tracker leak_tracker_testing material_color_utilities meta source_span stream_channel string_scanner term_glyph vm_service]
- mocktail 1.0.4 [collection matcher test_api]
- very_good_analysis 6.0.0

transitive dependencies:
- _fe_analyzer_shared 67.0.0 [meta]
- _flutterfire_internals 1.3.40 [collection firebase_core firebase_core_platform_interface flutter meta]
- adaptive_number 1.0.0 [fixnum]
- analyzer 6.4.1 [_fe_analyzer_shared collection convert crypto glob meta package_config path pub_semver source_span watcher yaml]
- args 2.5.0
- async 2.11.0 [collection meta]
- boolean_selector 2.1.1 [source_span string_scanner]
- characters 1.3.0
- clock 1.1.1
- collection 1.18.0
- convert 3.1.1 [typed_data]
- coverage 1.8.0 [args logging package_config path source_maps stack_trace vm_service]
- dart_jsonwebtoken 2.14.0 [crypto pointycastle convert collection ed25519_edwards clock]
- diff_match_patch 0.4.1
- ed25519_edwards 0.3.1 [collection crypto convert adaptive_number]
- equatable 2.0.5 [collection meta]
- fake_async 1.3.1 [clock collection]
- ffi 2.1.3
- file 7.0.0 [meta path]
- firebase_auth_platform_interface 7.4.3 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface]
- firebase_auth_web 5.12.5 [firebase_auth_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins http_parser meta web]
- firebase_core_platform_interface 5.2.0 [collection flutter flutter_test meta plugin_platform_interface]
- firebase_core_web 2.17.4 [firebase_core_platform_interface flutter flutter_web_plugins meta web]
- firebase_crashlytics_platform_interface 3.6.40 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface]
- fixnum 1.1.0
- flutter_web_plugins 0.0.0 [flutter characters collection material_color_utilities meta vector_math]
- frontend_server_client 4.0.0 [async path]
- glob 2.1.2 [async collection file path string_scanner]
- google_identity_services_web 0.3.1+1 [meta web]
- google_sign_in_android 6.1.27 [flutter google_sign_in_platform_interface]
- google_sign_in_ios 5.7.6 [flutter google_sign_in_platform_interface]
- google_sign_in_platform_interface 2.4.5 [flutter plugin_platform_interface]
- google_sign_in_web 0.12.4+1 [flutter flutter_web_plugins google_identity_services_web google_sign_in_platform_interface http web]
- http 1.2.1 [async http_parser meta web]
- http_multi_server 3.2.1 [async]
- http_parser 4.0.2 [collection source_span string_scanner typed_data]
- io 1.0.4 [meta path string_scanner]
- js 0.6.7 [meta]
- leak_tracker 10.0.5 [clock collection meta path vm_service]
- leak_tracker_flutter_testing 3.0.5 [flutter leak_tracker leak_tracker_testing matcher meta]
- leak_tracker_testing 3.0.1 [leak_tracker matcher meta]
- logging 1.2.0
- matcher 0.12.16+1 [async meta stack_trace term_glyph test_api]
- material_color_utilities 0.11.1 [collection]
- meta 1.15.0
- mime 1.0.5
- nested 1.0.0 [flutter]
- node_preamble 2.0.2
- package_config 2.1.0 [path]
- package_info_plus_platform_interface 3.0.1 [flutter meta plugin_platform_interface]
- path 1.9.0
- path_parsing 1.0.1 [vector_math meta]
- petitparser 6.0.2 [meta]
- plugin_platform_interface 2.1.8 [meta]
- pointer_interceptor_ios 0.10.1 [flutter plugin_platform_interface pointer_interceptor_platform_interface]
- pointer_interceptor_platform_interface 0.10.0+1 [flutter plugin_platform_interface]
- pointer_interceptor_web 0.10.2 [flutter flutter_web_plugins plugin_platform_interface pointer_interceptor_platform_interface web]
- pointycastle 3.9.1 [collection convert js]
- pool 1.5.1 [async stack_trace]
- provider 6.1.2 [collection flutter nested]
- pub_semver 2.1.4 [collection meta]
- shelf 1.4.1 [async collection http_parser path stack_trace stream_channel]
- shelf_packages_handler 3.0.2 [path shelf shelf_static]
- shelf_static 1.1.2 [convert http_parser mime path shelf]
- shelf_web_socket 1.0.4 [shelf stream_channel web_socket_channel]
- sky_engine 0.0.99
- source_map_stack_trace 2.1.1 [path source_maps stack_trace]
- source_maps 0.10.12 [source_span]
- source_span 1.10.0 [collection path term_glyph]
- sprintf 7.0.0
- stack_trace 1.11.1 [path]
- stream_channel 2.1.2 [async]
- string_scanner 1.2.0 [source_span]
- term_glyph 1.2.1
- test 1.25.7 [analyzer async boolean_selector collection coverage http_multi_server io js matcher node_preamble package_config path pool shelf shelf_packages_handler shelf_static shelf_web_socket source_span stack_trace stream_channel test_api test_core typed_data web_socket_channel webkit_inspection_protocol yaml]
- test_api 0.7.2 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph]
- test_core 0.6.4 [analyzer args async boolean_selector collection coverage frontend_server_client glob io meta package_config path pool source_map_stack_trace source_maps source_span stack_trace stream_channel test_api vm_service yaml]
- typed_data 1.3.2 [collection]
- url_launcher_android 6.3.3 [flutter url_launcher_platform_interface]
- url_launcher_ios 6.3.0 [flutter url_launcher_platform_interface]
- url_launcher_linux 3.1.1 [flutter url_launcher_platform_interface]
- url_launcher_macos 3.2.0 [flutter url_launcher_platform_interface]
- url_launcher_platform_interface 2.3.2 [flutter plugin_platform_interface]
- url_launcher_web 2.3.1 [flutter flutter_web_plugins url_launcher_platform_interface web]
- url_launcher_windows 3.1.1 [flutter url_launcher_platform_interface]
- uuid 4.4.2 [crypto sprintf meta fixnum]
- vector_graphics 1.1.11+1 [flutter http vector_graphics_codec]
- vector_graphics_codec 1.1.11+1
- vector_graphics_compiler 1.1.11+1 [args meta path_parsing xml vector_graphics_codec path]
- vector_math 2.1.4
- vm_service 14.2.5
- watcher 1.1.0 [async path]
- web 0.5.1
- web_socket_channel 2.4.5 [async crypto stream_channel web]
- webkit_inspection_protocol 1.2.1 [logging]
- webview_flutter_android 3.16.4 [flutter webview_flutter_platform_interface]
- webview_flutter_platform_interface 2.10.0 [flutter meta plugin_platform_interface]
- webview_flutter_wkwebview 3.13.1 [flutter path webview_flutter_platform_interface]
- win32 5.5.4 [ffi]
- xml 6.5.0 [collection meta petitparser]
- yaml 3.1.2 [collection source_span string_scanner]

Additional context and comments

This likely affects any sign in or linking methods that result in the 'email-already-in-use' error on iOS.

I suspect this is simply caused by the fact that the underlying firebase-ios-sdk package does not append the credential to the error object, but I don't have a strong enough understanding of how the code works to be confident in that. Certainly, for e.g. 'credential-already-in-use', the credential seems to be appended.

This is a video of sample code & logs:

iOS.email-already-in-use.null.credential.mov

I can also show it working in Android, but currently the logs include information I'd rather redact.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions