voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
1,342 lines (1,246 loc) • 83.2 kB
JavaScript
/**
* @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 The headless Auth class used for authenticating Firebase users.
*/
goog.provide('fireauth.Auth');
goog.require('fireauth.ActionCodeInfo');
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.AuthSettings');
goog.require('fireauth.AuthUser');
goog.require('fireauth.ConfirmationResult');
goog.require('fireauth.EmailAuthProvider');
goog.require('fireauth.MultiFactorError');
goog.require('fireauth.RpcHandler');
goog.require('fireauth.UserEventType');
goog.require('fireauth.authenum.Error');
goog.require('fireauth.constants');
goog.require('fireauth.deprecation');
goog.require('fireauth.idp');
goog.require('fireauth.iframeclient.IfcHandler');
goog.require('fireauth.object');
goog.require('fireauth.storage.RedirectUserManager');
goog.require('fireauth.storage.UserManager');
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');
/**
* Creates the Firebase Auth corresponding for the App provided.
*
* @param {!firebase.app.App} app The corresponding Firebase App.
* @constructor
* @implements {fireauth.AuthEventHandler}
* @implements {firebase.Service}
* @extends {goog.events.EventTarget}
*/
fireauth.Auth = function(app) {
/** @private {boolean} Whether this instance is deleted. */
this.deleted_ = false;
/** The Auth instance's settings object. */
fireauth.object.setReadonlyProperty(
this, 'settings', new fireauth.AuthSettings());
/** Auth's corresponding App. */
fireauth.object.setReadonlyProperty(this, 'app', app);
// Initialize RPC handler.
// API key is required for web client RPC calls.
if (this.app_().options && this.app_().options['apiKey']) {
var clientFullVersion = firebase.SDK_VERSION ?
fireauth.util.getClientVersion(
fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION) :
null;
this.rpcHandler_ = new fireauth.RpcHandler(
this.app_().options && this.app_().options['apiKey'],
// Get the client Auth endpoint used.
fireauth.constants.getEndpointConfig(fireauth.constants.clientEndpoint),
clientFullVersion);
} else {
throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_API_KEY);
}
/** @private {!Array<!goog.Promise<*, *>|!goog.Promise<void>>} List of
* pending promises. */
this.pendingPromises_ = [];
/** @private {!Array<!function(?string)>} Auth token listeners. */
this.authListeners_ = [];
/** @private {!Array<!function(?string)>} User change listeners. */
this.userChangeListeners_ = [];
/**
* @private {!firebase.Subscribe} The subscribe function to the Auth ID token
* change observer. This will trigger on ID token changes, including
* token refresh on the same user.
*/
this.onIdTokenChanged_ = firebase.INTERNAL.createSubscribe(
goog.bind(this.initIdTokenChangeObserver_, this));
/**
* @private {?string|undefined} The UID of the user that last triggered the
* user state change listener.
*/
this.userStateChangeUid_ = undefined;
/**
* @private {!firebase.Subscribe} The subscribe function to the user state
* change observer.
*/
this.onUserStateChanged_ = firebase.INTERNAL.createSubscribe(
goog.bind(this.initUserStateObserver_, this));
// Set currentUser to null.
this.setCurrentUser_(null);
/**
* @private {!fireauth.storage.UserManager} The Auth user storage
* manager instance.
*/
this.userStorageManager_ =
new fireauth.storage.UserManager(this.getStorageKey());
/**
* @private {!fireauth.storage.RedirectUserManager} The redirect user
* storagemanager instance.
*/
this.redirectUserStorageManager_ =
new fireauth.storage.RedirectUserManager(this.getStorageKey());
/**
* @private {!goog.Promise<undefined>} Promise that resolves when initial
* state is loaded from storage.
*/
this.authStateLoaded_ = this.registerPendingPromise_(this.initAuthState_());
/**
* @private {!goog.Promise<undefined>} Promise that resolves when state and
* redirect result is ready, after which sign in and sign out operations
* are safe to execute.
*/
this.redirectStateIsReady_ = this.registerPendingPromise_(
this.initAuthRedirectState_());
/** @private {boolean} Whether initial state has already been resolved. */
this.isStateResolved_ = false;
/**
* @private {!function()} The syncAuthChanges function with context set to
* auth instance.
*/
this.getSyncAuthUserChanges_ = goog.bind(this.syncAuthUserChanges_, this);
/** @private {!function(!fireauth.AuthUser):!goog.Promise} The handler for
* user state changes. */
this.userStateChangeListener_ =
goog.bind(this.handleUserStateChange_, this);
/** @private {!function(!Object)} The handler for user token changes. */
this.userTokenChangeListener_ =
goog.bind(this.handleUserTokenChange_, this);
/** @private {!function(!Object)} The handler for user deletion. */
this.userDeleteListener_ = goog.bind(this.handleUserDelete_, this);
/** @private {!function(!Object)} The handler for user invalidation. */
this.userInvalidatedListener_ = goog.bind(this.handleUserInvalidated_, this);
/**
* @private {?fireauth.AuthEventManager} The Auth event manager instance.
*/
this.authEventManager_ = null;
// TODO: find better way to enable or disable auth event manager.
if (fireauth.AuthEventManager.ENABLED) {
// Initialize Auth event manager to handle popup and redirect operations.
this.initAuthEventManager_();
}
// Export INTERNAL namespace.
this.INTERNAL = {};
this.INTERNAL['delete'] = goog.bind(this.delete, this);
this.INTERNAL['logFramework'] = goog.bind(this.logFramework, this);
/**
* @private {number} The number of Firebase services subscribed to Auth
* changes.
*/
this.firebaseServices_ = 0;
// Add call to superclass constructor.
fireauth.Auth.base(this, 'constructor');
// Initialize readable/writable Auth properties.
this.initializeReadableWritableProps_();
/**
* @private {!Array<string>} List of Firebase frameworks/libraries used. This
* is currently only used to log FirebaseUI.
*/
this.frameworks_ = [];
/**
* @private {?fireauth.constants.EmulatorSettings} The current
* emulator settings.
*/
this.emulatorConfig_ = null;
};
goog.inherits(fireauth.Auth, goog.events.EventTarget);
/**
* Language code change custom event.
* @param {?string} languageCode The new language code.
* @constructor
* @extends {goog.events.Event}
*/
fireauth.Auth.LanguageCodeChangeEvent = function(languageCode) {
goog.events.Event.call(
this, fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED);
this.languageCode = languageCode;
};
goog.inherits(fireauth.Auth.LanguageCodeChangeEvent, goog.events.Event);
/**
* Emulator config change custom event.
* @param {?fireauth.constants.EmulatorSettings} emulatorConfig The new
* emulator settings.
* @constructor
* @extends {goog.events.Event}
*/
fireauth.Auth.EmulatorConfigChangeEvent = function(emulatorConfig) {
goog.events.Event.call(this, fireauth.constants.AuthEventType.EMULATOR_CONFIG_CHANGED);
this.emulatorConfig = emulatorConfig;
};
goog.inherits(fireauth.Auth.EmulatorConfigChangeEvent, goog.events.Event);
/**
* Framework change custom event.
* @param {!Array<string>} frameworks The new frameworks array.
* @constructor
* @extends {goog.events.Event}
*/
fireauth.Auth.FrameworkChangeEvent = function(frameworks) {
goog.events.Event.call(
this, fireauth.constants.AuthEventType.FRAMEWORK_CHANGED);
this.frameworks = frameworks;
};
goog.inherits(fireauth.Auth.FrameworkChangeEvent, goog.events.Event);
/**
* Changes the Auth state persistence to the specified one.
* @param {!fireauth.authStorage.Persistence} persistence The Auth state
* persistence mechanism.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.setPersistence = function(persistence) {
// TODO: fix auth.delete() behavior and how this affects persistence
// change after deletion.
// Throw an error if already destroyed.
// Set current persistence.
var p = this.userStorageManager_.setPersistence(persistence);
return /** @type {!goog.Promise<void>} */ (this.registerPendingPromise_(p));
};
/**
* Get rid of Closure warning - the property is adding in the constructor.
* @type {!firebase.app.App}
*/
fireauth.Auth.prototype.app;
/**
* Sets the language code.
* @param {?string} languageCode
*/
fireauth.Auth.prototype.setLanguageCode = function(languageCode) {
// Don't do anything if no change detected.
if (this.languageCode_ !== languageCode && !this.deleted_) {
this.languageCode_ = languageCode;
// Update custom Firebase locale field.
this.rpcHandler_.updateCustomLocaleHeader(this.languageCode_);
// Notify external language code change listeners.
this.notifyLanguageCodeListeners_();
}
};
/**
* Returns the current auth instance's language code if available.
* @return {?string}
*/
fireauth.Auth.prototype.getLanguageCode = function() {
return this.languageCode_;
};
/**
* Sets the current language to the default device/browser preference.
*/
fireauth.Auth.prototype.useDeviceLanguage = function() {
this.setLanguageCode(fireauth.util.getUserLanguage());
};
/**
* Sets the emulator configuration (go/firebase-emulator-connection-api).
* @param {string} url The url for the Auth emulator.
* @param {?Object=} options Optional options to specify emulator settings.
*/
fireauth.Auth.prototype.useEmulator = function(url, options) {
// Emulator config can only be set once.
if (!this.emulatorConfig_) {
if (!/^https?:\/\//.test(url)) {
throw new fireauth.AuthError(
fireauth.authenum.Error.ARGUMENT_ERROR,
'Emulator URL must start with a valid scheme (http:// or https://).');
}
// Emit a warning so dev knows we are now in test mode.
const disableBanner = options ? !!options['disableWarnings'] : false;
this.emitEmulatorWarning_(disableBanner);
// Persist the config.
this.emulatorConfig_ = {url, disableWarnings: disableBanner};
// Disable app verification.
this.settings_().setAppVerificationDisabledForTesting(true);
// Update RPC handler endpoints.
this.rpcHandler_.updateEmulatorConfig(this.emulatorConfig_);
// Notify external event listeners.
this.notifyEmulatorConfigListeners_();
}
}
/**
* Emits a console info and a visual banner if emulator integration is
* enabled.
* @param {boolean} disableBanner Whether visual banner should be disabled.
* @private
*/
fireauth.Auth.prototype.emitEmulatorWarning_ = function(disableBanner) {
fireauth.util.consoleInfo('WARNING: You are using the Auth Emulator,' +
' which is intended for local testing only. Do not use with' +
' production credentials.');
if (goog.global.document && !disableBanner) {
fireauth.util.onDomReady().then(() => {
const ele = goog.global.document.createElement('div');
ele.innerText = 'Running in emulator mode. Do not use with production' +
' credentials.';
ele.style.position = 'fixed';
ele.style.width = '100%';
ele.style.backgroundColor = '#ffffff';
ele.style.border = '.1em solid #000000';
ele.style.color = '#b50000';
ele.style.bottom = '0px';
ele.style.left = '0px';
ele.style.margin = '0px';
ele.style.zIndex = 10000;
ele.style.textAlign = 'center';
ele.classList.add('firebase-emulator-warning');
goog.global.document.body.appendChild(ele);
});
}
}
/**
* @return {?fireauth.constants.EmulatorConfig}
*/
fireauth.Auth.prototype.getEmulatorConfig = function() {
if (!this.emulatorConfig_) {
return null;
}
const uri = goog.Uri.parse(this.emulatorConfig_.url);
return /** @type {!fireauth.constants.EmulatorConfig} */ (
fireauth.object.makeReadonlyCopy({
'protocol': uri.getScheme(),
'host': uri.getDomain(),
'port': uri.getPort(),
'options': fireauth.object.makeReadonlyCopy({
'disableWarnings': this.emulatorConfig_.disableWarnings,
}),
}));
}
/**
* @param {string} frameworkId The framework identifier.
*/
fireauth.Auth.prototype.logFramework = function(frameworkId) {
// Theoretically multiple frameworks could be used
// (angularfire and FirebaseUI). Once a framework is used, it is not going
// to be unused, so no point adding a method to remove the framework ID.
this.frameworks_.push(frameworkId);
// 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);
this.dispatchEvent(new fireauth.Auth.FrameworkChangeEvent(
this.frameworks_));
};
/** @return {!Array<string>} The current Firebase frameworks. */
fireauth.Auth.prototype.getFramework = function() {
return goog.array.clone(this.frameworks_);
};
/**
* Updates the framework list on the provided user and configures the user to
* listen to the Auth instance for any framework ID changes.
* @param {!fireauth.AuthUser} user The user to whose framework list needs to be
* updated.
* @private
*/
fireauth.Auth.prototype.setUserFramework_ = function(user) {
// Sets the framework ID on the user.
user.setFramework(this.frameworks_);
// Sets current Auth instance as framework list change dispatcher on the user.
user.setFrameworkChangeDispatcher(this);
};
/**
* Sets the tenant ID.
* @param {?string} tenantId The tenant ID of the tenant project if available.
*/
fireauth.Auth.prototype.setTenantId = function(tenantId) {
// Don't do anything if no change detected.
if (this.tenantId_ !== tenantId && !this.deleted_) {
this.tenantId_ = tenantId;
this.rpcHandler_.updateTenantId(this.tenantId_);
}
};
/**
* Returns the current Auth instance's tenant ID.
* @return {?string}
*/
fireauth.Auth.prototype.getTenantId = function() {
return this.tenantId_;
};
/**
* Initializes readable/writable properties on Auth.
* @suppress {invalidCasts}
* @private
*/
fireauth.Auth.prototype.initializeReadableWritableProps_ = function() {
Object.defineProperty(/** @type {!Object} */ (this), 'lc', {
/**
* @this {!Object}
* @return {?string} The current language code.
*/
get: function() {
return this.getLanguageCode();
},
/**
* @this {!Object}
* @param {string} value The new language code.
*/
set: function(value) {
this.setLanguageCode(value);
},
enumerable: false
});
// Initialize to null.
/** @private {?string} The current Auth instance's language code. */
this.languageCode_ = null;
// Initialize tenant ID.
Object.defineProperty(/** @type {!Object} */ (this), 'ti', {
/**
* @this {!Object}
* @return {?string} The current tenant ID.
*/
get: function() {
return this.getTenantId();
},
/**
* @this {!Object}
* @param {?string} value The new tenant ID.
*/
set: function(value) {
this.setTenantId(value);
},
enumerable: false
});
// Initialize to null.
/** @private {?string} The current Auth instance's tenant ID. */
this.tenantId_ = null;
// Add the emulator configuration property (readonly).
Object.defineProperty(/** @type {!Object} */ (this), 'emulatorConfig', {
/**
* @this {!Object}
* @return {?fireauth.constants.EmulatorConfig} The emulator config if
* enabled.
*/
get: function() {
return this.getEmulatorConfig();
},
enumerable: false
});
};
/**
* Notifies all external listeners of the language code change.
* @private
*/
fireauth.Auth.prototype.notifyLanguageCodeListeners_ = function() {
// Notify external listeners on the language code change.
this.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent(
this.getLanguageCode()));
};
/**
* Notifies all external listeners of the emulator config change.
* @private
*/
fireauth.Auth.prototype.notifyEmulatorConfigListeners_ = function() {
// Notify external listeners on the emulator config change.
this.dispatchEvent(
new fireauth.Auth.EmulatorConfigChangeEvent(this.emulatorConfig_));
}
/**
* @return {!Object} The object representation of the Auth instance.
* @override
*/
fireauth.Auth.prototype.toJSON = function() {
// Return the plain object representation in case JSON.stringify is called on
// an Auth instance.
return {
'apiKey': this.app_().options['apiKey'],
'authDomain': this.app_().options['authDomain'],
'appName': this.app_().name,
'currentUser': this.currentUser_() && this.currentUser_().toPlainObject()
};
};
/**
* Returns the Auth event manager promise.
* @return {!goog.Promise<!fireauth.AuthEventManager>}
* @private
*/
fireauth.Auth.prototype.getAuthEventManager_ = function() {
// Either return cached Auth event manager promise provider if available or a
// promise that rejects with missing Auth domain error.
return this.eventManagerProviderPromise_ ||
goog.Promise.reject(
new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN));
};
/**
* Initializes the Auth event manager when state is ready.
* @private
*/
fireauth.Auth.prototype.initAuthEventManager_ = function() {
// Initialize Auth event manager on initState.
var self = this;
var authDomain = this.app_().options['authDomain'];
var apiKey = this.app_().options['apiKey'];
// Make sure environment also supports popup and redirect.
if (authDomain && fireauth.util.isPopupRedirectSupported()) {
// Auth domain is required for Auth event manager to resolve.
// Auth state has to be loaded first. One reason is to process link events.
this.eventManagerProviderPromise_ = this.authStateLoaded_.then(function() {
if (self.deleted_) {
return;
}
// By this time currentUser should be ready if available and will be able
// to resolve linkWithRedirect if detected.
self.authEventManager_ = fireauth.AuthEventManager.getManager(
authDomain,
apiKey,
self.app_().name,
self.emulatorConfig_);
// Subscribe Auth instance.
self.authEventManager_.subscribe(self);
// Subscribe current user by enabling popup and redirect on that user.
if (self.currentUser_()) {
self.currentUser_().enablePopupRedirect();
}
// If a redirect user is present, subscribe to popup and redirect events.
// In case current user was not available and the developer called link
// with redirect on a signed out user, this will work and the linked
// logged out user will be returned in getRedirectResult.
// current user and redirect user are the same (was already logged in),
// currentUser will have priority as it is subscribed before redirect
// user. This change will also allow further popup and redirect events on
// the redirect user going forward.
if (self.redirectUser_) {
self.redirectUser_.enablePopupRedirect();
// Set the user language for the redirect user.
self.setUserLanguage_(
/** @type {!fireauth.AuthUser} */ (self.redirectUser_));
// Set the user Firebase frameworks for the redirect user.
self.setUserFramework_(
/** @type {!fireauth.AuthUser} */(self.redirectUser_));
// Set the user Emulator configuration for the redirect user.
self.setUserEmulatorConfig_(
/** @type {!fireauth.AuthUser} */(self.redirectUser_));
// Reference to redirect user no longer needed.
self.redirectUser_ = null;
}
return self.authEventManager_;
});
}
};
/**
* @param {!fireauth.AuthEvent.Type} mode The Auth type mode.
* @param {?string=} opt_eventId The event ID.
* @return {boolean} Whether the auth event handler can handler the provided
* event.
* @override
*/
fireauth.Auth.prototype.canHandleAuthEvent = function(mode, opt_eventId) {
// Only sign in events are handled.
switch (mode) {
// Accept all general sign in with redirect and unknowns.
// Migrating redirect events to use session storage will prevent this event
// from leaking to other tabs.
case fireauth.AuthEvent.Type.UNKNOWN:
case fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT:
return true;
case fireauth.AuthEvent.Type. SIGN_IN_VIA_POPUP:
// Pending sign in with popup event must match the stored popup event ID.
return this.popupEventId_ == opt_eventId &&
!!this.pendingPopupResolvePromise_;
default:
return false;
}
};
/**
* Completes the pending popup operation. If error is not null, rejects with the
* error. Otherwise, it resolves with the popup redirect result.
* @param {!fireauth.AuthEvent.Type} mode The Auth type mode.
* @param {?fireauth.AuthEventManager.Result} popupRedirectResult The result
* to resolve with when no error supplied.
* @param {?fireauth.AuthError} error When supplied, the promise will reject.
* @param {?string=} opt_eventId The event ID.
* @override
*/
fireauth.Auth.prototype.resolvePendingPopupEvent =
function(mode, popupRedirectResult, error, opt_eventId) {
// Only handles popup events of type sign in and which match popup event ID.
if (mode != fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP ||
this.popupEventId_ != opt_eventId) {
return;
}
if (error && this.pendingPopupRejectPromise_) {
// Reject with error for supplied mode.
this.pendingPopupRejectPromise_(error);
} else if (popupRedirectResult &&
!error &&
this.pendingPopupResolvePromise_) {
// Resolve with result for supplied mode.
this.pendingPopupResolvePromise_(popupRedirectResult);
}
// Now that event is resolved, delete popup timeout promise.
if (this.popupTimeoutPromise_) {
this.popupTimeoutPromise_.cancel();
this.popupTimeoutPromise_ = null;
}
// Delete pending promises.
delete this.pendingPopupResolvePromise_;
delete this.pendingPopupRejectPromise_;
};
/**
* Returns the handler's appropriate popup and redirect sign in operation
* finisher.
* @param {!fireauth.AuthEvent.Type} mode The Auth type mode.
* @param {?string=} opt_eventId The optional event ID.
* @return {?function(string, string, ?string,
* ?string=):!goog.Promise<!fireauth.AuthEventManager.Result>}
* @override
*/
fireauth.Auth.prototype.getAuthEventHandlerFinisher =
function(mode, opt_eventId) {
// Sign in events will be completed by finishPopupAndRedirectSignIn.
if (mode == fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT) {
return goog.bind(this.finishPopupAndRedirectSignIn, this);
} else if (mode == fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP &&
this.popupEventId_ == opt_eventId &&
this.pendingPopupResolvePromise_) {
return goog.bind(this.finishPopupAndRedirectSignIn, this);
}
return null;
};
/**
* Finishes the popup and redirect sign in operations.
* @param {string} requestUri The callback url with the oauth response.
* @param {string} sessionId The session id used to generate the authUri.
* @param {?string} tenantId The tenant ID.
* @param {?string=} opt_postBody The optional POST body content.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
*/
fireauth.Auth.prototype.finishPopupAndRedirectSignIn =
function(requestUri, sessionId, tenantId, opt_postBody) {
var self = this;
// Verify assertion request.
var request = {
'requestUri': requestUri,
'postBody': opt_postBody,
'sessionId': sessionId,
// Even if tenant ID is null, still pass it to RPC handler explicitly so
// that it won't be overridden by RPC handler's tenant ID.
'tenantId': tenantId
};
// Now that popup has responded, delete popup timeout promise.
if (this.popupTimeoutPromise_) {
this.popupTimeoutPromise_.cancel();
this.popupTimeoutPromise_ = null;
}
// When state is ready, run verify assertion request.
// This will only run either after initial and redirect state is ready for
// popups or after initial state is ready for redirect resolution.
return self.authStateLoaded_.then(function() {
return self.signInWithIdTokenProvider_(
self.rpcHandler_.verifyAssertion(request));
});
};
/**
* @return {string} The generated event ID used to identify a popup event.
* @private
*/
fireauth.Auth.prototype.generateEventId_ = function() {
return fireauth.util.generateEventId();
};
/**
* Signs in to Auth provider via popup.
* @param {!fireauth.AuthProvider} provider The Auth provider to sign in with.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
*/
fireauth.Auth.prototype.signInWithPopup = function(provider) {
// Check if popup and redirect are supported in this environment.
if (!fireauth.util.isPopupRedirectSupported()) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.OPERATION_NOT_SUPPORTED));
}
var mode = fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP;
var self = this;
// Popup the window immediately to make sure the browser associates the
// popup with the click that triggered it.
// Get provider settings.
var settings = fireauth.idp.getIdpSettings(provider['providerId']);
// There could multiple sign in with popup events in different windows.
// We need to match the correct popup to the correct pending promise.
var eventId = this.generateEventId_();
// If incapable of redirecting popup from opener, popup destination URL
// directly. This could also happen in a sandboxed iframe.
var oauthHelperWidgetUrl = null;
if ((!fireauth.util.runsInBackground() || fireauth.util.isIframe()) &&
this.app_().options['authDomain'] &&
provider['isOAuthProvider']) {
oauthHelperWidgetUrl =
fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl(
this.app_().options['authDomain'],
this.app_().options['apiKey'],
this.app_().name,
mode,
provider,
null,
eventId,
firebase.SDK_VERSION || null,
null,
null,
this.getTenantId(),
this.emulatorConfig_);
}
// The popup must have a name, otherwise when successive popups are triggered
// they will all render in the same instance and none will succeed since the
// popup cancel of first window will close the shared popup window instance.
var popupWin =
fireauth.util.popup(
oauthHelperWidgetUrl,
fireauth.util.generateRandomString(),
settings && settings.popupWidth,
settings && settings.popupHeight);
// Auth event manager must be available for popup sign in to be possible.
var p = this.getAuthEventManager_().then(function(manager) {
// Process popup request tagging it with newly created event ID.
return manager.processPopup(
popupWin, mode, provider, eventId, !!oauthHelperWidgetUrl,
self.getTenantId());
}).then(function() {
return new goog.Promise(function(resolve, reject) {
// Expire other pending promises if still available..
self.resolvePendingPopupEvent(
mode,
null,
new fireauth.AuthError(fireauth.authenum.Error.EXPIRED_POPUP_REQUEST),
// Existing pending popup event ID.
self.popupEventId_);
// Save current pending promises.
self.pendingPopupResolvePromise_ = resolve;
self.pendingPopupRejectPromise_ = reject;
// Overwrite popup event ID with new one corresponding to popup.
self.popupEventId_ = eventId;
// Keep track of timeout promise to cancel it on promise resolution before
// it times out.
self.popupTimeoutPromise_ =
self.authEventManager_.startPopupTimeout(
self, mode, /** @type {!Window} */ (popupWin), eventId);
});
}).then(function(result) {
// On resolution, close popup if still opened and pass result through.
if (popupWin) {
fireauth.util.closeWindow(popupWin);
}
if (result) {
return fireauth.object.makeReadonlyCopy(result);
}
return null;
}).thenCatch(function(error) {
if (popupWin) {
fireauth.util.closeWindow(popupWin);
}
throw error;
});
return /** @type {!goog.Promise<!fireauth.AuthEventManager.Result>} */ (
this.registerPendingPromise_(p));
};
/**
* Signs in to Auth provider via redirect.
* @param {!fireauth.AuthProvider} provider The Auth provider to sign in with.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.signInWithRedirect = function(provider) {
// Check if popup and redirect are supported in this environment.
if (!fireauth.util.isPopupRedirectSupported()) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.OPERATION_NOT_SUPPORTED));
}
var self = this;
var mode = fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT;
// Auth event manager must be available for sign in via redirect to be
// possible.
var p = this.getAuthEventManager_().then(function(manager) {
// Remember current persistence to apply it on the next page.
// This is the only time the state is passed to the next page (when user is
// not already logged in).
// This is not needed for link and reauthenticate as the user is already
// stored with specified persistence.
return self.userStorageManager_.savePersistenceForRedirect();
}).then(function() {
// Process redirect operation.
return self.authEventManager_.processRedirect(
mode, provider, undefined, self.getTenantId());
});
return /** @type {!goog.Promise<void>} */ (this.registerPendingPromise_(p));
};
/**
* Returns the redirect result. If coming back from a successful redirect sign
* in, will resolve to the signed in user. If coming back from an unsuccessful
* redirect sign, will reject with the proper error. If no redirect operation
* called, resolves with null.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
* @private
*/
fireauth.Auth.prototype.getRedirectResultWithoutClearing_ = function() {
// Check if popup and redirect are supported in this environment.
if (!fireauth.util.isPopupRedirectSupported()) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.OPERATION_NOT_SUPPORTED));
}
var self = this;
// Auth event manager must be available for get redirect result to be
// possible.
var p = this.getAuthEventManager_().then(function(manager) {
// Return redirect result when resolved.
return self.authEventManager_.getRedirectResult();
}).then(function(result) {
if (result) {
return fireauth.object.makeReadonlyCopy(result);
}
return null;
});
return /** @type {!goog.Promise<!fireauth.AuthEventManager.Result>} */ (
this.registerPendingPromise_(p));
};
/**
* In addition to returning the redirect result as in
* `getRedirectResultWithoutClearing_`, this will also clear the cached
* redirect result for security reasons.
* @return {!goog.Promise<!fireauth.AuthEventManager.Result>}
*/
fireauth.Auth.prototype.getRedirectResult = function() {
return this.getRedirectResultWithoutClearing_()
.then((result) => {
if (this.authEventManager_) {
this.authEventManager_.clearRedirectResult();
}
return result;
})
.thenCatch((error) => {
if (this.authEventManager_) {
this.authEventManager_.clearRedirectResult();
}
throw error;
});
};
/**
* Asynchronously sets the provided user as currentUser on the current Auth
* instance.
* @param {?fireauth.AuthUser} user The user to be copied to Auth instance.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.updateCurrentUser = function(user) {
if (!user) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.NULL_USER));
}
if (this.tenantId_ != user['tenantId']) {
return goog.Promise.reject(new fireauth.AuthError(
fireauth.authenum.Error.TENANT_ID_MISMATCH));
}
var self = this;
var options = {};
options['apiKey'] = this.app_().options['apiKey'];
options['authDomain'] = this.app_().options['authDomain'];
options['appName'] = this.app_().name;
var newUser = fireauth.AuthUser.copyUser(user, options,
self.redirectUserStorageManager_, self.getFramework());
return this.registerPendingPromise_(
this.redirectStateIsReady_.then(function() {
if (self.app_().options['apiKey'] != user.getApiKey()) {
// Throws auth/invalid-user-token if user doesn't belong to app.
// Throws auth/user-token-expired if token expires.
return newUser.reload();
}
}).then(function() {
if (self.currentUser_() && user['uid'] == self.currentUser_()['uid']) {
// Same user signed in. Update user data and notify Auth listeners.
// No need to resubscribe to user events.
// TODO: Check if the user to copy is older than current user and skip
// the copy logic in that case.
self.currentUser_().copy(user);
return self.handleUserStateChange_(user);
}
self.setCurrentUser_(newUser);
// Enable popup and redirect events.
newUser.enablePopupRedirect();
// Save user changes.
return self.handleUserStateChange_(newUser);
}).then(function(user) {
self.notifyAuthListeners_();
}));
};
/**
* Completes the headless sign in with the server response containing the STS
* access and refresh tokens, and sets the Auth user as current user while
* setting all listeners to it and saving it to storage.
* @param {!Object<string, string>} idTokenResponse The ID token response from
* the server.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.signInWithIdTokenResponse =
function(idTokenResponse) {
var self = this;
var options = {};
options['apiKey'] = self.app_().options['apiKey'];
options['authDomain'] = self.app_().options['authDomain'];
options['appName'] = self.app_().name;
if (self.emulatorConfig_) {
options['emulatorConfig'] = self.emulatorConfig_;
}
// Wait for state to be ready.
// This is used internally and is also used for redirect sign in so there is
// no need for waiting for redirect result to resolve since redirect result
// depends on it.
return this.authStateLoaded_.then(function() {
// Initialize an Auth user using the provided ID token response.
return fireauth.AuthUser.initializeFromIdTokenResponse(
options,
idTokenResponse,
/** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_),
// Pass frameworks so they are logged in getAccountInfo while populating
// the user info.
self.getFramework());
}).then(function(user) {
// Check if the same user is already signed in.
if (self.currentUser_() &&
user['uid'] == self.currentUser_()['uid']) {
// Same user signed in. Update user data and notify Auth listeners.
// No need to resubscribe to user events.
self.currentUser_().copy(user);
return self.handleUserStateChange_(user);
}
// New user.
// Set current user and attach all listeners to it.
self.setCurrentUser_(user);
// Enable popup and redirect events.
user.enablePopupRedirect();
// Save user changes.
return self.handleUserStateChange_(user);
}).then(function() {
// Notify external Auth listeners only when state is ready.
self.notifyAuthListeners_();
});
};
/**
* Updates the current auth user and attaches event listeners to changes on it.
* Also removes all event listeners from previously signed in user.
* @param {?fireauth.AuthUser} user The current user instance.
* @private
*/
fireauth.Auth.prototype.setCurrentUser_ = function(user) {
// Must be called first before updating currentUser reference.
this.attachEventListeners_(user);
// Update currentUser property.
fireauth.object.setReadonlyProperty(this, 'currentUser', user);
if (user) {
// If a user is available, set the language code on it and set current Auth
// instance as language code change dispatcher.
this.setUserLanguage_(user);
// Set the current frameworks used on the user and set current Auth instance
// as the framework change dispatcher.
this.setUserFramework_(user);
// If a user is available, set the emulator config on it and set current
// Auth instance as emulator config change dispatcher.
this.setUserEmulatorConfig_(user);
}
};
/**
* Signs out the current user while deleting the Auth user from storage and
* removing all listeners from it.
* @return {!goog.Promise<void>}
*/
fireauth.Auth.prototype.signOut = function() {
var self = this;
// Wait for final state to be ready first, otherwise a signed out user could
// come back to life.
var p = this.redirectStateIsReady_.then(function() {
// Clear any cached redirect result on sign out, even if user is already
// signed out. For example, sign in could fail due to account conflict
// error, the error in redirect result should still be cleared. There is
// also the use case where you keep a reference to a signed out user and
// call signedOutUser.linkWithRedirect(provider). Even though the user is
// signed out, getRedirectResult() will resolve with the modified signed
// out user. This could also throw an error
// (provider already linked, etc).
if (self.authEventManager_) {
self.authEventManager_.clearRedirectResult();
}
// Ignore if already signed out.
if (!self.currentUser_()) {
return goog.Promise.resolve();
}
// Detach all event listeners.
// Set current user to null.
self.setCurrentUser_(null);
// Remove current user from storage
return /** @type {!fireauth.storage.UserManager} */ (
self.userStorageManager_).removeCurrentUser()
.then(function() {
// Notify external Auth listeners of this Auth change event.
self.notifyAuthListeners_();
});
});
return /** @type {!goog.Promise<void>} */ (this.registerPendingPromise_(p));
};
/**
* @return {!goog.Promise} A promise that resolved when any stored redirect user
* is loaded and removed from session storage and then stored locally.
* @private
*/
fireauth.Auth.prototype.initRedirectUser_ = function() {
var self = this;
var authDomain = this.app_().options['authDomain'];
// Get any saved redirect user and delete from session storage.
// Override user's authDomain with app's authDomain if there is a mismatch.
var p = /** @type {!fireauth.storage.RedirectUserManager} */ (
this.redirectUserStorageManager_).getRedirectUser(authDomain)
.then(function(user) {
// Save redirect user.
self.redirectUser_ = user;
if (user) {
// Set redirect storage manager on user.
user.setRedirectStorageManager(
/** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_));
}
// Delete redirect user.
return /** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_).removeRedirectUser();
});
return /** @type {!goog.Promise<undefined>} */ (
this.registerPendingPromise_(p));
};
/**
* Loads the initial Auth state for current application from web storage and
* initializes Auth user accordingly to reflect that state. This routine does
* not wait for any pending redirect result to be resolved.
* @return {!goog.Promise<undefined>} Promise that resolves when state is ready,
* loaded from storage.
* @private
*/
fireauth.Auth.prototype.initAuthState_ = function() {
// Load current user from storage.
var self = this;
var authDomain = this.app_().options['authDomain'];
// Get any saved redirected user first.
var p = this.initRedirectUser_().then(function() {
// Override user's authDomain with app's authDomain if there is a mismatch.
return /** @type {!fireauth.storage.UserManager} */ (
self.userStorageManager_).getCurrentUser(authDomain, self.emulatorConfig_);
}).then(function(user) {
// Logged in user.
if (user) {
// Set redirect storage manager on user.
user.setRedirectStorageManager(
/** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_));
// If the current user is undergoing a redirect operation, do not reload
// as that could could potentially delete the user if the token is
// expired. Instead any token problems will be detected via the
// verifyAssertion flow or the remaining flow. This is critical for
// reauthenticateWithRedirect as this flow is potentially used to recover
// from a token expiration error.
if (self.redirectUser_ &&
self.redirectUser_.getRedirectEventId() ==
user.getRedirectEventId()) {
return user;
}
// Confirm user valid first before setting listeners.
return user.reload().then(function() {
// Force user saving after reload as state change listeners not
// subscribed yet below via setCurrentUser_. Changes may have happened
// externally such as email actions or changes on another device.
return self.userStorageManager_.setCurrentUser(user).then(function() {
return user;
});
}).thenCatch(function(error) {
if (error['code'] == 'auth/network-request-failed') {
// Do not delete the user from storage if connection is lost or app is
// offline.
return user;
}
// Invalid user, could be deleted, remove from storage and resolve with
// null.
return /** @type {!fireauth.storage.UserManager} */(
self.userStorageManager_).removeCurrentUser();
});
}
// No logged in user, resolve with null;
return null;
}).then(function(user) {
// Even though state not ready yet pending any redirect result.
// Current user needs to be available for link with redirect to complete.
// This will also set listener on the user changes in case state changes
// occur they would get updated in storage too.
self.setCurrentUser_(user || null);
});
// In case the app is deleted before it is initialized with state from
// storage.
return /** @type {!goog.Promise<undefined>} */ (
this.registerPendingPromise_(p));
};
/**
* After initial Auth state is loaded, waits for any pending redirect result,
* resolves it and then adds the external Auth state change listeners and
* triggers first state of all observers.
* @return {!goog.Promise<undefined>} Promise that resolves when state is ready
* taking into account any pending redirect result.
* @private
*/
fireauth.Auth.prototype.initAuthRedirectState_ = function() {
var self = this;
// Wait first for state to be loaded from storage.
return this.authStateLoaded_.then(function() {
// Resolve any pending redirect result.
return self.getRedirectResultWithoutClearing_();
}).thenCatch(function(error) {
// Ignore any error in the process. Redirect could be not supported.
return;
}).then(function() {
// Make sure instance was not deleted before proceeding.
if (self.deleted_) {
return;
}
// Between init Auth state and get redirect result resolution there
// could have been a sign in attempt in another window.
// Force sync and then add listener to run sync on change below.
return self.getSyncAuthUserChanges_();
}).thenCatch(function(error) {
// Ignore any error in the process.
return;
}).then(function() {
// Now that final state is ready, make sure instance was not deleted before
// proceeding.
if (self.deleted_) {
return;
}
// Initial state has been resolved.
self.isStateResolved_ = true;
// Add user state change listener so changes are synchronized with
// other windows and tabs.
/** @type {!fireauth.storage.UserManager} */ (self.userStorageManager_
).addCurrentUserChangeListener(self.getSyncAuthUserChanges_);
});
};
/**
* Synchronizes current Auth to stored auth state, used when external state
* changes occur.
* @return {!goog.Promise<void>}
* @private
*/
fireauth.Auth.prototype.syncAuthUserChanges_ = function() {
// Get Auth user state from storage and compare to current state.
// Safe to run when no external change is detected.
var self = this;
var authDomain = this.app_().options['authDomain'];
// Override user's authDomain with app's authDomain if there is a mismatch.
return /** @type {!fireauth.storage.UserManager} */ (
this.userStorageManager_).getCurrentUser(authDomain)
.then(function(user) {
// In case this was deleted.
if (self.deleted_) {
return;
}
// Since the authDomain could be modified here, saving to storage here
// could trigger an infinite loop of changes between this tab and
// another tab using different Auth domain but since sync Auth user
// changes does not save changes to storage, except for getToken below
// if the token needs refreshing but will stop triggering the first time
// the token is refreshed on one of the first tab that refreshes it.
// The latter should not happen anyway since getToken should be valid
// at all times since anything that triggers the storage change should
// have communicated with the backend and that requires a valid token.
// In addition, authDomain difference is an edge case to begin with.
// If the same user is to be synchronized.
if (self.currentUser_() &&
user &&
self.currentUser_().hasSameUserIdAs(user)) {
// Data update, simply copy data changes.
self.currentUser_().copy(user);
// If tokens changed from previous user tokens, this will trigger
// notifyAuthListeners_.
return self.currentUser_().getIdToken();
} else if (!self.currentUser_() && !user) {
// No change, do nothing (was signed out and remained signed out).
return;
} else {
// Update current Auth state. Either a new login or logout.
self.setCurrentUser_(user);
// If a new user is signed in, enabled popup and redirect on that
// user.
if (user) {
user.enablePopupRedirect();
// Set redirect storage manager on user.
user.setRedirectStorageManager(
/** @type {!fireauth.storage.RedirectUserManager} */ (
self.redirectUserStorageManager_));
}
if (self.authEventManager_) {
self.authEventManager_.subscribe(self);
}
// Notify external Auth changes of Auth change event.
self.notifyAuthListeners_();
}
});
};
/**
* Updates the language code on the provided user and configures the user to
* listen to the Auth instance for any language code changes.
* @param {!fireauth.AuthUser} user The user to whose language needs to be set.
* @private
*/
fireauth.Auth.prototype.setUserLanguage_ = function(user) {
// Sets the current language code on the user.
user.setLanguageCode(this.getLanguageCode());
// Sets current Auth instance as language code change dispatcher on the user.
user.setLanguageCodeChangeDispatcher(this);
};
/**
* Updates the emulator config on the provided user and configures the user
* to listen to the Auth instance for any emulator config changes.
* @param {!fireauth.AuthUser} user The user to whose emulator config needs
* to be set.
* @private
*/
fireauth.Auth.prototype.setUserEmulatorConfig_ = function(user) {
// Sets the current emulator config on the user.
user.setEmulatorConfig(this.emulatorConfig_);
// Sets current Auth instance as emulator config change dispatcher on the
// user.
user.setEmulatorConfigChangeDispatcher(this);
}
/**
* Handles user state changes.
* @param {!fireauth.AuthUser} user The user which triggered the state changes.
* @return {!goog.Promise} The promise that resolves when state changes are
* handled.
* @private
*/
fireauth.Auth.prototype.handleUserStateChange_ = function(user) {
// Save Auth user state changes.
return /** @type {!fireauth.storage.UserManager} */ (
this.userStorageManag