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';