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 extension package that strips external JS loading #7766

Merged
merged 8 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
},
hsubox76 marked this conversation as resolved.
Show resolved Hide resolved
"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='

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ?render= needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I copied the exact url from https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts#L35.

The full url we need to pass in _loadJS is 'https://www.google.com/recaptcha/enterprise.js?render=SITE-KEY'.

So if we remove '?render=' from here, we will have to update this line to be

url += '?render=' + siteKey;

});

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"
}
Loading
Loading