Firebase Auth skill for Codex

Verified tokens, claims-based roles, emulator-tested. Codex adds Firebase Authentication with the modular SDK, waits for restored auth state before rendering, and moves every authorization decision to verified ID tokens, session cookies or Security Rules. Roles live in custom claims set by server code, redirect sign-in is fixed for browsers that partition storage, and flows are tested on the Auth emulator before production.

  • Integrations
  • $firebase-auth
  • 194 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- firebase-auth

Installs to ~/.agents/skills/firebase-auth/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $firebase-auth, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Integrates Firebase Authentication in web, mobile and server code - modular JS SDK sign-in (email link, password, Google and Apple providers), onAuthStateChanged state, ID token verification and session cookies with the Admin SDK, custom claims for roles, Security Rules that use request.auth, the Auth emulator for tests, and signInWithRedirect fixes for browsers that block third-party storage. Use when adding Firebase Auth to an app, protecting an API with Firebase ID tokens, adding admin roles, moving SSR apps to session cookies, or debugging redirect, popup or auth/ errors.

Use it when

  • Adding Firebase Authentication to a web or mobile app
  • Protecting a backend API or SSR pages with Firebase identities
  • Implementing roles or tenants with custom claims
  • Debugging auth/ errors, redirect loops or users appearing signed out

Use something else when

  • You are designing Firestore data models and full rule sets: use firebase-firestore
  • You prefer a hosted auth product with prebuilt UI and organizations: use clerk-auth

How to check the result

Sign-in survives reload in Safari and Chrome, requests without a valid token get 401, and rules tests deny cross-tenant reads on the emulator.

The mistake it prevents

Checking roles or ownership only in the UI while the API and rules trust whatever the client sends.

~/.agents/skills/firebase-auth/SKILL.md

--- name: firebase-auth description: Integrates Firebase Authentication in web, mobile and server code - modular JS SDK sign-in (email link, password, Google and Apple providers),… license: MIT ---

Done well means: the client only proves identity, the server or Security Rules make every authorization decision from a verified token, roles live in custom claims set by trusted code, redirect sign-in works in Safari and Chrome with storage partitioning, and all of it is tested against the Auth emulator rather than the production project.

Inspect first

  1. Read AGENTS.md; search rg -n "firebase/auth|firebase-admin|getAuth\(|verifyIdToken|onAuthStateChanged" --glob '!node_modules'.
  2. Check SDK style in the lockfile: modular (import { getAuth } from 'firebase/auth', v9+) or the old namespaced API (firebase.auth()). Write new code modular; migrate namespaced code when touched.
  3. Read firebase.json and .firebaserc for project aliases and emulator ports. Confirm which project is dev and which is production before running any CLI command.
  4. Find where authorization happens today: Firestore/Storage rules, an API server, or (wrongly) only the UI.

Client setup (web)

// firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth, connectAuthEmulator } from 'firebase/auth';

const app = initializeApp({
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,         // public identifier, not a secret
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
});
export const auth = getAuth(app);
if (import.meta.env.DEV) connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
import { onAuthStateChanged, signInWithPopup, GoogleAuthProvider, signOut } from 'firebase/auth';

onAuthStateChanged(auth, (user) => setSession(user));   // single source of truth; fires after restore

export const signInWithGoogle = () => signInWithPopup(auth, new GoogleAuthProvider());
export const logout = () => signOut(auth);
  • Render nothing auth-dependent until the first onAuthStateChanged callback; auth.currentUser is null during startup even for signed-in users.
  • Popups fail on some mobile browsers and in-app webviews; fall back to redirect there.

Choose sign-in methods

NeedMethodNotes
Consumer app, low frictionGoogle / Apple providersApple is required on iOS if you offer other social logins
No passwordsEmail link (sendSignInLinkToEmail, signInWithEmailLink)Store the email locally to complete on the same device; handle cross-device by asking again
Classic accountsEmail + passwordEnable email verification; keep enumeration protection on
PhonePhone auth with reCAPTCHA / App CheckSMS costs money and attracts fraud; set region policies
Enterprise SSO, MFA, blocking functionsUpgrade to Identity PlatformAdds SAML/OIDC providers and pre-sign-in hooks

Projects created since late 2023 have email enumeration protection on, so methods like fetchSignInMethodsForEmail return no data; do not build flows that depend on it.

signInWithRedirect on modern browsers

