Firebase Auth → Behest
Map Firebase users to Behest users. Works with web, iOS, Android, and Firebase Cloud Functions.
1. Architecture
Client signs in with Firebase (google.com, email+pwd, phone, custom)
↓ Firebase ID token
POST /behest-token (Firebase ID token in Authorization)
↓ Cloud Function verifies + calls Behest mint
↓ returns Behest JWT
POST https://{slug}.behest.app/v1/chat/completions
2. Cloud Function token minter
// functions/src/behestToken.ts
import { onRequest } from "firebase-functions/v2/https";
import { initializeApp } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";
import { Behest } from "@behest/client-ts";
initializeApp();
const behest = new Behest(); // reads BEHEST_KEY + BEHEST_BASE_URL from env
export const behestToken = onRequest(
{ cors: true, secrets: ["BEHEST_KEY", "BEHEST_BASE_URL"] },
async (req, res) => {
const idToken = req.headers.authorization?.replace(/^Bearer /, "");
if (!idToken) return res.status(401).send("no id token");
let decoded;
try {
decoded = await getAuth().verifyIdToken(idToken);
} catch {
return res.status(401).send("invalid id token");
}
const uid = decoded.uid;
const tier = (decoded as any).plan ?? 1; // set via custom claims
try {
const { token, ttl, sessionId, expiresAt } = await behest.auth.mint({
user_id: uid,
tier,
ttl: 900,
});
res.json({ token, ttl, sessionId, expiresAt });
} catch (err) {
res.status(500).json({ error: String(err) });
}
}
);Deploy:
firebase functions:secrets:set BEHEST_KEY
firebase functions:secrets:set BEHEST_BASE_URL
firebase deploy --only functions:behestToken3. Web client
import { getAuth } from "firebase/auth";
import OpenAI from "openai";
type TokenBundle = {
token: string;
ttl: number;
sessionId: string;
expiresAt: number;
};
let cached: TokenBundle | null = null;
async function getBehestToken(): Promise<TokenBundle> {
const now = Math.floor(Date.now() / 1000);
if (cached && cached.expiresAt - now > 60) return cached;
const idToken = await getAuth().currentUser?.getIdToken();
const r = await fetch(
`https://<region>-<project>.cloudfunctions.net/behestToken`,
{
method: "POST",
headers: { Authorization: `Bearer ${idToken}` },
}
);
if (!r.ok) throw new Error(`token fetch failed: ${r.status}`);
cached = (await r.json()) as TokenBundle;
return cached;
}
export async function getOpenAI() {
const { token, sessionId } = await getBehestToken();
return new OpenAI({
apiKey: token,
baseURL: `${import.meta.env.VITE_BEHEST_BASE_URL}/v1`,
dangerouslyAllowBrowser: true,
defaultHeaders: { "X-Session-Id": sessionId },
});
}4. iOS / Android
Same pattern — client gets a Firebase ID token, calls your Cloud Function, receives a Behest JWT. Store it in memory only (not Keychain/Keystore). Re-fetch before expiresAt - now drops below 60s.
// iOS (swift)
Auth.auth().currentUser?.getIDToken { idToken, _ in
// POST to behestToken, parse { token, ttl, sessionId, expiresAt }
}// Android (kotlin)
val idToken = Firebase.auth.currentUser?.getIdToken(false)?.await()?.token
// POST idToken to behestToken, parse { token, ttl, sessionId, expiresAt }5. Custom claims for tier
await getAuth().setCustomUserClaims(uid, { plan: "pro" });Clients need to call user.getIdToken(true) once after the upgrade to pull a fresh ID token with the new claim. The next Behest mint reads it.
6. Option: trust Firebase JWKS directly
Behest's Kong plugin can verify Firebase-issued ID tokens if you add Firebase as a trusted issuer:
- Issuer:
https://securetoken.google.com/<your-project-id> - JWKS:
https://www.googleapis.com/robot/v1/metadata/jwks/securetoken@system.gserviceaccount.com - Claim mapping:
user_id→uid,plan→tier
Caveat: Firebase ID tokens don't include a tid/pid out of the box. Either inject them via a Cloud Functions-issued custom token, or stay with Option 2 (mint via Cloud Function).
7. Anonymous users
Firebase anonymous auth works — the uid is a stable anonymous id. Treat them as free-tier users:
await behest.auth.mint({
user_id: uid,
tier: decoded.firebase.sign_in_provider === "anonymous" ? 1 : tier,
ttl: 900,
});Good for a "try before sign up" experience.