emailverifier.dev
Posts

Firebase Auth Disposable Email Blocking Tutorial

| 5 min read | Usama Ejaz
Firebase Auth blocking function cover with an email stopped by an authentication gate.

This tutorial deploys a Firebase Authentication blocking function that checks an email before Firebase creates the user. You will initialize Cloud Functions, bind the emailverifier.dev key as a secret, deploy beforeUserCreated, and trigger it with a real Auth signup request.

Authentication blocking functions require Firebase Authentication with Identity Platform. The platform gives the function seven seconds to finish, so the external request must use a shorter timeout.

Prepare Firebase Authentication

Create or select a Firebase project, enable Authentication, and upgrade Authentication to Identity Platform. Install and authenticate the Firebase CLI if needed:

npm install -g firebase-tools
firebase login
firebase use your_firebase_project_id

Initialize TypeScript Cloud Functions:

firebase init functions

Select TypeScript and install dependencies when prompted. The tutorial changes:

functions/
└─ src/
   └─ index.ts

Firebase documents the product requirement and seven-second deadline in its authentication blocking-functions guide.

Store the project key as a secret

Create an emailverifier.dev project and copy its API key. Store it with Firebase:

firebase functions:secrets:set EMAILVERIFIER_API_KEY

Paste the key at the prompt. Bind the secret only to the function that needs it; Firebase will not expose it to the client SDK.

Install the current Functions package

cd functions
npm install firebase-functions@latest
cd ..

Implement beforeUserCreated

Replace functions/src/index.ts with:

import {
  beforeUserCreated,
  HttpsError
} from 'firebase-functions/v2/identity'
import { defineSecret } from 'firebase-functions/params'

const emailVerifierApiKey = defineSecret(
  'EMAILVERIFIER_API_KEY'
)

type VerificationResult = {
  email: string
  status: 'deliverable' | 'risky' | 'undeliverable' | 'unknown'
  action: 'allow' | 'block' | 'review'
  flagged: boolean
  signals: string[]
  suggestion: string | null
}

export const validateSignupEmail = beforeUserCreated(
  {
    secrets: [emailVerifierApiKey],
    timeoutSeconds: 7
  },
  async (event) => {
    const email = event.data.email?.trim()

    if (!email) {
      return
    }

    let response: Response
    try {
      response = await fetch(
        'https://emailverifier.dev/api/v1/verify',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-API-Key': emailVerifierApiKey.value()
          },
          body: JSON.stringify({ email }),
          signal: AbortSignal.timeout(4500)
        }
      )
    } catch {
      throw new HttpsError(
        'unavailable',
        'Email verification is temporarily unavailable.'
      )
    }

    if (!response.ok) {
      throw new HttpsError(
        'unavailable',
        'Email verification is temporarily unavailable.'
      )
    }

    const result =
      (await response.json()) as VerificationResult

    if (result.action !== 'block') {
      return
    }

    const message =
      result.signals.includes('disposable_address')
        ? 'Use a permanent email address.'
        : result.suggestion
          ? 'Check the email address and use the suggested correction.'
          : 'Check the email address and try again.'

    throw new HttpsError('invalid-argument', message)
  }
)

The function makes one emailverifier.dev request and leaves 2.5 seconds for Firebase to deliver the result. invalid-argument represents a blocking address decision. unavailable represents a check that did not finish. Those errors should not share the same user message.

Build and deploy the function

cd functions
npm run build
cd ..
firebase deploy --only functions:validateSignupEmail

A successful deployment registers the blocking trigger with the Firebase project. If deployment reports that Identity Platform is required, enable it before retrying.

Trigger an actual signup

The normal client call will now pass through the hook:

import {
  createUserWithEmailAndPassword,
  getAuth
} from 'firebase/auth'

const auth = getAuth()

try {
  const credential =
    await createUserWithEmailAndPassword(
      auth,
      email,
      password
    )

  console.log(credential.user.uid)
} catch (error) {
  console.error(error)
}

You can test without building a page by calling the Firebase Auth REST endpoint. Use the Web API key from Project settings → General; it is the public Firebase client key, not the emailverifier.dev secret:

curl -X POST \
  "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=$FIREBASE_WEB_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"email\":\"person@example.com\",\"password\":\"ChangeThis123!\",\"returnSecureToken\":true}"

An allowed or review result creates the Auth user. A blocking result returns a Firebase Auth error and does not create the user. Check the Authentication user list after each test.

Test the failure policy

The sample fails closed when email verification times out or returns an authenticated HTTP error. That is appropriate when signup immediately receives trial credit or another valuable benefit. It also means a verification outage pauses legitimate signups.

For a lower-risk signup, replace the two unavailable exceptions with return, then require email confirmation or restricted access in the application. Preserve a log or metric that shows the check was skipped. A timeout is not an invalid address.

Map the four results

  • deliverable: return and allow Firebase to create the user.
  • risky: block a disposable address; a typo or another review signal may continue to confirmation.
  • undeliverable: throw invalid-argument after a confirmed syntax, routing, or recipient failure.
  • unknown: return because recipient evidence was inconclusive, then use confirmation or restricted access.

The REST API reference defines the exact result fields. The tutorial is complete when an accepted test appears in Firebase Authentication, a blocked fixture does not, a missing secret prevents a silent bypass, and the emailverifier.dev key is absent from the client.

Use the disposable-email policy guide to decide whether Firebase should fail closed for the specific benefit being granted.

Grant trial credit only after Firebase creates the user and your application reaches the qualifying state. Keep that grant idempotent because an accepted address cannot establish one trial per person.

Continue reading