Skip to content

Commit

Permalink
Add web-extension package that strips external JS loading (#7766)
Browse files Browse the repository at this point in the history
* Add extension package that strips external JS loading

* Strip recaptcha enterprise script and unsupported methods in extension

* Remove signInWithRedirect from Extension package and dist/extension-esm5 from rollup file

* Add extension entry point to firebase/auth

* Resolve comments

* Rename 'extension' to 'chrome-extension'

* Fix test

* Rename 'chrome-extension' to 'web-extension

---------

Co-authored-by: Sam Olsen <samgho@google.com>
  • Loading branch information
NhienLam and sam-gc committed Jan 24, 2024
1 parent 895d0cf commit e929e3f
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 28 deletions.
68 changes: 68 additions & 0 deletions packages/auth/index.web-extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2023 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.
*/

// Core functionality shared by all clients
export * from './src';

import { ClientPlatform } from './src/core/util/version';

import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';

import {
TotpMultiFactorGenerator,
TotpSecret
} from './src/mfa/assertions/totp';
import { FirebaseApp, getApp, _getProvider } from '@firebase/app';
import { Auth, connectAuthEmulator, initializeAuth } from './index.shared';
import { getDefaultEmulatorHost } from '@firebase/util';
import { registerAuth } from './src/core/auth/register';

/**
* Returns the Auth instance associated with the provided {@link @firebase/app#FirebaseApp}.
* If no instance exists, initializes an Auth instance with platform-specific default dependencies.
*
* @param app - The Firebase App.
*
* @public
*/
function getAuth(app: FirebaseApp = getApp()): Auth {
const provider = _getProvider(app, 'auth');

if (provider.isInitialized()) {
return provider.getImmediate();
}

const auth = initializeAuth(app, {
persistence: [indexedDBLocalPersistence]
});

const authEmulatorHost = getDefaultEmulatorHost('auth');
if (authEmulatorHost) {
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
}

return auth;
}

registerAuth(ClientPlatform.WEB_EXTENSION);

export {
indexedDBLocalPersistence,
TotpMultiFactorGenerator,
TotpSecret,
getAuth
};
16 changes: 15 additions & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"browser": "dist/esm2017/index.js",
"module": "dist/esm2017/index.js",
"cordova": "dist/cordova/index.js",
"web-extension": "dist/web-extension-esm2017/index.js",
"webworker": "dist/index.webworker.esm5.js",
"esm5": "dist/esm5/index.js",
"exports": {
Expand Down Expand Up @@ -41,6 +42,12 @@
"types": "./dist/cordova/index.cordova.d.ts",
"default": "./dist/cordova/index.js"
},
"./web-extension": {
"types:": "./dist/web-extension-esm2017/index.web-extension.d.ts",
"import": "./dist/web-extension-esm2017/index.js",
"require": "./dist/web-extension-cjs/index.js",
"default": "./dist/web-extension-esm2017/index.js"
},
"./internal": {
"types": "./dist/internal/index.d.ts",
"node": {
Expand All @@ -61,14 +68,21 @@
"require": "./dist/browser-cjs/internal.js",
"import": "./dist/esm2017/internal.js"
},
"web-extension": {
"types:": "./dist/web-extension-cjs/internal/index.d.ts",
"import": "./dist/web-extension-esm2017/internal.js",
"require": "./dist/web-extension-cjs/internal.js",
"default": "./dist/web-extension-esm2017/internal.js"
},
"default": "./dist/esm2017/internal.js"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"cordova/package.json",
"internal/package.json"
"internal/package.json",
"web-extension/package.json"
],
"scripts": {
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",
Expand Down
32 changes: 32 additions & 0 deletions packages/auth/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,37 @@ const browserBuilds = [
}
];

const browserWebExtensionBuilds = [
{
input: {
index: 'index.web-extension.ts',
internal: 'internal/index.ts'
},
output: {
dir: 'dist/web-extension-esm2017',
format: 'es',
sourcemap: true
},
plugins: [
...es2017BuildPlugins,
replace(generateBuildTargetReplaceConfig('esm', 2017))
],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
},
{
input: {
index: 'index.web-extension.ts',
internal: 'internal/index.ts'
},
output: [{ dir: 'dist/web-extension-cjs', format: 'cjs', sourcemap: true }],
plugins: [
...es2017BuildPlugins,
replace(generateBuildTargetReplaceConfig('cjs', 2017))
],
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
}
];

const nodeBuilds = [
{
input: {
Expand Down Expand Up @@ -198,6 +229,7 @@ const webWorkerBuild = {

export default [
...browserBuilds,
...browserWebExtensionBuilds,
...nodeBuilds,
cordovaBuild,
rnBuild,
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/core/auth/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ function getVersionForPlatform(
return 'webworker';
case ClientPlatform.CORDOVA:
return 'cordova';
case ClientPlatform.WEB_EXTENSION:
return 'web-extension';
default:
return undefined;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/auth/src/core/util/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,13 @@ describe('core/util/_getClientVersion', () => {
);
});
});

context('Web Extension', () => {
it('should set the correct version', () => {
expect(_getClientVersion(ClientPlatform.WEB_EXTENSION)).to.eq(
`WebExtension/JsCore/${SDK_VERSION}/FirebaseCore-web`
);
});
});
}
});
3 changes: 2 additions & 1 deletion packages/auth/src/core/util/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const enum ClientPlatform {
NODE = 'Node',
REACT_NATIVE = 'ReactNative',
CORDOVA = 'Cordova',
WORKER = 'Worker'
WORKER = 'Worker',
WEB_EXTENSION = 'WebExtension'
}

/*
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/platform_browser/iframe/gapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function loadGapi(auth: AuthInternal): Promise<gapi.iframes.Context> {
};
// Load GApi loader.
return js
._loadJS(`https://apis.google.com/js/api.js?onload=${cbName}`)
._loadJS(`${js._gapiScriptUrl()}?onload=${cbName}`)
.catch(e => reject(e));
}
}).catch(error => {
Expand Down
31 changes: 31 additions & 0 deletions packages/auth/src/platform_browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import { indexedDBLocalPersistence } from './persistence/indexed_db';
import { browserPopupRedirectResolver } from './popup_redirect';
import { Auth, User } from '../model/public_types';
import { getDefaultEmulatorHost, getExperimentalSetting } from '@firebase/util';
import { _setExternalJSProvider } from './load_js';
import { _createError } from '../core/util/assert';
import { AuthErrorCode } from '../core/errors';

const DEFAULT_ID_TOKEN_MAX_AGE = 5 * 60;
const authIdTokenMaxAge =
Expand Down Expand Up @@ -103,4 +106,32 @@ export function getAuth(app: FirebaseApp = getApp()): Auth {
return auth;
}

function getScriptParentElement(): HTMLDocument | HTMLHeadElement {
return document.getElementsByTagName('head')?.[0] ?? document;
}

_setExternalJSProvider({
loadJS(url: string): Promise<Event> {
// TODO: consider adding timeout support & cancellation
return new Promise((resolve, reject) => {
const el = document.createElement('script');
el.setAttribute('src', url);
el.onload = resolve;
el.onerror = e => {
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
error.customData = e as unknown as Record<string, unknown>;
reject(error);
};
el.type = 'text/javascript';
el.charset = 'UTF-8';
getScriptParentElement().appendChild(el);
});
},

gapiScript: 'https://apis.google.com/js/api.js',
recaptchaV2Script: 'https://www.google.com/recaptcha/api.js',
recaptchaEnterpriseScript:
'https://www.google.com/recaptcha/enterprise.js?render='
});

registerAuth(ClientPlatform.BROWSER);
27 changes: 26 additions & 1 deletion packages/auth/src/platform_browser/load_js.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import { expect, use } from 'chai';
import * as sinon from 'sinon';
import sinonChai from 'sinon-chai';

import { _generateCallbackName, _loadJS } from './load_js';
import {
_generateCallbackName,
_loadJS,
_setExternalJSProvider
} from './load_js';
import { _createError } from '../core/util/assert';
import { AuthErrorCode } from '../core/errors';

use(sinonChai);

Expand All @@ -34,6 +40,25 @@ describe('platform-browser/load_js', () => {

describe('_loadJS', () => {
it('sets the appropriate properties', () => {
_setExternalJSProvider({
loadJS(url: string): Promise<Event> {
return new Promise((resolve, reject) => {
const el = document.createElement('script');
el.setAttribute('src', url);
el.onload = resolve;
el.onerror = e => {
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
error.customData = e as unknown as Record<string, unknown>;
reject(error);
};
el.type = 'text/javascript';
el.charset = 'UTF-8';
});
},
gapiScript: 'https://gapiScript',
recaptchaV2Script: 'https://recaptchaV2Script',
recaptchaEnterpriseScript: 'https://recaptchaEnterpriseScript'
});
const el = document.createElement('script');
sinon.stub(el); // Prevent actually setting the src attribute
sinon.stub(document, 'createElement').returns(el);
Expand Down
49 changes: 31 additions & 18 deletions packages/auth/src/platform_browser/load_js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,41 @@
* limitations under the License.
*/

import { AuthErrorCode } from '../core/errors';
import { _createError } from '../core/util/assert';
interface ExternalJSProvider {
loadJS(url: string): Promise<Event>;
recaptchaV2Script: string;
recaptchaEnterpriseScript: string;
gapiScript: string;
}

let externalJSProvider: ExternalJSProvider = {
async loadJS() {
throw new Error('Unable to load external scripts');
},

recaptchaV2Script: '',
recaptchaEnterpriseScript: '',
gapiScript: ''
};

function getScriptParentElement(): HTMLDocument | HTMLHeadElement {
return document.getElementsByTagName('head')?.[0] ?? document;
export function _setExternalJSProvider(p: ExternalJSProvider): void {
externalJSProvider = p;
}

export function _loadJS(url: string): Promise<Event> {
// TODO: consider adding timeout support & cancellation
return new Promise((resolve, reject) => {
const el = document.createElement('script');
el.setAttribute('src', url);
el.onload = resolve;
el.onerror = e => {
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
error.customData = e as unknown as Record<string, unknown>;
reject(error);
};
el.type = 'text/javascript';
el.charset = 'UTF-8';
getScriptParentElement().appendChild(el);
});
return externalJSProvider.loadJS(url);
}

export function _recaptchaV2ScriptUrl(): string {
return externalJSProvider.recaptchaV2Script;
}

export function _recaptchaEnterpriseScriptUrl(): string {
return externalJSProvider.recaptchaEnterpriseScript;
}

export function _gapiScriptUrl(): string {
return externalJSProvider.gapiScript;
}

export function _generateCallbackName(prefix: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ import { _castAuth } from '../../core/auth/auth_impl';
import * as jsHelpers from '../load_js';
import { AuthErrorCode } from '../../core/errors';

const RECAPTCHA_ENTERPRISE_URL =
'https://www.google.com/recaptcha/enterprise.js?render=';

export const RECAPTCHA_ENTERPRISE_VERIFIER_TYPE = 'recaptcha-enterprise';
export const FAKE_TOKEN = 'NO_RECAPTCHA';

Expand Down Expand Up @@ -134,8 +131,12 @@ export class RecaptchaEnterpriseVerifier {
);
return;
}
let url = jsHelpers._recaptchaEnterpriseScriptUrl();
if (url.length !== 0) {
url += siteKey;
}
jsHelpers
._loadJS(RECAPTCHA_ENTERPRISE_URL + siteKey)
._loadJS(url)
.then(() => {
retrieveRecaptchaToken(siteKey, resolve, reject);
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { MockReCaptcha } from './recaptcha_mock';
// to be kept around
export const _JSLOAD_CALLBACK = jsHelpers._generateCallbackName('rcb');
const NETWORK_TIMEOUT_DELAY = new Delay(30000, 60000);
const RECAPTCHA_BASE = 'https://www.google.com/recaptcha/api.js?';

/**
* We need to mark this interface as internal explicitly to exclude it in the public typings, because
Expand Down Expand Up @@ -91,7 +90,7 @@ export class ReCaptchaLoaderImpl implements ReCaptchaLoader {
resolve(recaptcha);
};

const url = `${RECAPTCHA_BASE}?${querystring({
const url = `${jsHelpers._recaptchaV2ScriptUrl()}?${querystring({
onload: _JSLOAD_CALLBACK,
render: 'explicit',
hl
Expand Down
8 changes: 8 additions & 0 deletions packages/auth/web-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@firebase/auth/web-extension",
"description": "A Chrome-Manifest-v3-specific build of the Firebase Auth JS SDK",
"main": "../dist/web-extension-cjs/index.js",
"browser": "../dist/web-extension-esm2017/index.js",
"module": "../dist/web-extension-esm2017/index.js",
"typings": "../dist/web-extension-esm2017/index.web-extension.d.ts"
}

0 comments on commit e929e3f

Please sign in to comment.