Skip to content

Commit 84a8a51

Browse files
fix(app-check)!: skip App Check during server rendering by default (#3773)
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
1 parent 38dfc3f commit 84a8a51

3 files changed

Lines changed: 98 additions & 5 deletions

File tree

‎src/app-check/app-check.module.ts‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,39 @@ import { registerVersion } from 'firebase/app';
1616
import { AppCheck as FirebaseAppCheck } from 'firebase/app-check';
1717
import { APP_CHECK_PROVIDER_NAME, AppCheck, AppCheckInstances } from './app-check';
1818

19-
export const PROVIDED_APP_CHECK_INSTANCES = new InjectionToken<AppCheck[]>('angularfire2.app-check-instances');
19+
export const PROVIDED_APP_CHECK_INSTANCES = new InjectionToken<(AppCheck|undefined)[]>('angularfire2.app-check-instances');
2020

21-
export function defaultAppCheckInstanceFactory(provided: FirebaseAppCheck[]|undefined, defaultApp: FirebaseApp) {
22-
const defaultAppCheck = ɵgetDefaultInstanceOf<FirebaseAppCheck>(APP_CHECK_PROVIDER_NAME, provided, defaultApp);
21+
/**
22+
* Set to `true` in a server-only config to run `provideAppCheck` during server rendering.
23+
* By default App Check is skipped there, and `AppCheck` injects as `null`.
24+
*/
25+
export const APP_CHECK_ON_SERVER = new InjectionToken<boolean>('angularfire2.app-check-on-server', {
26+
providedIn: 'root',
27+
factory: () => false,
28+
});
29+
30+
export function defaultAppCheckInstanceFactory(provided: (FirebaseAppCheck|undefined)[]|undefined, defaultApp: FirebaseApp) {
31+
// A skipped server render leaves an undefined entry, which ɵgetDefaultInstanceOf cannot read.
32+
const providedAppChecks = provided?.filter((appCheck): appCheck is FirebaseAppCheck => !!appCheck);
33+
const defaultAppCheck = ɵgetDefaultInstanceOf<FirebaseAppCheck>(APP_CHECK_PROVIDER_NAME, providedAppChecks, defaultApp);
2334
return defaultAppCheck && new AppCheck(defaultAppCheck);
2435
}
2536

2637
const LOCALHOSTS = ['localhost', '0.0.0.0', '127.0.0.1'];
2738
const isLocalhost = typeof window !== 'undefined' && LOCALHOSTS.includes(window.location.hostname);
2839

40+
let warnedServerSkip = false;
41+
2942
export function appCheckInstanceFactory(fn: (injector: Injector) => FirebaseAppCheck) {
3043
return (zone: NgZone, injector: Injector, platformId: unknown) => {
31-
// Node should use admin token provider, browser devmode and localhost should use debug token
44+
if (isPlatformServer(platformId) && !injector.get(APP_CHECK_ON_SERVER)) {
45+
if (isDevMode() && !warnedServerSkip) {
46+
warnedServerSkip = true;
47+
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");
48+
}
49+
return undefined;
50+
}
51+
// Browser dev mode and localhost use the debug token
3252
if (!isPlatformServer(platformId) && (isDevMode() || isLocalhost)) {
3353
globalThis.FIREBASE_APPCHECK_DEBUG_TOKEN ??= true;
3454
}

‎src/app-check/app-check.spec.ts‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { PLATFORM_ID } from '@angular/core';
12
import { TestBed } from '@angular/core/testing';
23
import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
4+
import { APP_CHECK_ON_SERVER, AppCheck, CustomProvider, initializeAppCheck, provideAppCheck } from '@angular/fire/app-check';
35
import { Auth, connectAuthEmulator, getAuth, provideAuth } from '@angular/fire/auth';
46
import { COMMON_CONFIG } from '../test-config';
57
import { rando } from '../utils';
@@ -37,3 +39,74 @@ describe('Auth', () => {
3739
});
3840

3941
});
42+
43+
describe('AppCheck', () => {
44+
let appName: string;
45+
let setupCalls: number;
46+
47+
const provideTestAppCheck = () => provideAppCheck(() => {
48+
setupCalls++;
49+
return initializeAppCheck(getApp(appName), {
50+
provider: new CustomProvider({ getToken: () => Promise.resolve({ token: 'test-token', expireTimeMillis: Date.now() + 3_600_000 }) }),
51+
});
52+
});
53+
54+
beforeEach(() => {
55+
appName = rando();
56+
setupCalls = 0;
57+
});
58+
59+
it('should skip App Check during server rendering by default', () => {
60+
TestBed.configureTestingModule({
61+
providers: [
62+
{ provide: PLATFORM_ID, useValue: 'server' },
63+
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
64+
provideTestAppCheck(),
65+
],
66+
});
67+
expect(TestBed.inject(AppCheck)).toBeNull();
68+
expect(setupCalls).toBe(0);
69+
});
70+
71+
it('should skip every App Check setup during server rendering when provided more than once', () => {
72+
TestBed.configureTestingModule({
73+
providers: [
74+
{ provide: PLATFORM_ID, useValue: 'server' },
75+
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
76+
provideTestAppCheck(),
77+
provideTestAppCheck(),
78+
],
79+
});
80+
expect(TestBed.inject(AppCheck)).toBeNull();
81+
expect(setupCalls).toBe(0);
82+
});
83+
84+
it('should run App Check during server rendering when APP_CHECK_ON_SERVER is true', () => {
85+
TestBed.configureTestingModule({
86+
providers: [
87+
{ provide: PLATFORM_ID, useValue: 'server' },
88+
{ provide: APP_CHECK_ON_SERVER, useValue: true },
89+
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
90+
provideTestAppCheck(),
91+
],
92+
});
93+
const appCheck = TestBed.inject(AppCheck);
94+
expect(setupCalls).toBe(1);
95+
expect(appCheck).toBeTruthy();
96+
expect(appCheck.app).toEqual(TestBed.inject(FirebaseApp));
97+
});
98+
99+
it('should run App Check in the browser', () => {
100+
TestBed.configureTestingModule({
101+
providers: [
102+
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
103+
provideTestAppCheck(),
104+
],
105+
});
106+
const appCheck = TestBed.inject(AppCheck);
107+
expect(setupCalls).toBe(1);
108+
expect(appCheck).toBeTruthy();
109+
expect(appCheck.app).toEqual(TestBed.inject(FirebaseApp));
110+
});
111+
112+
});

‎src/app-check/public_api.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { AppCheck, appCheckInstance$, AppCheckInstances } from './app-check';
2-
export { provideAppCheck, AppCheckModule } from './app-check.module';
2+
export { provideAppCheck, AppCheckModule, APP_CHECK_ON_SERVER } from './app-check.module';
33
export * from './firebase';

0 commit comments

Comments
 (0)