firebase / firebase/firebase-js-sdk

FirebaseServerApp: transiently failed authIdToken sign-in is cached and served to all requests until token rotation, with no recovery API

Open
#10,213 1 comment 0 reactions 0 assignees View on GitHub
api: auth feature request stack:NextJS
Dominant language
TypeScript
Stars
5.1k
Forks
1k
Avg merge
2d 21h
Merged PRs (30d)
37

Description

### Operating System

macOS 26 (local dev); Linux (Firebase App Hosting / Cloud Run) in production

### Environment (if applicable)

Node.js v22.22.3, server-side rendering (no browser). Next.js 16.2.11 App Router with Cache Components (abortable prerender passes).

### Firebase SDK Version

12.16.0

### Firebase SDK Product(s)

Auth, Component

### Project Tooling

Next.js 16.2.11 (App Router, `cacheComponents: true`) deployed on Firebase App Hosting; `initializeServerApp` + `getAuth` used per request to resume the client session server-side from the `Authorization` header; service worker attaches the ID token to document requests.

### Detailed Problem Description

**What we were trying to achieve:** resume a signed-in user's session server-side (`initializeServerApp` with `authIdToken`, then `getAuth().authStateReady()`) during SSR, where the framework may also start and later discard abortable prerender passes over the same route.

**What actually happened:** `initializeServerApp` deduplicates instances byhashing settings + options (`packages/app/src/api.ts`), so every callerpresenting the same `authIdToken` shares one `FirebaseServerApp`, and the `authIdToken` sign-in for that instance is attempted exactly once. When that one-shot sign-in fails *transiently*, the failure becomes sticky:

1. In our case, the login fetch started inside a prerender pass that Next.js later discarded, so the fetch was aborted:

```
FirebaseServerApp could not login user with provided authIdToken: Error [FirebaseError]: Firebase: Error (auth/network-request-failed).
at ignore-listed frames {
code: 'auth/network-request-failed',
customData: {
message: 'AbortError: This operation was aborted',
appName: '-120870960'
}
}
```
Any transient network error on the first login for a given token behaves the same way.

2. The shared instance now reports `currentUser === null`, permanently.
3. Because the instance is cached under the token hash, **every subsequent request presenting the same ID token receives the poisoned instance** and is treated as signed out — for the remaining lifetime of the token (typically up to an hour) — while the client remains fully authenticated. The only user-side fix is signing out and back in to mint a new token.
4. There is no public recovery path:
- Re-calling `initializeServerApp` with the same tokens returns the cached instance by design.
- `deleteApp` only decrements the refcount; with `releaseOnDeref` registrations outstanding from other requests, a single request's caller cannot deterministically evict the instance (`packages/app/src/firebaseServerApp.ts`).
- `FirebaseServerAppSettings` is `Omit`, so there is no typed field that can vary the identity hash to obtain a fresh instance.

The symptom is severe out of proportion to the trigger: one aborted render silently signs a user out server-side for up to an hour. Abortable server renders are now the default in Next.js (PPR / Cache Components), so logins beginning inside discardable passes is an easy state for SSR apps to reach. #8347 and #9317 report the same log line from other causes; this issue is about the *persistence* of the failure rather than any individual cause.

**Workaround we ship today (undocumented behavior):** the identity hash currently includes every settings entry except `releaseOnDeref`, so an extra untyped settings property forces a genuinely fresh instance, which retries the login:

```ts
const settings = { authIdToken, releaseOnDeref: requestHeaders };
(settings as Record).isolation = crypto.randomUUID();
const fresh = initializeServerApp(firebaseConfig, settings); // fresh login
```

This works, but it leans on unspecified behavior — we pin it with a test against SDK upgrades and would much rather use something supported.

**Requested resolution** (any one of these):

1. A supported identity/isolation mechanism — e.g. restore `name` on `FirebaseServerAppSettings`, or a typed field such as `instanceIdentity?: string` that participates in the instance hash, so callers can opt out of instance sharing per request.
2. A recovery API — re-attempt the `authIdToken` sign-in on an existing instance, or don't cache an instance whose initial sign-in failed, so the next `initializeServerApp` call gets a clean one.
3. At minimum, document that all settings entries participate in instance identity, so the workaround above is acknowledged as supported.

### Steps and code to reproduce issue

The failure mode is SDK-only; the framework is just one way to abort the first login fetch. Conceptually:

```ts
// Request A (later aborted): starts the one-shot login for token T
const a = initializeServerApp(config, { authIdToken: T, releaseOnDeref: headersA });
getAuth(a); // login fetch begins; the surrounding render — and its fetch — is aborted

// Requests B..N (healthy, same token T, for up to ~1h):
const b = initializeServerApp(config, { authIdToken: T, releaseOnDeref: headersB });
await getAuth(b).authStateReady();
getAuth(b).currentUser; // null — same poisoned instance, no way to retry
```

A standalone script can't easily interrupt a live token exchange, but the poisoned-sharing step is directly observable by aborting the first login's fetch (e.g. patch `fetch` to reject the first `accounts:lookup` / `getAccountInfo` call with an `AbortError`), then initializing again with the same token: the second `initializeServerApp` returns the same instance and `currentUser` stays `null` even though the token is valid.

Deterministic end-to-end reproduction (how we hit it): Next.js 16 with `cacheComponents: true`, a route that calls `initializeServerApp` with the request's `Authorization` bearer token during render, and direct navigation in a new tab so a runtime prerender pass begins the login and is then discarded. After one occurrence, every request with that token observes `currentUser === null` until the token rotates.

Contributor guide

Open the contributing guide

Research direction

Start with initializeServerApp in packages/app/src/api.ts and the reference-counting behavior in packages/app/src/firebaseServerApp.ts; then trace getAuth(a).authStateReady() in the supplied reproduction. Verify that an aborted first authIdToken login does not permanently poison later initialization with the same token, and that the chosen recovery or isolation behavior is supported and documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
firebase, nextjs, node.js, typescript
Domain
authentication, backend
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.