voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
331 lines (291 loc) • 10.2 kB
text/typescript
/**
* @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.
*/
import { IdTokenResult, ProviderId } from '../../model/public_types';
import { NextFn } from '@firebase/util';
import {
APIUserInfo,
deleteAccount
} from '../../api/account_management/account';
import { FinalizeMfaResponse } from '../../api/authentication/mfa';
import { AuthInternal } from '../../model/auth';
import { IdTokenResponse } from '../../model/id_token';
import {
MutableUserInfo,
UserInternal,
UserParameters
} from '../../model/user';
import { AuthErrorCode } from '../errors';
import { PersistedBlob } from '../persistence';
import { _assert } from '../util/assert';
import { getIdTokenResult } from './id_token_result';
import { _logoutIfInvalidated } from './invalidation';
import { ProactiveRefresh } from './proactive_refresh';
import { _reloadWithoutSaving, reload } from './reload';
import { StsTokenManager } from './token_manager';
import { UserMetadata } from './user_metadata';
function assertStringOrUndefined(
assertion: unknown,
appName: string
): asserts assertion is string | undefined {
_assert(
typeof assertion === 'string' || typeof assertion === 'undefined',
AuthErrorCode.INTERNAL_ERROR,
{ appName }
);
}
export class UserImpl implements UserInternal {
// For the user object, provider is always Firebase.
readonly providerId = ProviderId.FIREBASE;
stsTokenManager: StsTokenManager;
// Last known accessToken so we know when it changes
private accessToken: string | null;
uid: string;
auth: AuthInternal;
emailVerified = false;
isAnonymous = false;
tenantId: string | null = null;
readonly metadata: UserMetadata;
providerData: MutableUserInfo[] = [];
// Optional fields from UserInfo
displayName: string | null;
email: string | null;
phoneNumber: string | null;
photoURL: string | null;
_redirectEventId?: string;
private readonly proactiveRefresh = new ProactiveRefresh(this);
constructor({ uid, auth, stsTokenManager, ...opt }: UserParameters) {
this.uid = uid;
this.auth = auth;
this.stsTokenManager = stsTokenManager;
this.accessToken = stsTokenManager.accessToken;
this.displayName = opt.displayName || null;
this.email = opt.email || null;
this.phoneNumber = opt.phoneNumber || null;
this.photoURL = opt.photoURL || null;
this.isAnonymous = opt.isAnonymous || false;
this.metadata = new UserMetadata(
opt.createdAt || undefined,
opt.lastLoginAt || undefined
);
}
async getIdToken(forceRefresh?: boolean): Promise<string> {
const accessToken = await _logoutIfInvalidated(
this,
this.stsTokenManager.getToken(this.auth, forceRefresh)
);
_assert(accessToken, this.auth, AuthErrorCode.INTERNAL_ERROR);
if (this.accessToken !== accessToken) {
this.accessToken = accessToken;
await this.auth._persistUserIfCurrent(this);
this.auth._notifyListenersIfCurrent(this);
}
return accessToken;
}
getIdTokenResult(forceRefresh?: boolean): Promise<IdTokenResult> {
return getIdTokenResult(this, forceRefresh);
}
reload(): Promise<void> {
return reload(this);
}
private reloadUserInfo: APIUserInfo | null = null;
private reloadListener: NextFn<APIUserInfo> | null = null;
_assign(user: UserInternal): void {
if (this === user) {
return;
}
_assert(this.uid === user.uid, this.auth, AuthErrorCode.INTERNAL_ERROR);
this.displayName = user.displayName;
this.photoURL = user.photoURL;
this.email = user.email;
this.emailVerified = user.emailVerified;
this.phoneNumber = user.phoneNumber;
this.isAnonymous = user.isAnonymous;
this.tenantId = user.tenantId;
this.providerData = user.providerData.map(userInfo => ({ ...userInfo }));
this.metadata._copy(user.metadata);
this.stsTokenManager._assign(user.stsTokenManager);
}
_clone(auth: AuthInternal): UserInternal {
return new UserImpl({
...this,
auth,
stsTokenManager: this.stsTokenManager._clone()
});
}
_onReload(callback: NextFn<APIUserInfo>): void {
// There should only ever be one listener, and that is a single instance of MultiFactorUser
_assert(!this.reloadListener, this.auth, AuthErrorCode.INTERNAL_ERROR);
this.reloadListener = callback;
if (this.reloadUserInfo) {
this._notifyReloadListener(this.reloadUserInfo);
this.reloadUserInfo = null;
}
}
_notifyReloadListener(userInfo: APIUserInfo): void {
if (this.reloadListener) {
this.reloadListener(userInfo);
} else {
// If no listener is subscribed yet, save the result so it's available when they do subscribe
this.reloadUserInfo = userInfo;
}
}
_startProactiveRefresh(): void {
this.proactiveRefresh._start();
}
_stopProactiveRefresh(): void {
this.proactiveRefresh._stop();
}
async _updateTokensIfNecessary(
response: IdTokenResponse | FinalizeMfaResponse,
reload = false
): Promise<void> {
let tokensRefreshed = false;
if (
response.idToken &&
response.idToken !== this.stsTokenManager.accessToken
) {
this.stsTokenManager.updateFromServerResponse(response);
tokensRefreshed = true;
}
if (reload) {
await _reloadWithoutSaving(this);
}
await this.auth._persistUserIfCurrent(this);
if (tokensRefreshed) {
this.auth._notifyListenersIfCurrent(this);
}
}
async delete(): Promise<void> {
const idToken = await this.getIdToken();
await _logoutIfInvalidated(this, deleteAccount(this.auth, { idToken }));
this.stsTokenManager.clearRefreshToken();
// TODO: Determine if cancellable-promises are necessary to use in this class so that delete()
// cancels pending actions...
return this.auth.signOut();
}
toJSON(): PersistedBlob {
return {
uid: this.uid,
email: this.email || undefined,
emailVerified: this.emailVerified,
displayName: this.displayName || undefined,
isAnonymous: this.isAnonymous,
photoURL: this.photoURL || undefined,
phoneNumber: this.phoneNumber || undefined,
tenantId: this.tenantId || undefined,
providerData: this.providerData.map(userInfo => ({ ...userInfo })),
stsTokenManager: this.stsTokenManager.toJSON(),
// Redirect event ID must be maintained in case there is a pending
// redirect event.
_redirectEventId: this._redirectEventId,
...this.metadata.toJSON(),
// Required for compatibility with the legacy SDK (go/firebase-auth-sdk-persistence-parsing):
apiKey: this.auth.config.apiKey,
appName: this.auth.name
// Missing authDomain will be tolerated by the legacy SDK.
// stsTokenManager.apiKey isn't actually required (despite the legacy SDK persisting it).
};
}
get refreshToken(): string {
return this.stsTokenManager.refreshToken || '';
}
static _fromJSON(auth: AuthInternal, object: PersistedBlob): UserInternal {
const displayName = object.displayName ?? undefined;
const email = object.email ?? undefined;
const phoneNumber = object.phoneNumber ?? undefined;
const photoURL = object.photoURL ?? undefined;
const tenantId = object.tenantId ?? undefined;
const _redirectEventId = object._redirectEventId ?? undefined;
const createdAt = object.createdAt ?? undefined;
const lastLoginAt = object.lastLoginAt ?? undefined;
const {
uid,
emailVerified,
isAnonymous,
providerData,
stsTokenManager: plainObjectTokenManager
} = object;
_assert(uid && plainObjectTokenManager, auth, AuthErrorCode.INTERNAL_ERROR);
const stsTokenManager = StsTokenManager.fromJSON(
this.name,
plainObjectTokenManager as PersistedBlob
);
_assert(typeof uid === 'string', auth, AuthErrorCode.INTERNAL_ERROR);
assertStringOrUndefined(displayName, auth.name);
assertStringOrUndefined(email, auth.name);
_assert(
typeof emailVerified === 'boolean',
auth,
AuthErrorCode.INTERNAL_ERROR
);
_assert(
typeof isAnonymous === 'boolean',
auth,
AuthErrorCode.INTERNAL_ERROR
);
assertStringOrUndefined(phoneNumber, auth.name);
assertStringOrUndefined(photoURL, auth.name);
assertStringOrUndefined(tenantId, auth.name);
assertStringOrUndefined(_redirectEventId, auth.name);
assertStringOrUndefined(createdAt, auth.name);
assertStringOrUndefined(lastLoginAt, auth.name);
const user = new UserImpl({
uid,
auth,
email,
emailVerified,
displayName,
isAnonymous,
photoURL,
phoneNumber,
tenantId,
stsTokenManager,
createdAt,
lastLoginAt
});
if (providerData && Array.isArray(providerData)) {
user.providerData = providerData.map(userInfo => ({ ...userInfo }));
}
if (_redirectEventId) {
user._redirectEventId = _redirectEventId;
}
return user;
}
/**
* Initialize a User from an idToken server response
* @param auth
* @param idTokenResponse
*/
static async _fromIdTokenResponse(
auth: AuthInternal,
idTokenResponse: IdTokenResponse,
isAnonymous: boolean = false
): Promise<UserInternal> {
const stsTokenManager = new StsTokenManager();
stsTokenManager.updateFromServerResponse(idTokenResponse);
// Initialize the Firebase Auth user.
const user = new UserImpl({
uid: idTokenResponse.localId,
auth,
stsTokenManager,
isAnonymous
});
// Updates the user info and data and resolves with a user instance.
await _reloadWithoutSaving(user);
return user;
}
}