How to block fake signups in Firebase Authentication
Firebase Authentication can run your own code before it creates a user: a blocking function. That is the ideal place for a fraud check, because it runs for every signup, including scripts that call the Auth API directly.
Before you start
- Create a free account at signupvet.com/signup (250 checks a month, no card) and confirm your email.
- Create an API key in the dashboard.
- Add it to your server environment, never to browser code:
SIGNUP_API_KEY=sv_live_...
What you need
- Firebase Authentication with Identity Platform. Blocking functions only work after this upgrade, which you can do in the Firebase console. Check its pricing for your number of users first.
- Cloud Functions, which need the pay-as-you-go Blaze plan.
1. Store your API key as a secret
Run this once in your project and paste your sv_live_... key when asked. It is stored in Google Cloud Secret Manager, not in your code:
firebase functions:secrets:set SIGNUP_API_KEY
2. Add the helper
One API call with a 2 second timeout. It fails open: if the API cannot be reached or your monthly checks are used up, the signup goes through. That matters here, because a blocking function has to answer within 7 seconds or Firebase rejects the signup.
// functions/src/signup-check.ts: SignupVet check for Cloud Functions (fails open on errors and timeouts).
export type SignupCheck = {
recommendation: "allow" | "review" | "block";
risk_score: number | null;
reason_codes: string[];
failed_open?: boolean;
};
const FAIL_OPEN: SignupCheck = { recommendation: "allow", risk_score: null, reason_codes: [], failed_open: true };
export async function checkSignup(
apiKey: string,
signup: { email?: string; phone?: string; ip?: string; userAgent?: string },
baseUrl = process.env.SIGNUPVET_URL ?? "https://www.signupvet.com",
): Promise<SignupCheck> {
try {
const res = await fetch(`${baseUrl}/score`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
email: signup.email,
phone: signup.phone,
ip: signup.ip,
context: { user_agent: signup.userAgent },
}),
// Blocking functions must answer within 7 seconds, so keep this short.
signal: AbortSignal.timeout(2000),
});
return res.ok ? ((await res.json()) as SignupCheck) : FAIL_OPEN;
} catch {
return FAIL_OPEN;
}
}
3. Add the blocking function
// functions/src/index.ts (Firebase Authentication blocking function, Cloud Functions 2nd gen)
//
// Runs before Firebase creates a new user, for every sign-up method, including direct calls
// to the Auth API. Needs Firebase Authentication with Identity Platform.
import { defineSecret } from "firebase-functions/params";
import { beforeUserCreated, HttpsError } from "firebase-functions/v2/identity";
import { checkSignup } from "./signup-check";
const SIGNUP_API_KEY = defineSecret("SIGNUP_API_KEY");
export const checknewsignup = beforeUserCreated({ secrets: [SIGNUP_API_KEY] }, async (event) => {
const check = await checkSignup(SIGNUP_API_KEY.value(), {
email: event.data?.email,
phone: event.data?.phoneNumber,
ip: event.ipAddress,
userAgent: event.userAgent,
});
if (check.recommendation === "block") {
throw new HttpsError("permission-denied", "We could not create your account. Please use a different email address.");
}
// Custom claims end up in the user's ID token; users cannot change them.
return { customClaims: { signup_risk: check.recommendation } };
});
Firebase passes the signup's email, phone number, IP address and user agent to the function. On block the account is never created. Otherwise the result is saved as a custom claim, which your users cannot change.
Deploy it with firebase deploy --only functions. Afterwards, check in the Firebase console under the Authentication settings that the function is listed as the blocking function before account creation.
4. Use the result in your app
| recommendation | What to do |
|---|---|
allow | Create the account as usual. |
review | Create the account, but hold back free credits or the trial until the email is confirmed, or look at it manually. |
block | Do not create the account. Show a neutral message. |
Read the claim from the ID token: in the app with user.getIdTokenResult(), then claims.signup_risk; in your backend and security rules from the decoded token. Give review signups fewer free credits until you have looked at them.
Good to know
- The error reaches the user's browser. Firebase hands the error from your function to the client, so keep the message neutral and show it in your signup form.
- Phone sign-in works too: the phone number is passed along and checked.
- Firebase's own reCAPTCHA protection and SignupVet complement each other: one stops simple bots, the other checks the email, phone and network behind each signup.
Test it
Sign up with test123@mailinator.com: the signup fails with your neutral message and no user appears in Firebase. A normal address creates the user with the claim signup_risk: "allow".
The full examples are on GitHub: github.com/signupvet/examples.
Add it to your signup today
SignupVet checks the email, phone and IP of every new signup in one call and spots waves of signups from the same network. Free for 250 signups a month, no card needed.