Skip to main content

    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

    ts
    // 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:

    bash
    firebase functions:secrets:set BEHEST_KEY
    firebase functions:secrets:set BEHEST_BASE_URL
    firebase deploy --only functions:behestToken

    3. Web client

    ts
    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.

    swift
    // iOS (swift)
    Auth.auth().currentUser?.getIDToken { idToken, _ in
      // POST to behestToken, parse { token, ttl, sessionId, expiresAt }
    }
    kotlin
    // 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

    ts
    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:

    ts
    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.


    See also

    Enterprise Token FinOps: Enforce hard budgets and attribute costs per session.

    Learn more