| name | firebase-auth |
| description | Firebase Authentication patterns — token verification in FastAPI, custom claims for RBAC, multi-tenant auth, ID token validation, service account setup, and frontend Firebase SDK integration. |
| origin | local |
Firebase Authentication Patterns
Patterns for using Firebase Auth as the identity layer in FastAPI backends and React frontends.
When to Activate
- Verifying Firebase ID tokens in FastAPI
- Adding custom claims (roles, tenant_id) to tokens
- Setting up Firebase Admin SDK
- Implementing multi-tenant authentication
- Debugging auth errors (token expired, wrong audience, etc.)
- Integrating Firebase Auth in React with Zustand
Backend — FastAPI Integration
Setup Firebase Admin SDK
import firebase_admin
from firebase_admin import auth, credentials
from functools import lru_cache
@lru_cache(maxsize=1)
def get_firebase_app() -> firebase_admin.App:
"""Initialize Firebase Admin once per process."""
cred = credentials.ApplicationDefault()
return firebase_admin.initialize_app(cred, {
"projectId": settings.FIREBASE_PROJECT_ID,
})
Token Verification Dependency
from firebase_admin import auth
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
bearer = HTTPBearer()
class FirebaseUser(BaseModel):
uid: str
email: str | None = None
email_verified: bool = False
tenant_id: str | None = None
role: str = "member"
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(bearer),
) -> FirebaseUser:
token = credentials.credentials
try:
decoded = auth.verify_id_token(token, app=get_firebase_app())
except auth.ExpiredIdTokenError:
raise HTTPException(status_code=401, detail="Token expired")
except auth.RevokedIdTokenError:
raise HTTPException(status_code=401, detail="Token revoked")
except auth.InvalidIdTokenError as e:
raise HTTPException(status_code=401, detail=f"Invalid token: {e}")
return FirebaseUser(
uid=decoded[],
email=decoded.get(),
email_verified=decoded.get(, ),
tenant_id=decoded.get(),
role=decoded.get(, ),
)
() -> FirebaseUser:
user.email_verified:
HTTPException(status_code=, detail=)
user
Route Usage
@router.get("/me")
async def get_me(user: FirebaseUser = Depends(get_current_user)):
return {"uid": user.uid, "tenant": user.tenant_id}
@router.post("/conversations")
async def create_conversation(
body: ConversationCreate,
user: FirebaseUser = Depends(get_current_user),
db: AsyncSession = Depends(get_tenant_db),
):
...
Custom Claims
Custom claims persist in the Firebase ID token — no DB lookup on every request.
Set Claims (Server-side, Admin SDK)
from firebase_admin import auth
async def set_tenant_claims(uid: str, tenant_id: str, role: str = "member") -> None:
"""Set after user joins a tenant. Token refreshes on next sign-in."""
auth.set_custom_user_claims(uid, {
"tenant_id": tenant_id,
"role": role,
})
Claims Design
GOOD_CLAIMS = {
"tenant_id": "acme-corp",
"role": "admin",
}
BAD_CLAIMS = {
"permissions": ["read", "write", "delete", "admin", ...],
"user_data": {"name": "...", "avatar": "..."},
}
Invalidate Claims / Force Logout
async def revoke_user_tokens(uid: str) -> None:
"""Force all sessions to expire immediately."""
auth.revoke_refresh_tokens(uid)
decoded = auth.verify_id_token(token, check_revoked=True, app=get_firebase_app())
Multi-Tenant Auth
When a user can belong to multiple tenants, don't store tenant_id in claims. Instead, validate from the request:
async def get_current_user_for_tenant(
tenant_id: str,
user: FirebaseUser = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> tuple[FirebaseUser, TenantMembership]:
membership = await get_membership(db, tenant_id=tenant_id, user_id=user.uid)
if not membership:
raise HTTPException(status_code=403, detail="Not a member of this tenant")
return user, membership
Frontend — React Integration
Firebase Config
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const app = initializeApp({
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
});
export const auth = getAuth(app);
Auth Store (Zustand)
import { create } from "zustand";
import {
User,
onAuthStateChanged,
signInWithEmailAndPassword,
signOut,
GoogleAuthProvider,
signInWithPopup,
} from "firebase/auth";
import { auth } from "@/lib/firebase";
interface AuthState {
user: User | null;
loading: boolean;
token: string | null;
signIn: (email: string, password: string) => Promise<void>;
signInWithGoogle: () => Promise<void>;
signOut: () => Promise<void>;
refreshToken: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set, get) => ({
user: null,
loading: true,
token: ,
: (email, password) => {
(auth, email, password);
},
: () => {
provider = ();
(auth, provider);
},
: () => {
(auth);
({ : , : });
},
: () => {
user = auth.;
(user) {
token = user.();
({ token });
}
},
}));
(auth, (user) => {
(user) {
token = user.();
useAuthStore.({ user, token, : });
} {
useAuthStore.({ : , : , : });
}
});
Axios Interceptor
import axios from "axios";
import { auth } from "./firebase";
const api = axios.create({ baseURL: import.meta.env.VITE_API_URL });
api.interceptors.request.use(async (config) => {
const user = auth.currentUser;
if (user) {
const token = await user.getIdToken();
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
api.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status === 401) {
const user = auth.currentUser;
if (user) {
const token = await user.getIdToken(true);
error.config.headers. = ;
api.(error.);
}
}
.(error);
}
);
api;
Local Development
firebase emulators:start --only auth
export FIREBASE_AUTH_EMULATOR_HOST="localhost:9099"
Common Errors
| Error | Cause | Fix |
|---|
Token expired | ID token > 1 hour old | Call getIdToken(true) on frontend |
Wrong audience | projectId mismatch | Verify FIREBASE_PROJECT_ID |
Certificate fetch failed | No internet / firewall | Check Cloud Run egress, use Application Default Credentials |
Custom claims too large | Claims > 1000 bytes | Move non-auth data to DB |
Revoked token | revoke_refresh_tokens was called | User must re-authenticate |
Security Checklist
Reference Skills
- Multi-tenant auth flow → skill:
multi-tenant-saas
- FastAPI dependencies → skill:
fastapi-patterns
- FastAPI security → skill:
fastapi-security