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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- firebase-auth Installs to ~/.agents/skills/firebase-auth/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install firebase-auth The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project firebase-auth Installs to .agents/skills/firebase-auth/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/firebase-auth.zipunzip -o firebase-auth.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) firebase-auth PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
- Read
AGENTS.md; searchrg -n "firebase/auth|firebase-admin|getAuth\(|verifyIdToken|onAuthStateChanged" --glob '!node_modules'. - 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. - Read
firebase.jsonand.firebasercfor project aliases and emulator ports. Confirm which project is dev and which is production before running any CLI command. - 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
onAuthStateChangedcallback;auth.currentUserisnullduring 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
| Need | Method | Notes |
|---|---|---|
| Consumer app, low friction | Google / Apple providers | Apple is required on iOS if you offer other social logins |
| No passwords | Email link (sendSignInLinkToEmail, signInWithEmailLink) | Store the email locally to complete on the same device; handle cross-device by asking again |
| Classic accounts | Email + password | Enable email verification; keep enumeration protection on |
| Phone | Phone auth with reCAPTCHA / App Check | SMS costs money and attracts fraud; set region policies |
| Enterprise SSO, MFA, blocking functions | Upgrade to Identity Platform | Adds 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:
- Host the app on Firebase Hosting and set
authDomainto the app’s custom domain. - Proxy
/__/auth/*from the app’s domain tohttps://<project>.firebaseapp.com/__/auth/*and setauthDomainto the app’s domain. - Use
signInWithPopupinstead 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. setCustomUserClaimsoverwrites; merge with(await getAuth().getUser(uid)).customClaimsfirst.
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: callsignInWithPopupdirectly from the click handler, not after anawait.- Trusting
user.emailwithoutemail_verifiedlets anyone claim an address with password sign-up. - The web
apiKeyis 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 withlinkWithCredentialafter 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.yaml
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.