Skip to content

Commit e929e3f

Browse files
NhienLamsam-gc
andauthored
Add web-extension package that strips external JS loading (#7766)
* 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 <[email protected]>
1 parent 895d0cf commit e929e3f

File tree

16 files changed

+268
-28
lines changed

16 files changed

+268
-28
lines changed

packages/auth/index.web-extension.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// Core functionality shared by all clients
19+
export * from './src';
20+
21+
import { ClientPlatform } from './src/core/util/version';
22+
23+
import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
24+
25+
import {
26+
TotpMultiFactorGenerator,
27+
TotpSecret
28+
} from './src/mfa/assertions/totp';
29+
import { FirebaseApp, getApp, _getProvider } from '@firebase/app';
30+
import { Auth, connectAuthEmulator, initializeAuth } from './index.shared';
31+
import { getDefaultEmulatorHost } from '@firebase/util';
32+
import { registerAuth } from './src/core/auth/register';
33+
34+
/**
35+
* Returns the Auth instance associated with the provided {@link @firebase/app#FirebaseApp}.
36+
* If no instance exists, initializes an Auth instance with platform-specific default dependencies.
37+
*
38+
* @param app - The Firebase App.
39+
*
40+
* @public
41+
*/
42+
function getAuth(app: FirebaseApp = getApp()): Auth {
43+
const provider = _getProvider(app, 'auth');
44+
45+
if (provider.isInitialized()) {
46+
return provider.getImmediate();
47+
}
48+
49+
const auth = initializeAuth(app, {
50+
persistence: [indexedDBLocalPersistence]
51+
});
52+
53+
const authEmulatorHost = getDefaultEmulatorHost('auth');
54+
if (authEmulatorHost) {
55+
connectAuthEmulator(auth, `http://${authEmulatorHost}`);
56+
}
57+
58+
return auth;
59+
}
60+
61+
registerAuth(ClientPlatform.WEB_EXTENSION);
62+
63+
export {
64+
indexedDBLocalPersistence,
65+
TotpMultiFactorGenerator,
66+
TotpSecret,
67+
getAuth
68+
};

packages/auth/package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"browser": "dist/esm2017/index.js",
99
"module": "dist/esm2017/index.js",
1010
"cordova": "dist/cordova/index.js",
11+
"web-extension": "dist/web-extension-esm2017/index.js",
1112
"webworker": "dist/index.webworker.esm5.js",
1213
"esm5": "dist/esm5/index.js",
1314
"exports": {
@@ -41,6 +42,12 @@
4142
"types": "./dist/cordova/index.cordova.d.ts",
4243
"default": "./dist/cordova/index.js"
4344
},
45+
"./web-extension": {
46+
"types:": "./dist/web-extension-esm2017/index.web-extension.d.ts",
47+
"import": "./dist/web-extension-esm2017/index.js",
48+
"require": "./dist/web-extension-cjs/index.js",
49+
"default": "./dist/web-extension-esm2017/index.js"
50+
},
4451
"./internal": {
4552
"types": "./dist/internal/index.d.ts",
4653
"node": {
@@ -61,14 +68,21 @@
6168
"require": "./dist/browser-cjs/internal.js",
6269
"import": "./dist/esm2017/internal.js"
6370
},
71+
"web-extension": {
72+
"types:": "./dist/web-extension-cjs/internal/index.d.ts",
73+
"import": "./dist/web-extension-esm2017/internal.js",
74+
"require": "./dist/web-extension-cjs/internal.js",
75+
"default": "./dist/web-extension-esm2017/internal.js"
76+
},
6477
"default": "./dist/esm2017/internal.js"
6578
},
6679
"./package.json": "./package.json"
6780
},
6881
"files": [
6982
"dist",
7083
"cordova/package.json",
71-
"internal/package.json"
84+
"internal/package.json",
85+
"web-extension/package.json"
7286
],
7387
"scripts": {
7488
"lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'",

packages/auth/rollup.config.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,37 @@ const browserBuilds = [
112112
}
113113
];
114114

