Skip to content

Populates UserRecord with multi-factor related info #681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 221 additions & 12 deletions src/auth/user-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import {deepCopy} from '../utils/deep-copy';
import {isNonNullObject} from '../utils/validator';
import * as utils from '../utils';
import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error';

Expand All @@ -26,8 +27,8 @@ const B64_REDACTED = Buffer.from('REDACTED').toString('base64');
/**
* Parses a time stamp string or number and returns the corresponding date if valid.
*
* @param {any} time The unix timestamp string or number in milliseconds.
* @return {string} The corresponding date as a UTC string, if valid.
* @param time The unix timestamp string or number in milliseconds.
* @return The corresponding date as a UTC string, if valid.
*/
function parseDate(time: any): string {
try {
Expand Down Expand Up @@ -57,19 +58,219 @@ export interface CreateRequest extends UpdateRequest {
uid?: string;
}

export interface AuthFactorInfo {
mfaEnrollmentId: string;
displayName?: string;
phoneInfo?: string;
enrolledAt?: string;
[key: string]: any;
}

export interface ProviderUserInfo {
rawId: string;
displayName?: string;
email?: string;
photoUrl?: string;
phoneNumber?: string;
providerId: string;
federatedId?: string;
}

export interface GetAccountInfoUserResponse {
localId: string;
email?: string;
emailVerified?: boolean;
phoneNumber?: string;
displayName?: string;
photoUrl?: string;
disabled?: boolean;
passwordHash?: string;
salt?: string;
customAttributes?: string;
validSince?: string;
tenantId?: string;
providerUserInfo?: ProviderUserInfo[];
mfaInfo?: AuthFactorInfo[];
createdAt?: string;
lastLoginAt?: string;
[key: string]: any;
}

/** Enums for multi-factor identifiers. */
export enum MultiFactorId {
Phone = 'phone',
}

/**
* Abstract class representing a multi-factor info interface.
*/
export abstract class MultiFactorInfo {
public readonly uid: string;
public readonly displayName: string | null;
public readonly factorId: MultiFactorId;
public readonly enrollmentTime: string;

/**
* 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.
* @constructor
*/
public static initMultiFactorInfo(response: AuthFactorInfo): MultiFactorInfo | null {
let multiFactorInfo: MultiFactorInfo | null = null;
// Only PhoneMultiFactorInfo currently available.
try {
multiFactorInfo = new PhoneMultiFactorInfo(response);
} catch (e) {
// Ignore error.
}
return multiFactorInfo;
}

/**
* Initializes the MultiFactorInfo object using the server side response.
*
* @param response The server side response.
* @constructor
*/
constructor(response: AuthFactorInfo) {
this.initFromServerResponse(response);
}

/** @return The plain object representation. */
public toJSON(): any {
return {
uid: this.uid,
displayName: this.displayName,
factorId: this.factorId,
enrollmentTime: this.enrollmentTime,
};
}

/**
* Returns the factor ID based on the response provided.
*
* @param response The server side response.
* @return The multi-factor ID associated with the provided response. If the response is
* not associated with any known multi-factor ID, null is returned.
*/
protected abstract getFactorId(response: AuthFactorInfo): MultiFactorId | null;

/**
* Initializes the MultiFactorInfo object using the provided server response.
*
* @param response The server side response.
*/
private initFromServerResponse(response: AuthFactorInfo) {
const factorId = response && this.getFactorId(response);
if (!factorId || !response || !response.mfaEnrollmentId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check response before passing it to getFactorId()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a check for this. I also added a test when undefined is passed to the constructor that the expected error is caught and thrown.

throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Invalid multi-factor info response');
}
utils.addReadonlyGetter(this, 'uid', response.mfaEnrollmentId);
utils.addReadonlyGetter(this, 'factorId', factorId);
utils.addReadonlyGetter(this, 'displayName', response.displayName || null);
// 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) {
utils.addReadonlyGetter(
this, 'enrollmentTime', new Date(response.enrolledAt).toUTCString());
} else {
utils.addReadonlyGetter(this, 'enrollmentTime', null);
}
}
}

/** Class representing a phone MultiFactorInfo object. */
export class PhoneMultiFactorInfo extends MultiFactorInfo {
public readonly phoneNumber: string;

/**
* Initializes the PhoneMultiFactorInfo object using the server side response.
*
* @param response The server side response.
* @constructor
*/
constructor(response: AuthFactorInfo) {
super(response);
utils.addReadonlyGetter(this, 'phoneNumber', response.phoneInfo);
}

/** @return The plain object representation. */
public toJSON(): any {
return Object.assign(
super.toJSON(),
{
phoneNumber: this.phoneNumber,
});
}

/**
* Returns the factor ID based on the response provided.
*
* @param response The server side response.
* @return The multi-factor ID associated with the provided response. If the response is
* not associated with any known multi-factor ID, null is returned.
*/
protected getFactorId(response: AuthFactorInfo): MultiFactorId | null {
return !!(response && response.phoneInfo) ? MultiFactorId.Phone : null;
}
}

/** Class representing multi-factor related properties of a user. */
export class MultiFactor {
public readonly enrolledFactors: ReadonlyArray<MultiFactorInfo>;

/**
* Initializes the MultiFactor object using the server side or JWT format response.
*
* @param response The server side response.
* @constructor
*/
constructor(response: GetAccountInfoUserResponse) {
const parsedEnrolledFactors: MultiFactorInfo[] = [];
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.
utils.addReadonlyGetter(
this, 'enrolledFactors', Object.freeze(parsedEnrolledFactors));
}

/** @return The plain object representation. */
public toJSON(): any {
return {
enrolledFactors: this.enrolledFactors.map((info) => info.toJSON()),
};
}
}

/**
* User metadata class that provides metadata information like user account creation
* and last sign in time.
*
* @param {object} response The server side response returned from the getAccountInfo
* @param response The server side response returned from the getAccountInfo
* endpoint.
* @constructor
*/
export class UserMetadata {
public readonly creationTime: string;
public readonly lastSignInTime: string;

constructor(response: any) {
constructor(response: GetAccountInfoUserResponse) {
// 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.
Expand All @@ -78,7 +279,7 @@ export class UserMetadata {
utils.addReadonlyGetter(this, 'lastSignInTime', parseDate(response.lastLoginAt));
}

/** @return {object} The plain object representation of the user's metadata. */
/** @return The plain object representation of the user's metadata. */
public toJSON(): object {
return {
lastSignInTime: this.lastSignInTime,
Expand All @@ -91,7 +292,7 @@ export class UserMetadata {
* User info class that provides provider user information for different
* Firebase providers like google.com, facebook.com, password, etc.
*
* @param {object} response The server side response returned from the getAccountInfo
* @param response The server side response returned from the getAccountInfo
* endpoint.
* @constructor
*/
Expand All @@ -103,7 +304,7 @@ export class UserInfo {
public readonly providerId: string;
public readonly phoneNumber: string;

constructor(response: any) {
constructor(response: ProviderUserInfo) {
// Provider user id and provider id are required.
if (!response.rawId || !response.providerId) {
throw new FirebaseAuthError(
Expand All @@ -119,7 +320,7 @@ export class UserInfo {
utils.addReadonlyGetter(this, 'phoneNumber', response.phoneNumber);
}

/** @return {object} The plain object representation of the current provider data. */
/** @return The plain object representation of the current provider data. */
public toJSON(): object {
return {
uid: this.uid,
Expand All @@ -136,7 +337,7 @@ export class UserInfo {
* User record class that defines the Firebase user object populated from
* the Firebase Auth getAccountInfo response.
*
* @param {any} response The server side response returned from the getAccountInfo
* @param response The server side response returned from the getAccountInfo
* endpoint.
* @constructor
*/
Expand All @@ -155,8 +356,9 @@ export class UserRecord {
public readonly customClaims: object;
public readonly tenantId?: string | null;
public readonly tokensValidAfterTime?: string;
public readonly multiFactor?: MultiFactor;

constructor(response: any) {
constructor(response: GetAccountInfoUserResponse) {
// The Firebase user id is required.
if (!response.localId) {
throw new FirebaseAuthError(
Expand Down Expand Up @@ -199,13 +401,17 @@ export class UserRecord {
let validAfterTime: string = null;
// Convert validSince first to UTC milliseconds and then to UTC date string.
if (typeof response.validSince !== 'undefined') {
validAfterTime = parseDate(response.validSince * 1000);
validAfterTime = parseDate(parseInt(response.validSince, 10) * 1000);
}
utils.addReadonlyGetter(this, 'tokensValidAfterTime', validAfterTime || undefined);
utils.addReadonlyGetter(this, 'tenantId', response.tenantId);
const multiFactor = new MultiFactor(response);
if (multiFactor.enrolledFactors.length > 0) {
utils.addReadonlyGetter(this, 'multiFactor', multiFactor);
}
}

/** @return {object} The plain object representation of the user record. */
/** @return The plain object representation of the user record. */
public toJSON(): object {
const json: any = {
uid: this.uid,
Expand All @@ -223,6 +429,9 @@ export class UserRecord {
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.
Expand Down
Loading