[Firebase] Solution to the problem of email verified not being false when changing email addresses.

firebase-email-verified-not-working-error
This article can be read in about 21 minutes.

Introduction.

Developing “Mia,” a talking cat-shaped robot that speaks dialects.

Mia
The talking cat-shaped robot Mia shares sadness and joy with more than 100 different rich expressions. It also speaks in...

We use Firebase for user authentication in our app, but when a user wants to change his/her email address after authenticating with Firebase, there is an error that allows the user to change his/her email address without email authentication, so we are trying to resolve this issue. attempt to solve this problem.

Use verifyBeforeUpdateEmail() method when changing email address

As described in the Firebase documentation, when the user’s email address changes, by using the FirebaseAuth.instance.currentUser.verifyBeforeUpdateEmail() method and taking the new email address as an argument, The email authentication can be sent to the new email address.

User | JavaScript SDK  |  Firebase JavaScript API reference
Reference for User

In this case, the user’s email address registered in Firebase Authentication is switched from the old email address to the new email address at the time the user clicks the email authentication link sent to the new email address.

Then, FirebaseAuth.instance.currentUser.emailVerified can be used to determine if the email has been verified.

user.emailVerified is useless when changing email addresses

Since emailVerified is of course false when the email address is registered for the first time, emailVerified is useful for determining whether the email address has been verified or not. However, if the email address is changed after the initial email registration, the old email address’s However, if you change your email address after the initial email registration, the old email addresses emailVerified true value will be inherited, and the new email address will be passed as emailVerified true even if you do not click on the new email address verification.

await FirebaseAuth.instance.currentUser.reload() in front of it does not resolve this error.

In other words, in the code below, if (user ! = null && user.emailVerified) would slip through with true.

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

class MailCheckScreen extends StatefulWidget {
  final String email;

  const MailCheckScreen({super.key, required this.email});

  @override
  _MailCheckScreenState createState() => _MailCheckScreenState();
}

class _MailCheckScreenState extends State {
  final TextEditingController _authCodeController = TextEditingController();
  final _auth = FirebaseAuth.instance;
  bool isLoading = false;

  void _checkMail() async {
    setState(() {
      isLoading = true;
    });
    // 現在のユーザーを取得します
    User? user = _auth.currentUser;

    if (user != null) {
      // ユーザーの最新の情報を取得するためにリロードします
      await user.reload();
      user = _auth.currentUser;

      if (user != null && user.emailVerified) {
        setState(() {
          isLoading = false;
        });
        // メール認証が完了している場合
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => const LoginScreen(),
          ),
        );
      } else {
        // メール認証がまだ完了していない場合
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text("メールが未確認です。メールを確認してください。"),
          ),
        );
      }
      setState(() {
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return BaseContainer();
  }
}

So we need to be able to detect if a new email address has been authenticated by another method.

Someone else encountered the same Issue.
https://github.com/firebase/flutterfire/issues/6426

Provide a check class for email address changes

When changing email addresses, use widget.email to check if the new email address is authenticated, and to check for email address matches.

Since widget.email is the new email address but user.email is still the old email address (the email address is not updated by Firebase authentication) at the stage where the user has not authenticated the email sent to the new email, the email authentication user.email is still the old email address (the email address is not updated by firebase authentication).

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

class MailCheckScreen extends StatefulWidget {
  final String email;

  const MailCheckScreen({super.key, required this.email});

  @override
  _MailCheckScreenState createState() => _MailCheckScreenState();
}

class _MailCheckScreenState extends State {
  final TextEditingController _authCodeController = TextEditingController();
  final _auth = FirebaseAuth.instance;
  bool isLoading = false;

  void _checkMail() async {
    setState(() {
      isLoading = true;
    });
    // 現在のユーザーを取得します
    User? user = _auth.currentUser;

    if (user != null) {
      // ユーザーの最新の情報を取得するためにリロードします
      await user.reload();
      user = _auth.currentUser;

      if (user != null && user.email == widget.email && user.emailVerified) {
        setState(() {
          isLoading = false;
        });
        // メール認証が完了している場合
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => const LoginScreen(),
          ),
        );
      } else {
        // メール認証がまだ完了していない場合
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text("メールが未確認です。メールを確認してください。"),
          ),
        );
      }
      setState(() {
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return BaseContainer();
  }
}

The actual logs are as follows.

Note that user.emailVerified remains true whether or not email verification is used.

ShellScript

flutter: 現在のメール:変更前のメールアドレス
flutter: MailChangeCheckScreenに渡されたemailプロパティ:変更後の新しいメールアドレス
flutter: メール認証ステータス:true

