Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Installations Auth Id token to FirebaseServerApp #8623

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions .changeset/nine-clouds-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@firebase/installations': minor
'@firebase/app': minor
'firebase': minor
'@firebase/remote-config': patch
---

Add `installationsAuthToken` as an optional FirebaseServerApp variable. If present, then Installations `getId` and `getToken` will use the provided value instead of initializing the Installations SDK to retrieve those values dynamically. This should unlock SDKs that require these Installations values in a server environment where the Installations SDK isn't supported.
2 changes: 2 additions & 0 deletions common/api-review/app.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ export interface FirebaseOptions {

// @public
export interface FirebaseServerApp extends FirebaseApp {
readonly installationsId: string | null;
name: string;
readonly settings: FirebaseServerAppSettings;
}

// @public
export interface FirebaseServerAppSettings extends Omit<FirebaseAppSettings, 'name'> {
authIdToken?: string;
installationsAuthToken?: string;
releaseOnDeref?: object;
}

Expand Down
11 changes: 11 additions & 0 deletions docs-devsite/app.firebaseserverapp.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,20 @@ export interface FirebaseServerApp extends FirebaseApp

| Property | Type | Description |
| --- | --- | --- |
| [installationsId](./app.firebaseserverapp.md#firebaseserverappinstallationsid) | string \| null | The parsed Firebase Installations Id token if a <code>installationsAuthToken</code> was provided to [initializeServerApp()](./app.md#initializeserverapp_30ab697)<!-- -->. Null otherwise. |
| [name](./app.firebaseserverapp.md#firebaseserverappname) | string | There is no <code>getApp()</code> operation for <code>FirebaseServerApp</code>, so the name is not relevant for applications. However, it may be used internally, and is declared here so that <code>FirebaseServerApp</code> conforms to the <code>FirebaseApp</code> interface. |
| [settings](./app.firebaseserverapp.md#firebaseserverappsettings) | [FirebaseServerAppSettings](./app.firebaseserverappsettings.md#firebaseserverappsettings_interface) | The (read-only) configuration settings for this server app. These are the original parameters given in [initializeServerApp()](./app.md#initializeserverapp_30ab697)<!-- -->. |

## FirebaseServerApp.installationsId

The parsed Firebase Installations Id token if a `installationsAuthToken` was provided to [initializeServerApp()](./app.md#initializeserverapp_30ab697)<!-- -->. Null otherwise.

<b>Signature:</b>

```typescript
readonly installationsId: string | null;
```

## FirebaseServerApp.name

There is no `getApp()` operation for `FirebaseServerApp`<!-- -->, so the name is not relevant for applications. However, it may be used internally, and is declared here so that `FirebaseServerApp` conforms to the `FirebaseApp` interface.
Expand Down
19 changes: 19 additions & 0 deletions docs-devsite/app.firebaseserverappsettings.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface FirebaseServerAppSettings extends Omit<FirebaseAppSettings, 'na
| Property | Type | Description |
| --- | --- | --- |
| [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 <code>getAuth</code> with a <code>FirebaseServerApp</code> configured with a validated <code>authIdToken</code> causes an automatic attempt to sign in the user that the <code>authIdToken</code> 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 <code>onAuthStateChanged</code> callback is invoked with the <code>User</code> object as per standard Auth flows. However, <code>User</code> objects created via an <code>authIdToken</code> do not have a refresh token. Attempted <code>refreshToken</code> operations fail. |
| [installationsAuthToken](./app.firebaseserverappsettings.md#firebaseserverappsettingsinstallationsauthtoken) | string | An optional Installations Auth token which allows the use of Remote Config SDK in SSR enviornments.<!-- -->If provided, the <code>FirebaseServerApp</code> will attempt to parse the Installations id from the token.<!-- -->If the token is deemed to be malformed then an error will be thrown during the invocation of <code>initializeServerApp</code>.<!-- -->If the the Installations Id and the provided <code>installationsAuthToken</code> are successfully parsed, then they will be used by the Installations implementation when <code>getToken</code> and <code>getId</code> are invoked.<!-- -->Attempting to use Remote Config without providing an <code>installationsAuthToken</code> here will cause Installations to throw errors when Remote Config attempts to query the Installations id and authToken. |
| [releaseOnDeref](./app.firebaseserverappsettings.md#firebaseserverappsettingsreleaseonderef) | object | An optional object. If provided, the Firebase SDK uses a <code>FinalizationRegistry</code> object to monitor the garbage collection status of the provided object. The Firebase SDK releases its reference on the <code>FirebaseServerApp</code> instance when the provided <code>releaseOnDeref</code> 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 <code>FirebaseServerApp</code> 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 <code>FirebaseServerApp</code> instance by invoking <code>deleteApp</code>.<!-- -->If the application provides an object in this parameter, but the application is executed in a JavaScript engine that predates the support of <code>FinalizationRegistry</code> (introduced in node v14.6.0, for instance), then an error is thrown at <code>FirebaseServerApp</code> initialization. |

## FirebaseServerAppSettings.authIdToken
Expand All @@ -42,6 +43,24 @@ If a user is successfully signed in, then the Auth instance's `onAuthStateChange
authIdToken?: string;
```

## FirebaseServerAppSettings.installationsAuthToken

An optional Installations Auth token which allows the use of Remote Config SDK in SSR enviornments.

If provided, the `FirebaseServerApp` will attempt to parse the Installations id from the token.

If the token is deemed to be malformed then an error will be thrown during the invocation of `initializeServerApp`<!-- -->.

If the the Installations Id and the provided `installationsAuthToken` are successfully parsed, then they will be used by the Installations implementation when `getToken` and `getId` are invoked.

Attempting to use Remote Config without providing an `installationsAuthToken` here will cause Installations to throw errors when Remote Config attempts to query the Installations id and authToken.

<b>Signature:</b>

```typescript
installationsAuthToken?: string;
```

## FirebaseServerAppSettings.releaseOnDeref

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.
Expand Down
7 changes: 5 additions & 2 deletions packages/app/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ 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_INSTALLATIONS_AUTH_TOKEN = 'invalid-server-installations-auth-token'
}

const ERRORS: ErrorMap<AppError> = {
Expand Down Expand Up @@ -61,7 +62,9 @@ const ERRORS: ErrorMap<AppError> = {
[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_INSTALLATIONS_AUTH_TOKEN]:
'FirebaseServerApp could not initialize due to an invalid Installations auth token'
};

interface ErrorParams {
Expand Down
84 changes: 83 additions & 1 deletion packages/app/src/firebaseServerApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,19 @@ import { expect } from 'chai';
import '../test/setup';
import { ComponentContainer } from '@firebase/component';
import { FirebaseServerAppImpl } from './firebaseServerApp';
import { FirebaseServerAppSettings } from './public-types';
import { FirebaseApp, FirebaseServerAppSettings } from './public-types';

const VALID_INSTATLLATIONS_AUTH_TOKEN_SECONDPART: string =
'foo.eyJhcHBJZCI6IjE6MDAwMDAwMDAwMDAwOndlYjowMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiZXhwIjoiMDAwMDAwMD' +
'AwMCIsImZpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJwcm9qZWN0TnVtYmVyIjoiMDAwMDAwMDAwMDAwIn0.foo';

const INVALID_INSTATLLATIONS_AUTH_TOKEN: string =
'foo.eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9eyJhcHBJZCI6IjE6MDAwMDAwMDAwMDAwOndlYjowMDAwMDAwMDAwMD' +
'AwMDAwMDAwMDAwIiwiZXhwIjowMDAwMDAwMDAwLCJwcm9qZWN0TnVtYmVyIjowMDAwMDAwMDAwMDB9.foo';

const INVALID_INSTATLLATIONS_AUTH_TOKEN_ONE_PART: string =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9eyJhcHBJZCI6IjE6MDAwMDAwMDAwMDAwOndlYjowMDAwMDAwMDAwMD' +
'AwMDAwMDAwMDAwIiwiZXhwIjowMDAwMDAwMDAwLCJwcm9qZWN0TnVtYmVyIjowMDAwMDAwMDAwMDB9';

describe('FirebaseServerApp', () => {
it('has various accessors', () => {
Expand Down Expand Up @@ -155,4 +167,74 @@ describe('FirebaseServerApp', () => {

expect(JSON.stringify(app)).to.eql(undefined);
});

it('parses valid installationsAuthToken', () => {
const options = {
apiKey: 'APIKEY'
};

const serverAppSettings: FirebaseServerAppSettings = {
automaticDataCollectionEnabled: false,
installationsAuthToken: VALID_INSTATLLATIONS_AUTH_TOKEN_SECONDPART
};

let app: FirebaseApp | null = null;
try {
app = new FirebaseServerAppImpl(
options,
serverAppSettings,
'testName',
new ComponentContainer('test')
);
} catch (e) {}
expect(app).to.not.be.null;
});

it('invalid installationsAuthToken throws', () => {
const options = {
apiKey: 'APIKEY'
};

const serverAppSettings: FirebaseServerAppSettings = {
automaticDataCollectionEnabled: false,
installationsAuthToken: INVALID_INSTATLLATIONS_AUTH_TOKEN
};

let failed = false;
try {
new FirebaseServerAppImpl(
options,
serverAppSettings,
'testName',
new ComponentContainer('test')
);
} catch (e) {
failed = true;
}
expect(failed).to.be.true;
});

it('invalid single part installationsAuthToken throws', () => {
const options = {
apiKey: 'APIKEY'
};

const serverAppSettings: FirebaseServerAppSettings = {
automaticDataCollectionEnabled: false,
installationsAuthToken: INVALID_INSTATLLATIONS_AUTH_TOKEN_ONE_PART
};

let failed = false;
try {
new FirebaseServerAppImpl(
options,
serverAppSettings,
'testName',
new ComponentContainer('test')
);
} catch (e) {
failed = true;
}
expect(failed).to.be.true;
});
});
27 changes: 27 additions & 0 deletions packages/app/src/firebaseServerApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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';

export class FirebaseServerAppImpl
extends FirebaseAppImpl
Expand All @@ -34,6 +35,7 @@ export class FirebaseServerAppImpl
private readonly _serverConfig: FirebaseServerAppSettings;
private _finalizationRegistry: FinalizationRegistry<object> | null;
private _refCount: number;
private _installationsId: string | null;

constructor(
options: FirebaseOptions | FirebaseAppImpl,
Expand Down Expand Up @@ -67,6 +69,26 @@ export class FirebaseServerAppImpl
...serverConfig
};

// Parse the installationAuthToken if provided.
DellaBitta marked this conversation as resolved.
Show resolved Hide resolved
// TODO: kick off the token verification process.
this._installationsId = null;
if (this._serverConfig.installationsAuthToken !== undefined) {
try {
const decodedToken = base64Decode(
this._serverConfig.installationsAuthToken.split('.')[1]
);
const tokenJSON = JSON.parse(decodedToken ? decodedToken : '');
this._installationsId = tokenJSON.fid;
} catch (e) {
console.warn(e);
}
if (this._installationsId === null) {
throw ERROR_FACTORY.create(
AppError.INVALID_SERVER_APP_INSTALLATIONS_AUTH_TOKEN
);
}
}

this._finalizationRegistry = null;
if (typeof FinalizationRegistry !== 'undefined') {
this._finalizationRegistry = new FinalizationRegistry(() => {
Expand Down Expand Up @@ -125,6 +147,11 @@ export class FirebaseServerAppImpl
return this._serverConfig;
}

get installationsId(): string | null {
this.checkDestroyed();
return this._installationsId;
}

/**
* This function will throw an Error if the App has already been deleted -
* use before performing API actions on the App.
Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export interface FirebaseServerApp extends FirebaseApp {
* ```
*/
readonly settings: FirebaseServerAppSettings;

/**
* The parsed Firebase Installations Id token if a `installationsAuthToken` was provided to
* {@link (initializeServerApp:1) | initializeServerApp()}. Null otherwise.
*/
readonly installationsId: string | null;
}

/**
Expand Down Expand Up @@ -196,6 +202,26 @@ export interface FirebaseServerAppSettings
*/
authIdToken?: string;

/**
* An optional Installations Auth token which allows the use of Remote Config SDK in
* SSR enviornments.
*
* If provided, the `FirebaseServerApp` will attempt to parse the Installations id
* from the token.
*
* If the token is deemed to be malformed then an error will be
* thrown during the invocation of `initializeServerApp`.
*
* If the the Installations Id and the provided `installationsAuthToken` are successfully parsed,
* then they will be used by the Installations implementation when `getToken` and `getId` are
* invoked.
*
* Attempting to use Remote Config without providing an `installationsAuthToken` here will cause
* Installations to throw errors when Remote Config attempts to query the Installations id and
* authToken.
*/
installationsAuthToken?: string;

/**
* An optional object. If provided, the Firebase SDK uses a `FinalizationRegistry`
* object to monitor the garbage collection status of the provided object. The
Expand Down
1 change: 1 addition & 0 deletions packages/installations/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module.exports = function (config) {
...karmaBase,
// files to load into karma
files,
exclude: ['src/**/*-server-app.test.ts'],
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha']
Expand Down
8 changes: 5 additions & 3 deletions packages/installations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
"build:deps": "lerna run --scope @firebase/installations --include-dependencies build",
"build:release": "yarn build && yarn typings:public",
"dev": "rollup -c -w",
"test": "yarn type-check && yarn test:karma && yarn lint",
"test:ci": "node ../../scripts/run_tests_in_ci.js",
"test:karma": "karma start",
"test": "yarn type-check && yarn test:all && yarn lint",
"test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all",
"test:all": "run-p --npm-path npm test:browser test:node",
"test:browser" : "karma start",
"test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha src/**/*server-app.test.ts --config ../../config/mocharc.node.js",
"test:debug": "karma start --browsers=Chrome --auto-watch",
"trusted-type-check": "tsec -p tsconfig.json --noEmit",
"type-check": "tsc -p . --noEmit",
Expand Down
46 changes: 46 additions & 0 deletions packages/installations/src/api/get-id-server-app.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @license
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { expect } from 'chai';
import { getId } from './get-id';
import {
FAKE_INSTALLATIONS_ID,
getFakeInstallations,
getFakeServerApp
} from '../testing/fake-generators';

describe('getId-serverapp', () => {
it('getId with firebaseServerApp with authIdToken returns valid id', async () => {
const installationsAuthToken = 'fakeToken';
const serverApp = getFakeServerApp(installationsAuthToken);
const installations = getFakeInstallations(serverApp);
const fid = await getId(installations);
expect(fid).to.equal(FAKE_INSTALLATIONS_ID);
});
it('getId with firebaseServerApp without authIdToken throws', async () => {
const serverApp = getFakeServerApp();
const installations = getFakeInstallations(serverApp);
let fails = false;
try {
await getId(installations);
} catch (e) {
console.error(e);
fails = true;
}
expect(fails).to.be.true;
});
});
11 changes: 11 additions & 0 deletions packages/installations/src/api/get-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { getInstallationEntry } from '../helpers/get-installation-entry';
import { refreshAuthToken } from '../helpers/refresh-auth-token';
import { FirebaseInstallationsImpl } from '../interfaces/installation-impl';
import { Installations } from '../interfaces/public-types';
import { _isFirebaseServerApp } from '@firebase/app';
import { ERROR_FACTORY, ErrorCode } from '../util/errors';

/**
* Creates a Firebase Installation if there isn't one for the app and
Expand All @@ -28,6 +30,15 @@ import { Installations } from '../interfaces/public-types';
* @public
*/
export async function getId(installations: Installations): Promise<string> {
if (_isFirebaseServerApp(installations.app)) {
if (!installations.app.installationsId) {
throw ERROR_FACTORY.create(
ErrorCode.SERVER_APP_MISSING_INSTALLATIONS_AUTH_TOKEN
);
}
return installations.app.installationsId;
}

const installationsImpl = installations as FirebaseInstallationsImpl;
const { installationEntry, registrationPromise } = await getInstallationEntry(
installationsImpl
Expand Down
Loading
Loading