Problem
I'm searching for a way to prevent a user from signing in before they verify their email address. I tried three different approaches that I'll describe below, including ones involving blocking functions. So far with no success. While searching, I discovered that the logic behind the blocking functions is broken.
Context
I've been coding and writing an ultimate guide to Google Identity Platform (Firebase Authentication). In this guide, I walk the user through setup, using blocking functions to assign different user roles, and limiting access to resources based on these user roles.
I got to the point where I started implementing the email/password provider. Very common case is preventing the user from signing in before they verify their email address. So I knew there has to be the following flow implemented:
Let the user create an account using email and password (note: I want them to fill in both the email and password during this registration). This must create a record of the user.
User gets the verification email (since we already know their email address)
User must verify the email address by clicking the link
Only after the email address is verified can the user sign in
Different Approaches
First, I tried using
sendEmailVerificationfrom the client right aftercreateUserWithEmailAndPasswordsucceeds. The problem is that this create function (unintuitively) both creates as well as signs in the user at the same time. SosendEmailVerificationcould only be triggered after the user signs in too. So this approach didn't work for me.Thus I thought blocking functions would be useful in this case. I followed the (incorrect) documentation for this exact case here: Requiring email verification on registration. The docs say I can generate the link for the user even before the user is created (as it is the 'beforeCreate' func). I though "that's super cool" even though it doesn't make much sense. It also throws an error inside 'beforeSignIn' which means the user won't be signed in during the registration. Sweet. After implementing this, I found out you this is not possible as you cannot
generateEmailVerificationLinkbefore the user is created. So the docs are totally wrong.This got me thinking: if I move the
generateEmailVerificationLinkwith my customsendCustomVerificationEmailfunction to thebeforeSignInfunction (which is run during the creation flow too), the user should already be created so I can block them here and generate the verification link too. But no, thegenerateEmailVerificationLinkthrows an error that the user still does not exist. I thought: how is this possible if thebeforeCreatealready passed? The user is even listed under "Users" in the console.
So I went searching the wild places of Github and found that the engineering lead for Firebase Functions say in this Github Issue:
As far as I can tell, Google Cloud Identity Platform requires the user to successfully be created and logged in before persisting it.
So I thought that console must be then lying when it displays the user under "Users" (maybe bug I thought). But then I tried to run the registration for the same user again (just for fun) and this is where the total mess happens.
The client gets back auth/email-already-in-use error. BUT the blocking functions run anyway (Bug #1). When the beforeSignIn function runs, it can now find the user, generate the link, and send them the verification email. How is this possible? The beforeSignIn function never passed before (it threw an error during the first registration)(** Bug #2**).
So the claim that users are persisted when they're successfully created and logged in is not true.
Obviously this is no-go as the user would have to press "Register" button twice in a row (not mentioning the auth/email-already-in-use error that the client receives)
Question
Is there any way I could implement the (very common) 4-step process I described in the Context section above?
Replicating the issue in the code
- Make a simple client calling
createUserWithEmailAndPasswordfunction:
function registerWithEmail(email, password) {
firebase.auth().createUserWithEmailAndPassword(email, password)
.catch(err => console.log('Error in client: ', err));
}
- Make the following blocking function:
import gcipCloudFunctions from 'gcip-cloud-functions';
import admin from 'firebase-admin';
const app = admin.initializeApp();
export const beforeSignInHandler = async (user, context, adminEmails) => {
if (!user.emailVerified) {
console.log('User email is not verified');
try {
const link = await app.auth().generateEmailVerificationLink(user.email);
console.log('Generated email link: ', link);
await sendCustomVerificationEmail(user.email, link);
} catch (err) {
console.log('Error generating and sending email');
console.log(err);
throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Message unimportant`);
}
console.log('Throwing an error to prevent unverified email addresses to sign in. This is expected when the func is triggered first time - during registration flow');
throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `UNVERIFIED_EMAIL`);
}
const role = adminEmails.includes(user.email) ? 'admin' : 'user';
console.log(`Signing in user: ${user.email} with role: ${role}`);
return {
customClaims: { role },
}
};
const sendCustomVerificationEmail = async (email, link) => {
// The real implementation is not important
console.log('Sending the verification email');
};
Try calling the registration first time. You should get an error that the user does not yet exist so the link cannot be generated. The function blocks the sign-in process by throwing. BUT the user can be found in the console under "Users".
Try calling the registration second time. Now you should see "Sending the verification email" in the functions logs. AND You'll also see the "auth/email-already-in-use" error in the browser console
A big thank you to those who read this question and answered, I know it is long