From 6deeefd8101a6f14f3a265aebcbfd77aafe8a34a Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Fri, 25 Sep 2026 14:38:43 -0700 Subject: [PATCH] fix(app-check)!: skip App Check during server rendering by default provideAppCheck ran the app's setup function during server rendering, where the reCAPTCHA providers throw "document is not defined", so following docs/app-check.md broke every prerender and server render. The workaround users posted, returning nothing on the server, failed to compile and left an empty AppCheck object behind. The setup function now runs only in the browser, unless the server config provides APP_CHECK_ON_SERVER as true for a server-capable provider. A skipped server injects AppCheck as null, and development mode warns once. Firebase's documented route for App Check in server renders, an appCheckToken passed to initializeServerApp, is unaffected. BREAKING CHANGE: App Check no longer runs during server rendering unless APP_CHECK_ON_SERVER is provided as true. Fixes #3488 Fixes #3128 Refs #3089 --- src/app-check/app-check.module.ts | 28 ++++++++++-- src/app-check/app-check.spec.ts | 73 +++++++++++++++++++++++++++++++ src/app-check/public_api.ts | 2 +- 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/app-check/app-check.module.ts b/src/app-check/app-check.module.ts index b68f848bc..14860e3f1 100644 --- a/src/app-check/app-check.module.ts +++ b/src/app-check/app-check.module.ts @@ -16,19 +16,39 @@ import { registerVersion } from 'firebase/app'; import { AppCheck as FirebaseAppCheck } from 'firebase/app-check'; import { APP_CHECK_PROVIDER_NAME, AppCheck, AppCheckInstances } from './app-check'; -export const PROVIDED_APP_CHECK_INSTANCES = new InjectionToken('angularfire2.app-check-instances'); +export const PROVIDED_APP_CHECK_INSTANCES = new InjectionToken<(AppCheck|undefined)[]>('angularfire2.app-check-instances'); -export function defaultAppCheckInstanceFactory(provided: FirebaseAppCheck[]|undefined, defaultApp: FirebaseApp) { - const defaultAppCheck = ɵgetDefaultInstanceOf(APP_CHECK_PROVIDER_NAME, provided, defaultApp); +/** + * Set to `true` in a server-only config to run `provideAppCheck` during server rendering. + * By default App Check is skipped there, and `AppCheck` injects as `null`. + */ +export const APP_CHECK_ON_SERVER = new InjectionToken('angularfire2.app-check-on-server', { + providedIn: 'root', + factory: () => false, +}); + +export function defaultAppCheckInstanceFactory(provided: (FirebaseAppCheck|undefined)[]|undefined, defaultApp: FirebaseApp) { + // A skipped server render leaves an undefined entry, which ɵgetDefaultInstanceOf cannot read. + const providedAppChecks = provided?.filter((appCheck): appCheck is FirebaseAppCheck => !!appCheck); + const defaultAppCheck = ɵgetDefaultInstanceOf(APP_CHECK_PROVIDER_NAME, providedAppChecks, defaultApp); return defaultAppCheck && new AppCheck(defaultAppCheck); } const LOCALHOSTS = ['localhost', '0.0.0.0', '127.0.0.1']; const isLocalhost = typeof window !== 'undefined' && LOCALHOSTS.includes(window.location.hostname); +let warnedServerSkip = false; + export function appCheckInstanceFactory(fn: (injector: Injector) => FirebaseAppCheck) { return (zone: NgZone, injector: Injector, platformId: unknown) => { - // Node should use admin token provider, browser devmode and localhost should use debug token + if (isPlatformServer(platformId) && !injector.get(APP_CHECK_ON_SERVER)) { + if (isDevMode() && !warnedServerSkip) { + warnedServerSkip = true; + console.warn("AngularFire skips App Check during server rendering, so server-side Firebase requests carry no App Check token. To run App Check there, provide APP_CHECK_ON_SERVER as true in your server config. Find more at https://github.com/angular/angularfire/blob/main/docs/app-check.md"); + } + return undefined; + } + // Browser dev mode and localhost use the debug token if (!isPlatformServer(platformId) && (isDevMode() || isLocalhost)) { globalThis.FIREBASE_APPCHECK_DEBUG_TOKEN ??= true; } diff --git a/src/app-check/app-check.spec.ts b/src/app-check/app-check.spec.ts index b51e91e0e..dd88f1e13 100644 --- a/src/app-check/app-check.spec.ts +++ b/src/app-check/app-check.spec.ts @@ -1,5 +1,7 @@ +import { PLATFORM_ID } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app'; +import { APP_CHECK_ON_SERVER, AppCheck, CustomProvider, initializeAppCheck, provideAppCheck } from '@angular/fire/app-check'; import { Auth, connectAuthEmulator, getAuth, provideAuth } from '@angular/fire/auth'; import { COMMON_CONFIG } from '../test-config'; import { rando } from '../utils'; @@ -37,3 +39,74 @@ describe('Auth', () => { }); }); + +describe('AppCheck', () => { + let appName: string; + let setupCalls: number; + + const provideTestAppCheck = () => provideAppCheck(() => { + setupCalls++; + return initializeAppCheck(getApp(appName), { + provider: new CustomProvider({ getToken: () => Promise.resolve({ token: 'test-token', expireTimeMillis: Date.now() + 3_600_000 }) }), + }); + }); + + beforeEach(() => { + appName = rando(); + setupCalls = 0; + }); + + it('should skip App Check during server rendering by default', () => { + TestBed.configureTestingModule({ + providers: [ + { provide: PLATFORM_ID, useValue: 'server' }, + provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)), + provideTestAppCheck(), + ], + }); + expect(TestBed.inject(AppCheck)).toBeNull(); + expect(setupCalls).toBe(0); + }); + + it('should skip every App Check setup during server rendering when provided more than once', () => { + TestBed.configureTestingModule({ + providers: [ + { provide: PLATFORM_ID, useValue: 'server' }, + provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)), + provideTestAppCheck(), + provideTestAppCheck(), + ], + }); + expect(TestBed.inject(AppCheck)).toBeNull(); + expect(setupCalls).toBe(0); + }); + + it('should run App Check during server rendering when APP_CHECK_ON_SERVER is true', () => { + TestBed.configureTestingModule({ + providers: [ + { provide: PLATFORM_ID, useValue: 'server' }, + { provide: APP_CHECK_ON_SERVER, useValue: true }, + provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)), + provideTestAppCheck(), + ], + }); + const appCheck = TestBed.inject(AppCheck); + expect(setupCalls).toBe(1); + expect(appCheck).toBeTruthy(); + expect(appCheck.app).toEqual(TestBed.inject(FirebaseApp)); + }); + + it('should run App Check in the browser', () => { + TestBed.configureTestingModule({ + providers: [ + provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)), + provideTestAppCheck(), + ], + }); + const appCheck = TestBed.inject(AppCheck); + expect(setupCalls).toBe(1); + expect(appCheck).toBeTruthy(); + expect(appCheck.app).toEqual(TestBed.inject(FirebaseApp)); + }); + +}); diff --git a/src/app-check/public_api.ts b/src/app-check/public_api.ts index bab774cf3..c5704ef08 100644 --- a/src/app-check/public_api.ts +++ b/src/app-check/public_api.ts @@ -1,3 +1,3 @@ export { AppCheck, appCheckInstance$, AppCheckInstances } from './app-check'; -export { provideAppCheck, AppCheckModule } from './app-check.module'; +export { provideAppCheck, AppCheckModule, APP_CHECK_ON_SERVER } from './app-check.module'; export * from './firebase';