1

I am currently implementing a MFA system with Firebase Authentication & Google Authenticator.

Since my users are not allowed to authenticate with a non-verified email address, I'd like to prevent them from signing-in if their Firebase Authentication email_verified is set to false. To do that, I am using Google Cloud Identity Provider blocking functions, this works perfectly. However, when it comes to the registration beforeCreate blocking function hook, I can't find a way to generate an email verification link for the user currently being created, the documentation says:

Requiring email verification on registration The following example shows how to require a user to verify their email after registering:

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth()
            .generateEmailVerificationLink(user.email)
            .then((link) => {         
              return sendCustomVerificationEmail(
                user.email, link, locale
              );
    });
  }
});

export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new gcipCloudFunctions.https.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

However, as far as I understand, generateEmailVerificationLink() can only be called to generate email verification link of an existing Firebase Authentication user. At this stage (while running beforeCreate blocking function), the user is not created yet.

Now I am wondering, I am missing something or is the Google documentation wrong?

mmoya
  • 1,901
  • 1
  • 21
  • 30
Liyali
  • 5,643
  • 2
  • 26
  • 40
  • You can also use sign in with email link instead of sign in with password, that way you're sure signed in users are verified – Obum May 16 '22 at 22:17
  • 1
    @Liyali, hitting the same problem as you. The documentation is wrong. I'm copy/pasting the example and `generateEmailVerificationLink()` returns a error `{"code": "auth/user-not-found", "message": "There is no user record corresponding to the provided identifier."}`. – mmoya Nov 15 '22 at 12:27

5 Answers5

1

No.

User data is created upon registration in the database.

Then, you may send an Email-Verification with a link automatically.

This Email-Verification just updates the field emaiVerified of said user data.

If you want to prevent users with unverified Emails from logging in, you need to adjust your Login page and check whether emaiVerified is true.

Important: Google will sign in a user right upon registration whether the email is verified or not, as this is the expected behavior from the perspective of a user. Email verification is ensured on the second, manual login.

(Also, please do not screenshot code.)

Dabbel
  • 2,468
  • 1
  • 8
  • 25
  • Thanks for your explanation, I just replaced my screenshot accordingly, however this does not answer my question: is the documentation wrong or am I missing something? – Liyali May 17 '22 at 07:23
  • 1
    Also, note that it is possible to prevent user to login upon registration if their email is not verified by checking the email verified status in the `beforeSignin` hook, that's the whole point of these blocking functions AFAIU. – Liyali May 17 '22 at 07:25
1

You can let a user sign in via email link at first, and call firebase.User.updatePassword() to set its password. I am using Angular-Firebase, this is the logic code.

if (this.fireAuth.isSignInWithEmailLink(this.router.url)) {
  const email = this.storage.get(SIGN_IN_EMAIL_KEY) as string;

  this.storage.delete(SIGN_IN_EMAIL_KEY);
  this.emailVerified = true;
  this.accountCtrl.setValue(email);
  from(this.fireAuth.signInWithEmailLink(email, this.router.url)).pipe(
    catchError((error: FirebaseError) => {
      const notification = this.notification;

      notification.openError(notification.stripMessage(error.message));
      this.emailVerified = false;
      return of(null);
    }),
    filter((result) => !!result)
  ).subscribe((credential) => {
    this.user = credential.user;
  });
}



  const notification = this.notification;
  const info = form.value;

  this.requesting = true;
  form.control.disable();
  (this.emailVerified ? from(this.user.updatePassword(info.password)) : from(this.fireAuth.signInWithEmailLink(info.account))).pipe(
    catchError((error: FirebaseError) => {
      switch (error.code) {
        case AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY.POPUP_CLOSED_BY_USER:
          break;
        default:
          console.log(error.code);
          notification.openError(notification.stripMessage(error.message));
      }
      this.requesting = false;
      form.control.enable();
      return of(null);
    }),
    filter((result) => !!result)
  ).subscribe((result: firebase.auth.UserCredential) => {
    if (this.emailVerified) {
      if (result.user) {
        notification.openError(`注册成功。`);
        this.router.navigateByUrl(this.authService.redirectUrl || '');
      } else {
        notification.openError(`注册失败。`);
        this.requesting = false;
        form.control.enable();
      }
    } else {
      this.storage.set(SIGN_IN_EMAIL_KEY, info.account);
    }
  });
Gray Young
  • 41
  • 4
0

Mate, if database won't create a new user using his email and password, and you send him email verification which will create his account, how the heck database will know his password? If it didn't create his account in the first step? Stop overthinking and just secure database using rules and routes in application if you don't want user to read some data while he didn't confirm email address.

It is that simple:

match /secretCollection/{docId} {
   allow read, write: if isEmailVerified()
}

function isEmailVerified() {
   return request.auth.token.email_verified
}
Mises
  • 4,251
  • 2
  • 19
  • 32
0

As of July 2023, the code you posted results in error. As you said, the documentation is wrong.

You cannot generate the link before the first beforeCreate function execution. Only at the 2nd execution you can do that. This is unexpected behaviour since apparently the user is not persisted after the sign-up finishes (and before beforeCreate).

In case you use the OG email/password provider (not passwordless), the only way to achieve this behaviour is to sign out the user right after they're successfully registered through the Firebase Client SDK.

Since this is client, anyone can alter the code and stay signed in. So you also have to protect your resources (backend) and check if their email address is verified (email_verified == true) with every request.

Two common ways are Firebase rules (as @Mises suggested) and using middlewares (with Firebase Admin SDK).

I implemented both approaches and recently wrote Ultimate Guide to User Authorization with Identity Platform. You can see the sign out in this section and how to protect your data (checking email_verified == true) in the Protect Data section.

Michal Moravik
  • 1,213
  • 1
  • 12
  • 23
-1

I think the blocking function documentation is wrong.

beforeCreate: "Triggers before a new user is saved to the Firebase Authentication database, and before a token is returned to your client app."

generateEmailVerificationLink: "To generate an email verification link, provide the existing user’s unverified email... The operation will resolve with the email action link. The email used must belong to an existing user."

Has anyone come up with a work around while still using blocking functions?

Using firebase rules to check for verification isn't helpful if the goal is to perform some action in the blocking function, such as setting custom claims.

  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 16 '23 at 10:08