From 25f264fb2247e4f630a0cb6c2dd38681caabb637 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 22 Nov 2024 11:32:47 -0500 Subject: [PATCH 01/22] Initial implementation across all SDKs. --- common/api-review/app.api.md | 1 + packages/app/src/public-types.ts | 6 ++++++ packages/auth/src/core/auth/auth_impl.ts | 3 +++ packages/data-connect/src/api/DataConnect.ts | 12 +++++------- .../src/core/AppCheckTokenProvider.ts | 11 ++++++++++- packages/database/src/api/Database.ts | 2 +- .../database/src/core/AppCheckTokenProvider.ts | 14 ++++++++++++-- packages/firestore/lite/register.ts | 1 + packages/firestore/src/api/credentials.ts | 16 ++++++++++++++++ packages/firestore/src/register.ts | 1 + packages/functions/src/context.ts | 9 +++++++++ packages/functions/src/service.ts | 1 + packages/storage/src/service.ts | 9 ++++++++- packages/vertexai/src/models/generative-model.ts | 13 ++++++++++++- 14 files changed, 86 insertions(+), 13 deletions(-) diff --git a/common/api-review/app.api.md b/common/api-review/app.api.md index bdfb2a681f1..e9dccfb3e51 100644 --- a/common/api-review/app.api.md +++ b/common/api-review/app.api.md @@ -79,6 +79,7 @@ export interface FirebaseServerApp extends FirebaseApp { // @public export interface FirebaseServerAppSettings extends Omit { + appCheckToken?: string; authIdToken?: string; releaseOnDeref?: object; } diff --git a/packages/app/src/public-types.ts b/packages/app/src/public-types.ts index ff25de93a46..c793d9e44d5 100644 --- a/packages/app/src/public-types.ts +++ b/packages/app/src/public-types.ts @@ -196,6 +196,12 @@ export interface FirebaseServerAppSettings */ authIdToken?: string; + /** + * An optional App Check token. If provided, the Firebase SDKs that use App Check will utilizze + * this App Check token in lieu of requiring an instance of App Check to be initialized. + */ + appCheckToken?: string; + /** * An optional object. If provided, the Firebase SDK uses a `FinalizationRegistry` * object to monitor the garbage collection status of the provided object. The diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index fd6f1a82a76..45a2c99ea0b 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -845,6 +845,9 @@ export class AuthImpl implements AuthInternal, _FirebaseService { } async _getAppCheckToken(): Promise { + if (_isFirebaseServerApp(this.app) && this.app.settings.appCheckToken) { + return this.app.settings.appCheckToken; + } const appCheckTokenResult = await this.appCheckServiceProvider .getImmediate({ optional: true }) ?.getToken(); diff --git a/packages/data-connect/src/api/DataConnect.ts b/packages/data-connect/src/api/DataConnect.ts index 27ab83660fd..e3e37e232df 100644 --- a/packages/data-connect/src/api/DataConnect.ts +++ b/packages/data-connect/src/api/DataConnect.ts @@ -92,7 +92,7 @@ export class DataConnect { private _transportOptions?: TransportOptions; private _authTokenProvider?: AuthTokenProvider; _isUsingGeneratedSdk: boolean = false; - private _appCheckTokenProvider?: AppCheckTokenProvider; + private _appCheckTokenProvider: AppCheckTokenProvider; // @internal constructor( public readonly app: FirebaseApp, @@ -149,12 +149,10 @@ export class DataConnect { this._authProvider ); } - if (this._appCheckProvider) { - this._appCheckTokenProvider = new AppCheckTokenProvider( - this.app.name, - this._appCheckProvider - ); - } + this._appCheckTokenProvider = new AppCheckTokenProvider( + this.app, + this._appCheckProvider + ); this._initialized = true; this._transport = new this._transportClass( diff --git a/packages/data-connect/src/core/AppCheckTokenProvider.ts b/packages/data-connect/src/core/AppCheckTokenProvider.ts index d9cdaeb6f39..90f7ae805d1 100644 --- a/packages/data-connect/src/core/AppCheckTokenProvider.ts +++ b/packages/data-connect/src/core/AppCheckTokenProvider.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { FirebaseApp, _isFirebaseServerApp } from '@firebase/app'; import { AppCheckInternalComponentName, AppCheckTokenListener, @@ -29,10 +30,14 @@ import { Provider } from '@firebase/component'; */ export class AppCheckTokenProvider { private appCheck?: FirebaseAppCheckInternal; + private serverAppAppCheckToken?: string; constructor( - private appName_: string, + app: FirebaseApp, private appCheckProvider?: Provider ) { + if (_isFirebaseServerApp(app) && app.settings.appCheckToken) { + this.serverAppAppCheckToken = app.settings.appCheckToken; + } this.appCheck = appCheckProvider?.getImmediate({ optional: true }); if (!this.appCheck) { void appCheckProvider @@ -43,6 +48,10 @@ export class AppCheckTokenProvider { } getToken(forceRefresh?: boolean): Promise { + if (this.serverAppAppCheckToken) { + return Promise.resolve({ token: this.serverAppAppCheckToken }); + } + if (!this.appCheck) { return new Promise((resolve, reject) => { // Support delayed initialization of FirebaseAppCheck. This allows our diff --git a/packages/database/src/api/Database.ts b/packages/database/src/api/Database.ts index 3182365dda0..72ae85c08a1 100644 --- a/packages/database/src/api/Database.ts +++ b/packages/database/src/api/Database.ts @@ -164,7 +164,7 @@ export function repoManagerDatabaseFromApp( repoInfo, app, authTokenProvider, - new AppCheckTokenProvider(app.name, appCheckProvider) + new AppCheckTokenProvider(app, appCheckProvider) ); return new Database(repo, app); } diff --git a/packages/database/src/core/AppCheckTokenProvider.ts b/packages/database/src/core/AppCheckTokenProvider.ts index 2bdd8fdadac..7876bbd27e2 100644 --- a/packages/database/src/core/AppCheckTokenProvider.ts +++ b/packages/database/src/core/AppCheckTokenProvider.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { FirebaseApp, _isFirebaseServerApp } from '@firebase/app'; // eslint-disable-line import/no-extraneous-dependencies import { AppCheckInternalComponentName, AppCheckTokenListener, @@ -30,10 +31,16 @@ import { warn } from './util/util'; */ export class AppCheckTokenProvider { private appCheck?: FirebaseAppCheckInternal; + private serverAppAppCheckToken?: string; + private appName: string; constructor( - private appName_: string, + app: FirebaseApp, private appCheckProvider?: Provider ) { + this.appName = app.name; + if (_isFirebaseServerApp(app) && app.settings.appCheckToken) { + this.serverAppAppCheckToken = app.settings.appCheckToken; + } this.appCheck = appCheckProvider?.getImmediate({ optional: true }); if (!this.appCheck) { appCheckProvider?.get().then(appCheck => (this.appCheck = appCheck)); @@ -41,6 +48,9 @@ export class AppCheckTokenProvider { } getToken(forceRefresh?: boolean): Promise { + if (this.serverAppAppCheckToken) { + return Promise.resolve({ token: this.serverAppAppCheckToken }); + } if (!this.appCheck) { return new Promise((resolve, reject) => { // Support delayed initialization of FirebaseAppCheck. This allows our @@ -67,7 +77,7 @@ export class AppCheckTokenProvider { notifyForInvalidToken(): void { warn( - `Provided AppCheck credentials for the app named "${this.appName_}" ` + + `Provided AppCheck credentials for the app named "${this.appName}" ` + 'are invalid. This usually indicates your app was not initialized correctly.' ); } diff --git a/packages/firestore/lite/register.ts b/packages/firestore/lite/register.ts index 9bd7b014fa2..300f9d5ec94 100644 --- a/packages/firestore/lite/register.ts +++ b/packages/firestore/lite/register.ts @@ -49,6 +49,7 @@ export function registerFirestore(): void { container.getProvider('auth-internal') ), new LiteAppCheckTokenProvider( + app, container.getProvider('app-check-internal') ), databaseIdFromApp(app, databaseId), diff --git a/packages/firestore/src/api/credentials.ts b/packages/firestore/src/api/credentials.ts index 5da9a32209b..cbd02444282 100644 --- a/packages/firestore/src/api/credentials.ts +++ b/packages/firestore/src/api/credentials.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { FirebaseApp, _isFirebaseServerApp } from '@firebase/app'; import { AppCheckInternalComponentName, AppCheckTokenListener, @@ -497,6 +498,7 @@ export class FirebaseAppCheckTokenProvider private latestAppCheckToken: string | null = null; constructor( + private app: FirebaseApp, private appCheckProvider: Provider ) {} @@ -562,6 +564,11 @@ export class FirebaseAppCheckTokenProvider } getToken(): Promise { + if (_isFirebaseServerApp(this.app) && this.app.settings.appCheckToken) { + return Promise.resolve( + new AppCheckToken(this.app.settings.appCheckToken) + ); + } debugAssert( this.tokenListener != null, 'FirebaseAppCheckTokenProvider not started.' @@ -622,16 +629,25 @@ export class EmptyAppCheckTokenProvider implements CredentialsProvider { /** AppCheck token provider for the Lite SDK. */ export class LiteAppCheckTokenProvider implements CredentialsProvider { private appCheck: FirebaseAppCheckInternal | null = null; + private serverAppAppCheckToken?: string; constructor( + private app: FirebaseApp, private appCheckProvider: Provider ) { + if (_isFirebaseServerApp(app) && app.settings.appCheckToken) { + this.serverAppAppCheckToken = app.settings.appCheckToken; + } appCheckProvider.onInit(appCheck => { this.appCheck = appCheck; }); } getToken(): Promise { + if (this.serverAppAppCheckToken) { + return Promise.resolve(new AppCheckToken(this.serverAppAppCheckToken)); + } + if (!this.appCheck) { return Promise.resolve(null); } diff --git a/packages/firestore/src/register.ts b/packages/firestore/src/register.ts index 573ac6f2020..82b450b3834 100644 --- a/packages/firestore/src/register.ts +++ b/packages/firestore/src/register.ts @@ -47,6 +47,7 @@ export function registerFirestore( container.getProvider('auth-internal') ), new FirebaseAppCheckTokenProvider( + app, container.getProvider('app-check-internal') ), databaseIdFromApp(app, databaseId), diff --git a/packages/functions/src/context.ts b/packages/functions/src/context.ts index 0013e2c54f6..37483c9f4fd 100644 --- a/packages/functions/src/context.ts +++ b/packages/functions/src/context.ts @@ -16,6 +16,7 @@ */ import { Provider } from '@firebase/component'; +import { _isFirebaseServerApp, FirebaseApp } from '@firebase/app'; import { AppCheckInternalComponentName, FirebaseAppCheckInternal @@ -47,11 +48,16 @@ export class ContextProvider { private auth: FirebaseAuthInternal | null = null; private messaging: MessagingInternal | null = null; private appCheck: FirebaseAppCheckInternal | null = null; + private serverAppAppCheckToken: string | null = null; constructor( + readonly app: FirebaseApp, authProvider: Provider, messagingProvider: Provider, appCheckProvider: Provider ) { + if (_isFirebaseServerApp(app) && app.settings.appCheckToken) { + this.serverAppAppCheckToken = app.settings.appCheckToken; + } this.auth = authProvider.getImmediate({ optional: true }); this.messaging = messagingProvider.getImmediate({ optional: true @@ -122,6 +128,9 @@ export class ContextProvider { async getAppCheckToken( limitedUseAppCheckTokens?: boolean ): Promise { + if (this.serverAppAppCheckToken) { + return this.serverAppAppCheckToken; + } if (this.appCheck) { const result = limitedUseAppCheckTokens ? await this.appCheck.getLimitedUseToken() diff --git a/packages/functions/src/service.ts b/packages/functions/src/service.ts index 986dcbc735d..becc454255f 100644 --- a/packages/functions/src/service.ts +++ b/packages/functions/src/service.ts @@ -107,6 +107,7 @@ export class FunctionsService implements _FirebaseService { regionOrCustomDomain: string = DEFAULT_REGION ) { this.contextProvider = new ContextProvider( + app, authProvider, messagingProvider, appCheckProvider diff --git a/packages/storage/src/service.ts b/packages/storage/src/service.ts index 6777cb7b659..422e3e1a188 100644 --- a/packages/storage/src/service.ts +++ b/packages/storage/src/service.ts @@ -24,7 +24,11 @@ import { Provider } from '@firebase/component'; import { FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; // eslint-disable-next-line import/no-extraneous-dependencies -import { FirebaseApp, FirebaseOptions } from '@firebase/app'; +import { + FirebaseApp, + FirebaseOptions, + _isFirebaseServerApp +} from '@firebase/app'; import { CONFIG_STORAGE_BUCKET_KEY, DEFAULT_HOST, @@ -262,6 +266,9 @@ export class FirebaseStorageImpl implements FirebaseStorage { } async _getAppCheckToken(): Promise { + if (_isFirebaseServerApp(this.app) && this.app.settings.appCheckToken) { + return this.app.settings.appCheckToken; + } const appCheck = this._appCheckProvider.getImmediate({ optional: true }); if (appCheck) { const result = await appCheck.getToken(); diff --git a/packages/vertexai/src/models/generative-model.ts b/packages/vertexai/src/models/generative-model.ts index e719529967c..0bc439235ba 100644 --- a/packages/vertexai/src/models/generative-model.ts +++ b/packages/vertexai/src/models/generative-model.ts @@ -46,6 +46,7 @@ import { import { VertexAI } from '../public-types'; import { ApiSettings } from '../types/internal'; import { VertexAIService } from '../service'; +import { _isFirebaseServerApp } from '@firebase/app'; /** * Class for generative model APIs. @@ -82,7 +83,17 @@ export class GenerativeModel { project: vertexAI.app.options.projectId, location: vertexAI.location }; - if ((vertexAI as VertexAIService).appCheck) { + + if ( + vertexAI.app && + _isFirebaseServerApp(vertexAI.app) && + vertexAI.app.settings.appCheckToken + ) { + const token = vertexAI.app.settings.appCheckToken; + this._apiSettings.getAppCheckToken = () => { + return Promise.resolve({ token }); + }; + } else if ((vertexAI as VertexAIService).appCheck) { this._apiSettings.getAppCheckToken = () => (vertexAI as VertexAIService).appCheck!.getToken(); } From b971b89be119a75ba28c366c0cf3f7734f85250b Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 4 Dec 2024 10:02:29 -0500 Subject: [PATCH 02/22] Exp validation at FiresbaseServerApp init. --- packages/app/src/errors.ts | 12 +++++++-- packages/app/src/firebaseServerApp.ts | 37 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/app/src/errors.ts b/packages/app/src/errors.ts index 0149ef3dcb1..06861a7634f 100644 --- a/packages/app/src/errors.ts +++ b/packages/app/src/errors.ts @@ -31,7 +31,9 @@ export const enum AppError { IDB_WRITE = 'idb-set', IDB_DELETE = 'idb-delete', FINALIZATION_REGISTRY_NOT_SUPPORTED = 'finalization-registry-not-supported', - INVALID_SERVER_APP_ENVIRONMENT = 'invalid-server-app-environment' + INVALID_SERVER_APP_ENVIRONMENT = 'invalid-server-app-environment', + INVALID_SERVER_APP_TOKEN_FORMAT = 'invalid-server-app-token-format', + SERVER_APP_TOKEN_EXPIRED = 'server-app-token-expired' } const ERRORS: ErrorMap = { @@ -61,7 +63,11 @@ const ERRORS: ErrorMap = { [AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED]: 'FirebaseServerApp deleteOnDeref field defined but the JS runtime does not support FinalizationRegistry.', [AppError.INVALID_SERVER_APP_ENVIRONMENT]: - 'FirebaseServerApp is not for use in browser environments.' + 'FirebaseServerApp is not for use in browser environments.', + [AppError.INVALID_SERVER_APP_TOKEN_FORMAT]: + 'FirebaseServerApp {$tokenName} could not be parsed.', + [AppError.SERVER_APP_TOKEN_EXPIRED]: + 'FirebaseServerApp {$tokenName} could not be parsed.' }; interface ErrorParams { @@ -75,6 +81,8 @@ interface ErrorParams { [AppError.IDB_WRITE]: { originalErrorMessage?: string }; [AppError.IDB_DELETE]: { originalErrorMessage?: string }; [AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED]: { appName?: string }; + [AppError.INVALID_SERVER_APP_TOKEN_FORMAT]: { tokenName: string }; + [AppError.SERVER_APP_TOKEN_EXPIRED]: { tokenName: string }; } export const ERROR_FACTORY = new ErrorFactory( diff --git a/packages/app/src/firebaseServerApp.ts b/packages/app/src/firebaseServerApp.ts index 0c41d4cd607..ee6e9831966 100644 --- a/packages/app/src/firebaseServerApp.ts +++ b/packages/app/src/firebaseServerApp.ts @@ -26,6 +26,33 @@ import { ComponentContainer } from '@firebase/component'; import { FirebaseAppImpl } from './firebaseApp'; import { ERROR_FACTORY, AppError } from './errors'; import { name as packageName, version } from '../package.json'; +import { base64Decode } from '@firebase/util'; + +// Parse the token and check to see if the `exp` claim is in the future. +// Throws an error if the token or claim could not be parsed, or if `exp` is in the past. +function validateTokenTTL(base64Token: string, tokenName: string): void { + const secondPart = base64Decode(base64Token.split('.')[1]); + if (secondPart === null) { + throw ERROR_FACTORY.create(AppError.INVALID_SERVER_APP_TOKEN_FORMAT, { + tokenName + }); + } + const expClaim = JSON.parse(secondPart).exp; + if (expClaim === undefined) { + throw ERROR_FACTORY.create(AppError.INVALID_SERVER_APP_TOKEN_FORMAT, { + tokenName + }); + } + const exp = JSON.parse(secondPart).exp * 1000; + const now = new Date().getTime(); + // const now = new Date(new Date().getDate() - 1).now() + const diff = exp - now; + if (diff <= 0) { + throw ERROR_FACTORY.create(AppError.SERVER_APP_TOKEN_EXPIRED, { + tokenName + }); + } +} export class FirebaseServerAppImpl extends FirebaseAppImpl @@ -67,6 +94,16 @@ export class FirebaseServerAppImpl ...serverConfig }; + // Validate the authIdtoken validation window. + if (this._serverConfig.authIdToken) { + validateTokenTTL(this._serverConfig.authIdToken, 'authIdToken'); + } + + // Validate the appCheckToken validation window. + if (this._serverConfig.appCheckToken) { + validateTokenTTL(this._serverConfig.appCheckToken, 'appCheckToken'); + } + this._finalizationRegistry = null; if (typeof FinalizationRegistry !== 'undefined') { this._finalizationRegistry = new FinalizationRegistry(() => { From 7c8ec93683847b3bb9a3b3a44fb933ee7f375ec5 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 4 Dec 2024 10:02:39 -0500 Subject: [PATCH 03/22] FiresbaseServerApp init tests --- packages/app/src/firebaseServerApp.test.ts | 159 +++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/packages/app/src/firebaseServerApp.test.ts b/packages/app/src/firebaseServerApp.test.ts index bf2da5c06d5..ebc8b0de13f 100644 --- a/packages/app/src/firebaseServerApp.test.ts +++ b/packages/app/src/firebaseServerApp.test.ts @@ -20,6 +20,21 @@ import '../test/setup'; import { ComponentContainer } from '@firebase/component'; import { FirebaseServerAppImpl } from './firebaseServerApp'; import { FirebaseServerAppSettings } from './public-types'; +import { base64Encode } from '@firebase/util'; + +const BASE64_DUMMY = base64Encode('dummystrings'); // encodes to ZHVtbXlzdHJpbmdz + +// Creates a three part dummy token with an expiration claim in the second part. The expration +// time is based on the date offset provided. +function createServerAppTokenWithOffset(daysOffset: number): string { + const timeInSeconds = Math.trunc( + new Date().setDate(new Date().getDate() + daysOffset) / 1000 + ); + const secondPart = JSON.stringify({ exp: timeInSeconds }); + const token = + BASE64_DUMMY + '.' + base64Encode(secondPart) + '.' + BASE64_DUMMY; + return token; +} describe('FirebaseServerApp', () => { it('has various accessors', () => { @@ -155,4 +170,148 @@ describe('FirebaseServerApp', () => { expect(JSON.stringify(app)).to.eql(undefined); }); + + it('accepts a valid authIdToken expiration', () => { + const options = { apiKey: 'APIKEY' }; + const authIdToken = createServerAppTokenWithOffset(/*daysOffset=*/ 1); + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + releaseOnDeref: options, + authIdToken + }; + let encounteredError = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + encounteredError = true; + } + expect(encounteredError).to.be.false; + }); + + it('throws when authIdToken has expired', () => { + const options = { apiKey: 'APIKEY' }; + const authIdToken = createServerAppTokenWithOffset(/*daysOffset=*/ -1); + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + releaseOnDeref: options, + authIdToken + }; + let encounteredError = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + encounteredError = true; + expect((e as Error).toString()).to.contain( + 'app/server-app-token-expired' + ); + } + expect(encounteredError).to.be.true; + }); + + it('throws when authIdToken has too few parts', () => { + const options = { apiKey: 'APIKEY' }; + const authIdToken = 'blah'; + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + releaseOnDeref: options, + authIdToken: base64Encode(authIdToken) + }; + let encounteredError = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + encounteredError = true; + expect((e as Error).toString()).to.contain( + 'Unexpected end of JSON input' + ); + } + expect(encounteredError).to.be.true; + }); + + it('accepts a valid appCheckToken expiration', () => { + const options = { apiKey: 'APIKEY' }; + const appCheckToken = createServerAppTokenWithOffset(/*daysOffset=*/ 1); + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + releaseOnDeref: options, + appCheckToken + }; + let encounteredError = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + encounteredError = true; + } + expect(encounteredError).to.be.false; + }); + + it('throws when appCheckToken has expired', () => { + const options = { apiKey: 'APIKEY' }; + const appCheckToken = createServerAppTokenWithOffset(/*daysOffset=*/ -1); + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + releaseOnDeref: options, + appCheckToken + }; + let encounteredError = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + encounteredError = true; + expect((e as Error).toString()).to.contain( + 'app/server-app-token-expired' + ); + } + expect(encounteredError).to.be.true; + }); + + it('throws when appCheckToken has too few parts', () => { + const options = { apiKey: 'APIKEY' }; + const appCheckToken = 'blah'; + const serverAppSettings: FirebaseServerAppSettings = { + automaticDataCollectionEnabled: false, + releaseOnDeref: options, + appCheckToken: base64Encode(appCheckToken) + }; + let encounteredError = false; + try { + new FirebaseServerAppImpl( + options, + serverAppSettings, + 'testName', + new ComponentContainer('test') + ); + } catch (e) { + encounteredError = true; + expect((e as Error).toString()).to.contain( + 'Unexpected end of JSON input' + ); + } + expect(encounteredError).to.be.true; + }); }); From de89ecd29e35510f05f0f1bcb77f194d574586b4 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 11:02:27 -0500 Subject: [PATCH 04/22] Firestore cache appCheckToken instead of full app. --- packages/firestore/src/api/credentials.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/firestore/src/api/credentials.ts b/packages/firestore/src/api/credentials.ts index cbd02444282..ecaa982dcc8 100644 --- a/packages/firestore/src/api/credentials.ts +++ b/packages/firestore/src/api/credentials.ts @@ -496,11 +496,16 @@ export class FirebaseAppCheckTokenProvider private forceRefresh = false; private appCheck: FirebaseAppCheckInternal | null = null; private latestAppCheckToken: string | null = null; + private serverAppAppCheckToken: string | null = null; constructor( - private app: FirebaseApp, + app: FirebaseApp, private appCheckProvider: Provider - ) {} + ) { + if (_isFirebaseServerApp(app) && app.settings.appCheckToken) { + this.serverAppAppCheckToken = app.settings.appCheckToken; + } + } start( asyncQueue: AsyncQueue, @@ -564,10 +569,8 @@ export class FirebaseAppCheckTokenProvider } getToken(): Promise { - if (_isFirebaseServerApp(this.app) && this.app.settings.appCheckToken) { - return Promise.resolve( - new AppCheckToken(this.app.settings.appCheckToken) - ); + if (this.serverAppAppCheckToken !== null) { + return Promise.resolve(new AppCheckToken(this.serverAppAppCheckToken)); } debugAssert( this.tokenListener != null, From e632eeb215c5e2c3f3c8be8eb3a4c3d46cd559ce Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 11:07:45 -0500 Subject: [PATCH 05/22] again for LiteAppCheckTokenProvider --- packages/firestore/src/api/credentials.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/firestore/src/api/credentials.ts b/packages/firestore/src/api/credentials.ts index ecaa982dcc8..dfc4ce5c218 100644 --- a/packages/firestore/src/api/credentials.ts +++ b/packages/firestore/src/api/credentials.ts @@ -632,10 +632,10 @@ export class EmptyAppCheckTokenProvider implements CredentialsProvider { /** AppCheck token provider for the Lite SDK. */ export class LiteAppCheckTokenProvider implements CredentialsProvider { private appCheck: FirebaseAppCheckInternal | null = null; - private serverAppAppCheckToken?: string; + private serverAppAppCheckToken: string | null = null; constructor( - private app: FirebaseApp, + app: FirebaseApp, private appCheckProvider: Provider ) { if (_isFirebaseServerApp(app) && app.settings.appCheckToken) { @@ -647,7 +647,7 @@ export class LiteAppCheckTokenProvider implements CredentialsProvider { } getToken(): Promise { - if (this.serverAppAppCheckToken) { + if (this.serverAppAppCheckToken !== null) { return Promise.resolve(new AppCheckToken(this.serverAppAppCheckToken)); } From 1e511b5b6e00fd52fadc89ba7d7aa4b98422da5d Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 11:17:32 -0500 Subject: [PATCH 06/22] Changeset --- .changeset/kind-pets-sin.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/kind-pets-sin.md diff --git a/.changeset/kind-pets-sin.md b/.changeset/kind-pets-sin.md new file mode 100644 index 00000000000..5c2bcbd827d --- /dev/null +++ b/.changeset/kind-pets-sin.md @@ -0,0 +1,13 @@ +--- +'@firebase/app': minor +'firebase': minor +'@firebase/data-connect': patch +'@firebase/firestore': patch +'@firebase/functions': patch +'@firebase/database': patch +'@firebase/vertexai': patch +'@firebase/storage': patch +'@firebase/auth': patch +--- + +FirebaseServerApp may now be initalized with an App Check which will be used by SDKs in lieu of initializing an instance of App Check to get the current token. This should unblock the use of App Check enforced products in SSR environments. From 33e48897f41e8a51ab6b1ab38717d5fa41a2e5a7 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 15:11:34 -0500 Subject: [PATCH 07/22] Update app.firebaseserverappsettings.md --- docs-devsite/app.firebaseserverappsettings.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs-devsite/app.firebaseserverappsettings.md b/docs-devsite/app.firebaseserverappsettings.md index bc46c5292d0..75bd869283b 100644 --- a/docs-devsite/app.firebaseserverappsettings.md +++ b/docs-devsite/app.firebaseserverappsettings.md @@ -23,9 +23,20 @@ export interface FirebaseServerAppSettings extends OmitInvoking getAuth with a FirebaseServerApp configured with a validated authIdToken causes an automatic attempt to sign in the user that the authIdToken represents. The token needs to have been recently minted for this operation to succeed.If the token fails local verification, or if the Auth service has failed to validate it when the Auth SDK is initialized, then a warning is logged to the console and the Auth SDK will not sign in a user on initialization.If a user is successfully signed in, then the Auth instance's onAuthStateChanged callback is invoked with the User object as per standard Auth flows. However, User objects created via an authIdToken do not have a refresh token. Attempted refreshToken operations fail. | | [releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) | object | An optional object. If provided, the Firebase SDK uses a FinalizationRegistry object to monitor the garbage collection status of the provided object. The Firebase SDK releases its reference on the FirebaseServerApp instance when the provided releaseOnDeref object is garbage collected.You can use this field to reduce memory management overhead for your application. If provided, an app running in a SSR pass does not need to perform FirebaseServerApp cleanup, so long as the reference object is deleted (by falling out of SSR scope, for instance.)If an object is not provided then the application must clean up the FirebaseServerApp instance by invoking deleteApp.If the application provides an object in this parameter, but the application is executed in a JavaScript engine that predates the support of FinalizationRegistry (introduced in node v14.6.0, for instance), then an error is thrown at FirebaseServerApp initialization. | +## FirebaseServerAppSettings.appCheckToken + +An optional App Check token. If provided, the Firebase SDKs that use App Check will utilizze this App Check token in lieu of requiring an instance of App Check to be initialized. + +Signature: + +```typescript +appCheckToken?: string; +``` + ## FirebaseServerAppSettings.authIdToken An optional Auth ID token used to resume a signed in user session from a client runtime environment. From 02708d3ccb53010feb9a8ba2e5b15d72cc0b3cb2 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 17:40:23 -0500 Subject: [PATCH 08/22] Remove auth's invalid token test It's covered elsewhere now that we do exp testing. --- .../flows/firebaseserverapp.test.ts | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/packages/auth/test/integration/flows/firebaseserverapp.test.ts b/packages/auth/test/integration/flows/firebaseserverapp.test.ts index fc61bf561bf..71c64b8554a 100644 --- a/packages/auth/test/integration/flows/firebaseserverapp.test.ts +++ b/packages/auth/test/integration/flows/firebaseserverapp.test.ts @@ -166,37 +166,6 @@ describe('Integration test: Auth FirebaseServerApp tests', () => { await deleteApp(serverApp); }); - it('invalid token does not sign in user', async () => { - if (isBrowser()) { - return; - } - const authIdToken = '{ invalid token }'; - const firebaseServerAppSettings = { authIdToken }; - - const serverApp = initializeServerApp( - getAppConfig(), - firebaseServerAppSettings - ); - const serverAppAuth = getTestInstanceForServerApp(serverApp); - expect(serverAppAuth.currentUser).to.be.null; - - let numberServerLogins = 0; - onAuthStateChanged(serverAppAuth, serverAuthUser => { - if (serverAuthUser) { - numberServerLogins++; - } - }); - - await new Promise(resolve => { - setTimeout(resolve, signInWaitDuration); - }); - - expect(numberServerLogins).to.equal(0); - expect(serverAppAuth.currentUser).to.be.null; - - await deleteApp(serverApp); - }); - it('signs in with email credentials user', async () => { if (isBrowser()) { return; From c1a1322b80e40f2b9a1733417ba6ffea645d42f5 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 19:23:20 -0500 Subject: [PATCH 09/22] Check encounteredError only --- packages/app/src/firebaseServerApp.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/firebaseServerApp.test.ts b/packages/app/src/firebaseServerApp.test.ts index ebc8b0de13f..b58c90ff65e 100644 --- a/packages/app/src/firebaseServerApp.test.ts +++ b/packages/app/src/firebaseServerApp.test.ts @@ -308,9 +308,9 @@ describe('FirebaseServerApp', () => { ); } catch (e) { encounteredError = true; - expect((e as Error).toString()).to.contain( - 'Unexpected end of JSON input' - ); + //expect((e as Error).toString()).to.contain( + // 'Unexpected end of JSON input' + //); } expect(encounteredError).to.be.true; }); From 34372c42aa2cb383d02491a74f9aca33a09fe002 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 16 Dec 2024 19:32:53 -0500 Subject: [PATCH 10/22] Update firebaseServerApp.test.ts --- packages/app/src/firebaseServerApp.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/app/src/firebaseServerApp.test.ts b/packages/app/src/firebaseServerApp.test.ts index b58c90ff65e..58a4c13c60e 100644 --- a/packages/app/src/firebaseServerApp.test.ts +++ b/packages/app/src/firebaseServerApp.test.ts @@ -236,9 +236,6 @@ describe('FirebaseServerApp', () => { ); } catch (e) { encounteredError = true; - expect((e as Error).toString()).to.contain( - 'Unexpected end of JSON input' - ); } expect(encounteredError).to.be.true; }); From a5075a2a4c422f5f7cf41c140df7fe3c0a300c62 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 17 Dec 2024 09:01:28 -0500 Subject: [PATCH 11/22] Changeset rewording --- .changeset/kind-pets-sin.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/kind-pets-sin.md b/.changeset/kind-pets-sin.md index 5c2bcbd827d..bdef132916b 100644 --- a/.changeset/kind-pets-sin.md +++ b/.changeset/kind-pets-sin.md @@ -10,4 +10,6 @@ '@firebase/auth': patch --- -FirebaseServerApp may now be initalized with an App Check which will be used by SDKs in lieu of initializing an instance of App Check to get the current token. This should unblock the use of App Check enforced products in SSR environments. +`FirebaseServerApp` may now be initalized with an App Check token in leu of invoking the App Check +`getToken` method. This should unblock the use of App Check enforced products in SSR environments +where the App Check SDK cannot be initialized. From a218674fcf2907dcd748fd1ffeb44a60df886691 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 17 Dec 2024 09:20:18 -0500 Subject: [PATCH 12/22] revert unneeded data connect change. --- packages/data-connect/src/api/DataConnect.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/data-connect/src/api/DataConnect.ts b/packages/data-connect/src/api/DataConnect.ts index 873464942f9..30ec344f3ef 100644 --- a/packages/data-connect/src/api/DataConnect.ts +++ b/packages/data-connect/src/api/DataConnect.ts @@ -92,7 +92,7 @@ export class DataConnect { private _transportOptions?: TransportOptions; private _authTokenProvider?: AuthTokenProvider; _isUsingGeneratedSdk: boolean = false; - private _appCheckTokenProvider: AppCheckTokenProvider; + private _appCheckTokenProvider?: AppCheckTokenProvider; // @internal constructor( public readonly app: FirebaseApp, @@ -149,10 +149,12 @@ export class DataConnect { this._authProvider ); } - this._appCheckTokenProvider = new AppCheckTokenProvider( - this.app, - this._appCheckProvider - ); + if (this._appCheckProvider) { + this._appCheckTokenProvider = new AppCheckTokenProvider( + this.app.name, + this._appCheckProvider + ); + } this._initialized = true; this._transport = new this._transportClass( From e6b6625f4c75e0ab3f915b94d93e95c6d7e87119 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 17 Dec 2024 09:34:23 -0500 Subject: [PATCH 13/22] Update comments --- packages/app/src/firebaseServerApp.test.ts | 3 --- packages/app/src/firebaseServerApp.ts | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/app/src/firebaseServerApp.test.ts b/packages/app/src/firebaseServerApp.test.ts index 58a4c13c60e..92420d14839 100644 --- a/packages/app/src/firebaseServerApp.test.ts +++ b/packages/app/src/firebaseServerApp.test.ts @@ -305,9 +305,6 @@ describe('FirebaseServerApp', () => { ); } catch (e) { encounteredError = true; - //expect((e as Error).toString()).to.contain( - // 'Unexpected end of JSON input' - //); } expect(encounteredError).to.be.true; }); diff --git a/packages/app/src/firebaseServerApp.ts b/packages/app/src/firebaseServerApp.ts index ee6e9831966..17c6f9a4d54 100644 --- a/packages/app/src/firebaseServerApp.ts +++ b/packages/app/src/firebaseServerApp.ts @@ -45,7 +45,6 @@ function validateTokenTTL(base64Token: string, tokenName: string): void { } const exp = JSON.parse(secondPart).exp * 1000; const now = new Date().getTime(); - // const now = new Date(new Date().getDate() - 1).now() const diff = exp - now; if (diff <= 0) { throw ERROR_FACTORY.create(AppError.SERVER_APP_TOKEN_EXPIRED, { @@ -94,12 +93,12 @@ export class FirebaseServerAppImpl ...serverConfig }; - // Validate the authIdtoken validation window. + // Ensure that the current time is within the authIdtoken window of validity. if (this._serverConfig.authIdToken) { validateTokenTTL(this._serverConfig.authIdToken, 'authIdToken'); } - // Validate the appCheckToken validation window. + // Ensure that the current time is within the appCheckToken window of validity. if (this._serverConfig.appCheckToken) { validateTokenTTL(this._serverConfig.appCheckToken, 'appCheckToken'); } From 9da69bc833ed8e53842f8d42d4e9d8b0b5241998 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 17 Dec 2024 10:38:45 -0500 Subject: [PATCH 14/22] Fix error introduced in data connect revert --- packages/data-connect/src/api/DataConnect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/data-connect/src/api/DataConnect.ts b/packages/data-connect/src/api/DataConnect.ts index 30ec344f3ef..53eb0c97ed5 100644 --- a/packages/data-connect/src/api/DataConnect.ts +++ b/packages/data-connect/src/api/DataConnect.ts @@ -151,7 +151,7 @@ export class DataConnect { } if (this._appCheckProvider) { this._appCheckTokenProvider = new AppCheckTokenProvider( - this.app.name, + this.app, this._appCheckProvider ); } From 9a1299b979a6c3396fe0cdd6f48e88b5475c2e58 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 19 Dec 2024 11:57:03 -0500 Subject: [PATCH 15/22] update to isFirebaseServerApp to take null | undef --- packages/app/src/internal.test.ts | 27 +++++++++++++++++++++++++-- packages/app/src/internal.ts | 5 ++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/app/src/internal.test.ts b/packages/app/src/internal.test.ts index 47ea2e80c40..78baf400b8c 100644 --- a/packages/app/src/internal.test.ts +++ b/packages/app/src/internal.test.ts @@ -19,7 +19,7 @@ import { expect } from 'chai'; import { stub } from 'sinon'; import '../test/setup'; import { createTestComponent, TestService } from '../test/util'; -import { initializeApp, getApps, deleteApp } from './api'; +import { initializeApp, initializeServerApp, getApps, deleteApp } from './api'; import { FirebaseAppImpl } from './firebaseApp'; import { _addComponent, @@ -28,9 +28,11 @@ import { _components, _clearComponents, _getProvider, - _removeServiceInstance + _removeServiceInstance, + _isFirebaseServerApp } from './internal'; import { logger } from './logger'; +import { isBrowser } from '@firebase/util'; declare module '@firebase/component' { interface NameServiceMapping { @@ -161,4 +163,25 @@ describe('Internal API tests', () => { expect(instance1).to.not.equal(instance2); }); }); + + describe('_isFirebaseServerApp', () => { + it('detects a valid FirebaseServerApp', () => { + if (!isBrowser()) { + // FirebaseServerApp isn't supported for execution in browser environments. + const app = initializeServerApp({}, {}); + expect(_isFirebaseServerApp(app)).to.be.true; + } + }); + it('a standard FirebaseApp returns false', () => { + const app = initializeApp({}); + expect(_isFirebaseServerApp(app)).to.be.false; + }); + it('a null object returns false', () => { + expect(_isFirebaseServerApp(null)).to.be.false; + }); + it('undefined returns false', () => { + let app: undefined; + expect(_isFirebaseServerApp(app)).to.be.false; + }); + }); }); diff --git a/packages/app/src/internal.ts b/packages/app/src/internal.ts index 7e0c1545962..cbcdcb26501 100644 --- a/packages/app/src/internal.ts +++ b/packages/app/src/internal.ts @@ -168,8 +168,11 @@ export function _isFirebaseApp( * @internal */ export function _isFirebaseServerApp( - obj: FirebaseApp | FirebaseServerApp + obj: FirebaseApp | FirebaseServerApp | null | undefined ): obj is FirebaseServerApp { + if (obj === null || obj === undefined) { + return false; + } return (obj as FirebaseServerApp).settings !== undefined; } From b3a1c4f9ad73690eb64e65354ad968da046e1fcc Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 19 Dec 2024 17:08:30 +0000 Subject: [PATCH 16/22] Update API reports --- common/api-review/app.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/api-review/app.api.md b/common/api-review/app.api.md index e9dccfb3e51..4e93f1ae87f 100644 --- a/common/api-review/app.api.md +++ b/common/api-review/app.api.md @@ -116,7 +116,7 @@ export function initializeServerApp(options: FirebaseOptions | FirebaseApp, conf export function _isFirebaseApp(obj: FirebaseApp | FirebaseOptions): obj is FirebaseApp; // @internal (undocumented) -export function _isFirebaseServerApp(obj: FirebaseApp | FirebaseServerApp): obj is FirebaseServerApp; +export function _isFirebaseServerApp(obj: FirebaseApp | FirebaseServerApp | null | undefined): obj is FirebaseServerApp; // @public export function onLog(logCallback: LogCallback | null, options?: LogOptions): void; From 037041f0cd753646672f07eb59e1d7b220a3182b Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 19 Dec 2024 13:41:10 -0500 Subject: [PATCH 17/22] Fixes or PR feedback. --- .../data-connect/src/core/AppCheckTokenProvider.ts | 6 +++--- packages/database/src/core/AppCheckTokenProvider.ts | 11 +++++++++++ packages/functions/src/context.ts | 2 +- packages/vertexai/src/models/generative-model.ts | 1 - 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/data-connect/src/core/AppCheckTokenProvider.ts b/packages/data-connect/src/core/AppCheckTokenProvider.ts index 90f7ae805d1..4b49a8f674a 100644 --- a/packages/data-connect/src/core/AppCheckTokenProvider.ts +++ b/packages/data-connect/src/core/AppCheckTokenProvider.ts @@ -47,7 +47,7 @@ export class AppCheckTokenProvider { } } - getToken(forceRefresh?: boolean): Promise { + getToken(): Promise { if (this.serverAppAppCheckToken) { return Promise.resolve({ token: this.serverAppAppCheckToken }); } @@ -60,14 +60,14 @@ export class AppCheckTokenProvider { // becomes available before the timoeout below expires. setTimeout(() => { if (this.appCheck) { - this.getToken(forceRefresh).then(resolve, reject); + this.getToken().then(resolve, reject); } else { resolve(null); } }, 0); }); } - return this.appCheck.getToken(forceRefresh); + return this.appCheck.getToken(); } addTokenChangeListener(listener: AppCheckTokenListener): void { diff --git a/packages/database/src/core/AppCheckTokenProvider.ts b/packages/database/src/core/AppCheckTokenProvider.ts index 7876bbd27e2..ce9ce5575f6 100644 --- a/packages/database/src/core/AppCheckTokenProvider.ts +++ b/packages/database/src/core/AppCheckTokenProvider.ts @@ -49,6 +49,17 @@ export class AppCheckTokenProvider { getToken(forceRefresh?: boolean): Promise { if (this.serverAppAppCheckToken) { + if (forceRefresh) { + return new Promise(resolve => { + const appCheckTokenResult = { + token: 'ERROR', + error: new Error( + 'Attempted reuse of FirebaseServerApp.appCheckToken after previous usage failed.' + ) + }; + resolve(appCheckTokenResult); + }); + } return Promise.resolve({ token: this.serverAppAppCheckToken }); } if (!this.appCheck) { diff --git a/packages/functions/src/context.ts b/packages/functions/src/context.ts index 37483c9f4fd..4ac4bbe2cb9 100644 --- a/packages/functions/src/context.ts +++ b/packages/functions/src/context.ts @@ -82,7 +82,7 @@ export class ContextProvider { } if (!this.appCheck) { - appCheckProvider.get().then( + appCheckProvider?.get().then( appCheck => (this.appCheck = appCheck), () => { /* get() never rejects */ diff --git a/packages/vertexai/src/models/generative-model.ts b/packages/vertexai/src/models/generative-model.ts index 0bc439235ba..b44dc1131a3 100644 --- a/packages/vertexai/src/models/generative-model.ts +++ b/packages/vertexai/src/models/generative-model.ts @@ -85,7 +85,6 @@ export class GenerativeModel { }; if ( - vertexAI.app && _isFirebaseServerApp(vertexAI.app) && vertexAI.app.settings.appCheckToken ) { From d6e19179627040ef085d86a7fa15b52eecf4fbae Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 19 Dec 2024 13:59:31 -0500 Subject: [PATCH 18/22] Database throw error instead of reject promise --- packages/database/src/core/AppCheckTokenProvider.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/database/src/core/AppCheckTokenProvider.ts b/packages/database/src/core/AppCheckTokenProvider.ts index ce9ce5575f6..612cb902e5f 100644 --- a/packages/database/src/core/AppCheckTokenProvider.ts +++ b/packages/database/src/core/AppCheckTokenProvider.ts @@ -50,15 +50,9 @@ export class AppCheckTokenProvider { getToken(forceRefresh?: boolean): Promise { if (this.serverAppAppCheckToken) { if (forceRefresh) { - return new Promise(resolve => { - const appCheckTokenResult = { - token: 'ERROR', - error: new Error( - 'Attempted reuse of FirebaseServerApp.appCheckToken after previous usage failed.' - ) - }; - resolve(appCheckTokenResult); - }); + throw new Error( + 'Attempted reuse of `FirebaseServerApp.appCheckToken` after previous usage failed.' + ); } return Promise.resolve({ token: this.serverAppAppCheckToken }); } From 4fc151f05d60bc38028dca55c49789f27af8a6d1 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 14 Jan 2025 11:06:07 -0500 Subject: [PATCH 19/22] Fixes for typos & formatting in comments --- packages/app/src/firebaseServerApp.ts | 4 ++-- packages/app/src/public-types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/firebaseServerApp.ts b/packages/app/src/firebaseServerApp.ts index 17c6f9a4d54..1ebd9e5f912 100644 --- a/packages/app/src/firebaseServerApp.ts +++ b/packages/app/src/firebaseServerApp.ts @@ -93,12 +93,12 @@ export class FirebaseServerAppImpl ...serverConfig }; - // Ensure that the current time is within the authIdtoken window of validity. + // Ensure that the current time is within the `authIdtoken` window of validity. if (this._serverConfig.authIdToken) { validateTokenTTL(this._serverConfig.authIdToken, 'authIdToken'); } - // Ensure that the current time is within the appCheckToken window of validity. + // Ensure that the current time is within the `appCheckToken` window of validity. if (this._serverConfig.appCheckToken) { validateTokenTTL(this._serverConfig.appCheckToken, 'appCheckToken'); } diff --git a/packages/app/src/public-types.ts b/packages/app/src/public-types.ts index c793d9e44d5..cc210492aaf 100644 --- a/packages/app/src/public-types.ts +++ b/packages/app/src/public-types.ts @@ -197,8 +197,8 @@ export interface FirebaseServerAppSettings authIdToken?: string; /** - * An optional App Check token. If provided, the Firebase SDKs that use App Check will utilizze - * this App Check token in lieu of requiring an instance of App Check to be initialized. + * An optional App Check token. If provided, the Firebase SDKs that use App Check will utilize + * this App Check token in place of requiring an instance of App Check to be initialized. */ appCheckToken?: string; From 302e1dc73aff47656228f3a5566b0f70567af688 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 14 Jan 2025 11:13:49 -0500 Subject: [PATCH 20/22] docgen --- docs-devsite/app.firebaseserverappsettings.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-devsite/app.firebaseserverappsettings.md b/docs-devsite/app.firebaseserverappsettings.md index 75bd869283b..8a2e338ce20 100644 --- a/docs-devsite/app.firebaseserverappsettings.md +++ b/docs-devsite/app.firebaseserverappsettings.md @@ -23,13 +23,13 @@ export interface FirebaseServerAppSettings extends OmitInvoking getAuth with a FirebaseServerApp configured with a validated authIdToken causes an automatic attempt to sign in the user that the authIdToken represents. The token needs to have been recently minted for this operation to succeed.If the token fails local verification, or if the Auth service has failed to validate it when the Auth SDK is initialized, then a warning is logged to the console and the Auth SDK will not sign in a user on initialization.If a user is successfully signed in, then the Auth instance's onAuthStateChanged callback is invoked with the User object as per standard Auth flows. However, User objects created via an authIdToken do not have a refresh token. Attempted refreshToken operations fail. | | [releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) | object | An optional object. If provided, the Firebase SDK uses a FinalizationRegistry object to monitor the garbage collection status of the provided object. The Firebase SDK releases its reference on the FirebaseServerApp instance when the provided releaseOnDeref object is garbage collected.You can use this field to reduce memory management overhead for your application. If provided, an app running in a SSR pass does not need to perform FirebaseServerApp cleanup, so long as the reference object is deleted (by falling out of SSR scope, for instance.)If an object is not provided then the application must clean up the FirebaseServerApp instance by invoking deleteApp.If the application provides an object in this parameter, but the application is executed in a JavaScript engine that predates the support of FinalizationRegistry (introduced in node v14.6.0, for instance), then an error is thrown at FirebaseServerApp initialization. | ## FirebaseServerAppSettings.appCheckToken -An optional App Check token. If provided, the Firebase SDKs that use App Check will utilizze this App Check token in lieu of requiring an instance of App Check to be initialized. +An optional App Check token. If provided, the Firebase SDKs that use App Check will utilize this App Check token in place of requiring an instance of App Check to be initialized. Signature: From 0526b87471deb6664e21aaf183265fdaca3e54b2 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 14 Jan 2025 15:21:41 -0500 Subject: [PATCH 21/22] Review fixes. --- docs-devsite/app.firebaseserverappsettings.md | 10 +- packages/app/src/errors.ts | 12 +-- packages/app/src/firebaseServerApp.test.ts | 94 ------------------- packages/app/src/firebaseServerApp.ts | 23 +++-- packages/app/src/public-types.ts | 11 ++- packages/firestore/src/api/credentials.ts | 4 +- 6 files changed, 32 insertions(+), 122 deletions(-) diff --git a/docs-devsite/app.firebaseserverappsettings.md b/docs-devsite/app.firebaseserverappsettings.md index 8a2e338ce20..79d4fbe022a 100644 --- a/docs-devsite/app.firebaseserverappsettings.md +++ b/docs-devsite/app.firebaseserverappsettings.md @@ -23,14 +23,16 @@ export interface FirebaseServerAppSettings extends OmitInvoking getAuth with a FirebaseServerApp configured with a validated authIdToken causes an automatic attempt to sign in the user that the authIdToken represents. The token needs to have been recently minted for this operation to succeed.If the token fails local verification, or if the Auth service has failed to validate it when the Auth SDK is initialized, then a warning is logged to the console and the Auth SDK will not sign in a user on initialization.If a user is successfully signed in, then the Auth instance's onAuthStateChanged callback is invoked with the User object as per standard Auth flows. However, User objects created via an authIdToken do not have a refresh token. Attempted refreshToken operations fail. | +| [appCheckToken](./app.firebaseserverappsettings.md#firebaseserverappsettingsappchecktoken) | string | An optional App Check token. If provided, the Firebase SDKs that use App Check will utilize this App Check token in place of requiring an instance of App Check to be initialized.If the token fails local verification due to expiration or parsing errors, then a console error is logged at the time of initialization of the FirebaseServerApp instance. | +| [authIdToken](./app.firebaseserverappsettings.md#firebaseserverappsettingsauthidtoken) | string | An optional Auth ID token used to resume a signed in user session from a client runtime environment.Invoking getAuth with a FirebaseServerApp configured with a validated authIdToken causes an automatic attempt to sign in the user that the authIdToken represents. The token needs to have been recently minted for this operation to succeed.If the token fails local verification due to expiration or parsing errors, then a console error is logged at the time of initialization of the FirebaseServerApp instance.If the Auth service has failed to validate the token when the Auth SDK is initialized, then an warning is logged to the console and the Auth SDK will not sign in a user on initialization.If a user is successfully signed in, then the Auth instance's onAuthStateChanged callback is invoked with the User object as per standard Auth flows. However, User objects created via an authIdToken do not have a refresh token. Attempted refreshToken operations fail. | | [releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) | object | An optional object. If provided, the Firebase SDK uses a FinalizationRegistry object to monitor the garbage collection status of the provided object. The Firebase SDK releases its reference on the FirebaseServerApp instance when the provided releaseOnDeref object is garbage collected.You can use this field to reduce memory management overhead for your application. If provided, an app running in a SSR pass does not need to perform FirebaseServerApp cleanup, so long as the reference object is deleted (by falling out of SSR scope, for instance.)If an object is not provided then the application must clean up the FirebaseServerApp instance by invoking deleteApp.If the application provides an object in this parameter, but the application is executed in a JavaScript engine that predates the support of FinalizationRegistry (introduced in node v14.6.0, for instance), then an error is thrown at FirebaseServerApp initialization. | ## FirebaseServerAppSettings.appCheckToken An optional App Check token. If provided, the Firebase SDKs that use App Check will utilize this App Check token in place of requiring an instance of App Check to be initialized. +If the token fails local verification due to expiration or parsing errors, then a console error is logged at the time of initialization of the `FirebaseServerApp` instance. + Signature: ```typescript @@ -43,7 +45,9 @@ An optional Auth ID token used to resume a signed in user session from a client Invoking `getAuth` with a `FirebaseServerApp` configured with a validated `authIdToken` causes an automatic attempt to sign in the user that the `authIdToken` represents. The token needs to have been recently minted for this operation to succeed. -If the token fails local verification, or if the Auth service has failed to validate it when the Auth SDK is initialized, then a warning is logged to the console and the Auth SDK will not sign in a user on initialization. +If the token fails local verification due to expiration or parsing errors, then a console error is logged at the time of initialization of the `FirebaseServerApp` instance. + +If the Auth service has failed to validate the token when the Auth SDK is initialized, then an warning is logged to the console and the Auth SDK will not sign in a user on initialization. If a user is successfully signed in, then the Auth instance's `onAuthStateChanged` callback is invoked with the `User` object as per standard Auth flows. However, `User` objects created via an `authIdToken` do not have a refresh token. Attempted `refreshToken` operations fail. diff --git a/packages/app/src/errors.ts b/packages/app/src/errors.ts index 06861a7634f..0149ef3dcb1 100644 --- a/packages/app/src/errors.ts +++ b/packages/app/src/errors.ts @@ -31,9 +31,7 @@ export const enum AppError { IDB_WRITE = 'idb-set', IDB_DELETE = 'idb-delete', FINALIZATION_REGISTRY_NOT_SUPPORTED = 'finalization-registry-not-supported', - INVALID_SERVER_APP_ENVIRONMENT = 'invalid-server-app-environment', - INVALID_SERVER_APP_TOKEN_FORMAT = 'invalid-server-app-token-format', - SERVER_APP_TOKEN_EXPIRED = 'server-app-token-expired' + INVALID_SERVER_APP_ENVIRONMENT = 'invalid-server-app-environment' } const ERRORS: ErrorMap = { @@ -63,11 +61,7 @@ const ERRORS: ErrorMap = { [AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED]: 'FirebaseServerApp deleteOnDeref field defined but the JS runtime does not support FinalizationRegistry.', [AppError.INVALID_SERVER_APP_ENVIRONMENT]: - 'FirebaseServerApp is not for use in browser environments.', - [AppError.INVALID_SERVER_APP_TOKEN_FORMAT]: - 'FirebaseServerApp {$tokenName} could not be parsed.', - [AppError.SERVER_APP_TOKEN_EXPIRED]: - 'FirebaseServerApp {$tokenName} could not be parsed.' + 'FirebaseServerApp is not for use in browser environments.' }; interface ErrorParams { @@ -81,8 +75,6 @@ interface ErrorParams { [AppError.IDB_WRITE]: { originalErrorMessage?: string }; [AppError.IDB_DELETE]: { originalErrorMessage?: string }; [AppError.FINALIZATION_REGISTRY_NOT_SUPPORTED]: { appName?: string }; - [AppError.INVALID_SERVER_APP_TOKEN_FORMAT]: { tokenName: string }; - [AppError.SERVER_APP_TOKEN_EXPIRED]: { tokenName: string }; } export const ERROR_FACTORY = new ErrorFactory( diff --git a/packages/app/src/firebaseServerApp.test.ts b/packages/app/src/firebaseServerApp.test.ts index 92420d14839..de00687948e 100644 --- a/packages/app/src/firebaseServerApp.test.ts +++ b/packages/app/src/firebaseServerApp.test.ts @@ -193,53 +193,6 @@ describe('FirebaseServerApp', () => { expect(encounteredError).to.be.false; }); - it('throws when authIdToken has expired', () => { - const options = { apiKey: 'APIKEY' }; - const authIdToken = createServerAppTokenWithOffset(/*daysOffset=*/ -1); - const serverAppSettings: FirebaseServerAppSettings = { - automaticDataCollectionEnabled: false, - releaseOnDeref: options, - authIdToken - }; - let encounteredError = false; - try { - new FirebaseServerAppImpl( - options, - serverAppSettings, - 'testName', - new ComponentContainer('test') - ); - } catch (e) { - encounteredError = true; - expect((e as Error).toString()).to.contain( - 'app/server-app-token-expired' - ); - } - expect(encounteredError).to.be.true; - }); - - it('throws when authIdToken has too few parts', () => { - const options = { apiKey: 'APIKEY' }; - const authIdToken = 'blah'; - const serverAppSettings: FirebaseServerAppSettings = { - automaticDataCollectionEnabled: false, - releaseOnDeref: options, - authIdToken: base64Encode(authIdToken) - }; - let encounteredError = false; - try { - new FirebaseServerAppImpl( - options, - serverAppSettings, - 'testName', - new ComponentContainer('test') - ); - } catch (e) { - encounteredError = true; - } - expect(encounteredError).to.be.true; - }); - it('accepts a valid appCheckToken expiration', () => { const options = { apiKey: 'APIKEY' }; const appCheckToken = createServerAppTokenWithOffset(/*daysOffset=*/ 1); @@ -261,51 +214,4 @@ describe('FirebaseServerApp', () => { } expect(encounteredError).to.be.false; }); - - it('throws when appCheckToken has expired', () => { - const options = { apiKey: 'APIKEY' }; - const appCheckToken = createServerAppTokenWithOffset(/*daysOffset=*/ -1); - const serverAppSettings: FirebaseServerAppSettings = { - automaticDataCollectionEnabled: false, - releaseOnDeref: options, - appCheckToken - }; - let encounteredError = false; - try { - new FirebaseServerAppImpl( - options, - serverAppSettings, - 'testName', - new ComponentContainer('test') - ); - } catch (e) { - encounteredError = true; - expect((e as Error).toString()).to.contain( - 'app/server-app-token-expired' - ); - } - expect(encounteredError).to.be.true; - }); - - it('throws when appCheckToken has too few parts', () => { - const options = { apiKey: 'APIKEY' }; - const appCheckToken = 'blah'; - const serverAppSettings: FirebaseServerAppSettings = { - automaticDataCollectionEnabled: false, - releaseOnDeref: options, - appCheckToken: base64Encode(appCheckToken) - }; - let encounteredError = false; - try { - new FirebaseServerAppImpl( - options, - serverAppSettings, - 'testName', - new ComponentContainer('test') - ); - } catch (e) { - encounteredError = true; - } - expect(encounteredError).to.be.true; - }); }); diff --git a/packages/app/src/firebaseServerApp.ts b/packages/app/src/firebaseServerApp.ts index 1ebd9e5f912..21232869c3c 100644 --- a/packages/app/src/firebaseServerApp.ts +++ b/packages/app/src/firebaseServerApp.ts @@ -29,27 +29,30 @@ import { name as packageName, version } from '../package.json'; import { base64Decode } from '@firebase/util'; // Parse the token and check to see if the `exp` claim is in the future. -// Throws an error if the token or claim could not be parsed, or if `exp` is in the past. +// Reports an error to the console if the token or claim could not be parsed, or if `exp` is in +// the past. function validateTokenTTL(base64Token: string, tokenName: string): void { const secondPart = base64Decode(base64Token.split('.')[1]); if (secondPart === null) { - throw ERROR_FACTORY.create(AppError.INVALID_SERVER_APP_TOKEN_FORMAT, { - tokenName - }); + console.error( + `FirebaseServerApp ${tokenName} is invalid: second part could not be parsed.` + ); + return; } const expClaim = JSON.parse(secondPart).exp; if (expClaim === undefined) { - throw ERROR_FACTORY.create(AppError.INVALID_SERVER_APP_TOKEN_FORMAT, { - tokenName - }); + console.error( + `FirebaseServerApp ${tokenName} is invalid: expiration claim could not be parsed` + ); + return; } const exp = JSON.parse(secondPart).exp * 1000; const now = new Date().getTime(); const diff = exp - now; if (diff <= 0) { - throw ERROR_FACTORY.create(AppError.SERVER_APP_TOKEN_EXPIRED, { - tokenName - }); + console.error( + `FirebaseServerApp ${tokenName} is invalid: the token has expired.` + ); } } diff --git a/packages/app/src/public-types.ts b/packages/app/src/public-types.ts index cc210492aaf..ea68579a7e9 100644 --- a/packages/app/src/public-types.ts +++ b/packages/app/src/public-types.ts @@ -185,9 +185,11 @@ export interface FirebaseServerAppSettings * causes an automatic attempt to sign in the user that the `authIdToken` represents. The token * needs to have been recently minted for this operation to succeed. * - * If the token fails local verification, or if the Auth service has failed to validate it when - * the Auth SDK is initialized, then a warning is logged to the console and the Auth SDK will not - * sign in a user on initialization. + * If the token fails local verification due to expiration or parsing errors, then a console error + * is logged at the time of initialization of the `FirebaseServerApp` instance. + * + * If the Auth service has failed to validate the token when the Auth SDK is initialized, then an + * warning is logged to the console and the Auth SDK will not sign in a user on initialization. * * If a user is successfully signed in, then the Auth instance's `onAuthStateChanged` callback * is invoked with the `User` object as per standard Auth flows. However, `User` objects @@ -199,6 +201,9 @@ export interface FirebaseServerAppSettings /** * An optional App Check token. If provided, the Firebase SDKs that use App Check will utilize * this App Check token in place of requiring an instance of App Check to be initialized. + * + * If the token fails local verification due to expiration or parsing errors, then a console error + * is logged at the time of initialization of the `FirebaseServerApp` instance. */ appCheckToken?: string; diff --git a/packages/firestore/src/api/credentials.ts b/packages/firestore/src/api/credentials.ts index dfc4ce5c218..b542ec80b91 100644 --- a/packages/firestore/src/api/credentials.ts +++ b/packages/firestore/src/api/credentials.ts @@ -569,7 +569,7 @@ export class FirebaseAppCheckTokenProvider } getToken(): Promise { - if (this.serverAppAppCheckToken !== null) { + if (this.serverAppAppCheckToken) { return Promise.resolve(new AppCheckToken(this.serverAppAppCheckToken)); } debugAssert( @@ -647,7 +647,7 @@ export class LiteAppCheckTokenProvider implements CredentialsProvider { } getToken(): Promise { - if (this.serverAppAppCheckToken !== null) { + if (this.serverAppAppCheckToken) { return Promise.resolve(new AppCheckToken(this.serverAppAppCheckToken)); } From 3352b7f4c21c4680eeac7acb841778bee470f60a Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 17 Jan 2025 13:46:34 -0500 Subject: [PATCH 22/22] Changelist copy update. --- .changeset/kind-pets-sin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/kind-pets-sin.md b/.changeset/kind-pets-sin.md index bdef132916b..7debbf4b220 100644 --- a/.changeset/kind-pets-sin.md +++ b/.changeset/kind-pets-sin.md @@ -10,6 +10,6 @@ '@firebase/auth': patch --- -`FirebaseServerApp` may now be initalized with an App Check token in leu of invoking the App Check +`FirebaseServerApp` can now be initalized with an App Check token instead of invoking the App Check `getToken` method. This should unblock the use of App Check enforced products in SSR environments where the App Check SDK cannot be initialized.