Skip to content

Commit

Permalink
Merge da58c9a into ffbf5a6
Browse files Browse the repository at this point in the history
  • Loading branch information
DellaBitta authored Nov 22, 2024
2 parents ffbf5a6 + da58c9a commit ed6ed8b
Show file tree
Hide file tree
Showing 18 changed files with 339 additions and 14 deletions.
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.
// 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

0 comments on commit ed6ed8b

Please sign in to comment.