@firebase/auth
Version:
The Firebase Authenticaton component of the Firebase JS SDK.
1,213 lines (1,202 loc) • 149 kB
JavaScript
'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