Skip to content

Commit 3b87f96

Browse files
committed
Adding TotpInfo to userRecord
1 parent d250310 commit 3b87f96

File tree

2 files changed

+228
-10
lines changed

2 files changed

+228
-10
lines changed

src/auth/user-record.ts

+70-5
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,15 @@ export interface MultiFactorInfoResponse {
4747
mfaEnrollmentId: string;
4848
displayName?: string;
4949
phoneInfo?: string;
50+
totpInfo?: TotpInfoResponse;
5051
enrolledAt?: string;
5152
[key: string]: any;
5253
}
5354

55+
export interface TotpInfoResponse {
56+
[key: string]: any;
57+
}
58+
5459
export interface ProviderUserInfoResponse {
5560
rawId: string;
5661
displayName?: string;
@@ -84,6 +89,7 @@ export interface GetAccountInfoUserResponse {
8489

8590
enum MultiFactorId {
8691
Phone = 'phone',
92+
Totp = 'totp',
8793
}
8894

8995
/**
@@ -102,7 +108,9 @@ export abstract class MultiFactorInfo {
102108
public readonly displayName?: string;
103109

104110
/**
105-
* The type identifier of the second factor. For SMS second factors, this is `phone`.
111+
* The type identifier of the second factor.
112+
* For SMS second factors, this is `phone`.
113+
* For TOTP second factors, this is `totp`.
106114
*/
107115
public readonly factorId: string;
108116

@@ -120,9 +128,13 @@ export abstract class MultiFactorInfo {
120128
*/
121129
public static initMultiFactorInfo(response: MultiFactorInfoResponse): MultiFactorInfo | null {
122130
let multiFactorInfo: MultiFactorInfo | null = null;
123-
// Only PhoneMultiFactorInfo currently available.
131+
// PhoneMultiFactorInfo, TotpMultiFactorInfo currently available.
124132
try {
125-
multiFactorInfo = new PhoneMultiFactorInfo(response);
133+
if (response.phoneInfo !== undefined) {
134+
multiFactorInfo = new PhoneMultiFactorInfo(response);
135+
} else if (response.totpInfo !== undefined) {
136+
multiFactorInfo = new TotpMultiFactorInfo(response);
137+
}
126138
} catch (e) {
127139
// Ignore error.
128140
}
@@ -225,7 +237,6 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
225237
phoneNumber: this.phoneNumber,
226238
});
227239
}
228-
229240
/**
230241
* Returns the factor ID based on the response provided.
231242
*
@@ -240,14 +251,68 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
240251
}
241252
}
242253

254+
/**
255+
* TotpInfo struct associated with a second factor
256+
*/
257+
export class TotpInfo {
258+
259+
}
260+
261+
/**
262+
* Interface representing a TOTP specific user-enrolled second factor.
263+
*/
264+
export class TotpMultiFactorInfo extends MultiFactorInfo {
265+
266+
/**
267+
* TotpInfo struct associated with a second factor
268+
*/
269+
public readonly totpInfo: TotpInfo;
270+
271+
/**
272+
* Initializes the TotpMultiFactorInfo object using the server side response.
273+
*
274+
* @param response - The server side response.
275+
* @constructor
276+
* @internal
277+
*/
278+
constructor(response: MultiFactorInfoResponse) {
279+
super(response);
280+
utils.addReadonlyGetter(this, 'totpInfo', response.totpInfo);
281+
}
282+
283+
/**
284+
* {@inheritdoc MultiFactorInfo.toJSON}
285+
*/
286+
public toJSON(): object {
287+
return Object.assign(
288+
super.toJSON(),
289+
{
290+
totpInfo: this.totpInfo,
291+
});
292+
}
293+
294+
/**
295+
* Returns the factor ID based on the response provided.
296+
*
297+
* @param response - The server side response.
298+
* @returns The multi-factor ID associated with the provided response. If the response is
299+
* not associated with any known multi-factor ID, null is returned.
300+
*
301+
* @internal
302+
*/
303+
protected getFactorId(response: MultiFactorInfoResponse): string | null {
304+
return (response && response.totpInfo) ? MultiFactorId.Totp : null;
305+
}
306+
}
307+
243308
/**
244309
* The multi-factor related user settings.
245310
*/
246311
export class MultiFactorSettings {
247312

248313
/**
249314
* List of second factors enrolled with the current user.
250-
* Currently only phone second factors are supported.
315+
* Currently only phone and totp second factors are supported.
251316
*/
252317
public enrolledFactors: MultiFactorInfo[];
253318

test/unit/auth/user-record.spec.ts

+158-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import * as chaiAsPromised from 'chai-as-promised';
2121

2222
import { deepCopy } from '../../../src/utils/deep-copy';
2323
import {
24-
GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse,
24+
GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo,
2525
} from '../../../src/auth/user-record';
2626
import {
2727
UserInfo, UserMetadata, UserRecord, MultiFactorSettings, MultiFactorInfo, PhoneMultiFactorInfo,
@@ -379,18 +379,157 @@ describe('PhoneMultiFactorInfo', () => {
379379
});
380380
});
381381

382-
describe('MultiFactorInfo', () => {
382+
describe('TotpMultiFactorInfo', () => {
383383
const serverResponse: MultiFactorInfoResponse = {
384+
mfaEnrollmentId: 'enrollmentId1',
385+
displayName: 'displayName1',
386+
enrolledAt: now.toISOString(),
387+
totpInfo: {},
388+
};
389+
const totpMultiFactorInfo = new TotpMultiFactorInfo(serverResponse);
390+
const totpMultiFactorInfoMissingFields = new TotpMultiFactorInfo({
391+
mfaEnrollmentId: serverResponse.mfaEnrollmentId,
392+
totpInfo: serverResponse.totpInfo,
393+
});
394+
395+
describe('constructor', () => {
396+
it('should throw when an empty object is provided', () => {
397+
expect(() => {
398+
return new TotpMultiFactorInfo({} as any);
399+
}).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response');
400+
});
401+
402+
it('should throw when an undefined response is provided', () => {
403+
expect(() => {
404+
return new TotpMultiFactorInfo(undefined as any);
405+
}).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response');
406+
});
407+
408+
it('should succeed when mfaEnrollmentId and totpInfo are both provided', () => {
409+
expect(() => {
410+
return new TotpMultiFactorInfo({
411+
mfaEnrollmentId: 'enrollmentId1',
412+
totpInfo: {},
413+
});
414+
}).not.to.throw(Error);
415+
});
416+
417+
it('should throw when only mfaEnrollmentId is provided', () => {
418+
expect(() => {
419+
return new TotpMultiFactorInfo({
420+
mfaEnrollmentId: 'enrollmentId1',
421+
} as any);
422+
}).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response');
423+
});
424+
425+
it('should throw when only totpInfo is provided', () => {
426+
expect(() => {
427+
return new TotpMultiFactorInfo({
428+
totpInfo: {},
429+
} as any);
430+
}).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response');
431+
});
432+
});
433+
434+
describe('getters', () => {
435+
it('should set missing optional fields to null', () => {
436+
expect(totpMultiFactorInfoMissingFields.uid).to.equal(serverResponse.mfaEnrollmentId);
437+
expect(totpMultiFactorInfoMissingFields.displayName).to.be.undefined;
438+
expect(totpMultiFactorInfoMissingFields.totpInfo).to.equal(serverResponse.totpInfo);
439+
expect(totpMultiFactorInfoMissingFields.enrollmentTime).to.be.null;
440+
expect(totpMultiFactorInfoMissingFields.factorId).to.equal('totp');
441+
});
442+
443+
it('should return expected factorId', () => {
444+
expect(totpMultiFactorInfo.factorId).to.equal('totp');
445+
});
446+
447+
it('should throw when modifying readonly factorId property', () => {
448+
expect(() => {
449+
(totpMultiFactorInfo as any).factorId = 'other';
450+
}).to.throw(Error);
451+
});
452+
453+
it('should return expected displayName', () => {
454+
expect(totpMultiFactorInfo.displayName).to.equal(serverResponse.displayName);
455+
});
456+
457+
it('should throw when modifying readonly displayName property', () => {
458+
expect(() => {
459+
(totpMultiFactorInfo as any).displayName = 'Modified';
460+
}).to.throw(Error);
461+
});
462+
463+
it('should return expected totpInfo object', () => {
464+
expect(totpMultiFactorInfo.totpInfo).to.equal(serverResponse.totpInfo);
465+
});
466+
467+
it('should return expected uid', () => {
468+
expect(totpMultiFactorInfo.uid).to.equal(serverResponse.mfaEnrollmentId);
469+
});
470+
471+
it('should throw when modifying readonly uid property', () => {
472+
expect(() => {
473+
(totpMultiFactorInfo as any).uid = 'modifiedEnrollmentId';
474+
}).to.throw(Error);
475+
});
476+
477+
it('should return expected enrollmentTime', () => {
478+
expect(totpMultiFactorInfo.enrollmentTime).to.equal(now.toUTCString());
479+
});
480+
481+
it('should throw when modifying readonly uid property', () => {
482+
expect(() => {
483+
(totpMultiFactorInfo as any).enrollmentTime = new Date().toISOString();
484+
}).to.throw(Error);
485+
});
486+
});
487+
488+
describe('toJSON', () => {
489+
it('should return expected JSON object', () => {
490+
expect(totpMultiFactorInfo.toJSON()).to.deep.equal({
491+
uid: 'enrollmentId1',
492+
displayName: 'displayName1',
493+
enrollmentTime: now.toUTCString(),
494+
totpInfo: {},
495+
factorId: 'totp',
496+
});
497+
});
498+
499+
it('should return expected JSON object with missing fields set to null', () => {
500+
expect(totpMultiFactorInfoMissingFields.toJSON()).to.deep.equal({
501+
uid: 'enrollmentId1',
502+
displayName: undefined,
503+
enrollmentTime: null,
504+
totpInfo: {},
505+
factorId: 'totp',
506+
});
507+
});
508+
});
509+
});
510+
511+
describe('MultiFactorInfo', () => {
512+
const phoneServerResponse: MultiFactorInfoResponse = {
384513
mfaEnrollmentId: 'enrollmentId1',
385514
displayName: 'displayName1',
386515
enrolledAt: now.toISOString(),
387516
phoneInfo: '+16505551234',
388517
};
389-
const phoneMultiFactorInfo = new PhoneMultiFactorInfo(serverResponse);
518+
const phoneMultiFactorInfo = new PhoneMultiFactorInfo(phoneServerResponse);
519+
const totpServerResponse: MultiFactorInfoResponse = {
520+
mfaEnrollmentId: 'enrollmentId1',
521+
displayName: 'displayName1',
522+
enrolledAt: now.toISOString(),
523+
totpInfo: {},
524+
};
525+
const totpMultiFactorInfo = new TotpMultiFactorInfo(totpServerResponse);
390526

391527
describe('initMultiFactorInfo', () => {
392528
it('should return expected PhoneMultiFactorInfo', () => {
393-
expect(MultiFactorInfo.initMultiFactorInfo(serverResponse)).to.deep.equal(phoneMultiFactorInfo);
529+
expect(MultiFactorInfo.initMultiFactorInfo(phoneServerResponse)).to.deep.equal(phoneMultiFactorInfo);
530+
});
531+
it('should return expected TotpMultiFactorInfo', () => {
532+
expect(MultiFactorInfo.initMultiFactorInfo(totpServerResponse)).to.deep.equal(totpMultiFactorInfo);
394533
});
395534

396535
it('should return null for invalid MultiFactorInfo', () => {
@@ -425,6 +564,12 @@ describe('MultiFactorSettings', () => {
425564
enrolledAt: now.toISOString(),
426565
secretKey: 'SECRET_KEY',
427566
},
567+
{
568+
mfaEnrollmentId: 'enrollmentId5',
569+
displayName: 'displayName1',
570+
enrolledAt: now.toISOString(),
571+
totpInfo: {},
572+
},
428573
],
429574
};
430575
const expectedMultiFactorInfo = [
@@ -439,6 +584,12 @@ describe('MultiFactorSettings', () => {
439584
enrolledAt: now.toISOString(),
440585
phoneInfo: '+16505556789',
441586
}),
587+
new TotpMultiFactorInfo({
588+
mfaEnrollmentId: 'enrollmentId5',
589+
displayName: 'displayName1',
590+
enrolledAt: now.toISOString(),
591+
totpInfo: {},
592+
})
442593
];
443594

444595
describe('constructor', () => {
@@ -457,9 +608,10 @@ describe('MultiFactorSettings', () => {
457608
it('should populate expected enrolledFactors', () => {
458609
const multiFactor = new MultiFactorSettings(serverResponse);
459610

460-
expect(multiFactor.enrolledFactors.length).to.equal(2);
611+
expect(multiFactor.enrolledFactors.length).to.equal(3);
461612
expect(multiFactor.enrolledFactors[0]).to.deep.equal(expectedMultiFactorInfo[0]);
462613
expect(multiFactor.enrolledFactors[1]).to.deep.equal(expectedMultiFactorInfo[1]);
614+
expect(multiFactor.enrolledFactors[2]).to.deep.equal(expectedMultiFactorInfo[2]);
463615
});
464616
});
465617

@@ -504,6 +656,7 @@ describe('MultiFactorSettings', () => {
504656
enrolledFactors: [
505657
expectedMultiFactorInfo[0].toJSON(),
506658
expectedMultiFactorInfo[1].toJSON(),
659+
expectedMultiFactorInfo[2].toJSON(),
507660
],
508661
});
509662
});

0 commit comments

Comments
 (0)