ContactLintAPIMCPPricingFree toolsCompareGet a key

Home / Integrations / Firebase Authentication

Validate Email Addresses in Firebase Authentication Signups

Published 2026-10-18

This guide adds a beforeUserCreated blocking function that checks each new email address with ContactLint before Firebase creates the account. Typos like gmial.com, disposable domains and domains that cannot receive mail are stopped on the server, where no client can skip the check. If ContactLint is slow or unreachable, the signup goes through.

Where the check goes, and why on the server

Firebase client SDKs create accounts straight from the browser or app, so anything you check in the client can be bypassed by calling the Auth API directly. A blocking function runs on Google's side before the user record is saved. If it throws, the account is never created.

Blocking functions need Firebase Authentication with Identity Platform. If your project still uses the basic Authentication product, upgrade it in the Firebase console under Authentication settings first. Deploying Cloud Functions also depends on your billing setup, so check your plan.

Use beforeUserCreated from firebase-functions/v2/identity. It fires for every new account, including email and password signups and federated providers. Your secret ContactLint key stays in Secret Manager and never reaches the browser.

Setup: key, secret and blocking function

  1. Get a free secret key at /signup. It is shown once, so save it.
  2. Store it as a function secret from your project folder: firebase functions:secrets:set CONTACTLINT_KEY
  3. Use Node 18 or later for your functions runtime so fetch and AbortSignal.timeout are built in.
  4. Add the code below to functions/index.js and run firebase deploy --only functions. Then confirm in the console, under Authentication settings, that the function is registered as the before-create blocking function.
const { beforeUserCreated, HttpsError } = require("firebase-functions/v2/identity");
const { defineSecret } = require("firebase-functions/params");
const logger = require("firebase-functions/logger");

const CONTACTLINT_KEY = defineSecret("CONTACTLINT_KEY");

async function checkEmail(email) {
  try {
    const res = await fetch("https://contactlint.com/v1/email/validate", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + CONTACTLINT_KEY.value(),
        "content-type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) {
      logger.warn("ContactLint returned " + res.status + ", allowing signup");
      return null; // 401, 402, 429 or other error: fail open
    }
    const { result } = await res.json();
    return result;
  } catch (err) {
    logger.warn("ContactLint unreachable, allowing signup", err);
    return null; // timeout or network error: fail open
  }
}

exports.beforecreated = beforeUserCreated({ secrets: [CONTACTLINT_KEY] }, async (event) => {
  const user = event.data;
  // Phone and anonymous signups have no email. Provider-verified emails need no check.
  if (!user || !user.email || user.emailVerified) return;

  const result = await checkEmail(user.email);
  if (!result) return; // fail open

  if (result.verdict === "invalid") {
    throw new HttpsError("invalid-argument", "CL_INVALID That email address cannot receive mail.");
  }
  if (result.did_you_mean) {
    throw new HttpsError("invalid-argument", "CL_TYPO:" + result.did_you_mean);
  }
  if (result.disposable) {
    throw new HttpsError("invalid-argument", "CL_DISPOSABLE Please use a permanent email address.");
  }
  // valid, unknown and role addresses are accepted
});

The order matters. did_you_mean is checked before disposable because gmial.com is both a typo and on the disposable list. Checking it first means the person sees a fix instead of a rejection.

Show each verdict in your signup UI

When the blocking function throws, the client call such as createUserWithEmailAndPassword rejects. The way Firebase wraps the message can vary between SDKs, so the function puts a short token (CL_TYPO, CL_INVALID, CL_DISPOSABLE) in the message and the client looks for it:

import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";

export async function signUp(email, password, showError) {
  try {
    return await createUserWithEmailAndPassword(getAuth(), email, password);
  } catch (err) {
    const msg = String(err && err.message);
    const typo = msg.match(/CL_TYPO:([\w.+-]+@[\w.-]+)/);
    if (typo) {
      showError("Did you mean " + typo[1] + "?", typo[1]);
    } else if (msg.includes("CL_INVALID")) {
      showError("That email address cannot receive mail.");
    } else if (msg.includes("CL_DISPOSABLE")) {
      showError("Please use a permanent email address.");
    } else {
      showError("Signup failed. Please try again.");
    }
    return null;
  }
}

A blocking function cannot ask the person to confirm a typo and then let it through. For a softer flow, add the browser widget with a cl_pub_ publishable key so people see the hint before they press submit. Keep this function as the backstop. React setup is in the framework guide.

Test it

Use a test project and sign up from your app with these addresses:

Then test fail open. Set the secret to a wrong value with firebase functions:secrets:set CONTACTLINT_KEY, redeploy, and sign up again. ContactLint returns 401, the function logs a warning, and the account is created. Restore the real key afterwards. Check the function logs in the console to see each warning.

Limits and costs

Questions

Do I need Identity Platform to use this?

Yes. Blocking functions such as beforeUserCreated only work on Firebase Authentication with Identity Platform. Upgrade in the Firebase console under Authentication settings, and check your plan for any billing changes.

Does the function check Google or other social sign-ins?

The function runs for every new account, but the code skips users whose email the provider already marked as verified, and users with no email, such as phone or anonymous signups. Remove that check if you want to validate every address.

What happens to signups if ContactLint is down?

They go through. The request has a 3 second timeout, and any timeout, network error or non-200 response, including 401, 402 and 429, makes the function return without throwing, so Firebase creates the account.

Other integrations

Get a free API key. 100 checks per month, no card required. Or try a live check first.