115+
const browserWebExtensionBuilds = [
116+
{
117+
input: {
118+
index: 'index.web-extension.ts',
119+
internal: 'internal/index.ts'
120+
},
121+
output: {
122+
dir: 'dist/web-extension-esm2017',
123+
format: 'es',
124+
sourcemap: true
125+
},
126+
plugins: [
127+
...es2017BuildPlugins,
128+
replace(generateBuildTargetReplaceConfig('esm', 2017))
129+
],
130+
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
131+
},
132+
{
133+
input: {
134+
index: 'index.web-extension.ts',
135+
internal: 'internal/index.ts'
136+
},
137+
output: [{ dir: 'dist/web-extension-cjs', format: 'cjs', sourcemap: true }],
138+
plugins: [
139+
...es2017BuildPlugins,
140+
replace(generateBuildTargetReplaceConfig('cjs', 2017))
141+
],
142+
external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`))
143+
}
144+
];
145+
115146
const nodeBuilds = [
116147
{
117148
input: {
@@ -198,6 +229,7 @@ const webWorkerBuild = {
198229

199230
export default [
200231
...browserBuilds,
232+
...browserWebExtensionBuilds,
201233
...nodeBuilds,
202234
cordovaBuild,
203235
rnBuild,

packages/auth/src/core/auth/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ function getVersionForPlatform(
4949
return 'webworker';
5050
case ClientPlatform.CORDOVA:
5151
return 'cordova';
52+
case ClientPlatform.WEB_EXTENSION:
53+
return 'web-extension';
5254
default:
5355
return undefined;
5456
}

packages/auth/src/core/util/version.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,13 @@ describe('core/util/_getClientVersion', () => {
5656
);
5757
});
5858
});
59+
60+
context('Web Extension', () => {
61+
it('should set the correct version', () => {
62+
expect(_getClientVersion(ClientPlatform.WEB_EXTENSION)).to.eq(
63+
`WebExtension/JsCore/${SDK_VERSION}/FirebaseCore-web`
64+
);
65+
});
66+
});
5967
}
6068
});

packages/auth/src/core/util/version.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export const enum ClientPlatform {
3131
NODE = 'Node',
3232
REACT_NATIVE = 'ReactNative',
3333
CORDOVA = 'Cordova',
34-
WORKER = 'Worker'
34+
WORKER = 'Worker',
35+
WEB_EXTENSION = 'WebExtension'
3536
}
3637

3738
/*

packages/auth/src/platform_browser/iframe/gapi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function loadGapi(auth: AuthInternal): Promise<gapi.iframes.Context> {
104104
};
105105
// Load GApi loader.
106106
return js
107-
._loadJS(`https://apis.google.com/js/api.js?onload=${cbName}`)
107+
._loadJS(`${js._gapiScriptUrl()}?onload=${cbName}`)
108108
.catch(e => reject(e));
109109
}
110110
}).catch(error => {

packages/auth/src/platform_browser/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import { indexedDBLocalPersistence } from './persistence/indexed_db';
3131
import { browserPopupRedirectResolver } from './popup_redirect';
3232
import { Auth, User } from '../model/public_types';
3333
import { getDefaultEmulatorHost, getExperimentalSetting } from '@firebase/util';
34+
import { _setExternalJSProvider } from './load_js';
35+
import { _createError } from '../core/util/assert';
36+
import { AuthErrorCode } from '../core/errors';
3437

3538
const DEFAULT_ID_TOKEN_MAX_AGE = 5 * 60;
3639
const authIdTokenMaxAge =
@@ -103,4 +106,32 @@ export function getAuth(app: FirebaseApp = getApp()): Auth {
103106
return auth;
104107
}
105108

109+
function getScriptParentElement(): HTMLDocument | HTMLHeadElement {
110+
return document.getElementsByTagName('head')?.[0] ?? document;
111+
}
112+
113+
_setExternalJSProvider({
114+
loadJS(url: string): Promise<Event> {
115+
// TODO: consider adding timeout support & cancellation
116+
return new Promise((resolve, reject) => {
117+
const el = document.createElement('script');
118+
el.setAttribute('src', url);
119+
el.onload = resolve;
120+
el.onerror = e => {
121+
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
122+
error.customData = e as unknown as Record<string, unknown>;
123+
reject(error);
124+
};
125+
el.type = 'text/javascript';
126+
el.charset = 'UTF-8';
127+
getScriptParentElement().appendChild(el);
128+
});
129+
},
130+
131+
gapiScript: 'https://apis.google.com/js/api.js',
132+
recaptchaV2Script: 'https://www.google.com/recaptcha/api.js',
133+
recaptchaEnterpriseScript:
134+
'https://www.google.com/recaptcha/enterprise.js?render='
135+
});
136+
106137
registerAuth(ClientPlatform.BROWSER);

packages/auth/src/platform_browser/load_js.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ import { expect, use } from 'chai';
1919
import * as sinon from 'sinon';
2020
import sinonChai from 'sinon-chai';
2121

22-
import { _generateCallbackName, _loadJS } from './load_js';
22+
import {
23+
_generateCallbackName,
24+
_loadJS,
25+
_setExternalJSProvider
26+
} from './load_js';
27+
import { _createError } from '../core/util/assert';
28+
import { AuthErrorCode } from '../core/errors';
2329

2430
use(sinonChai);
2531

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

3541
describe('_loadJS', () => {
3642
it('sets the appropriate properties', () => {
43+
_setExternalJSProvider({
44+
loadJS(url: string): Promise<Event> {
45+
return new Promise((resolve, reject) => {
46+
const el = document.createElement('script');
47+
el.setAttribute('src', url);
48+
el.onload = resolve;
49+
el.onerror = e => {
50+
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
51+
error.customData = e as unknown as Record<string, unknown>;
52+
reject(error);
53+
};
54+
el.type = 'text/javascript';
55+
el.charset = 'UTF-8';
56+
});
57+
},
58+
gapiScript: 'https://gapiScript',
59+
recaptchaV2Script: 'https://recaptchaV2Script',
60+
recaptchaEnterpriseScript: 'https://recaptchaEnterpriseScript'
61+
});
3762
const el = document.createElement('script');
3863
sinon.stub(el); // Prevent actually setting the src attribute
3964
sinon.stub(document, 'createElement').returns(el);

packages/auth/src/platform_browser/load_js.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,41 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AuthErrorCode } from '../core/errors';
19-
import { _createError } from '../core/util/assert';
18+
interface ExternalJSProvider {
19+
loadJS(url: string): Promise<Event>;
20+
recaptchaV2Script: string;
21+
recaptchaEnterpriseScript: string;
22+
gapiScript: string;
23+
}
24+
25+
let externalJSProvider: ExternalJSProvider = {
26+
async loadJS() {
27+
throw new Error('Unable to load external scripts');
28+
},
29+
30+
recaptchaV2Script: '',
31+
recaptchaEnterpriseScript: '',
32+
gapiScript: ''
33+
};
2034

21-
function getScriptParentElement(): HTMLDocument | HTMLHeadElement {
22-
return document.getElementsByTagName('head')?.[0] ?? document;
35+
export function _setExternalJSProvider(p: ExternalJSProvider): void {
36+
externalJSProvider = p;
2337
}
2438

2539
export function _loadJS(url: string): Promise<Event> {
26-
// TODO: consider adding timeout support & cancellation
27-
return new Promise((resolve, reject) => {
28-
const el = document.createElement('script');
29-
el.setAttribute('src', url);
30-
el.onload = resolve;
31-
el.onerror = e => {
32-
const error = _createError(AuthErrorCode.INTERNAL_ERROR);
33-
error.customData = e as unknown as Record<string, unknown>;
34-
reject(error);
35-
};
36-
el.type = 'text/javascript';
37-
el.charset = 'UTF-8';
38-
getScriptParentElement().appendChild(el);
39-
});
40+
return externalJSProvider.loadJS(url);
41+
}
42+
43+
export function _recaptchaV2ScriptUrl(): string {
44+
return externalJSProvider.recaptchaV2Script;
45+
}
46+
47+
export function _recaptchaEnterpriseScriptUrl(): string {
48+
return externalJSProvider.recaptchaEnterpriseScript;
49+
}
50+
51+
export function _gapiScriptUrl(): string {
52+
return externalJSProvider.gapiScript;
4053
}
4154

4255
export function _generateCallbackName(prefix: string): string {

packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ import { _castAuth } from '../../core/auth/auth_impl';
3131
import * as jsHelpers from '../load_js';
3232
import { AuthErrorCode } from '../../core/errors';
3333

34-
const RECAPTCHA_ENTERPRISE_URL =
35-
'https://www.google.com/recaptcha/enterprise.js?render=';
36-
3734
export const RECAPTCHA_ENTERPRISE_VERIFIER_TYPE = 'recaptcha-enterprise';
3835
export const FAKE_TOKEN = 'NO_RECAPTCHA';
3936

@@ -134,8 +131,12 @@ export class RecaptchaEnterpriseVerifier {
134131
);
135132
return;
136133
}
134+
let url = jsHelpers._recaptchaEnterpriseScriptUrl();
135+
if (url.length !== 0) {
136+
url += siteKey;
137+
}
137138
jsHelpers
138-
._loadJS(RECAPTCHA_ENTERPRISE_URL + siteKey)
139+
._loadJS(url)
139140
.then(() => {
140141
retrieveRecaptchaToken(siteKey, resolve, reject);
141142
})

packages/auth/src/platform_browser/recaptcha/recaptcha_loader.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import { MockReCaptcha } from './recaptcha_mock';
3030
// to be kept around
3131
export const _JSLOAD_CALLBACK = jsHelpers._generateCallbackName('rcb');
3232
const NETWORK_TIMEOUT_DELAY = new Delay(30000, 60000);
33-
const RECAPTCHA_BASE = 'https://www.google.com/recaptcha/api.js?';
3433

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

94-
const url = `${RECAPTCHA_BASE}?${querystring({
93+
const url = `${jsHelpers._recaptchaV2ScriptUrl()}?${querystring({
9594
onload: _JSLOAD_CALLBACK,
9695
render: 'explicit',
9796
hl
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@firebase/auth/web-extension",
3+
"description": "A Chrome-Manifest-v3-specific build of the Firebase Auth JS SDK",
4+
"main": "../dist/web-extension-cjs/index.js",
5+
"browser": "../dist/web-extension-esm2017/index.js",
6+
"module": "../dist/web-extension-esm2017/index.js",
7+
"typings": "../dist/web-extension-esm2017/index.web-extension.d.ts"
8+
}

0 commit comments

Comments
 (0)