firebase-auth-cloudflare-workers
Version:
Zero-dependencies firebase auth library for Cloudflare Workers.
599 lines (598 loc) • 20.4 kB
JavaScript
import { AuthClientErrorCode, FirebaseAuthError } from './errors';
import { isNonNullObject } from './validator';
/**
* 'REDACTED', encoded as a base64 string.
*/
const B64_REDACTED = 'UkVEQUNURUQ='; // Buffer.from('REDACTED').toString('base64');
/**
* Parses a time stamp string or number and returns the corresponding date if valid.
*
* @param time - The unix timestamp string or number in milliseconds.
* @returns The corresponding date as a UTC string, if valid. Otherwise, null.
*/
function parseDate(time) {
try {
const date = new Date(parseInt(time, 10));
if (!isNaN(date.getTime())) {
return date.toUTCString();
}
}
catch (e) {
// Do nothing. null will be returned.
}
return null;
}
var MultiFactorId;
(function (MultiFactorId) {
MultiFactorId["Phone"] = "phone";
MultiFactorId["Totp"] = "totp";
})(MultiFactorId || (MultiFactorId = {}));
/**
* Interface representing the common properties of a user-enrolled second factor.
*/
export class MultiFactorInfo {
/**
* The ID of the enrolled second factor. This ID is unique to the user.
*/
uid;
/**
* The optional display name of the enrolled second factor.
*/
displayName;
/**
* The type identifier of the second factor.
* For SMS second factors, this is `phone`.
* For TOTP second factors, this is `totp`.
*/
factorId;
/**
* The optional date the second factor was enrolled, formatted as a UTC string.
*/
enrollmentTime;
/**
* Initializes the MultiFactorInfo associated subclass using the server side.
* If no MultiFactorInfo is associated with the response, null is returned.
*
* @param response - The server side response.
* @internal
*/
static initMultiFactorInfo(response) {
let multiFactorInfo = null;
// PhoneMultiFactorInfo, TotpMultiFactorInfo currently available.
try {
if (response.phoneInfo !== undefined) {
multiFactorInfo = new PhoneMultiFactorInfo(response);
}
else if (response.totpInfo !== undefined) {
multiFactorInfo = new TotpMultiFactorInfo(response);
}
else {
// Ignore the other SDK unsupported MFA factors to prevent blocking developers using the current SDK.
}
}
catch (e) {
// Ignore error.
}
return multiFactorInfo;
}
/**
* Initializes the MultiFactorInfo object using the server side response.
*
* @param response - The server side response.
* @constructor
* @internal
*/
constructor(response) {
this.initFromServerResponse(response);
}
/**
* Returns a JSON-serializable representation of this object.
*
* @returns A JSON-serializable representation of this object.
*/
toJSON() {
return {
uid: this.uid,
displayName: this.displayName,
factorId: this.factorId,
enrollmentTime: this.enrollmentTime,
};
}
/**
* Initializes the MultiFactorInfo object using the provided server response.
*
* @param response - The server side response.
*/
initFromServerResponse(response) {
const factorId = response && this.getFactorId(response);
if (!factorId || !response || !response.mfaEnrollmentId) {
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid multi-factor info response');
}
addReadonlyGetter(this, 'uid', response.mfaEnrollmentId);
addReadonlyGetter(this, 'factorId', factorId);
addReadonlyGetter(this, 'displayName', response.displayName);
// Encoded using [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format.
// For example, "2017-01-15T01:30:15.01Z".
// This can be parsed directly via Date constructor.
// This can be computed using Data.prototype.toISOString.
if (response.enrolledAt) {
addReadonlyGetter(this, 'enrollmentTime', new Date(response.enrolledAt).toUTCString());
}
else {
addReadonlyGetter(this, 'enrollmentTime', null);
}
}
}
/**
* Interface representing a phone specific user-enrolled second factor.
*/
export class PhoneMultiFactorInfo extends MultiFactorInfo {
/**
* The phone number associated with a phone second factor.
*/
phoneNumber;
/**
* Initializes the PhoneMultiFactorInfo object using the server side response.
*
* @param response - The server side response.
* @constructor
* @internal
*/
constructor(response) {
super(response);
addReadonlyGetter(this, 'phoneNumber', response.phoneInfo);
}
/**
* {@inheritdoc MultiFactorInfo.toJSON}
*/
toJSON() {
return Object.assign(super.toJSON(), {
phoneNumber: this.phoneNumber,
});
}
/**
* Returns the factor ID based on the response provided.
*
* @param response - The server side response.
* @returns The multi-factor ID associated with the provided response. If the response is
* not associated with any known multi-factor ID, null is returned.
*
* @internal
*/
getFactorId(response) {
return response && response.phoneInfo ? MultiFactorId.Phone : null;
}
}
/**
* `TotpInfo` struct associated with a second factor
*/
export class TotpInfo {
}
/**
* Interface representing a TOTP specific user-enrolled second factor.
*/
export class TotpMultiFactorInfo extends MultiFactorInfo {
/**
* `TotpInfo` struct associated with a second factor
*/
totpInfo;
/**
* Initializes the `TotpMultiFactorInfo` object using the server side response.
*
* @param response - The server side response.
* @constructor
* @internal
*/
constructor(response) {
super(response);
addReadonlyGetter(this, 'totpInfo', response.totpInfo);
}
/**
* {@inheritdoc MultiFactorInfo.toJSON}
*/
toJSON() {
return Object.assign(super.toJSON(), {
totpInfo: this.totpInfo,
});
}
/**
* Returns the factor ID based on the response provided.
*
* @param response - The server side response.
* @returns The multi-factor ID associated with the provided response. If the response is
* not associated with any known multi-factor ID, `null` is returned.
*
* @internal
*/
getFactorId(response) {
return response && response.totpInfo ? MultiFactorId.Totp : null;
}
}
/**
* The multi-factor related user settings.
*/
export class MultiFactorSettings {
/**
* List of second factors enrolled with the current user.
* Currently only phone and TOTP second factors are supported.
*/
enrolledFactors;
/**
* Initializes the `MultiFactor` object using the server side or JWT format response.
*
* @param response - The server side response.
* @constructor
* @internal
*/
constructor(response) {
const parsedEnrolledFactors = [];
if (!isNonNullObject(response)) {
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid multi-factor response');
}
else if (response.mfaInfo) {
response.mfaInfo.forEach(factorResponse => {
const multiFactorInfo = MultiFactorInfo.initMultiFactorInfo(factorResponse);
if (multiFactorInfo) {
parsedEnrolledFactors.push(multiFactorInfo);
}
});
}
// Make enrolled factors immutable.
addReadonlyGetter(this, 'enrolledFactors', Object.freeze(parsedEnrolledFactors));
}
/**
* Returns a JSON-serializable representation of this multi-factor object.
*
* @returns A JSON-serializable representation of this multi-factor object.
*/
toJSON() {
return {
enrolledFactors: this.enrolledFactors.map(info => info.toJSON()),
};
}
}
/**
* Represents a user's metadata.
*/
export class UserMetadata {
/**
* The date the user was created, formatted as a UTC string.
*/
creationTime;
/**
* The date the user last signed in, formatted as a UTC string.
*/
lastSignInTime;
/**
* The time at which the user was last active (ID token refreshed),
* formatted as a UTC Date string (eg 'Sat, 03 Feb 2001 04:05:06 GMT').
* Returns null if the user was never active.
*/
lastRefreshTime;
/**
* @param response - The server side response returned from the `getAccountInfo`
* endpoint.
* @constructor
* @internal
*/
constructor(response) {
// Creation date should always be available but due to some backend bugs there
// were cases in the past where users did not have creation date properly set.
// This included legacy Firebase migrating project users and some anonymous users.
// These bugs have already been addressed since then.
addReadonlyGetter(this, 'creationTime', parseDate(response.createdAt));
addReadonlyGetter(this, 'lastSignInTime', parseDate(response.lastLoginAt));
const lastRefreshAt = response.lastRefreshAt ? new Date(response.lastRefreshAt).toUTCString() : null;
addReadonlyGetter(this, 'lastRefreshTime', lastRefreshAt);
}
/**
* Returns a JSON-serializable representation of this object.
*
* @returns A JSON-serializable representation of this object.
*/
toJSON() {
return {
lastSignInTime: this.lastSignInTime,
creationTime: this.creationTime,
lastRefreshTime: this.lastRefreshTime,
};
}
}
/**
* Represents a user's info from a third-party identity provider
* such as Google or Facebook.
*/
export class UserInfo {
/**
* The user identifier for the linked provider.
*/
uid;
/**
* The display name for the linked provider.
*/
displayName;
/**
* The email for the linked provider.
*/
email;
/**
* The photo URL for the linked provider.
*/
photoURL;
/**
* The linked provider ID (for example, "google.com" for the Google provider).
*/
providerId;
/**
* The phone number for the linked provider.
*/
phoneNumber;
/**
* @param response - The server side response returned from the `getAccountInfo`
* endpoint.
* @constructor
* @internal
*/
constructor(response) {
// Provider user id and provider id are required.
if (!response.rawId || !response.providerId) {
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid user info response');
}
addReadonlyGetter(this, 'uid', response.rawId);
addReadonlyGetter(this, 'displayName', response.displayName);
addReadonlyGetter(this, 'email', response.email);
addReadonlyGetter(this, 'photoURL', response.photoUrl);
addReadonlyGetter(this, 'providerId', response.providerId);
addReadonlyGetter(this, 'phoneNumber', response.phoneNumber);
}
/**
* Returns a JSON-serializable representation of this object.
*
* @returns A JSON-serializable representation of this object.
*/
toJSON() {
return {
uid: this.uid,
displayName: this.displayName,
email: this.email,
photoURL: this.photoURL,
providerId: this.providerId,
phoneNumber: this.phoneNumber,
};
}
}
/**
* Represents a user.
*/
export class UserRecord {
/**
* The user's `uid`.
*/
uid;
/**
* The user's primary email, if set.
*/
email;
/**
* Whether or not the user's primary email is verified.
*/
emailVerified;
/**
* The user's display name.
*/
displayName;
/**
* The user's photo URL.
*/
photoURL;
/**
* The user's primary phone number, if set.
*/
phoneNumber;
/**
* Whether or not the user is disabled: `true` for disabled; `false` for
* enabled.
*/
disabled;
/**
* Additional metadata about the user.
*/
metadata;
/**
* An array of providers (for example, Google, Facebook) linked to the user.
*/
providerData;
/**
* The user's hashed password (base64-encoded), only if Firebase Auth hashing
* algorithm (SCRYPT) is used. If a different hashing algorithm had been used
* when uploading this user, as is typical when migrating from another Auth
* system, this will be an empty string. If no password is set, this is
* null. This is only available when the user is obtained from
* {@link BaseAuth.listUsers}.
*/
passwordHash;
/**
* The user's password salt (base64-encoded), only if Firebase Auth hashing
* algorithm (SCRYPT) is used. If a different hashing algorithm had been used to
* upload this user, typical when migrating from another Auth system, this will
* be an empty string. If no password is set, this is null. This is only
* available when the user is obtained from {@link BaseAuth.listUsers}.
*/
passwordSalt;
/**
* The user's custom claims object if available, typically used to define
* user roles and propagated to an authenticated user's ID token.
* This is set via {@link BaseAuth.setCustomUserClaims}
*/
customClaims;
/**
* The ID of the tenant the user belongs to, if available.
*/
tenantId;
/**
* The date the user's tokens are valid after, formatted as a UTC string.
* This is updated every time the user's refresh token are revoked either
* from the {@link BaseAuth.revokeRefreshTokens}
* API or from the Firebase Auth backend on big account changes (password
* resets, password or email updates, etc).
*/
tokensValidAfterTime;
/**
* The multi-factor related properties for the current user, if available.
*/
multiFactor;
/**
* @param response - The server side response returned from the getAccountInfo
* endpoint.
* @constructor
* @internal
*/
constructor(response) {
// The Firebase user id is required.
if (!response.localId) {
throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid user response');
}
addReadonlyGetter(this, 'uid', response.localId);
addReadonlyGetter(this, 'email', response.email);
addReadonlyGetter(this, 'emailVerified', !!response.emailVerified);
addReadonlyGetter(this, 'displayName', response.displayName);
addReadonlyGetter(this, 'photoURL', response.photoUrl);
addReadonlyGetter(this, 'phoneNumber', response.phoneNumber);
// If disabled is not provided, the account is enabled by default.
addReadonlyGetter(this, 'disabled', response.disabled || false);
addReadonlyGetter(this, 'metadata', new UserMetadata(response));
const providerData = [];
for (const entry of response.providerUserInfo || []) {
providerData.push(new UserInfo(entry));
}
addReadonlyGetter(this, 'providerData', providerData);
// If the password hash is redacted (probably due to missing permissions)
// then clear it out, similar to how the salt is returned. (Otherwise, it
// *looks* like a b64-encoded hash is present, which is confusing.)
if (response.passwordHash === B64_REDACTED) {
addReadonlyGetter(this, 'passwordHash', undefined);
}
else {
addReadonlyGetter(this, 'passwordHash', response.passwordHash);
}
addReadonlyGetter(this, 'passwordSalt', response.salt);
if (response.customAttributes) {
addReadonlyGetter(this, 'customClaims', JSON.parse(response.customAttributes));
}
let validAfterTime = null;
// Convert validSince first to UTC milliseconds and then to UTC date string.
if (typeof response.validSince !== 'undefined') {
validAfterTime = parseDate(parseInt(response.validSince, 10) * 1000);
}
addReadonlyGetter(this, 'tokensValidAfterTime', validAfterTime || undefined);
addReadonlyGetter(this, 'tenantId', response.tenantId);
const multiFactor = new MultiFactorSettings(response);
if (multiFactor.enrolledFactors.length > 0) {
addReadonlyGetter(this, 'multiFactor', multiFactor);
}
}
/**
* Returns a JSON-serializable representation of this object.
*
* @returns A JSON-serializable representation of this object.
*/
toJSON() {
const json = {
uid: this.uid,
email: this.email,
emailVerified: this.emailVerified,
displayName: this.displayName,
photoURL: this.photoURL,
phoneNumber: this.phoneNumber,
disabled: this.disabled,
// Convert metadata to json.
metadata: this.metadata.toJSON(),
passwordHash: this.passwordHash,
passwordSalt: this.passwordSalt,
customClaims: deepCopy(this.customClaims),
tokensValidAfterTime: this.tokensValidAfterTime,
tenantId: this.tenantId,
};
if (this.multiFactor) {
json.multiFactor = this.multiFactor.toJSON();
}
json.providerData = [];
for (const entry of this.providerData) {
// Convert each provider data to json.
json.providerData.push(entry.toJSON());
}
return json;
}
}
/**
* Defines a new read-only property directly on an object and returns the object.
*
* @param obj - The object on which to define the property.
* @param prop - The name of the property to be defined or modified.
* @param value - The value associated with the property.
*/
function addReadonlyGetter(obj, prop, value) {
Object.defineProperty(obj, prop, {
value,
// Make this property read-only.
writable: false,
// Include this property during enumeration of obj's properties.
enumerable: true,
});
}
/**
* Returns a deep copy of an object or array.
*
* @param value - The object or array to deep copy.
* @returns A deep copy of the provided object or array.
*/
function deepCopy(value) {
return deepExtend(undefined, value);
}
/**
* Copies properties from source to target (recursively allows extension of objects and arrays).
* Scalar values in the target are over-written. If target is undefined, an object of the
* appropriate type will be created (and returned).
*
* We recursively copy all child properties of plain objects in the source - so that namespace-like
* objects are merged.
*
* Note that the target can be a function, in which case the properties in the source object are
* copied onto it as static properties of the function.
*
* @param target - The value which is being extended.
* @param source - The value whose properties are extending the target.
* @returns The target value.
*/
function deepExtend(target, source) {
if (!(source instanceof Object)) {
return source;
}
switch (source.constructor) {
case Date: {
// Treat Dates like scalars; if the target date object had any child
// properties - they will be lost!
const dateValue = source;
return new Date(dateValue.getTime());
}
case Object:
if (target === undefined) {
target = {};
}
break;
case Array:
// Always copy the array source and overwrite the target.
target = [];
break;
default:
// Not a plain Object - treat it as a scalar.
return source;
}
for (const prop in source) {
if (!Object.prototype.hasOwnProperty.call(source, prop)) {
continue;
}
target[prop] = deepExtend(target[prop], source[prop]);
}
return target;
}