[go: nahoru, domu]

Skip to content

Commit

Permalink
feat(auth): Add TotpInfo field to UserRecord (#2197)
Browse files Browse the repository at this point in the history
* Adding TotpInfo to userRecord

* Changing type from `any` to `unknown` for type safety.

* Addressing feedback
  • Loading branch information
pragatimodi committed Jun 27, 2023
1 parent 4e7e072 commit f182c36
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 10 deletions.
78 changes: 73 additions & 5 deletions src/auth/user-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ export interface MultiFactorInfoResponse {
mfaEnrollmentId: string;
displayName?: string;
phoneInfo?: string;
totpInfo?: TotpInfoResponse;
enrolledAt?: string;
[key: string]: any;
[key: string]: unknown;
}

export interface TotpInfoResponse {
[key: string]: unknown;
}

export interface ProviderUserInfoResponse {
Expand Down Expand Up @@ -84,6 +89,7 @@ export interface GetAccountInfoUserResponse {

enum MultiFactorId {
Phone = 'phone',
Totp = 'totp',
}

/**
Expand All @@ -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;

Expand All @@ -120,9 +128,15 @@ 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);
} else {
// Ignore the other SDK unsupported MFA factors to prevent blocking developers using the current SDK.
}
} catch (e) {
// Ignore error.
}
Expand Down Expand Up @@ -240,14 +254,68 @@ 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.
*/
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[];

Expand Down
163 changes: 158 additions & 5 deletions test/unit/auth/user-record.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -425,6 +564,12 @@ describe('MultiFactorSettings', () => {
enrolledAt: now.toISOString(),
secretKey: 'SECRET_KEY',
},
{
mfaEnrollmentId: 'enrollmentId5',
displayName: 'displayName1',
enrolledAt: now.toISOString(),
totpInfo: {},
},
],
};
const expectedMultiFactorInfo = [
Expand All @@ -439,6 +584,12 @@ describe('MultiFactorSettings', () => {
enrolledAt: now.toISOString(),
phoneInfo: '+16505556789',
}),
new TotpMultiFactorInfo({
mfaEnrollmentId: 'enrollmentId5',
displayName: 'displayName1',
enrolledAt: now.toISOString(),
totpInfo: {},
})
];

describe('constructor', () => {
Expand All @@ -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]);
});
});

Expand Down Expand Up @@ -504,6 +656,7 @@ describe('MultiFactorSettings', () => {
enrolledFactors: [
expectedMultiFactorInfo[0].toJSON(),
expectedMultiFactorInfo[1].toJSON(),
expectedMultiFactorInfo[2].toJSON(),
],
});
});
Expand Down

0 comments on commit f182c36

Please sign in to comment.