UNPKG

@firebase/auth

Version:

The Firebase Authenticaton component of the Firebase JS SDK.

1,213 lines (1,202 loc) 149 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var register = require('./register-dd6291f1.js'); var util = require('@firebase/util'); var app = require('@firebase/app'); require('tslib'); require('@firebase/component'); require('@firebase/logger'); /** * @license * Copyright 2021 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. */ /** * An enum of factors that may be used for multifactor authentication. * * @public */ const FactorId = { /** Phone as second factor */ PHONE: 'phone', TOTP: 'totp' }; /** * Enumeration of supported providers. * * @public */ const ProviderId = { /** Facebook provider ID */ FACEBOOK: 'facebook.com', /** GitHub provider ID */ GITHUB: 'github.com', /** Google provider ID */ GOOGLE: 'google.com', /** Password provider */ PASSWORD: 'password', /** Phone provider */ PHONE: 'phone', /** Twitter provider ID */ TWITTER: 'twitter.com' }; /** * Enumeration of supported sign-in methods. * * @public */ const SignInMethod = { /** Email link sign in method */ EMAIL_LINK: 'emailLink', /** Email/password sign in method */ EMAIL_PASSWORD: 'password', /** Facebook sign in method */ FACEBOOK: 'facebook.com', /** GitHub sign in method */ GITHUB: 'github.com', /** Google sign in method */ GOOGLE: 'google.com', /** Phone sign in method */ PHONE: 'phone', /** Twitter sign in method */ TWITTER: 'twitter.com' }; /** * Enumeration of supported operation types. * * @public */ const OperationType = { /** Operation involving linking an additional provider to an already signed-in user. */ LINK: 'link', /** Operation involving using a provider to reauthenticate an already signed-in user. */ REAUTHENTICATE: 'reauthenticate', /** Operation involving signing in a user. */ SIGN_IN: 'signIn' }; /** * An enumeration of the possible email action types. * * @public */ const ActionCodeOperation = { /** The email link sign-in action. */ EMAIL_SIGNIN: 'EMAIL_SIGNIN', /** The password reset action. */ PASSWORD_RESET: 'PASSWORD_RESET', /** The email revocation action. */ RECOVER_EMAIL: 'RECOVER_EMAIL', /** The revert second factor addition email action. */ REVERT_SECOND_FACTOR_ADDITION: 'REVERT_SECOND_FACTOR_ADDITION', /** The revert second factor addition email action. */ VERIFY_AND_CHANGE_EMAIL: 'VERIFY_AND_CHANGE_EMAIL', /** The email verification action. */ VERIFY_EMAIL: 'VERIFY_EMAIL' }; /** * @license * Copyright 2019 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. */ // There are two different browser persistence types: local and session. // Both have the same implementation but use a different underlying storage // object. class BrowserPersistenceClass { constructor(storageRetriever, type) { this.storageRetriever = storageRetriever; this.type = type; } _isAvailable() { try { if (!this.storage) { return Promise.resolve(false); } this.storage.setItem(register.STORAGE_AVAILABLE_KEY, '1'); this.storage.removeItem(register.STORAGE_AVAILABLE_KEY); return Promise.resolve(true); } catch (_a) { return Promise.resolve(false); } } _set(key, value) { this.storage.setItem(key, JSON.stringify(value)); return Promise.resolve(); } _get(key) { const json = this.storage.getItem(key); return Promise.resolve(json ? JSON.parse(json) : null); } _remove(key) { this.storage.removeItem(key); return Promise.resolve(); } get storage() { return this.storageRetriever(); } } /** * @license * Copyright 2020 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. */ // The polling period in case events are not supported const _POLLING_INTERVAL_MS = 1000; // The IE 10 localStorage cross tab synchronization delay in milliseconds const IE10_LOCAL_STORAGE_SYNC_DELAY = 10; class BrowserLocalPersistence extends BrowserPersistenceClass { constructor() { super(() => window.localStorage, "LOCAL" /* PersistenceType.LOCAL */); this.boundEventHandler = (event, poll) => this.onStorageEvent(event, poll); this.listeners = {}; this.localCache = {}; // setTimeout return value is platform specific // eslint-disable-next-line @typescript-eslint/no-explicit-any this.pollTimer = null; // Whether to use polling instead of depending on window events this.fallbackToPolling = register._isMobileBrowser(); this._shouldAllowMigration = true; } forAllChangedKeys(cb) { // Check all keys with listeners on them. for (const key of Object.keys(this.listeners)) { // Get value from localStorage. const newValue = this.storage.getItem(key); const oldValue = this.localCache[key]; // If local map value does not match, trigger listener with storage event. // Differentiate this simulated event from the real storage event. if (newValue !== oldValue) { cb(key, oldValue, newValue); } } } onStorageEvent(event, poll = false) { // Key would be null in some situations, like when localStorage is cleared if (!event.key) { this.forAllChangedKeys((key, _oldValue, newValue) => { this.notifyListeners(key, newValue); }); return; } const key = event.key; // Check the mechanism how this event was detected. // The first event will dictate the mechanism to be used. if (poll) { // Environment detects storage changes via polling. // Remove storage event listener to prevent possible event duplication. this.detachListener(); } else { // Environment detects storage changes via storage event listener. // Remove polling listener to prevent possible event duplication. this.stopPolling(); } const triggerListeners = () => { // Keep local map up to date in case storage event is triggered before // poll. const storedValue = this.storage.getItem(key); if (!poll && this.localCache[key] === storedValue) { // Real storage event which has already been detected, do nothing. // This seems to trigger in some IE browsers for some reason. return; } this.notifyListeners(key, storedValue); }; const storedValue = this.storage.getItem(key); if (register._isIE10() && storedValue !== event.newValue && event.newValue !== event.oldValue) { // IE 10 has this weird bug where a storage event would trigger with the // correct key, oldValue and newValue but localStorage.getItem(key) does // not yield the updated value until a few milliseconds. This ensures // this recovers from that situation. setTimeout(triggerListeners, IE10_LOCAL_STORAGE_SYNC_DELAY); } else { triggerListeners(); } } notifyListeners(key, value) { this.localCache[key] = value; const listeners = this.listeners[key]; if (listeners) { for (const listener of Array.from(listeners)) { listener(value ? JSON.parse(value) : value); } } } startPolling() { this.stopPolling(); this.pollTimer = setInterval(() => { this.forAllChangedKeys((key, oldValue, newValue) => { this.onStorageEvent(new StorageEvent('storage', { key, oldValue, newValue }), /* poll */ true); }); }, _POLLING_INTERVAL_MS); } stopPolling() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } } attachListener() { window.addEventListener('storage', this.boundEventHandler); } detachListener() { window.removeEventListener('storage', this.boundEventHandler); } _addListener(key, listener) { if (Object.keys(this.listeners).length === 0) { // Whether browser can detect storage event when it had already been pushed to the background. // This may happen in some mobile browsers. A localStorage change in the foreground window // will not be detected in the background window via the storage event. // This was detected in iOS 7.x mobile browsers if (this.fallbackToPolling) { this.startPolling(); } else { this.attachListener(); } } if (!this.listeners[key]) { this.listeners[key] = new Set(); // Populate the cache to avoid spuriously triggering on first poll. this.localCache[key] = this.storage.getItem(key); } this.listeners[key].add(listener); } _removeListener(key, listener) { if (this.listeners[key]) { this.listeners[key].delete(listener); if (this.listeners[key].size === 0) { delete this.listeners[key]; } } if (Object.keys(this.listeners).length === 0) { this.detachListener(); this.stopPolling(); } } // Update local cache on base operations: async _set(key, value) { await super._set(key, value); this.localCache[key] = JSON.stringify(value); } async _get(key) { const value = await super._get(key); this.localCache[key] = JSON.stringify(value); return value; } async _remove(key) { await super._remove(key); delete this.localCache[key]; } } BrowserLocalPersistence.type = 'LOCAL'; /** * An implementation of {@link Persistence} of type `LOCAL` using `localStorage` * for the underlying storage. * * @public */ const browserLocalPersistence = BrowserLocalPersistence; /** * @license * Copyright 2025 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. */ const POLLING_INTERVAL_MS = 1000; // Pull a cookie value from document.cookie function getDocumentCookie(name) { var _a, _b; const escapedName = name.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); const matcher = RegExp(`${escapedName}=([^;]+)`); return (_b = (_a = document.cookie.match(matcher)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; } // Produce a sanitized cookie name from the persistence key function getCookieName(key) { // __HOST- doesn't work in localhost https://issues.chromium.org/issues/40196122 but it has // desirable security properties, so lets use a different cookie name while in dev-mode. // Already checked isSecureContext in _isAvailable, so if it's http we're hitting local. const isDevMode = window.location.protocol === 'http:'; return `${isDevMode ? '__dev_' : '__HOST-'}FIREBASE_${key.split(':')[3]}`; } class CookiePersistence { constructor() { this.type = "COOKIE" /* PersistenceType.COOKIE */; this.listenerUnsubscribes = new Map(); } // used to get the URL to the backend to proxy to _getFinalTarget(originalUrl) { if (typeof window === undefined) { return originalUrl; } const url = new URL(`${window.location.origin}/__cookies__`); url.searchParams.set('finalTarget', originalUrl); return url; } // To be a usable persistence method in a chain browserCookiePersistence ensures that // prerequisites have been met, namely that we're in a secureContext, navigator and document are // available and cookies are enabled. Not all UAs support these method, so fallback accordingly. async _isAvailable() { var _a; if (typeof isSecureContext === 'boolean' && !isSecureContext) { return false; } if (typeof navigator === 'undefined' || typeof document === 'undefined') { return false; } return (_a = navigator.cookieEnabled) !== null && _a !== void 0 ? _a : true; } // Set should be a noop as we expect middleware to handle this async _set(_key, _value) { return; } // Attempt to get the cookie from cookieStore, fallback to document.cookie async _get(key) { if (!this._isAvailable()) { return null; } const name = getCookieName(key); if (window.cookieStore) { const cookie = await window.cookieStore.get(name); return cookie === null || cookie === void 0 ? void 0 : cookie.value; } return getDocumentCookie(name); } // Log out by overriding the idToken with a sentinel value of "" async _remove(key) { if (!this._isAvailable()) { return; } // To make sure we don't hit signout over and over again, only do this operation if we need to // with the logout sentinel value of "" this can cause race conditions. Unnecessary set-cookie // headers will reduce CDN hit rates too. const existingValue = await this._get(key); if (!existingValue) { return; } const name = getCookieName(key); document.cookie = `${name}=;Max-Age=34560000;Partitioned;Secure;SameSite=Strict;Path=/;Priority=High`; await fetch(`/__cookies__`, { method: 'DELETE' }).catch(() => undefined); } // Listen for cookie changes, both cookieStore and fallback to polling document.cookie _addListener(key, listener) { if (!this._isAvailable()) { return; } const name = getCookieName(key); if (window.cookieStore) { const cb = ((event) => { const changedCookie = event.changed.find(change => change.name === name); if (changedCookie) { listener(changedCookie.value); } const deletedCookie = event.deleted.find(change => change.name === name); if (deletedCookie) { listener(null); } }); const unsubscribe = () => window.cookieStore.removeEventListener('change', cb); this.listenerUnsubscribes.set(listener, unsubscribe); return window.cookieStore.addEventListener('change', cb); } let lastValue = getDocumentCookie(name); const interval = setInterval(() => { const currentValue = getDocumentCookie(name); if (currentValue !== lastValue) { listener(currentValue); lastValue = currentValue; } }, POLLING_INTERVAL_MS); const unsubscribe = () => clearInterval(interval); this.listenerUnsubscribes.set(listener, unsubscribe); } _removeListener(_key, listener) { const unsubscribe = this.listenerUnsubscribes.get(listener); if (!unsubscribe) { return; } unsubscribe(); this.listenerUnsubscribes.delete(listener); } } CookiePersistence.type = 'COOKIE'; /** * An implementation of {@link Persistence} of type `COOKIE`, for use on the client side in * applications leveraging hybrid rendering and middleware. * * @remarks This persistence method requires companion middleware to function, such as that provided * by {@link https://firebaseopensource.com/projects/firebaseextended/reactfire/ | ReactFire} for * NextJS. * @beta */ const browserCookiePersistence = CookiePersistence; /** * @license * Copyright 2020 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. */ class BrowserSessionPersistence extends BrowserPersistenceClass { constructor() { super(() => window.sessionStorage, "SESSION" /* PersistenceType.SESSION */); } _addListener(_key, _listener) { // Listeners are not supported for session storage since it cannot be shared across windows return; } _removeListener(_key, _listener) { // Listeners are not supported for session storage since it cannot be shared across windows return; } } BrowserSessionPersistence.type = 'SESSION'; /** * An implementation of {@link Persistence} of `SESSION` using `sessionStorage` * for the underlying storage. * * @public */ const browserSessionPersistence = BrowserSessionPersistence; /** * @license * Copyright 2020 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. */ // ReCaptcha will load using the same callback, so the callback function needs // to be kept around const _JSLOAD_CALLBACK = register._generateCallbackName('rcb'); const NETWORK_TIMEOUT_DELAY = new register.Delay(30000, 60000); /** * Loader for the GReCaptcha library. There should only ever be one of this. */ class ReCaptchaLoaderImpl { constructor() { var _a; this.hostLanguage = ''; this.counter = 0; /** * Check for `render()` method. `window.grecaptcha` will exist if the Enterprise * version of the ReCAPTCHA script was loaded by someone else (e.g. App Check) but * `window.grecaptcha.render()` will not. Another load will add it. */ this.librarySeparatelyLoaded = !!((_a = register._window().grecaptcha) === null || _a === void 0 ? void 0 : _a.render); } load(auth, hl = '') { register._assert(isHostLanguageValid(hl), auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); if (this.shouldResolveImmediately(hl) && register.isV2(register._window().grecaptcha)) { return Promise.resolve(register._window().grecaptcha); } return new Promise((resolve, reject) => { const networkTimeout = register._window().setTimeout(() => { reject(register._createError(auth, "network-request-failed" /* AuthErrorCode.NETWORK_REQUEST_FAILED */)); }, NETWORK_TIMEOUT_DELAY.get()); register._window()[_JSLOAD_CALLBACK] = () => { register._window().clearTimeout(networkTimeout); delete register._window()[_JSLOAD_CALLBACK]; const recaptcha = register._window().grecaptcha; if (!recaptcha || !register.isV2(recaptcha)) { reject(register._createError(auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */)); return; } // Wrap the recaptcha render function so that we know if the developer has // called it separately const render = recaptcha.render; recaptcha.render = (container, params) => { const widgetId = render(container, params); this.counter++; return widgetId; }; this.hostLanguage = hl; resolve(recaptcha); }; const url = `${register._recaptchaV2ScriptUrl()}?${util.querystring({ onload: _JSLOAD_CALLBACK, render: 'explicit', hl })}`; register._loadJS(url).catch(() => { clearTimeout(networkTimeout); reject(register._createError(auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */)); }); }); } clearedOneInstance() { this.counter--; } shouldResolveImmediately(hl) { var _a; // We can resolve immediately if: // • grecaptcha is already defined AND ( // 1. the requested language codes are the same OR // 2. there exists already a ReCaptcha on the page // 3. the library was already loaded by the app // In cases (2) and (3), we _can't_ reload as it would break the recaptchas // that are already in the page return (!!((_a = register._window().grecaptcha) === null || _a === void 0 ? void 0 : _a.render) && (hl === this.hostLanguage || this.counter > 0 || this.librarySeparatelyLoaded)); } } function isHostLanguageValid(hl) { return hl.length <= 6 && /^\s*[a-zA-Z0-9\-]*\s*$/.test(hl); } class MockReCaptchaLoaderImpl { async load(auth) { return new register.MockReCaptcha(auth); } clearedOneInstance() { } } /** * @license * Copyright 2020 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. */ const RECAPTCHA_VERIFIER_TYPE = 'recaptcha'; const DEFAULT_PARAMS = { theme: 'light', type: 'image' }; /** * An {@link https://www.google.com/recaptcha/ | reCAPTCHA}-based application verifier. * * @remarks * `RecaptchaVerifier` does not work in a Node.js environment. * * @public */ class RecaptchaVerifier { /** * @param authExtern - The corresponding Firebase {@link Auth} instance. * * @param containerOrId - The reCAPTCHA container parameter. * * @remarks * This has different meaning depending on whether the reCAPTCHA is hidden or visible. For a * visible reCAPTCHA the container must be empty. If a string is used, it has to correspond to * an element ID. The corresponding element must also must be in the DOM at the time of * initialization. * * @param parameters - The optional reCAPTCHA parameters. * * @remarks * Check the reCAPTCHA docs for a comprehensive list. All parameters are accepted except for * the sitekey. Firebase Auth backend provisions a reCAPTCHA for each project and will * configure this upon rendering. For an invisible reCAPTCHA, a size key must have the value * 'invisible'. */ constructor(authExtern, containerOrId, parameters = Object.assign({}, DEFAULT_PARAMS)) { this.parameters = parameters; /** * The application verifier type. * * @remarks * For a reCAPTCHA verifier, this is 'recaptcha'. */ this.type = RECAPTCHA_VERIFIER_TYPE; this.destroyed = false; this.widgetId = null; this.tokenChangeListeners = new Set(); this.renderPromise = null; this.recaptcha = null; this.auth = register._castAuth(authExtern); this.isInvisible = this.parameters.size === 'invisible'; register._assert(typeof document !== 'undefined', this.auth, "operation-not-supported-in-this-environment" /* AuthErrorCode.OPERATION_NOT_SUPPORTED */); const container = typeof containerOrId === 'string' ? document.getElementById(containerOrId) : containerOrId; register._assert(container, this.auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); this.container = container; this.parameters.callback = this.makeTokenCallback(this.parameters.callback); this._recaptchaLoader = this.auth.settings.appVerificationDisabledForTesting ? new MockReCaptchaLoaderImpl() : new ReCaptchaLoaderImpl(); this.validateStartingState(); // TODO: Figure out if sdk version is needed } /** * Waits for the user to solve the reCAPTCHA and resolves with the reCAPTCHA token. * * @returns A Promise for the reCAPTCHA token. */ async verify() { this.assertNotDestroyed(); const id = await this.render(); const recaptcha = this.getAssertedRecaptcha(); const response = recaptcha.getResponse(id); if (response) { return response; } return new Promise(resolve => { const tokenChange = (token) => { if (!token) { return; // Ignore token expirations. } this.tokenChangeListeners.delete(tokenChange); resolve(token); }; this.tokenChangeListeners.add(tokenChange); if (this.isInvisible) { recaptcha.execute(id); } }); } /** * Renders the reCAPTCHA widget on the page. * * @returns A Promise that resolves with the reCAPTCHA widget ID. */ render() { try { this.assertNotDestroyed(); } catch (e) { // This method returns a promise. Since it's not async (we want to return the // _same_ promise if rendering is still occurring), the API surface should // reject with the error rather than just throw return Promise.reject(e); } if (this.renderPromise) { return this.renderPromise; } this.renderPromise = this.makeRenderPromise().catch(e => { this.renderPromise = null; throw e; }); return this.renderPromise; } /** @internal */ _reset() { this.assertNotDestroyed(); if (this.widgetId !== null) { this.getAssertedRecaptcha().reset(this.widgetId); } } /** * Clears the reCAPTCHA widget from the page and destroys the instance. */ clear() { this.assertNotDestroyed(); this.destroyed = true; this._recaptchaLoader.clearedOneInstance(); if (!this.isInvisible) { this.container.childNodes.forEach(node => { this.container.removeChild(node); }); } } validateStartingState() { register._assert(!this.parameters.sitekey, this.auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); register._assert(this.isInvisible || !this.container.hasChildNodes(), this.auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); register._assert(typeof document !== 'undefined', this.auth, "operation-not-supported-in-this-environment" /* AuthErrorCode.OPERATION_NOT_SUPPORTED */); } makeTokenCallback(existing) { return token => { this.tokenChangeListeners.forEach(listener => listener(token)); if (typeof existing === 'function') { existing(token); } else if (typeof existing === 'string') { const globalFunc = register._window()[existing]; if (typeof globalFunc === 'function') { globalFunc(token); } } }; } assertNotDestroyed() { register._assert(!this.destroyed, this.auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */); } async makeRenderPromise() { await this.init(); if (!this.widgetId) { let container = this.container; if (!this.isInvisible) { const guaranteedEmpty = document.createElement('div'); container.appendChild(guaranteedEmpty); container = guaranteedEmpty; } this.widgetId = this.getAssertedRecaptcha().render(container, this.parameters); } return this.widgetId; } async init() { register._assert(register._isHttpOrHttps() && !register._isWorker(), this.auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */); await domReady(); this.recaptcha = await this._recaptchaLoader.load(this.auth, this.auth.languageCode || undefined); const siteKey = await register.getRecaptchaParams(this.auth); register._assert(siteKey, this.auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */); this.parameters.sitekey = siteKey; } getAssertedRecaptcha() { register._assert(this.recaptcha, this.auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */); return this.recaptcha; } } function domReady() { let resolver = null; return new Promise(resolve => { if (document.readyState === 'complete') { resolve(); return; } // Document not ready, wait for load before resolving. // Save resolver, so we can remove listener in case it was externally // cancelled. resolver = () => resolve(); window.addEventListener('load', resolver); }).catch(e => { if (resolver) { window.removeEventListener('load', resolver); } throw e; }); } /** * @license * Copyright 2020 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. */ class ConfirmationResultImpl { constructor(verificationId, onConfirmation) { this.verificationId = verificationId; this.onConfirmation = onConfirmation; } confirm(verificationCode) { const authCredential = register.PhoneAuthCredential._fromVerification(this.verificationId, verificationCode); return this.onConfirmation(authCredential); } } /** * Asynchronously signs in using a phone number. * * @remarks * This method sends a code via SMS to the given * phone number, and returns a {@link ConfirmationResult}. After the user * provides the code sent to their phone, call {@link ConfirmationResult.confirm} * with the code to sign the user in. * * For abuse prevention, this method requires a {@link ApplicationVerifier}. * This SDK includes an implementation based on reCAPTCHA v2, {@link RecaptchaVerifier}. * This function can work on other platforms that do not support the * {@link RecaptchaVerifier} (like React Native), but you need to use a * third-party {@link ApplicationVerifier} implementation. * * If you've enabled project-level reCAPTCHA Enterprise bot protection in * Enforce mode, you can omit the {@link ApplicationVerifier}. * * This method does not work in a Node.js environment or with {@link Auth} instances created with a * {@link @firebase/app#FirebaseServerApp}. * * @example * ```javascript * // 'recaptcha-container' is the ID of an element in the DOM. * const applicationVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container'); * const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, applicationVerifier); * // Obtain a verificationCode from the user. * const credential = await confirmationResult.confirm(verificationCode); * ``` * * @param auth - The {@link Auth} instance. * @param phoneNumber - The user's phone number in E.164 format (e.g. +16505550101). * @param appVerifier - The {@link ApplicationVerifier}. * * @public */ async function signInWithPhoneNumber(auth, phoneNumber, appVerifier) { if (app._isFirebaseServerApp(auth.app)) { return Promise.reject(register._serverAppCurrentUserOperationNotSupportedError(auth)); } const authInternal = register._castAuth(auth); const verificationId = await _verifyPhoneNumber(authInternal, phoneNumber, util.getModularInstance(appVerifier)); return new ConfirmationResultImpl(verificationId, cred => register.signInWithCredential(authInternal, cred)); } /** * Links the user account with the given phone number. * * @remarks * This method does not work in a Node.js environment. * * @param user - The user. * @param phoneNumber - The user's phone number in E.164 format (e.g. +16505550101). * @param appVerifier - The {@link ApplicationVerifier}. * * @public */ async function linkWithPhoneNumber(user, phoneNumber, appVerifier) { const userInternal = util.getModularInstance(user); await register._assertLinkedStatus(false, userInternal, "phone" /* ProviderId.PHONE */); const verificationId = await _verifyPhoneNumber(userInternal.auth, phoneNumber, util.getModularInstance(appVerifier)); return new ConfirmationResultImpl(verificationId, cred => register.linkWithCredential(userInternal, cred)); } /** * Re-authenticates a user using a fresh phone credential. * * @remarks * Use before operations such as {@link updatePassword} that require tokens from recent sign-in attempts. * * This method does not work in a Node.js environment or on any {@link User} signed in by * {@link Auth} instances created with a {@link @firebase/app#FirebaseServerApp}. * * @param user - The user. * @param phoneNumber - The user's phone number in E.164 format (e.g. +16505550101). * @param appVerifier - The {@link ApplicationVerifier}. * * @public */ async function reauthenticateWithPhoneNumber(user, phoneNumber, appVerifier) { const userInternal = util.getModularInstance(user); if (app._isFirebaseServerApp(userInternal.auth.app)) { return Promise.reject(register._serverAppCurrentUserOperationNotSupportedError(userInternal.auth)); } const verificationId = await _verifyPhoneNumber(userInternal.auth, phoneNumber, util.getModularInstance(appVerifier)); return new ConfirmationResultImpl(verificationId, cred => register.reauthenticateWithCredential(userInternal, cred)); } /** * Returns a verification ID to be used in conjunction with the SMS code that is sent. * */ async function _verifyPhoneNumber(auth, options, verifier) { var _a; if (!auth._getRecaptchaConfig()) { try { await register._initializeRecaptchaConfig(auth); } catch (error) { // If an error occurs while fetching the config, there is no way to know the enablement state // of Phone provider, so we proceed with recaptcha V2 verification. // The error is likely "recaptchaKey undefined", as reCAPTCHA Enterprise is not // enabled for any provider. console.log('Failed to initialize reCAPTCHA Enterprise config. Triggering the reCAPTCHA v2 verification.'); } } try { let phoneInfoOptions; if (typeof options === 'string') { phoneInfoOptions = { phoneNumber: options }; } else { phoneInfoOptions = options; } if ('session' in phoneInfoOptions) { const session = phoneInfoOptions.session; if ('phoneNumber' in phoneInfoOptions) { register._assert(session.type === "enroll" /* MultiFactorSessionType.ENROLL */, auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */); const startPhoneMfaEnrollmentRequest = { idToken: session.credential, phoneEnrollmentInfo: { phoneNumber: phoneInfoOptions.phoneNumber, clientType: "CLIENT_TYPE_WEB" /* RecaptchaClientType.WEB */ } }; const startEnrollPhoneMfaActionCallback = async (authInstance, request) => { // If reCAPTCHA Enterprise token is FAKE_TOKEN, fetch reCAPTCHA v2 token and inject into request. if (request.phoneEnrollmentInfo.captchaResponse === register.FAKE_TOKEN) { register._assert((verifier === null || verifier === void 0 ? void 0 : verifier.type) === RECAPTCHA_VERIFIER_TYPE, authInstance, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); const requestWithRecaptchaV2 = await injectRecaptchaV2Token(authInstance, request, verifier); return register.startEnrollPhoneMfa(authInstance, requestWithRecaptchaV2); } return register.startEnrollPhoneMfa(authInstance, request); }; const startPhoneMfaEnrollmentResponse = register.handleRecaptchaFlow(auth, startPhoneMfaEnrollmentRequest, "mfaSmsEnrollment" /* RecaptchaActionName.MFA_SMS_ENROLLMENT */, startEnrollPhoneMfaActionCallback, "PHONE_PROVIDER" /* RecaptchaAuthProvider.PHONE_PROVIDER */); const response = await startPhoneMfaEnrollmentResponse.catch(error => { return Promise.reject(error); }); return response.phoneSessionInfo.sessionInfo; } else { register._assert(session.type === "signin" /* MultiFactorSessionType.SIGN_IN */, auth, "internal-error" /* AuthErrorCode.INTERNAL_ERROR */); const mfaEnrollmentId = ((_a = phoneInfoOptions.multiFactorHint) === null || _a === void 0 ? void 0 : _a.uid) || phoneInfoOptions.multiFactorUid; register._assert(mfaEnrollmentId, auth, "missing-multi-factor-info" /* AuthErrorCode.MISSING_MFA_INFO */); const startPhoneMfaSignInRequest = { mfaPendingCredential: session.credential, mfaEnrollmentId, phoneSignInInfo: { clientType: "CLIENT_TYPE_WEB" /* RecaptchaClientType.WEB */ } }; const startSignInPhoneMfaActionCallback = async (authInstance, request) => { // If reCAPTCHA Enterprise token is FAKE_TOKEN, fetch reCAPTCHA v2 token and inject into request. if (request.phoneSignInInfo.captchaResponse === register.FAKE_TOKEN) { register._assert((verifier === null || verifier === void 0 ? void 0 : verifier.type) === RECAPTCHA_VERIFIER_TYPE, authInstance, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); const requestWithRecaptchaV2 = await injectRecaptchaV2Token(authInstance, request, verifier); return register.startSignInPhoneMfa(authInstance, requestWithRecaptchaV2); } return register.startSignInPhoneMfa(authInstance, request); }; const startPhoneMfaSignInResponse = register.handleRecaptchaFlow(auth, startPhoneMfaSignInRequest, "mfaSmsSignIn" /* RecaptchaActionName.MFA_SMS_SIGNIN */, startSignInPhoneMfaActionCallback, "PHONE_PROVIDER" /* RecaptchaAuthProvider.PHONE_PROVIDER */); const response = await startPhoneMfaSignInResponse.catch(error => { return Promise.reject(error); }); return response.phoneResponseInfo.sessionInfo; } } else { const sendPhoneVerificationCodeRequest = { phoneNumber: phoneInfoOptions.phoneNumber, clientType: "CLIENT_TYPE_WEB" /* RecaptchaClientType.WEB */ }; const sendPhoneVerificationCodeActionCallback = async (authInstance, request) => { // If reCAPTCHA Enterprise token is FAKE_TOKEN, fetch reCAPTCHA v2 token and inject into request. if (request.captchaResponse === register.FAKE_TOKEN) { register._assert((verifier === null || verifier === void 0 ? void 0 : verifier.type) === RECAPTCHA_VERIFIER_TYPE, authInstance, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); const requestWithRecaptchaV2 = await injectRecaptchaV2Token(authInstance, request, verifier); return register.sendPhoneVerificationCode(authInstance, requestWithRecaptchaV2); } return register.sendPhoneVerificationCode(authInstance, request); }; const sendPhoneVerificationCodeResponse = register.handleRecaptchaFlow(auth, sendPhoneVerificationCodeRequest, "sendVerificationCode" /* RecaptchaActionName.SEND_VERIFICATION_CODE */, sendPhoneVerificationCodeActionCallback, "PHONE_PROVIDER" /* RecaptchaAuthProvider.PHONE_PROVIDER */); const response = await sendPhoneVerificationCodeResponse.catch(error => { return Promise.reject(error); }); return response.sessionInfo; } } finally { verifier === null || verifier === void 0 ? void 0 : verifier._reset(); } } /** * Updates the user's phone number. * * @remarks * This method does not work in a Node.js environment or on any {@link User} signed in by * {@link Auth} instances created with a {@link @firebase/app#FirebaseServerApp}. * * @example * ``` * // 'recaptcha-container' is the ID of an element in the DOM. * const applicationVerifier = new RecaptchaVerifier('recaptcha-container'); * const provider = new PhoneAuthProvider(auth); * const verificationId = await provider.verifyPhoneNumber('+16505550101', applicationVerifier); * // Obtain the verificationCode from the user. * const phoneCredential = PhoneAuthProvider.credential(verificationId, verificationCode); * await updatePhoneNumber(user, phoneCredential); * ``` * * @param user - The user. * @param credential - A credential authenticating the new phone number. * * @public */ async function updatePhoneNumber(user, credential) { const userInternal = util.getModularInstance(user); if (app._isFirebaseServerApp(userInternal.auth.app)) { return Promise.reject(register._serverAppCurrentUserOperationNotSupportedError(userInternal.auth)); } await register._link(userInternal, credential); } // Helper function that fetches and injects a reCAPTCHA v2 token into the request. async function injectRecaptchaV2Token(auth, request, recaptchaV2Verifier) { register._assert(recaptchaV2Verifier.type === RECAPTCHA_VERIFIER_TYPE, auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); const recaptchaV2Token = await recaptchaV2Verifier.verify(); register._assert(typeof recaptchaV2Token === 'string', auth, "argument-error" /* AuthErrorCode.ARGUMENT_ERROR */); const newRequest = Object.assign({}, request); if ('phoneEnrollmentInfo' in newRequest) { const phoneNumber = newRequest.phoneEnrollmentInfo.phoneNumber; const captchaResponse = newRequest.phoneEnrollmentInfo.captchaResponse; const clientType = newRequest .phoneEnrollmentInfo.clientType; const recaptchaVersion = newRequest.phoneEnrollmentInfo.recaptchaVersion; Object.assign(newRequest, { 'phoneEnrollmentInfo': { phoneNumber, recaptchaToken: recaptchaV2Token, captchaResponse, clientType, recaptchaVersion } }); return newRequest; } else if ('phoneSignInInfo' in newRequest) { const captchaResponse = newRequest.phoneSignInInfo.captchaResponse; const clientType = newRequest .phoneSignInInfo.clientType; const recaptchaVersion = newRequest.phoneSignInInfo.recaptchaVersion; Object.assign(newRequest, { 'phoneSignInInfo': { recaptchaToken: recaptchaV2Token, captchaResponse, clientType, recaptchaVersion } }); return newRequest; } else { Object.assign(newRequest, { 'recaptchaToken': recaptchaV2Token }); return newRequest; } } /** * @license * Copyright 2020 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. */ /** * Provider for generating an {@link PhoneAuthCredential}. * * @remarks * `PhoneAuthProvider` does not work in a Node.js environment. * * @example * ```javascript * // 'recaptcha-container' is the ID of an element in the DOM. * const applicationVerifier = new RecaptchaVerifier('recaptcha-container'); * const provider = new PhoneAuthProvider(auth); * const verificationId = await provider.verifyPhoneNumber('+16505550101', applicationVerifier); * // Obtain the verificationCode from the user. * const phoneCredential = PhoneAuthProvider.credential(verificationId, verificationCode); * const userCredential = await signInWithCredential(auth, phoneCredential); * ``` * * @public */ class PhoneAuthProvider { /** * @param auth - The Firebase {@link Auth} instance in which sign-ins should occur. * */ constructor(auth) { /** Always set to {@link ProviderId}.PHONE. */ this.providerId = PhoneAuthProvider.PROVIDER_ID; this.auth = register._castAuth(auth); } /** * * Starts a phone number authentication flow by sending a verification code to the given phone * number. * * @example * ```javascript * const provider = new PhoneAuthProvider(auth); * const verificationId = await provider.verifyPhoneNumber(phoneNumber, applicationVerifier); * // Obtain verificationCode from the user. * const authCredential = PhoneAuthProvider.credential(verificationId, verificationCode); * const userCredential = await signInWithCredential(auth, authCredential); * ``` * * @example * An alternative flow is provided using the `signInWithPhoneNumber` method. * ```javascript * const confirmationResult = signInWithPhoneNumber(auth, phoneNumber, applicationVerifier); * // Obtain verificationCode from the user. * const userCredential = confirmationResult.confirm(verificationCode); * ``` * * @param phoneInfoOptions - The user's {@link PhoneInfoOptions}. The phone number should be in * E.164 format (e.g. +16505550101). * @param applicationVerifier - An {@link ApplicationVerifier}, which prevents * requests from unauthorized clients. This SDK includes an implementation * based on reCAPTCHA v2, {@link RecaptchaVerifier}. If you've enabled * reCAPTCHA Enterprise bot protection in Enforce mode, this parameter is * opti