You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 7d4a339
Browse filesBrowse the repository at this point in the historyBrowse files
docs(auth): build the server app with initializeBrowserOrServerApp
The server-rendering guide's step 4 becomes the one call, the five
details become three, the App Check token gets a relay section, the
response gets a Cache-Control header, and the manual callback stays in
a "Building the app yourself" section that step 4 points at, for apps
that prefer it or whose AngularFire version has no such function. The
release modes say what a throwing ngOnDestroy does to a timed release.
Copy file name to clipboardExpand all lines: docs/app-check.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,7 +52,9 @@ App Check's reCAPTCHA providers need a browser, so AngularFire skips App Check d
52
52
53
53
### Firebase requests made during server rendering
54
54
55
-
With no App Check instance on the server, requests to Firebase made while the server renders a page carry no App Check token. If you [enforce App Check](https://firebase.google.com/docs/app-check/enable-enforcement) for a Firebase product, those requests are rejected. Firebase's recommended fix is to hand a token from the browser to the server and pass it to `initializeServerApp` as `appCheckToken`, described in [Use App Check in SSR environments](https://firebase.google.com/docs/web/ssr-apps#use-app-check).
55
+
With no App Check instance on the server, requests to Firebase made while the server renders a page carry no App Check token unless the browser relays one. If you [enforce App Check](https://firebase.google.com/docs/app-check/enable-enforcement) for a Firebase product, requests without a token are rejected. The fix is to hand the browser's token to the server and build the server app with `initializeBrowserOrServerApp`, which passes it on as `appCheckToken`. [Relaying the App Check token](auth.md#relaying-the-app-check-token) in the Authentication guide walks through it. A relayed token wins over `APP_CHECK_ON_SERVER` for Firestore, which sends the server app's token whatever App Check instance the server holds.
56
+
57
+
In development mode the server console says so once per process: "AngularFire skips App Check during server rendering, so server renders without a relayed App Check token send none", followed by the two ways to change that.
@@ -57,7 +57,7 @@ Update the imports from `import { ... } from 'firebase/auth'` to `import { ... }
57
57
58
58
## Server-side Rendering
59
59
60
-
When Angular renders your app on the server, the server does not know which user is visiting. To render the page as that user, pass their Auth ID token to `initializeServerApp`, which gives you a Firebase app that is already signed in as them.
60
+
When Angular renders your app on the server, the server does not know which user is visiting. To render the page as that user, hand their Auth ID token to `initializeBrowserOrServerApp`, which gives you a Firebase app that is already signed in as them on the server, and the ordinary Firebase app in the browser.
61
61
62
62
Getting the token to the server is your app's job. This guide keeps it in a cookie, because the browser attaches cookies to every request on its own.
The rest of this guide has no effect on routes rendered any other way.
80
80
81
+
The request context that step 3 fills and step 4 reads is provided by `AngularNodeAppEngine` and `AngularAppEngine`, the engines `ng new --ssr` sets up. An app still rendering through the older `CommonEngine` has to provide it itself, on every render:
Keep it where the generated block already was, below the block that serves static files, so real files are still served before Angular tries to render them. The rest of the file, including the part that starts the server, stays as it is.
186
194
187
-
The second argument to `handle` is what the render reads back as `REQUEST_CONTEXT`.
195
+
The second argument to `handle` is what the render reads back as `REQUEST_CONTEXT`, and `initializeBrowserOrServerApp` reads the `authIdToken` key from it. If you forget the second argument, AngularFire says so once in the server console in development mode.
196
+
197
+
The `Cache-Control` header is not optional. A signed-in render puts that visitor's data into the HTML, so a CDN or proxy that cached the response would hand it to the next visitor.
188
198
189
-
### 4. Build the server app from the token
199
+
### 4. Build the app with `initializeBrowserOrServerApp`
190
200
191
-
In `app.config.ts`, choose the Firebase app based on where the code is running, and pass that app to every Firebase provider:
201
+
In `app.config.ts`, build the Firebase app with `initializeBrowserOrServerApp` and pass that app to every Firebase provider:
`initializeBrowserOrServerApp` returns the right kind of app for where the code is running:
222
+
223
+
- In the browser, the ordinary Firebase app, as `initializeApp(firebaseConfig)` would.
224
+
- On the server, when the request context carries an `authIdToken`, a [`FirebaseServerApp`](https://firebase.google.com/docs/web/ssr-apps) signed in as that visitor. The same goes for an `appCheckToken`, see [Relaying the App Check token](#relaying-the-app-check-token).
225
+
- On the server with neither token, which is what prerendering and a signed-out visitor look like, the ordinary Firebase app, so the page renders signed out.
226
+
- On the server with a token Firebase cannot parse, the ordinary Firebase app after a warning in the server console, so the page renders signed out instead of failing the request.
227
+
228
+
#### Three details make this work
229
+
230
+
-**Keep exactly one `provideFirebaseApp`.** AngularFire hands you the app you provided only when a single one is registered, and falls back to the default app otherwise. With a second registration anywhere in your configuration, a signed-in render asks for a default app that does not exist on a freshly started server and fails.
231
+
-**Pass `inject(FirebaseApp)` to every provider, not just `provideAuth`.**`ng add @angular/fire` writes them without an argument, which resolves the default app. On a signed-in request the app is a server app instead, so a provider that asks for the default app fails outright on a freshly started server.
232
+
-**Never call `deleteApp` on the injected app.**`initializeBrowserOrServerApp` releases the server app itself. A second release from your code can delete an app another render is still using.
233
+
234
+
#### When the server app is released
235
+
236
+
By default the server app is released once the render has been garbage collected, which is also what the manual setup below does. Until then, a repeat request from the same visitor, such as a reload or a link opened in a new tab, reuses the app, and that reuse is what keeps such a request as fast as a signed-out one. A fresh server app costs a new Firestore connection and an account lookup, about 300 to 400 ms before the HTML is sent.
237
+
238
+
To release at a fixed time instead, pass `releaseAfterRender`: `0` releases the app when the render ends, which bounds memory to the renders in flight and pays that cost on every repeat request, and a positive number keeps the app for that many milliseconds after the render ends.
-**Keep exactly one `provideFirebaseApp`.** AngularFire hands you the app you provided only when a single one is registered, and falls back to the default app otherwise. A second registration anywhere in your configuration would make the server app be silently ignored.
239
-
-**Pass `inject(FirebaseApp)` to every provider, not just `provideAuth`.**`ng add @angular/fire` writes them without an argument, which resolves the default app. On a signed-in request the factory above builds a server app instead, so a provider that asks for the default app fails outright on a freshly started server.
240
-
-**Keep the signed-out fallback.** There is no request context when Angular prerenders a page, and no token when the visitor is signed out, so the fallback builds an ordinary Firebase app and the page renders signed out.
241
-
-**Pass `releaseOnDeref`.** It tells the SDK when it may release the server app. The SDK watches the object you give it and releases once that object is garbage collected, so pass one that lives exactly as long as the render, such as the request context itself. Leave it out and the SDK requires you to call `deleteApp` yourself for each server app you create.
242
-
-**The cast is needed** because Angular types `REQUEST_CONTEXT` as `unknown`.
244
+
A runtime without `FinalizationRegistry` cannot release at garbage collection, so the app is released when the render ends there whatever the setting.
243
245
244
-
AngularFire's [sample app](https://github.com/angular/angularfire/tree/main/sample) does this differently, giving the browser and the server their own `app.config.client.ts` and `app.config.server.ts` instead of deciding at runtime, inside a single `provideFirebaseApp` factory, which of `initializeApp` and `initializeServerApp` to call. That is also fine, and it keeps the server-only code out of the browser bundle, at the cost of an extra file to wire up.
246
+
With `releaseAfterRender` set, the release runs when Angular destroys the render, after your services' `ngOnDestroy` hooks. A service whose `ngOnDestroy` throws stops that teardown, so the server app is never released and one stays behind for every signed-in render. The default release does not depend on teardown and is not affected.
245
247
246
-
ID tokens are short-lived, and a returning visitor's browser can send one that expired while the tab was closed. The server cannot refresh it, because a user restored from an ID token has no refresh token, so Firebase logs an error and the page renders signed out. The browser then refreshes the token and the page updates.
248
+
AngularFire's [sample app](https://github.com/angular/angularfire/tree/main/sample) gives the browser and the server their own `app.config.client.ts` and `app.config.server.ts`. `initializeBrowserOrServerApp` works there too, in the server file, with the client file keeping `initializeApp`.
249
+
250
+
ID tokens are short-lived, and a returning visitor's browser can send one that expired while the tab was closed. The server cannot refresh it, because a user restored from an ID token has no refresh token, so Firebase logs an error and the page renders signed out. The browser then refreshes the token and the page updates. A token that cannot be parsed at all, such as a cookie another site set under the same name, also renders signed out, with one warning in the server console.
251
+
252
+
If you would rather build the app yourself, or your AngularFire version has no `initializeBrowserOrServerApp`, see [Building the app yourself](#building-the-app-yourself) below.
247
253
248
254
### Using `REQUEST` instead of a cookie
249
255
250
-
Angular also exposes the request itself through the `REQUEST` token, so you can read the ID token from an `Authorization` header rather than a cookie. Firebase's [session management with service workers](https://firebase.google.com/docs/auth/web/service-worker-sessions) guide covers attaching that header. Steps 1, 3 and 4 stay the same apart from the server half of the factory, which becomes:
256
+
Angular also exposes the request itself through the `REQUEST` token, so you can read the ID token from an `Authorization` header rather than a cookie. Firebase's [session management with service workers](https://firebase.google.com/docs/auth/web/service-worker-sessions) guide covers attaching that header. Steps 1, 3 and 4 stay the same apart from the call in step 4, which passes the token explicitly:
`REQUEST` is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), so headers are read with `headers.get(...)`. Angular sets it to `null` during builds, during static site generation, and during route extraction in development, and it is only supplied at all on `RenderMode.Server` routes. A missing header gives `undefined`, and `initializeBrowserOrServerApp` then looks in the request context and falls back to the ordinary app.
267
+
268
+
### Relaying the App Check token
269
+
270
+
If you [enforce App Check](https://firebase.google.com/docs/app-check/enable-enforcement) for a Firebase product, server renders need an App Check token too, or their requests are rejected. The server cannot mint one, so the browser relays its own, and `initializeBrowserOrServerApp` passes it to the server app as `appCheckToken`.
271
+
272
+
Relay the ordinary token from `getToken`, not a limited-use one. Replay protection only applies where a backend consumes the token, such as a callable Cloud Function with `consumeAppCheckToken`, and product enforcement for Firestore, Realtime Database and Storage does not.
273
+
274
+
Keep both tokens in the one `__session` cookie, since behind Firebase Hosting no other cookie reaches your server. In the cookie sync from step 2, write the cookie as JSON and rewrite it whenever either token changes:
The `beforeAuthStateChanged` part of step 2 stays as it is, writing `authIdToken` and calling `writeSessionCookie()` instead of passing the token in. In `server.ts`, unpack the cookie and pass both keys:
-**Every visitor's first server render builds a server app.** An App Check token is per device, so even signed-out visitors get a server app carrying it, and that first render pays the 300 to 400 ms described above. Relay the token only if you enforce App Check.
340
+
-**A stale cookie fails the first request after the token's lifetime.** The browser rewrites the cookie only after the page's JavaScript has run and Firebase has refreshed the token. A visitor who returns after about an hour sends the old one, so that first server render's Firebase requests are rejected under enforcement, and the page shows whatever your app shows for a failed read until it hydrates and reads again. For the ID token the same stale first request only means a signed-out render. If that first render must succeed, Firebase's [service-worker relay](https://firebase.google.com/docs/web/ssr-apps#use-app-check) refreshes the token before the request leaves the browser, at the cost of a service worker your app registers itself.
341
+
-**A relayed token wins over `APP_CHECK_ON_SERVER` for Firestore.** When the server app carries an `appCheckToken`, Firestore sends it whatever App Check instance the server holds.
342
+
343
+
### Building the app yourself
344
+
345
+
If you would rather build the app yourself, or your AngularFire version does not export `initializeBrowserOrServerApp`, choose the app in step 4 with your own callback. The `try` around `initializeServerApp` is what keeps a token Firebase cannot parse from failing the request with an HTTP 500, and `releaseOnDeref` is what releases the server app, the way `initializeBrowserOrServerApp` does by default:
if (!hasAuthIdToken(requestContext) ||!requestContext.authIdToken) {
361
+
returninitializeApp(firebaseConfig);
362
+
}
363
+
try {
364
+
returninitializeServerApp(firebaseConfig, {
365
+
authIdToken: requestContext.authIdToken,
366
+
releaseOnDeref: requestContext,
367
+
});
368
+
} catch {
262
369
returninitializeApp(firebaseConfig);
263
370
}
264
-
returninitializeServerApp(firebaseConfig, {
265
-
authIdToken,
266
-
releaseOnDeref: request,
267
-
});
268
371
}),
269
372
```
270
373
271
-
`REQUEST` is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), so headers are read with `headers.get(...)`. Angular sets it to `null` during builds, during static site generation, and during route extraction in development, and it is only supplied at all on `RenderMode.Server` routes, so keep the signed-out fallback for those passes.
374
+
Keep the signed-out fallback, since there is no request context when Angular prerenders a page and no token when the visitor is signed out, and keep the first two of the three details above. An app that already has this callback can switch to `initializeBrowserOrServerApp` in one deploy. The callback keeps working on later versions too.
0 commit comments