> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Federated with Firebase

> Firebase Authentication owns the account. Verify the ID token on your server, sign the uid, and UserKit trusts the claim.

Firebase Authentication is your source of truth. UserKit needs to know *which*
of your users is on the page, and it will not take the page's word for it — your
server signs the claim.

<Warning>
  **The identity secret never enters the bundle.** Never prefix it with
  `NEXT_PUBLIC_`, never render it into HTML, never send it to the browser to save
  a round trip. Whoever holds it can mint a **verified** session for any of your
  users.
</Warning>

The contract is the one on the
[federated identity](/en/customer-auth/federated) page and nothing here changes it:

```
hash = hex( HMAC-SHA256( identity_secret, external_id ) )
```

## What Firebase supplies

One value: `uid`. Firebase is the provider that does not arrive as a cookie your
server can read — the browser holds an **ID token** and sends it, and a token is
a string anybody can compose. `verifyIdToken` is what turns it into a fact.

```ts lib/firebase.ts theme={null}
import "server-only";

import { getAuth } from "firebase-admin/auth";

export async function currentExternalId(idToken: string | null): Promise<string | null> {
  if (!idToken) return null;
  try {
    const decoded = await getAuth(admin()).verifyIdToken(idToken);
    return decoded.uid; // ← the external_id
  } catch {
    return null;
  }
}
```

<Warning>
  `user.uid` is also sitting in the browser, and it is deliberately not what gets
  sent. An id the page supplies is an id the page chose — signing it would let any
  visitor be anyone.
</Warning>

## The server half

```ts app/api/userkit-boot/route.ts theme={null}
import { createHmac } from "node:crypto";
import { NextResponse, type NextRequest } from "next/server";

import { currentExternalId } from "@/lib/firebase";

export const dynamic = "force-dynamic";
// The Admin SDK is a Node library; it does not run on the edge runtime.
export const runtime = "nodejs";

export async function GET(request: NextRequest) {
  const header = request.headers.get("authorization") ?? "";
  const idToken = header.toLowerCase().startsWith("bearer ") ? header.slice(7) : null;

  const externalId = await currentExternalId(idToken);
  if (!externalId) {
    return NextResponse.json({ error: "not_signed_in" }, { status: 401, headers: NO_STORE });
  }

  const hash = createHmac("sha256", process.env.USERKIT_IDENTITY_SECRET!)
    .update(externalId)
    .digest("hex");

  return NextResponse.json({ external_id: externalId, hash }, { headers: NO_STORE });
}

// This body identifies one person. A CDN that kept it would hand the next
// visitor a proof of somebody else's identity.
const NO_STORE = { "Cache-Control": "no-store" };
```

An ID token that will not verify answers `401`, exactly as an absent one does.
The difference is a detail about your verification that the browser has no use
for.

## The client half

```tsx theme={null}
"use client";

import { createClient } from "@userkit/js";

const userkit = createClient({
  publishableKey: process.env.NEXT_PUBLIC_USERKIT_PUBLISHABLE_KEY!,
});

const user = getAuth().currentUser;

if (!user) {
  // Still a visitor. An anonymous boot is where first-touch attribution lands.
  await userkit.boot();
} else {
  const response = await fetch("/api/userkit-boot", {
    cache: "no-store",
    headers: { Authorization: `Bearer ${await user.getIdToken()}` },
  });
  const { external_id, hash } = await response.json();
  await userkit.boot({ externalId: external_id, hash });
}
```

`getState().verified` is `true` only when the HMAC checked out. Signed in to
Firebase with `verified: false` means either the secret belongs to the other
environment or the message signed was not exactly the `external_id`.

## Session cookies work too

`createSessionCookie` is the other shape, and it makes this endpoint look like
the Supabase one: read the cookie, verify it with `verifySessionCookie`, sign
the uid. Nothing above the provider line changes.

## Configuration

| Variable                                                          | Where it lives                                                                      |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `USERKIT_IDENTITY_SECRET`                                         | **Server only.** `GET /v1/organization/environments/{id}/identity`, per environment |
| `NEXT_PUBLIC_USERKIT_PUBLISHABLE_KEY`                             | The browser. It identifies the environment and authenticates nothing                |
| The service account (`project_id`, `client_email`, `private_key`) | **Server only.** What the Admin SDK verifies with                                   |
| The Firebase web config                                           | The browser. The `apiKey` identifies a project and authenticates nothing            |

## What does not travel

A Firebase ID token carries an `email` and an `email_verified` claim. They are
statements about Firebase's records, and they do not become identities in
UserKit: an email sent through `/v1/boot` is stored as an attribute, does not
resolve to an existing contact and does not become an identity edge. The HMAC
proves the `external_id` and only the `external_id`.

To prove an address, send a [magic link or an email
code](/en/customer-auth/email-proof) — the two flows that arrive in it.

<Card title="The full example" icon="github" href="https://github.com/userkithq/monorepo/tree/main/examples/federated-firebase">
  A runnable Next app with these files, an `.env.example` and the pieces this page
  leaves out.
</Card>
