From 3b87f969381ae581256ece2af1530c53ad63ed4b Mon Sep 17 00:00:00 2001 From: Pragati Date: Fri, 26 May 2023 14:07:59 -0700 Subject: [PATCH 1/3] Adding TotpInfo to userRecord --- src/auth/user-record.ts | 75 ++++++++++++- test/unit/auth/user-record.spec.ts | 163 ++++++++++++++++++++++++++++- 2 files changed, 228 insertions(+), 10 deletions(-) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 2bd2fdacbd..a5d21b085a 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -47,10 +47,15 @@ export interface MultiFactorInfoResponse { mfaEnrollmentId: string; displayName?: string; phoneInfo?: string; + totpInfo?: TotpInfoResponse; enrolledAt?: string; [key: string]: any; } +export interface TotpInfoResponse { + [key: string]: any; +} + export interface ProviderUserInfoResponse { rawId: string; displayName?: string; @@ -84,6 +89,7 @@ export interface GetAccountInfoUserResponse { enum MultiFactorId { Phone = 'phone', + Totp = 'totp', } /** @@ -102,7 +108,9 @@ export abstract class MultiFactorInfo { public readonly displayName?: string; /** - * The type identifier of the second factor. For SMS second factors, this is `phone`. + * The type identifier of the second factor. + * For SMS second factors, this is `phone`. + * For TOTP second factors, this is `totp`. */ public readonly factorId: string; @@ -120,9 +128,13 @@ export abstract class MultiFactorInfo { */ public static initMultiFactorInfo(response: MultiFactorInfoResponse): MultiFactorInfo | null { let multiFactorInfo: MultiFactorInfo | null = null; - // Only PhoneMultiFactorInfo currently available. + // PhoneMultiFactorInfo, TotpMultiFactorInfo currently available. try { - multiFactorInfo = new PhoneMultiFactorInfo(response); + if (response.phoneInfo !== undefined) { + multiFactorInfo = new PhoneMultiFactorInfo(response); + } else if (response.totpInfo !== undefined) { + multiFactorInfo = new TotpMultiFactorInfo(response); + } } catch (e) { // Ignore error. } @@ -225,7 +237,6 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { phoneNumber: this.phoneNumber, }); } - /** * Returns the factor ID based on the response provided. * @@ -240,6 +251,60 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { } } +/** + * 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 + */ + public readonly totpInfo: TotpInfo; + + /** + * Initializes the TotpMultiFactorInfo object using the server side response. + * + * @param response - The server side response. + * @constructor + * @internal + */ + constructor(response: MultiFactorInfoResponse) { + super(response); + utils.addReadonlyGetter(this, 'totpInfo', response.totpInfo); + } + + /** + * {@inheritdoc MultiFactorInfo.toJSON} + */ + public toJSON(): object { + 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 + */ + protected getFactorId(response: MultiFactorInfoResponse): string | null { + return (response && response.totpInfo) ? MultiFactorId.Totp : null; + } +} + /** * The multi-factor related user settings. */ @@ -247,7 +312,7 @@ export class MultiFactorSettings { /** * List of second factors enrolled with the current user. - * Currently only phone second factors are supported. + * Currently only phone and totp second factors are supported. */ public enrolledFactors: MultiFactorInfo[]; diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index 4ec43af305..dc332c13b9 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -21,7 +21,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; import { - GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, + GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo, } from '../../../src/auth/user-record'; import { UserInfo, UserMetadata, UserRecord, MultiFactorSettings, MultiFactorInfo, PhoneMultiFactorInfo, @@ -379,18 +379,157 @@ describe('PhoneMultiFactorInfo', () => { }); }); -describe('MultiFactorInfo', () => { +describe('TotpMultiFactorInfo', () => { const serverResponse: MultiFactorInfoResponse = { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }; + const totpMultiFactorInfo = new TotpMultiFactorInfo(serverResponse); + const totpMultiFactorInfoMissingFields = new TotpMultiFactorInfo({ + mfaEnrollmentId: serverResponse.mfaEnrollmentId, + totpInfo: serverResponse.totpInfo, + }); + + describe('constructor', () => { + it('should throw when an empty object is provided', () => { + expect(() => { + return new TotpMultiFactorInfo({} as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should throw when an undefined response is provided', () => { + expect(() => { + return new TotpMultiFactorInfo(undefined as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should succeed when mfaEnrollmentId and totpInfo are both provided', () => { + expect(() => { + return new TotpMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + totpInfo: {}, + }); + }).not.to.throw(Error); + }); + + it('should throw when only mfaEnrollmentId is provided', () => { + expect(() => { + return new TotpMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + } as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should throw when only totpInfo is provided', () => { + expect(() => { + return new TotpMultiFactorInfo({ + totpInfo: {}, + } as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + }); + + describe('getters', () => { + it('should set missing optional fields to null', () => { + expect(totpMultiFactorInfoMissingFields.uid).to.equal(serverResponse.mfaEnrollmentId); + expect(totpMultiFactorInfoMissingFields.displayName).to.be.undefined; + expect(totpMultiFactorInfoMissingFields.totpInfo).to.equal(serverResponse.totpInfo); + expect(totpMultiFactorInfoMissingFields.enrollmentTime).to.be.null; + expect(totpMultiFactorInfoMissingFields.factorId).to.equal('totp'); + }); + + it('should return expected factorId', () => { + expect(totpMultiFactorInfo.factorId).to.equal('totp'); + }); + + it('should throw when modifying readonly factorId property', () => { + expect(() => { + (totpMultiFactorInfo as any).factorId = 'other'; + }).to.throw(Error); + }); + + it('should return expected displayName', () => { + expect(totpMultiFactorInfo.displayName).to.equal(serverResponse.displayName); + }); + + it('should throw when modifying readonly displayName property', () => { + expect(() => { + (totpMultiFactorInfo as any).displayName = 'Modified'; + }).to.throw(Error); + }); + + it('should return expected totpInfo object', () => { + expect(totpMultiFactorInfo.totpInfo).to.equal(serverResponse.totpInfo); + }); + + it('should return expected uid', () => { + expect(totpMultiFactorInfo.uid).to.equal(serverResponse.mfaEnrollmentId); + }); + + it('should throw when modifying readonly uid property', () => { + expect(() => { + (totpMultiFactorInfo as any).uid = 'modifiedEnrollmentId'; + }).to.throw(Error); + }); + + it('should return expected enrollmentTime', () => { + expect(totpMultiFactorInfo.enrollmentTime).to.equal(now.toUTCString()); + }); + + it('should throw when modifying readonly uid property', () => { + expect(() => { + (totpMultiFactorInfo as any).enrollmentTime = new Date().toISOString(); + }).to.throw(Error); + }); + }); + + describe('toJSON', () => { + it('should return expected JSON object', () => { + expect(totpMultiFactorInfo.toJSON()).to.deep.equal({ + uid: 'enrollmentId1', + displayName: 'displayName1', + enrollmentTime: now.toUTCString(), + totpInfo: {}, + factorId: 'totp', + }); + }); + + it('should return expected JSON object with missing fields set to null', () => { + expect(totpMultiFactorInfoMissingFields.toJSON()).to.deep.equal({ + uid: 'enrollmentId1', + displayName: undefined, + enrollmentTime: null, + totpInfo: {}, + factorId: 'totp', + }); + }); + }); +}); + +describe('MultiFactorInfo', () => { + const phoneServerResponse: MultiFactorInfoResponse = { mfaEnrollmentId: 'enrollmentId1', displayName: 'displayName1', enrolledAt: now.toISOString(), phoneInfo: '+16505551234', }; - const phoneMultiFactorInfo = new PhoneMultiFactorInfo(serverResponse); + const phoneMultiFactorInfo = new PhoneMultiFactorInfo(phoneServerResponse); + const totpServerResponse: MultiFactorInfoResponse = { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }; + const totpMultiFactorInfo = new TotpMultiFactorInfo(totpServerResponse); describe('initMultiFactorInfo', () => { it('should return expected PhoneMultiFactorInfo', () => { - expect(MultiFactorInfo.initMultiFactorInfo(serverResponse)).to.deep.equal(phoneMultiFactorInfo); + expect(MultiFactorInfo.initMultiFactorInfo(phoneServerResponse)).to.deep.equal(phoneMultiFactorInfo); + }); + it('should return expected TotpMultiFactorInfo', () => { + expect(MultiFactorInfo.initMultiFactorInfo(totpServerResponse)).to.deep.equal(totpMultiFactorInfo); }); it('should return null for invalid MultiFactorInfo', () => { @@ -425,6 +564,12 @@ describe('MultiFactorSettings', () => { enrolledAt: now.toISOString(), secretKey: 'SECRET_KEY', }, + { + mfaEnrollmentId: 'enrollmentId5', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }, ], }; const expectedMultiFactorInfo = [ @@ -439,6 +584,12 @@ describe('MultiFactorSettings', () => { enrolledAt: now.toISOString(), phoneInfo: '+16505556789', }), + new TotpMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId5', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }) ]; describe('constructor', () => { @@ -457,9 +608,10 @@ describe('MultiFactorSettings', () => { it('should populate expected enrolledFactors', () => { const multiFactor = new MultiFactorSettings(serverResponse); - expect(multiFactor.enrolledFactors.length).to.equal(2); + expect(multiFactor.enrolledFactors.length).to.equal(3); expect(multiFactor.enrolledFactors[0]).to.deep.equal(expectedMultiFactorInfo[0]); expect(multiFactor.enrolledFactors[1]).to.deep.equal(expectedMultiFactorInfo[1]); + expect(multiFactor.enrolledFactors[2]).to.deep.equal(expectedMultiFactorInfo[2]); }); }); @@ -504,6 +656,7 @@ describe('MultiFactorSettings', () => { enrolledFactors: [ expectedMultiFactorInfo[0].toJSON(), expectedMultiFactorInfo[1].toJSON(), + expectedMultiFactorInfo[2].toJSON(), ], }); }); From ae023fca7a11e9830baf6ac6c0b8e7a76f4843fd Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Tue, 30 May 2023 23:35:04 +0000 Subject: [PATCH 2/3] Changing type from `any` to `unknown` for type safety. --- src/auth/user-record.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index a5d21b085a..ddda950a66 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -49,11 +49,11 @@ export interface MultiFactorInfoResponse { phoneInfo?: string; totpInfo?: TotpInfoResponse; enrolledAt?: string; - [key: string]: any; + [key: string]: unknown; } export interface TotpInfoResponse { - [key: string]: any; + [key: string]: unknown; } export interface ProviderUserInfoResponse { From 998afeba56920abd6a284d38e4b6c0f9ad2924ea Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Fri, 2 Jun 2023 16:43:20 +0000 Subject: [PATCH 3/3] Addressing feedback --- src/auth/user-record.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index ddda950a66..18725c8279 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -134,6 +134,8 @@ export abstract class MultiFactorInfo { 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. @@ -237,6 +239,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { phoneNumber: this.phoneNumber, }); } + /** * Returns the factor ID based on the response provided. *