UNPKG

voluptasmollitia

Version:
1,458 lines (1,307 loc) 91.3 kB
/** * @license * Copyright 2017 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. */ /** * @fileoverview Defines the user info pertaining to an identity provider and * the Firebase user object. */ goog.provide('fireauth.AuthUser'); goog.provide('fireauth.AuthUser.AccountInfo'); goog.provide('fireauth.AuthUserInfo'); goog.provide('fireauth.TokenRefreshTime'); goog.provide('fireauth.UserMetadata'); goog.require('fireauth.ActionCodeSettings'); goog.require('fireauth.AdditionalUserInfo'); goog.require('fireauth.AuthCredential'); goog.require('fireauth.AuthError'); goog.require('fireauth.AuthEvent'); goog.require('fireauth.AuthEventHandler'); goog.require('fireauth.AuthEventManager'); goog.require('fireauth.AuthProvider'); goog.require('fireauth.ConfirmationResult'); goog.require('fireauth.IdTokenResult'); goog.require('fireauth.MultiFactorError'); goog.require('fireauth.MultiFactorUser'); goog.require('fireauth.PhoneAuthProvider'); goog.require('fireauth.ProactiveRefresh'); goog.require('fireauth.RpcHandler'); goog.require('fireauth.StsTokenManager'); goog.require('fireauth.UserEvent'); goog.require('fireauth.UserEventType'); goog.require('fireauth.authenum.Error'); goog.require('fireauth.constants'); goog.require('fireauth.constants.AuthEventType'); goog.require('fireauth.deprecation'); goog.require('fireauth.idp'); goog.require('fireauth.iframeclient.IfcHandler'); goog.require('fireauth.object'); goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.array'); goog.require('goog.events'); goog.require('goog.events.Event'); goog.require('goog.events.EventTarget'); goog.require('goog.object'); /** * Initializes an instance of a user metadata object. * @param {?string=} opt_createdAt The optional creation date UTC timestamp. * @param {?string=} opt_lastLoginAt The optional last login date UTC timestamp. * @constructor */ fireauth.UserMetadata = function(opt_createdAt, opt_lastLoginAt) { /** @private {?string} The created at UTC timestamp. */ this.createdAt_ = opt_createdAt || null; /** @private {?string} The last login at UTC timestamp. */ this.lastLoginAt_ = opt_lastLoginAt || null; fireauth.object.setReadonlyProperties(this, { 'lastSignInTime': fireauth.util.utcTimestampToDateString( opt_lastLoginAt || null), 'creationTime': fireauth.util.utcTimestampToDateString( opt_createdAt || null), }); }; /** * @return {!fireauth.UserMetadata} A clone of the current user metadata object. */ fireauth.UserMetadata.prototype.clone = function() { return new fireauth.UserMetadata(this.createdAt_, this.lastLoginAt_); }; /** * @return {!Object} The object representation of the user metadata instance. */ fireauth.UserMetadata.prototype.toPlainObject = function() { return { 'lastLoginAt': this.lastLoginAt_, 'createdAt': this.createdAt_ }; }; /** * Initializes an instance of the user info for an identity provider. * @param {string} uid The user ID. * @param {!fireauth.idp.ProviderId} providerId The provider ID. * @param {?string=} opt_email The optional user email. * @param {?string=} opt_displayName The optional display name. * @param {?string=} opt_photoURL The optional photo URL. * @param {?string=} opt_phoneNumber The optional phone number. * @constructor */ fireauth.AuthUserInfo = function( uid, providerId, opt_email, opt_displayName, opt_photoURL, opt_phoneNumber) { fireauth.object.setReadonlyProperties(this, { 'uid': uid, 'displayName': opt_displayName || null, 'photoURL': opt_photoURL || null, 'email': opt_email || null, 'phoneNumber': opt_phoneNumber || null, 'providerId': providerId }); }; /** * Defines the proactive token refresh time constraints in milliseconds. * @enum {number} */ fireauth.TokenRefreshTime = { /** * The offset time before token natural expiration to run the refresh. * This is currently 5 minutes. */ OFFSET_DURATION: 5 * 60 * 1000, /** * This is the first retrial wait after an error. This is currently * 30 seconds. */ RETRIAL_MIN_WAIT: 30 * 1000, /** * This is the maximum retrial wait, currently 16 minutes. */ RETRIAL_MAX_WAIT: 16 * 60 * 1000 }; /** * The Firebase user. * @param {!Object} appOptions The application options. * @param {!Object} stsTokenResponse The server STS token response. * @param {?Object=} opt_accountInfo The optional user account info. * @constructor * @extends {goog.events.EventTarget} * @implements {fireauth.AuthEventHandler} */ fireauth.AuthUser = function(appOptions, stsTokenResponse, opt_accountInfo) { /** @private {!Array<!goog.Promise<*, *>|!goog.Promise<void>>} List of pending * promises. */ this.pendingPromises_ = []; // User is only created via Auth so API key should always be available. /** @private {string} The API key. */ this.apiKey_ = /** @type {string} */ (appOptions['apiKey']); // This is needed to associate a user to the corresponding Auth instance. /** @private {string} The App name. */ this.appName_ = /** @type {string} */ (appOptions['appName']); /** @private {?string} The Auth domain. */ this.authDomain_ = appOptions['authDomain'] || null; var clientFullVersion = firebase.SDK_VERSION ? fireauth.util.getClientVersion( fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION) : null; /** @private {!fireauth.RpcHandler} The RPC handler instance. */ this.rpcHandler_ = new fireauth.RpcHandler( this.apiKey_, // Get the client Auth endpoint used. fireauth.constants.getEndpointConfig(fireauth.constants.clientEndpoint), clientFullVersion); if (appOptions['emulatorConfig']) { this.rpcHandler_.updateEmulatorConfig(appOptions['emulatorConfig']); } // TODO: Consider having AuthUser take a fireauth.StsTokenManager // instance instead of a token response but make sure lastAccessToken_ also // initialized at the right time. In this case initializeFromIdTokenResponse // will take in a token response object and convert it to an instance of // fireauth.StsTokenManager to properly initialize user. /** @private {!fireauth.StsTokenManager} The STS token manager instance. */ this.stsTokenManager_ = new fireauth.StsTokenManager(this.rpcHandler_); this.setLastAccessToken_( stsTokenResponse[fireauth.RpcHandler.AuthServerField.ID_TOKEN]); // STS token manager will always be populated using server response. this.stsTokenManager_.parseServerResponse(stsTokenResponse); fireauth.object.setReadonlyProperty( this, 'refreshToken', this.stsTokenManager_.getRefreshToken()); this.setAccountInfo(/** @type {!fireauth.AuthUser.AccountInfo} */ ( opt_accountInfo || {})); // Add call to superclass constructor. fireauth.AuthUser.base(this, 'constructor'); /** @private {boolean} Whether popup and redirect is enabled on the user. */ this.popupRedirectEnabled_ = false; if (this.authDomain_ && fireauth.AuthEventManager.ENABLED && // Make sure popup and redirects are supported in the current environment. fireauth.util.isPopupRedirectSupported()) { // Get the Auth event manager associated with this user. this.authEventManager_ = fireauth.AuthEventManager.getManager( this.authDomain_, this.apiKey_, this.appName_); } /** @private {!Array<!function(!fireauth.AuthUser):!goog.Promise>} The list of * state change listeners. This is needed to make sure state changes are * resolved before resolving user API promises. For example redirect * operations should make sure the associated event ID is saved before * redirecting. */ this.stateChangeListeners_ = []; /** * @private {?fireauth.AuthError} The user invalidation error if it exists. */ this.userInvalidatedError_ = null; /** * @private {!fireauth.ProactiveRefresh} The reference to the proactive token * refresher utility for the current user. */ this.proactiveRefresh_ = this.initializeProactiveRefreshUtility_(); /** * @private {!function(!Object)} The handler for user token changes used to * realign the proactive token refresh with external token refresh calls. */ this.userTokenChangeListener_ = goog.bind(this.handleUserTokenChange_, this); var self = this; /** @private {?string} The current user's language code. */ this.languageCode_ = null; /** * @private {function(!goog.events.Event)} The on language code changed event * handler. */ this.onLanguageCodeChanged_ = function(event) { // Update the user language code. self.setLanguageCode(event.languageCode); }; /** * @private {?goog.events.EventTarget} The language code change event * dispatcher. */ this.languageCodeChangeEventDispatcher_ = null; /** * @private {function(!goog.events.Event)} The on emulator config changed * event handler. */ this.onEmulatorConfigChanged_ = function (event) { // Update the emulator config. self.setEmulatorConfig(event.emulatorConfig); }; /** * @private {?goog.events.EventTarget} The emulator code change event * dispatcher. */ this.emulatorConfigChangeEventDispatcher_ = null; /** @private {!Array<string>} The current Firebase frameworks. */ this.frameworks_ = []; /** * @private {function(!goog.events.Event)} The on framework list changed event * handler. */ this.onFrameworkChanged_ = function(event) { // Update the Firebase frameworks. self.setFramework(event.frameworks); }; /** * @private {?goog.events.EventTarget} The framework change event dispatcher. */ this.frameworkChangeEventDispatcher_ = null; /** * @const @private {!fireauth.MultiFactorUser} The multifactor user instance. */ this.multiFactorUser_ = new fireauth.MultiFactorUser( this, /** @type {?fireauth.AuthUser.AccountInfo|undefined} */ ( opt_accountInfo)); fireauth.object.setReadonlyProperty( this, 'multiFactor', this.multiFactorUser_); }; goog.inherits(fireauth.AuthUser, goog.events.EventTarget); /** * Updates the user language code. * @param {?string} languageCode The current language code to use in user * requests. */ fireauth.AuthUser.prototype.setLanguageCode = function(languageCode) { // Save current language. this.languageCode_ = languageCode; // Update the custom locale header. this.rpcHandler_.updateCustomLocaleHeader(languageCode); }; /** * Updates the emulator config. * @param {?fireauth.constants.EmulatorSettings} emulatorConfig The current * emulator config to use in user requests. */ fireauth.AuthUser.prototype.setEmulatorConfig = function(emulatorConfig) { // Update the emulator config. this.rpcHandler_.updateEmulatorConfig(emulatorConfig); }; /** @return {?string} The current user's language code. */ fireauth.AuthUser.prototype.getLanguageCode = function() { return this.languageCode_; }; /** * Listens to language code changes triggered by the provided dispatcher. * @param {?goog.events.EventTarget} dispatcher The language code changed event * dispatcher. */ fireauth.AuthUser.prototype.setLanguageCodeChangeDispatcher = function(dispatcher) { // Remove any previous listener. if (this.languageCodeChangeEventDispatcher_) { goog.events.unlisten( this.languageCodeChangeEventDispatcher_, fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED, this.onLanguageCodeChanged_); } // Update current dispatcher. this.languageCodeChangeEventDispatcher_ = dispatcher; // Using an event listener makes it easy for non-currentUsers to detect // language changes on the parent Auth instance. A developer could still call // APIs that require localization on signed out user references. if (dispatcher) { goog.events.listen( dispatcher, fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED, this.onLanguageCodeChanged_); } }; /** * Listens to emulator config changes triggered by the provided dispatcher. * @param {?goog.events.EventTarget} dispatcher The emulator config changed * event dispatcher. */ fireauth.AuthUser.prototype.setEmulatorConfigChangeDispatcher = function(dispatcher) { // Remove any previous listener. if (this.emulatorConfigChangeEventDispatcher_) { goog.events.unlisten( this.emulatorConfigChangeEventDispatcher_, fireauth.constants.AuthEventType.EMULATOR_CONFIG_CHANGED, this.onEmulatorConfigChanged_); } // Update current dispatcher. this.emulatorConfigChangeEventDispatcher_ = dispatcher; // Using an event listener makes it easy for non-currentUsers to detect // emulator changes on the parent Auth instance. A developer could still // call APIs that require emulation on signed out user references. if (dispatcher) { goog.events.listen( dispatcher, fireauth.constants.AuthEventType.EMULATOR_CONFIG_CHANGED, this.onEmulatorConfigChanged_); } } /** * Updates the Firebase frameworks on the current user. * @param {!Array<string>} framework The list of Firebase frameworks. */ fireauth.AuthUser.prototype.setFramework = function(framework) { // Save current frameworks. this.frameworks_ = framework; // Update the client version in RPC handler with the new frameworks. this.rpcHandler_.updateClientVersion(firebase.SDK_VERSION ? fireauth.util.getClientVersion( fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, this.frameworks_) : null); }; /** @return {!Array<string>} The current Firebase frameworks. */ fireauth.AuthUser.prototype.getFramework = function() { return goog.array.clone(this.frameworks_); }; /** * Listens to framework changes triggered by the provided dispatcher. * @param {?goog.events.EventTarget} dispatcher The framework changed event * dispatcher. */ fireauth.AuthUser.prototype.setFrameworkChangeDispatcher = function(dispatcher) { // Remove any previous listener. if (this.frameworkChangeEventDispatcher_) { goog.events.unlisten( this.frameworkChangeEventDispatcher_, fireauth.constants.AuthEventType.FRAMEWORK_CHANGED, this.onFrameworkChanged_); } // Update current dispatcher. this.frameworkChangeEventDispatcher_ = dispatcher; // Using an event listener makes it easy for non-currentUsers to detect // framework changes on the parent Auth instance. if (dispatcher) { goog.events.listen( dispatcher, fireauth.constants.AuthEventType.FRAMEWORK_CHANGED, this.onFrameworkChanged_); } }; /** * Handles user token changes. Currently used to realign the proactive token * refresh internal timing with successful external token refreshes. * @param {!Object} event The token change event. * @private */ fireauth.AuthUser.prototype.handleUserTokenChange_ = function(event) { // If an external service refreshes the token, reset the proactive token // refresh utility in case it is still running so the next run time is // up to date. // This will currently also trigger when the proactive refresh succeeds. // This is not ideal but should not have any downsides. It just adds a // redundant reset which can be optimized not to run in the future. if (this.proactiveRefresh_.isRunning()) { this.proactiveRefresh_.stop(); this.proactiveRefresh_.start(); } }; /** * @return {!fireauth.Auth} The corresponding Auth instance that created the * current user. * @private */ fireauth.AuthUser.prototype.getAuth_ = function() { try { // Get the Auth instance for the current app identified by the App name. // This could fail if, for example, the App instance was deleted. return firebase['app'](this.appName_)['auth'](); } catch (e) { // Throw appropriate error. throw new fireauth.AuthError( fireauth.authenum.Error.INTERNAL_ERROR, 'No firebase.auth.Auth instance is available for the Firebase App ' + '\'' + this.appName_ + '\'!'); } }; /** * @return {string} The user's API key. */ fireauth.AuthUser.prototype.getApiKey = function() { return this.apiKey_; }; /** * Returns the RPC handler of the user. * @return {!fireauth.RpcHandler} The RPC handler. */ fireauth.AuthUser.prototype.getRpcHandler = function() { return this.rpcHandler_; }; /** * Used to initialize the current user's proactive token refresher utility. * @return {!fireauth.ProactiveRefresh} The user's proactive token refresh * utility. * @private */ fireauth.AuthUser.prototype.initializeProactiveRefreshUtility_ = function() { var self = this; return new fireauth.ProactiveRefresh( // Force ID token refresh right before expiration. function() { // Keep in mind when this fails for any reason other than a network // error, it will effectively stop the proactive refresh. return self.getIdToken(true); }, // Retry only on network errors. function(error) { if (error && error.code == 'auth/network-request-failed') { return true; } return false; }, // Return next time to run with offset applied. function() { // Get time until expiration minus the refresh offset. var waitInterval = self.stsTokenManager_.getExpirationTime() - Date.now() - fireauth.TokenRefreshTime.OFFSET_DURATION; // Set to zero if wait interval is negative. return waitInterval > 0 ? waitInterval : 0; }, // Retrial minimum wait. fireauth.TokenRefreshTime.RETRIAL_MIN_WAIT, // Retrial maximum wait. fireauth.TokenRefreshTime.RETRIAL_MAX_WAIT, // Do not run in background as it is common to have multiple tabs open // in a browser and this could increase QPS on server. false); }; /** Starts token proactive refresh. */ fireauth.AuthUser.prototype.startProactiveRefresh = function() { // Only allow if not destroyed and not already started. if (!this.destroyed_ && !this.proactiveRefresh_.isRunning()) { this.proactiveRefresh_.start(); // Unlisten any previous token change listener. goog.events.unlisten( this, fireauth.UserEventType.TOKEN_CHANGED, this.userTokenChangeListener_); // Listen to token changes to reset the token refresher. goog.events.listen( this, fireauth.UserEventType.TOKEN_CHANGED, this.userTokenChangeListener_); } }; /** Stops token proactive refresh. */ fireauth.AuthUser.prototype.stopProactiveRefresh = function() { // Remove internal token change listener. goog.events.unlisten( this, fireauth.UserEventType.TOKEN_CHANGED, this.userTokenChangeListener_); // Stop proactive token refresh. this.proactiveRefresh_.stop(); }; /** * Sets latest access token for the AuthUser object. * @param {string} lastAccessToken * @private */ fireauth.AuthUser.prototype.setLastAccessToken_ = function(lastAccessToken) { /** @private {?string} Latest access token. */ this.lastAccessToken_ = lastAccessToken; fireauth.object.setReadonlyProperty(this, '_lat', lastAccessToken); }; /** * @param {function(!fireauth.AuthUser):!goog.Promise} listener The listener * to state changes to add. */ fireauth.AuthUser.prototype.addStateChangeListener = function(listener) { this.stateChangeListeners_.push(listener); }; /** * @param {function(!fireauth.AuthUser):!goog.Promise} listener The listener * to state changes to remove. */ fireauth.AuthUser.prototype.removeStateChangeListener = function(listener) { goog.array.removeAllIf(this.stateChangeListeners_, function(ele) { return ele == listener; }); }; /** * Executes all state change listener promises and when all fulfilled, resolves * with the current user. * @return {!goog.Promise} A promise that resolves when all state listeners * fulfilled. * @private */ fireauth.AuthUser.prototype.notifyStateChangeListeners_ = function() { var promises = []; var self = this; for (var i = 0; i < this.stateChangeListeners_.length; i++) { // Run listener with Auth user instance and add to list of promises. promises.push(this.stateChangeListeners_[i](this)); } return goog.Promise.allSettled(promises).then(function(results) { // State change errors should be recoverable even if errors occur. return self; }); }; /** * Sets the user current pending popup event ID. * @param {string} eventId The pending popup event ID. */ fireauth.AuthUser.prototype.setPopupEventId = function(eventId) { // Saving a popup event in a separate property other than redirectEventId // would prevent a pending redirect event from being overwritten by a newly // called popup operation. this.popupEventId_ = eventId; }; /** * @return {?string} The pending popup event ID. */ fireauth.AuthUser.prototype.getPopupEventId = function() { return this.popupEventId_ || null; }; /** * Sets the user current pending redirect event ID. * @param {string} eventId The pending redirect event ID. */ fireauth.AuthUser.prototype.setRedirectEventId = function(eventId) { this.redirectEventId_ = eventId; }; /** * @return {?string} The pending redirect event ID. */ fireauth.AuthUser.prototype.getRedirectEventId = function() { return this.redirectEventId_ || null; }; /** * Subscribes to Auth event manager to handle popup and redirect events. * This is an explicit operation as users could exist in temporary states. For * example a user change could be detected in another tab. When syncing to those * changes, a temporary user is retrieved from storage and then copied to * existing user. The temporary user should not subscribe to Auth event changes. */ fireauth.AuthUser.prototype.enablePopupRedirect = function() { // Subscribe to Auth event manager if available. if (this.authEventManager_ && !this.popupRedirectEnabled_) { this.popupRedirectEnabled_ = true; this.authEventManager_.subscribe(this); } }; /** * getAccountInfo users field. * @const {string} */ fireauth.AuthUser.GET_ACCOUNT_INFO_USERS = 'users'; /** * getAccountInfo response user fields. * @enum {string} */ fireauth.AuthUser.GetAccountInfoField = { CREATED_AT: 'createdAt', DISPLAY_NAME: 'displayName', EMAIL: 'email', EMAIL_VERIFIED: 'emailVerified', LAST_LOGIN_AT: 'lastLoginAt', LOCAL_ID: 'localId', PASSWORD_HASH: 'passwordHash', PASSWORD_UPDATED_AT: 'passwordUpdatedAt', PHONE_NUMBER: 'phoneNumber', PHOTO_URL: 'photoUrl', PROVIDER_USER_INFO: 'providerUserInfo', TENANT_ID: 'tenantId' }; /** * setAccountInfo response user fields. * @enum {string} */ fireauth.AuthUser.SetAccountInfoField = { DISPLAY_NAME: 'displayName', EMAIL: 'email', PHOTO_URL: 'photoUrl', PROVIDER_ID: 'providerId', PROVIDER_USER_INFO: 'providerUserInfo' }; /** * getAccountInfo response provider user info fields. * @enum {string} */ fireauth.AuthUser.GetAccountInfoProviderField = { DISPLAY_NAME: 'displayName', EMAIL: 'email', PHOTO_URL: 'photoUrl', PHONE_NUMBER: 'phoneNumber', PROVIDER_ID: 'providerId', RAW_ID: 'rawId' }; /** * verifyAssertion response fields. * @enum {string} */ fireauth.AuthUser.VerifyAssertionField = { ID_TOKEN: 'idToken', PROVIDER_ID: 'providerId' }; /** @return {!fireauth.StsTokenManager} The STS token manager instance */ fireauth.AuthUser.prototype.getStsTokenManager = function() { return this.stsTokenManager_; }; /** * Sets the user account info. * @param {!fireauth.AuthUser.AccountInfo} accountInfo The account information * from the default provider. */ fireauth.AuthUser.prototype.setAccountInfo = function(accountInfo) { fireauth.object.setReadonlyProperties(this, { 'uid': accountInfo['uid'], 'displayName': accountInfo['displayName'] || null, 'photoURL': accountInfo['photoURL'] || null, 'email': accountInfo['email'] || null, 'emailVerified': accountInfo['emailVerified'] || false, 'phoneNumber': accountInfo['phoneNumber'] || null, 'isAnonymous': accountInfo['isAnonymous'] || false, 'tenantId': accountInfo['tenantId'] || null, 'metadata': new fireauth.UserMetadata( accountInfo['createdAt'], accountInfo['lastLoginAt']), 'providerData': [] }); // Sets the tenant ID on RPC handler. For requests with ID tokens, the source // of truth is the tenant ID in the ID token. If the request body has a // tenant ID (optional here), the backend will confirm it matches the // tenant ID in the ID token, otherwise throw an error. If no tenant ID is // passed in the request, it will be determined from the ID token. this.rpcHandler_.updateTenantId(this['tenantId']); }; /** * Type specifying the parameters that can be passed to the * {@code fireauth.AuthUser} constructor. * @typedef {{ * uid: (?string|undefined), * displayName: (?string|undefined), * photoURL: (?string|undefined), * email: (?string|undefined), * emailVerified: ?boolean, * phoneNumber: (?string|undefined), * isAnonymous: ?boolean, * createdAt: (?string|undefined), * lastLoginAt: (?string|undefined), * tenantId: (?string|undefined), * multiFactor: ({ * enrolledFactors: (?Array<!fireauth.MultiFactorInfo>|undefined) * }|undefined) * }} */ fireauth.AuthUser.AccountInfo; /** * The provider for all fireauth.AuthUser objects is 'firebase'. */ fireauth.object.setReadonlyProperty(fireauth.AuthUser.prototype, 'providerId', fireauth.idp.ProviderId.FIREBASE); /** * Returns nothing. This can be used to consume the output of a Promise. * @private */ fireauth.AuthUser.returnNothing_ = function() { // Return nothing. Intentionally left empty. }; /** * Ensures the user is still logged in before moving to the next promise * resolution. * @return {!goog.Promise<undefined,undefined>} * @private */ fireauth.AuthUser.prototype.checkDestroyed_ = function() { var self = this; return goog.Promise.resolve().then(function() { if (self.destroyed_) { throw new fireauth.AuthError(fireauth.authenum.Error.MODULE_DESTROYED); } }); }; /** * @return {!Array<!fireauth.idp.ProviderId>} The list of provider IDs. */ fireauth.AuthUser.prototype.getProviderIds = function() { return goog.array.map(this['providerData'], function(userInfo) { return userInfo['providerId']; }); }; /** * Adds the provided user info to list of providers' data. * @param {?fireauth.AuthUserInfo} providerData Provider data to store for user. */ fireauth.AuthUser.prototype.addProviderData = function(providerData) { if (!providerData) { return; } this.removeProviderData(providerData['providerId']); this['providerData'].push(providerData); }; /** * @param {!fireauth.idp.ProviderId} providerId The provider ID whose * data should be removed. */ fireauth.AuthUser.prototype.removeProviderData = function(providerId) { goog.array.removeAllIf(this['providerData'], function(userInfo) { return userInfo['providerId'] == providerId; }); }; /** * @param {string} propName The property name to modify. * @param {?string|boolean} value The new value to set. */ fireauth.AuthUser.prototype.updateProperty = function(propName, value) { // User ID is required. if (propName == 'uid' && !value) { return; } if (this.hasOwnProperty(propName)) { fireauth.object.setReadonlyProperty(this, propName, value); } }; /** * @param {!fireauth.AuthUser} otherUser The other user to compare to. * @return {boolean} True if both User objects have the same user ID. */ fireauth.AuthUser.prototype.hasSameUserIdAs = function(otherUser) { var thisId = this['uid']; var thatId = otherUser['uid']; if (thisId === undefined || thisId === null || thisId === '' || thatId === undefined || thatId === null || thatId === '') { return false; } return thisId == thatId; }; /** * Copies all properties and STS token manager instance from userToCopy to * current user without triggering any Auth state change or token change * listener. * @param {!fireauth.AuthUser} userToCopy The updated user to overwrite current * user. */ fireauth.AuthUser.prototype.copy = function(userToCopy) { var self = this; // Copy to self. if (self == userToCopy) { return; } fireauth.object.setReadonlyProperties(this, { 'uid': userToCopy['uid'], 'displayName': userToCopy['displayName'], 'photoURL': userToCopy['photoURL'], 'email': userToCopy['email'], 'emailVerified': userToCopy['emailVerified'], 'phoneNumber': userToCopy['phoneNumber'], 'isAnonymous': userToCopy['isAnonymous'], 'tenantId': userToCopy['tenantId'], 'providerData': [] }); // This should always be available but just in case there is a conflict with // a user from an older version. if (userToCopy['metadata']) { fireauth.object.setReadonlyProperty( this, 'metadata', /** @type{!fireauth.UserMetadata} */ (userToCopy['metadata']).clone()); } else { // User to copy has no metadata. Align with that. fireauth.object.setReadonlyProperty( this, 'metadata', new fireauth.UserMetadata()); } goog.array.forEach(userToCopy['providerData'], function(userInfo) { self.addProviderData(userInfo); }); this.stsTokenManager_.copy(userToCopy.getStsTokenManager()); fireauth.object.setReadonlyProperty( this, 'refreshToken', this.stsTokenManager_.getRefreshToken()); // Copy multi-factor info to current user. // This should be backward compatible. // If the userToCopy is loaded from an older version, multiFactorUser // enrolled factors will be initialized empty and copied empty to current // multiFactorUser. this.multiFactorUser_.copy(userToCopy.multiFactorUser_); }; /** * Set the Auth user redirect storage manager. * @param {?fireauth.storage.RedirectUserManager} redirectStorageManager The * utility used to store or delete the user on redirect. */ fireauth.AuthUser.prototype.setRedirectStorageManager = function(redirectStorageManager) { /** * @private {?fireauth.storage.RedirectUserManager} The redirect user storage * manager. */ this.redirectStorageManager_ = redirectStorageManager; }; /** * Refreshes the current user, if signed in. * @return {!goog.Promise<void>} */ fireauth.AuthUser.prototype.reload = function() { var self = this; // Register this pending promise. This will also check for user invalidation. return this.registerPendingPromise_(this.checkDestroyed_().then(function() { return self.reloadWithoutSaving_() .then(function() { return self.notifyStateChangeListeners_(); }) .then(fireauth.AuthUser.returnNothing_); })); }; /** * Refreshes the current user, if signed in. * @return {!goog.Promise<string>} Promise that resolves with the idToken. * @private */ fireauth.AuthUser.prototype.reloadWithoutSaving_ = function() { var self = this; // ID token is required to refresh the user's data. // If this is called after invalidation, getToken will throw the cached error. return this.getIdToken().then(function(idToken) { var isAnonymous = self['isAnonymous']; return self.setUserAccountInfoFromToken_(idToken) .then(function(user) { if (!isAnonymous) { // Preserves the not anonymous status of the stored user, // even if no more credentials (federated or email/password) // linked to the user. self.updateProperty('isAnonymous', false); } return idToken; }); }); }; /** * This operation resolves with the Firebase ID token result which contains * the entire payload claims. * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. * @return {!goog.Promise<!fireauth.IdTokenResult>} A Promise that resolves with * the ID token result. */ fireauth.AuthUser.prototype.getIdTokenResult = function(opt_forceRefresh) { return this.getIdToken(opt_forceRefresh).then(function(idToken) { return new fireauth.IdTokenResult(idToken); }); }; /** * This operation resolves with the Firebase ID token. * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. * @return {!goog.Promise<string>} A Promise that resolves with the ID token. */ fireauth.AuthUser.prototype.getIdToken = function(opt_forceRefresh) { var self = this; // Register this pending promise. This will also check for user invalidation. return this.registerPendingPromise_(this.checkDestroyed_().then(function() { return self.stsTokenManager_.getToken(opt_forceRefresh); }).then(function(response) { if (!response) { // If the user exists, the token manager should be initialized. throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); } // Only if the access token is refreshed, notify Auth listeners. if (response['accessToken'] != self.lastAccessToken_) { self.setLastAccessToken_(response['accessToken']); // Auth state change, notify listeners. self.notifyAuthListeners_(); } self.updateProperty('refreshToken', response['refreshToken']); return response['accessToken']; })); }; /** * Checks if the error corresponds to a user invalidation action. * @param {*} error The error returned by a user operation. * @return {boolean} Whether the user is invalidated based on the error * provided. * @private */ fireauth.AuthUser.isUserInvalidated_ = function(error) { return !!(error && (error.code == 'auth/user-disabled' || error.code == 'auth/user-token-expired')); }; /** * Updates the current tokens using a server response, if new tokens are * present and are different from the current ones, and notify the Auth * listeners. * @param {!Object} response The response from the server. */ fireauth.AuthUser.prototype.updateTokensIfPresent = function(response) { if (response[fireauth.RpcHandler.AuthServerField.ID_TOKEN] && this.lastAccessToken_ != response[ fireauth.RpcHandler.AuthServerField.ID_TOKEN]) { this.stsTokenManager_.parseServerResponse(response); this.notifyAuthListeners_(); this.setLastAccessToken_(response[ fireauth.RpcHandler.AuthServerField.ID_TOKEN]); // Update refresh token property. this.updateProperty( 'refreshToken', this.stsTokenManager_.getRefreshToken()); } }; /** * Called internally on Auth (access token) changes to notify listeners. * @private */ fireauth.AuthUser.prototype.notifyAuthListeners_ = function() { this.dispatchEvent( new fireauth.UserEvent(fireauth.UserEventType.TOKEN_CHANGED)); }; /** * Called internally on user deletion to notify listeners. * @private */ fireauth.AuthUser.prototype.notifyUserDeletedListeners_ = function() { this.dispatchEvent( new fireauth.UserEvent(fireauth.UserEventType.USER_DELETED)); }; /** * Called internally on user session invalidation to notify listeners. * @private */ fireauth.AuthUser.prototype.notifyUserInvalidatedListeners_ = function() { this.dispatchEvent( new fireauth.UserEvent(fireauth.UserEventType.USER_INVALIDATED)); }; /** * Queries the backend using the provided ID token for all linked accounts to * build the Firebase user object. * @param {string} idToken The ID token string. * @return {!goog.Promise<undefined>} * @private */ fireauth.AuthUser.prototype.setUserAccountInfoFromToken_ = function (idToken) { return this.rpcHandler_.getAccountInfoByIdToken(idToken) .then(goog.bind(this.parseAccountInfo_, this)); }; /** * Parses the response from the getAccountInfo endpoint. * @param {!Object} resp The backend response. * @private */ fireauth.AuthUser.prototype.parseAccountInfo_ = function(resp) { var users = resp[fireauth.AuthUser.GET_ACCOUNT_INFO_USERS]; if (!users || !users.length) { throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); } var user = users[0]; var accountInfo = /** @type {!fireauth.AuthUser.AccountInfo} */ ({ 'uid': /** @type {string} */ ( user[fireauth.AuthUser.GetAccountInfoField.LOCAL_ID]), 'displayName': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.DISPLAY_NAME]), 'photoURL': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.PHOTO_URL]), 'email': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.EMAIL]), 'emailVerified': !!user[fireauth.AuthUser.GetAccountInfoField.EMAIL_VERIFIED], 'phoneNumber': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.PHONE_NUMBER]), 'lastLoginAt': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.LAST_LOGIN_AT]), 'createdAt': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.CREATED_AT]), 'tenantId': /** @type {?string|undefined} */ ( user[fireauth.AuthUser.GetAccountInfoField.TENANT_ID]) }); this.setAccountInfo(accountInfo); var linkedAccounts = this.extractLinkedAccounts_(user); for (var i = 0; i < linkedAccounts.length; i++) { this.addProviderData(linkedAccounts[i]); } // Sets the isAnonymous flag based on email, passwordHash and providerData. var isAnonymous = !(this['email'] && user[fireauth.AuthUser.GetAccountInfoField.PASSWORD_HASH]) && !(this['providerData'] && this['providerData'].length); this.updateProperty('isAnonymous', isAnonymous); // Notify external listeners of the reload. this.dispatchEvent(new fireauth.UserEvent( fireauth.UserEventType.USER_RELOADED, {userServerResponse: user})); }; /** * Extracts the linked accounts from getAccountInfo response and returns an * array of corresponding provider data. * @param {!Object} resp The response object. * @return {!Array<!fireauth.AuthUserInfo>} The linked accounts. * @private */ fireauth.AuthUser.prototype.extractLinkedAccounts_ = function(resp) { var providerInfo = resp[fireauth.AuthUser.GetAccountInfoField.PROVIDER_USER_INFO]; if (!providerInfo || !providerInfo.length) { return []; } return goog.array.map(providerInfo, function(info) { return new fireauth.AuthUserInfo( info[fireauth.AuthUser.GetAccountInfoProviderField.RAW_ID], info[fireauth.AuthUser.GetAccountInfoProviderField.PROVIDER_ID], info[fireauth.AuthUser.GetAccountInfoProviderField.EMAIL], info[fireauth.AuthUser.GetAccountInfoProviderField.DISPLAY_NAME], info[fireauth.AuthUser.GetAccountInfoProviderField.PHOTO_URL], info[fireauth.AuthUser.GetAccountInfoProviderField.PHONE_NUMBER]); }); }; /** * Reauthenticates a user using a fresh credential, to be used before operations * such as updatePassword that require tokens from recent login attempts. It * also returns any additional user info data or credentials returned form the * backend. It has been deprecated in favor of reauthenticateWithCredential. * @param {!fireauth.AuthCredential} credential * @return {!goog.Promise<!fireauth.AuthEventManager.Result>} */ fireauth.AuthUser.prototype.reauthenticateAndRetrieveDataWithCredential = function(credential) { fireauth.deprecation.log( fireauth.deprecation.Deprecations.REAUTH_WITH_CREDENTIAL); return this.reauthenticateWithCredential(credential); }; /** * Reauthenticates a user using a fresh credential, to be used before operations * such as updatePassword that require tokens from recent login attempts. It * also returns any additional user info data or credentials returned form the * backend. * @param {!fireauth.AuthCredential} credential * @return {!goog.Promise<!fireauth.AuthEventManager.Result>} */ fireauth.AuthUser.prototype.reauthenticateWithCredential = function(credential) { var self = this; var userCredential = null; // Register this pending promise but bypass user invalidation check. return this.registerPendingPromise_( // Match ID token from credential with the current user UID. credential.matchIdTokenWithUid(this.rpcHandler_, this['uid']) .then(function(response) { // If the credential is valid and matches the current user ID, then // update the tokens accordingly. self.updateTokensIfPresent(response); // Get user credential. userCredential = self.getUserCredential_( response, fireauth.constants.OperationType.REAUTHENTICATE); // This could potentially validate an invalidated user. This happens in // the case a password reset was applied. The refresh token is expired. // Reauthentication should revalidate the user. // User would remain non current if already signed out, but should be // enabled again. self.userInvalidatedError_ = null; return self.reload(); }).then(function() { // Return user credential after reauthenticated user is reloaded. return userCredential; }), // Skip invalidation check as reauthentication could revalidate a user. true); }; /** * Reloads the user and then checks if a provider is already linked. If so, * this returns a Promise that rejects. Note that state change listeners are not * notified on success, so that operations using this can make changes and then * do one final listener notification. * @param {string} providerId * @return {!goog.Promise<void>} * @private */ fireauth.AuthUser.prototype.checkIfAlreadyLinked_ = function(providerId) { var self = this; // Reload first in case the user was updated elsewhere. return this.reloadWithoutSaving_() .then(function() { if (goog.array.contains(self.getProviderIds(), providerId)) { return self.notifyStateChangeListeners_() .then(function() { throw new fireauth.AuthError( fireauth.authenum.Error.PROVIDER_ALREADY_LINKED); }); } }); }; /** * Links a provider to the current user and returns any additional user info * data or credentials returned form the backend. It has been deprecated in * favor of linkWithCredential. * @param {!fireauth.AuthCredential} credential The credential from the Auth * provider. * @return {!goog.Promise<!fireauth.AuthEventManager.Result>} */ fireauth.AuthUser.prototype.linkAndRetrieveDataWithCredential = function(credential) { fireauth.deprecation.log( fireauth.deprecation.Deprecations.LINK_WITH_CREDENTIAL); return this.linkWithCredential(credential); }; /** * Links a provider to the current user and returns any additional user info * data or credentials returned form the backend. * @param {!fireauth.AuthCredential} credential The credential from the Auth * provider. * @return {!goog.Promise<!fireauth.AuthEventManager.Result>} */ fireauth.AuthUser.prototype.linkWithCredential = function(credential) { var self = this; var userCredential = null; // Register this pending promise. This will also check for user invalidation. return this.registerPendingPromise_( this.checkIfAlreadyLinked_(credential['providerId']) .then(function() { return self.getIdToken(); }) .then(function(idToken) { return credential.linkToIdToken(self.rpcHandler_, idToken); }) .then(function(response) { // Get user credential. userCredential = self.getUserCredential_( response, fireauth.constants.OperationType.LINK); // Finalize linking. return self.finalizeLinking_(response); }) .then(function(user) { // Return user credential after finalizing linking. return userCredential; }) ); }; /** * Links a phone number using the App verifier instance and returns a * promise that resolves with the confirmation result which on confirmation * will resolve with the UserCredential object. * @param {string} phoneNumber The phone number to authenticate with. * @param {!firebase.auth.ApplicationVerifier} appVerifier The application * verifier. * @return {!goog.Promise<!fireauth.ConfirmationResult>} */ fireauth.AuthUser.prototype.linkWithPhoneNumber = function(phoneNumber, appVerifier) { var self = this; return /** @type {!goog.Promise<!fireauth.ConfirmationResult>} */ ( this.registerPendingPromise_( // Check if linked already. If so, throw an error. // This is redundant but is needed to prevent the need to send the // SMS (worth the cost). this.checkIfAlreadyLinked_(fireauth.idp.ProviderId.PHONE) .then(function() { return fireauth.ConfirmationResult.initialize( self.getAuth_(), phoneNumber, appVerifier, // This will check again if the credential is linked. goog.bind(self.linkWithCredential, self)); }))); }; /** * Reauthenticates a user with a phone number using the App verifier instance * and returns a promise that resolves with the confirmation result which on * confirmation will resolve with the UserCredential object. * @param {string} phoneNumber The phone number to authenticate with. * @param {!firebase.auth.ApplicationVerifier} appVerifier The application * verifier. * @return {!goog.Promise<!fireauth.ConfirmationResult>} */ fireauth.AuthUser.prototype.reauthenticateWithPhoneNumber = function(phoneNumber, appVerifier) { var self = this; return /** @type {!goog.Promise<!fireauth.ConfirmationResult>} */ ( this.registerPendingPromise_( // Wrap this operation in a Promise since self.getAuth_() may throw an // error synchronously. goog.Promise.resolve().then(function() { return fireauth.ConfirmationResult.initialize( // Get corresponding Auth instance. self.getAuth_(), phoneNumber, appVerifier, goog.bind(self.reauthenticateWithCredential, self)); }), // Skip invalidation check as reauthentication could revalidate a // user. true)); }; /** * Converts an ID token response (eg. verifyAssertion) to a UserCredential * object. * @param {!Object} idTokenResponse The ID token response. * @param {!fireauth.constants.OperationType} operationType The operation type * to set in the user credential. * @return {!fireauth.AuthEventManager.Result} The UserCredential object * constructed from the response. * @private */ fireauth.AuthUser.prototype.getUserCredential_ = function(idTokenResponse, operationType) { // Get credential if available in the response. var credential = fireauth.AuthProvider.getCredentialFromResponse( idTokenResponse); // Get additional user info data if available in the response. var additionalUserInfo = fireauth.AdditionalUserInfo.fromPlainObject( idTokenResponse); // Return the readonly copy of the user credential object. return fireauth.object.makeReadonlyCopy({ // Return the current user reference. 'user': this, // Return any credential passed from the backend. 'credential': credential, // Return any additional IdP data passed from the backend. 'additionalUserInfo': additionalUserInfo, // Return the operation type in the user credential object. 'operationType': operationType }); }; /** * Finalizes a linking flow, updating idToken and user's data using the * RPC linking response. * @param {!Object} response The RPC linking response. * @return {!goog.Promise<!fireauth.AuthUser>} * @private */ fireauth.AuthUser.prototype.finalizeLinking_ = function(response) { // The response may contain a new access token, // so we should update them just like a new sign in. this.updateTokensIfPresent(response); // This will take care of saving the updated state. var self = this; return this.reload().then(function() { return self; }); }; /** * Updates the user's email. * @param {string} newEmail The new email. * @return {!goog.Promise<void>} */ fireauth.AuthUser.prototype.updateEmail = function(newEmail) { var self = this; // Register this pending promise. This will also check for user invalidation. return this.registerPendingPromise_(this.getIdToken() .then(function(idToken) { return self.rpcHandler_.updateEmail(idToken, newEmail); }) .then(function(response) { // Calls to SetAccountInfo may invalidate old tokens. self.updateTokensIfPresent(response); // Reloads the user to update emailVerified. return self.reload(); })); }; /** * Updates the user's phone number. * @param {!fireauth.PhoneAuthCredential} phoneCredential * @return {!goog.Promise<void>} */ fireauth.AuthUser.prototype.updatePhoneNumber = function(phoneCredential) { var self = this; return this.registerPendingPromise_(this.getIdToken() .then(function(idToken) { // The backend always overwrites the existing phone number during a // link operation. return phoneCredential.linkToIdToken(self.rpcHandler_, idToken); }) .then(function(response) { self.updateTokensIfPresent(response); return self.reload();