diff --git a/packages/auth/index.web-extension.ts b/packages/auth/index.web-extension.ts new file mode 100644 index 00000000000..d7296e9c839 --- /dev/null +++ b/packages/auth/index.web-extension.ts @@ -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 +}; diff --git a/packages/auth/package.json b/packages/auth/package.json index 57300a8c00f..bd2d47d3fd5 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -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": { @@ -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": { @@ -61,6 +68,12 @@ "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" @@ -68,7 +81,8 @@ "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'", diff --git a/packages/auth/rollup.config.js b/packages/auth/rollup.config.js index 01e358a9aff..c31417ca1a4 100644 --- a/packages/auth/rollup.config.js +++ b/packages/auth/rollup.config.js @@ -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: { @@ -198,6 +229,7 @@ const webWorkerBuild = { export default [ ...browserBuilds, + ...browserWebExtensionBuilds, ...nodeBuilds, cordovaBuild, rnBuild, diff --git a/packages/auth/src/core/auth/register.ts b/packages/auth/src/core/auth/register.ts index c203d682d00..4b891618d3a 100644 --- a/packages/auth/src/core/auth/register.ts +++ b/packages/auth/src/core/auth/register.ts @@ -49,6 +49,8 @@ function getVersionForPlatform( return 'webworker'; case ClientPlatform.CORDOVA: return 'cordova'; + case ClientPlatform.WEB_EXTENSION: + return 'web-extension'; default: return undefined; } diff --git a/packages/auth/src/core/util/version.test.ts b/packages/auth/src/core/util/version.test.ts index 86ab624f22d..80cfc41b378 100644 --- a/packages/auth/src/core/util/version.test.ts +++ b/packages/auth/src/core/util/version.test.ts @@ -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` + ); + }); + }); } }); diff --git a/packages/auth/src/core/util/version.ts b/packages/auth/src/core/util/version.ts index dae126296d6..777b4eadc6d 100644 --- a/packages/auth/src/core/util/version.ts +++ b/packages/auth/src/core/util/version.ts @@ -31,7 +31,8 @@ export const enum ClientPlatform { NODE = 'Node', REACT_NATIVE = 'ReactNative', CORDOVA = 'Cordova', - WORKER = 'Worker' + WORKER = 'Worker', + WEB_EXTENSION = 'WebExtension' } /* diff --git a/packages/auth/src/platform_browser/iframe/gapi.ts b/packages/auth/src/platform_browser/iframe/gapi.ts index a76cf886d34..7d9633043fe 100644 --- a/packages/auth/src/platform_browser/iframe/gapi.ts +++ b/packages/auth/src/platform_browser/iframe/gapi.ts @@ -104,7 +104,7 @@ function loadGapi(auth: AuthInternal): Promise { }; // 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 => { diff --git a/packages/auth/src/platform_browser/index.ts b/packages/auth/src/platform_browser/index.ts index 9bfe4e3b01a..6399072d713 100644 --- a/packages/auth/src/platform_browser/index.ts +++ b/packages/auth/src/platform_browser/index.ts @@ -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 = @@ -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 { + // 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; + 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); diff --git a/packages/auth/src/platform_browser/load_js.test.ts b/packages/auth/src/platform_browser/load_js.test.ts index df6e994f614..972fb292065 100644 --- a/packages/auth/src/platform_browser/load_js.test.ts +++ b/packages/auth/src/platform_browser/load_js.test.ts @@ -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); @@ -34,6 +40,25 @@ describe('platform-browser/load_js', () => { describe('_loadJS', () => { it('sets the appropriate properties', () => { + _setExternalJSProvider({ + loadJS(url: string): Promise { + 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; + 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); diff --git a/packages/auth/src/platform_browser/load_js.ts b/packages/auth/src/platform_browser/load_js.ts index 229442b0b51..b7eb0ce4690 100644 --- a/packages/auth/src/platform_browser/load_js.ts +++ b/packages/auth/src/platform_browser/load_js.ts @@ -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; + 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 { - // 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; - 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 { diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts index 8d188032e36..cccd737defc 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha_enterprise_verifier.ts @@ -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'; @@ -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); }) diff --git a/packages/auth/src/platform_browser/recaptcha/recaptcha_loader.ts b/packages/auth/src/platform_browser/recaptcha/recaptcha_loader.ts index 2b945464cea..f510436223f 100644 --- a/packages/auth/src/platform_browser/recaptcha/recaptcha_loader.ts +++ b/packages/auth/src/platform_browser/recaptcha/recaptcha_loader.ts @@ -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 @@ -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 diff --git a/packages/auth/web-extension/package.json b/packages/auth/web-extension/package.json new file mode 100644 index 00000000000..1a46dcf1f90 --- /dev/null +++ b/packages/auth/web-extension/package.json @@ -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" +} \ No newline at end of file diff --git a/packages/firebase/auth/web-extension/index.ts b/packages/firebase/auth/web-extension/index.ts new file mode 100644 index 00000000000..52c9bee35eb --- /dev/null +++ b/packages/firebase/auth/web-extension/index.ts @@ -0,0 +1,18 @@ +/** + * @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. + */ + +export * from '@firebase/auth/web-extension'; diff --git a/packages/firebase/auth/web-extension/package.json b/packages/firebase/auth/web-extension/package.json new file mode 100644 index 00000000000..6ff8f79531e --- /dev/null +++ b/packages/firebase/auth/web-extension/package.json @@ -0,0 +1,7 @@ +{ + "name": "firebase/auth/web-extension", + "main": "dist/index.cjs.js", + "browser": "dist/esm/index.esm.js", + "module": "dist/esm/index.esm.js", + "typings": "dist/auth/web-extension/index.d.ts" +} \ No newline at end of file diff --git a/packages/firebase/package.json b/packages/firebase/package.json index d6c9606a29b..a06cc56183c 100644 --- a/packages/firebase/package.json +++ b/packages/firebase/package.json @@ -83,6 +83,18 @@ }, "default": "./auth/cordova/dist/esm/index.esm.js" }, + "./auth/web-extension": { + "types": "./auth/web-extension/dist/auth/web-extension/index.d.ts", + "node": { + "require": "./auth/web-extension/dist/index.cjs.js", + "import": "./auth/web-extension/dist/index.mjs" + }, + "browser": { + "require": "./auth/web-extension/dist/index.cjs.js", + "import": "./auth/web-extension/dist/esm/index.esm.js" + }, + "default": "./auth/web-extension/dist/esm/index.esm.js" + }, "./database": { "types": "./database/dist/database/index.d.ts", "node": { @@ -409,6 +421,7 @@ "app-check", "auth", "auth/cordova", + "auth/web-extension", "functions", "firestore", "firestore/lite",