By adding the condition user.email == widget.email, the SnackBar now displays an error when a user presses confirm at a stage where they have not authenticated their new email.

Token expiration issue after email authentication is addressed.

Incidentally, the above is not enough; even if the user authenticates the e-mail, the following error occurs and the user does not move to the login screen.

ShellScript
flutter: error [firebase_auth/user-token-expired] The user's credential is no longer valid. The user must sign in again.

This is because even though the Firebase Authentication email address has changed at the time of the new email verification with the verifyBeforeUpdateEmail() method, the Flutter screen has not yet transitioned to the login screen, so the old This is due to the fact that the user pressed the confirm complete button with the old email address. This causes a token expired error at the await user.reload(); stage.

So, the whole thing including await user.reload(); should be enclosed in a try-catch method to prompt the user to log in again if the token expires.

The final email change confirmation screen is below.

Dart
import 'package:clocky_app/screens/login/login_screen.dart';
import 'package:clocky_app/screens/registration/email_registration_screen.dart';
import 'package:clocky_app/widgets/base_container.dart';
import 'package:clocky_app/widgets/buttons.dart';
import 'package:clocky_app/widgets/spacing.dart';
import 'package:clocky_app/widgets/texts.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

class MailChangeCheckScreen extends StatefulWidget {
  final String email;

  const MailChangeCheckScreen({super.key, required this.email});

  @override
  _MailChangeCheckScreenState createState() => _MailChangeCheckScreenState();
}

class _MailChangeCheckScreenState extends State {
  final TextEditingController _authCodeController = TextEditingController();
  final _auth = FirebaseAuth.instance;
  bool isLoading = false;

  void _checkMail() async {
    setState(() {
      isLoading = true;
    });
    // 現在のユーザーを取得
    User? user = _auth.currentUser;

    if (user != null) {
      try {
        // ユーザーの最新の情報を取得するためにリロード
        await user.reload();
        user = _auth.currentUser;

        debugPrint('現在のメール:${user?.email}');
        debugPrint('MailChangeCheckScreenに渡されたemailプロパティ:${widget.email}');
        debugPrint('メール認証ステータス:${user?.emailVerified}');

        if (user != null && user.email == widget.email && user.emailVerified) {
          setState(() {
            isLoading = false;
          });
          // メール認証が完了している場合
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(
              builder: (context) => const LoginScreen(),
            ),
          );
        } else {
          // メール認証がまだ完了していない場合
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text("メールが未確認です。メールを確認してください。"),
            ),
          );
        }
      } catch (e) {
        debugPrint("user info cannot reload");
        debugPrint('error ${e.toString()}');
        if (e.toString().contains('user-token-expired')) {
          // トークンが期限切れの場合
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(
              builder: (context) => const LoginScreen(),
            ),
          );
        } else {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('エラーが発生しました: $e'),
            ),
          );
        }
      } finally {
        setState(() {
          isLoading = false;
        });
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return BaseContainer(
      children: [
        const HeaderText("認証メールを送信しました"),
        Spacing.h16(),
        const Text(
            "入力いただいたメールアドレスに届いた認証リンクをクリックしてください。メール認証を終えたら、再度この画面に戻り「確認完了」ボタンを押してください。"),
        Spacing.h16(),
        AppButton(
          onPressed: _checkMail,
          text: '確認完了',
        ),
        Spacing.h16(),
        AppButton(
          onPressed: () async {
            // メール再送信
            _auth.currentUser?.sendEmailVerification().then((value) => {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text("認証メールを再送信しました"),
                    ),
                  )
                });
          },
          text: 'メールを再送する',
          styleType: ButtonStyleType.SECONDARY,
        ),
        Spacing.h16(),
        AppButton(
          onPressed: () async {
            // logout
            _auth.signOut().then((value) => {
                  Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(
                      builder: (context) => const EmailRegistrationScreen(),
                    ),
                  )
                });
          },
          text: '最初からやり直す',
          styleType: ButtonStyleType.SECONDARY,
        ),
      ],
    );
  }
}

With the above change, even if a user successfully authenticates by e-mail, the user is now redirected to the login screen.

Operation check

The overall screen transition is as follows.

When the user enters a new email address and clicks Update, he/she is taken to the screen for sending the authentication email.

Then, if the user presses Confirm Completed without email authentication, an error is displayed in the SnackBar and the user is not taken to the login screen. If the user presses Confirm Completed with email authentication, the user is taken to the login screen and the user signs in again with the new email address and password.

If you can sign in successfully, the out-of-token problem will be resolved.

Copied title and URL