Browsers that partition third-party storage break redirect sign-in when the app is served from a different domain than authDomain (<project>.firebaseapp.com). Fix with one of:

  1. Host the app on Firebase Hosting and set authDomain to the app’s custom domain.
  2. Proxy /__/auth/* from the app’s domain to https://<project>.firebaseapp.com/__/auth/* and set authDomain to the app’s domain.
  3. Use signInWithPopup instead of redirect.

Then add the domain to Authentication > Settings > Authorized domains and to the OAuth client’s redirect URIs (https://<your-domain>/__/auth/handler). Changing provider console settings on production is the user’s call; list the exact changes.

Server: verify tokens

import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';

initializeApp({ credential: applicationDefault() });   // service account via env, never in the repo

export async function requireUser(req, res, next) {
  const header = req.get('Authorization') ?? '';
  const token = header.startsWith('Bearer ') ? header.slice(7) : null;
  if (!token) return res.status(401).end();
  try {
    req.user = await getAuth().verifyIdToken(token, /* checkRevoked */ true);
    next();
  } catch {
    res.status(401).end();
  }
}

The client sends await auth.currentUser.getIdToken() per request; the SDK refreshes the one-hour ID token automatically. checkRevoked: true costs a lookup; use it for sensitive routes.

Python: firebase_admin.auth.verify_id_token(token, check_revoked=True). Without the Admin SDK, verify the JWT against Google’s public keys: iss = https://securetoken.google.com/<projectId>, aud = project ID, exp in the future, sub non-empty.

SSR apps: session cookies

// POST /session with the ID token right after sign-in
const expiresIn = 5 * 24 * 60 * 60 * 1000;             // 5 minutes to 14 days allowed
const cookie = await getAuth().createSessionCookie(idToken, { expiresIn });
res.cookie('__session', cookie, { maxAge: expiresIn, httpOnly: true, secure: true, sameSite: 'lax' });

// on each request
const claims = await getAuth().verifySessionCookie(req.cookies.__session ?? '', true);

Require a recent sign-in (auth_time within a few minutes) before minting the cookie, protect the endpoint against CSRF, and clear the cookie plus revokeRefreshTokens(uid) on sign-out everywhere. Firebase Hosting only forwards a cookie named __session to Cloud Functions / Cloud Run.

Roles with custom claims

await getAuth().setCustomUserClaims(uid, { role: 'admin', org: 'acme' });  // replaces all claims
  • Set claims only from trusted server code (admin script, callable function with checks), never from the client. Claims are limited to 1000 bytes; store IDs and roles, not profiles.
  • New claims appear after the token refreshes: call getIdToken(true) on the client after a role change.
  • setCustomUserClaims overwrites; merge with (await getAuth().getUser(uid)).customClaims first.

Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{db}/documents {
    match /orgs/{orgId}/projects/{projectId} {
      allow read: if request.auth != null && request.auth.token.org == orgId;
      allow write: if request.auth != null && request.auth.token.org == orgId
                   && request.auth.token.role in ['admin', 'editor'];
    }
  }
}

Data modelling and rule depth belong to $firebase-firestore; this skill only ensures rules read identity from request.auth, never from fields the client wrote.

Emulator tests

firebase emulators:start --only auth,firestore
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099 npm test     # Admin SDK talks to the emulator
firebase emulators:exec --only auth "npm test"           # CI: start, run, stop

The emulator accepts unsigned tokens and fake providers; never point emulator-style code at production. Seed users through the Admin SDK in test setup.

Destructive operations

Deleting users (deleteUser, deleteUsers), bulk importing with password hashes, revoking tokens for everyone and disabling providers affect real accounts. Run them against production only after the user confirms the project ID and the affected set, and export users first (firebase auth:export users.json --project <id>).

Verification

  • Sign-in, sign-out and page reload work against the emulator and on a deployed preview domain in Safari and Chrome (redirect flow included if used).
  • An API call without a token or with a token from another project returns 401.
  • A user whose role was removed loses access after token refresh; revoked users fail checkRevoked.
  • Rules tests (@firebase/rules-unit-testing) deny cross-org reads and client-written role fields.
  • No service account JSON or Admin credentials in the client bundle or repo.

Gotchas

  • auth/unauthorized-domain: the domain is missing from Authorized domains.
  • auth/popup-blocked: call signInWithPopup directly from the click handler, not after an await.
  • Trusting user.email without email_verified lets anyone claim an address with password sign-up.
  • The web apiKey is public; restrict it in Google Cloud and protect resources with rules and App Check.
  • Account linking: the same email across providers raises auth/account-exists-with-different-credential; handle it with linkWithCredential after signing in with the existing method.

Output

Report: providers enabled, client auth state handling, server verification method (ID token or session cookie), claims schema, rules changes, emulator test results, and console changes the user must make.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Firebase Auth"
  short_description: "Verified tokens, claims-based roles, emulator-tested"
  default_prompt: "$firebase-auth Add Google sign-in with Firebase Auth and protect our Express API"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc