Skip to content

Commit 70b7da9

Browse files
Adds multi-factor support for updateUser (#706)
* Adds multi-factor support for updateUser. This will allow the ability to update the list of second factors on an existing user record.
1 parent 5ac091b commit 70b7da9

File tree

6 files changed

+349
-45
lines changed

6 files changed

+349
-45
lines changed

src/auth/auth-api-request.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import {CreateRequest, UpdateRequest} from './user-record';
2626
import {
2727
UserImportBuilder, UserImportOptions, UserImportRecord,
28-
UserImportResult, AuthFactorInfo,
28+
UserImportResult, AuthFactorInfo, convertMultiFactorInfoToServerFormat,
2929
} from './user-import-builder';
3030
import * as utils from '../utils/index';
3131
import {ActionCodeSettings, ActionCodeSettingsBuilder} from './action-code-settings-builder';
@@ -304,6 +304,8 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
304304
lastLoginAt: uploadAccountRequest,
305305
providerUserInfo: uploadAccountRequest,
306306
mfaInfo: uploadAccountRequest,
307+
// Only for non-uploadAccount requests.
308+
mfa: !uploadAccountRequest,
307309
};
308310
// Remove invalid keys from original request.
309311
for (const key in request) {
@@ -442,12 +444,20 @@ function validateCreateEditRequest(request: any, uploadAccountRequest: boolean =
442444
validateProviderUserInfo(providerUserInfoEntry);
443445
});
444446
}
445-
// mfaInfo has to be an array of valid AuthFactorInfo requests.
447+
// mfaInfo is used for importUsers.
448+
// mfa.enrollments is used for setAccountInfo.
449+
// enrollments has to be an array of valid AuthFactorInfo requests.
450+
let enrollments: AuthFactorInfo[];
446451
if (request.mfaInfo) {
447-
if (!validator.isArray(request.mfaInfo)) {
452+
enrollments = request.mfaInfo;
453+
} else if (request.mfa && request.mfa.enrollments) {
454+
enrollments = request.mfa.enrollments;
455+
}
456+
if (enrollments) {
457+
if (!validator.isArray(enrollments)) {
448458
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS);
449459
}
450-
request.mfaInfo.forEach((authFactorInfoEntry: AuthFactorInfo) => {
460+
enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => {
451461
validateAuthFactorInfo(authFactorInfoEntry);
452462
});
453463
}
@@ -1049,6 +1059,28 @@ export abstract class AbstractAuthRequestHandler {
10491059
request.disableUser = request.disabled;
10501060
delete request.disabled;
10511061
}
1062+
// Construct mfa related user data.
1063+
if (validator.isNonNullObject(request.multiFactor)) {
1064+
if (request.multiFactor.enrolledFactors === null) {
1065+
// Remove all second factors.
1066+
request.mfa = {};
1067+
} else if (validator.isArray(request.multiFactor.enrolledFactors)) {
1068+
request.mfa = {
1069+
enrollments: [],
1070+
};
1071+
try {
1072+
request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => {
1073+
request.mfa.enrollments.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
1074+
});
1075+
} catch (e) {
1076+
return Promise.reject(e);
1077+
}
1078+
if (request.mfa.enrollments.length === 0) {
1079+
delete request.mfa.enrollments;
1080+
}
1081+
}
1082+
delete request.multiFactor;
1083+
}
10521084
return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request)
10531085
.then((response: any) => {
10541086
return response.localId as string;

src/auth/user-import-builder.ts

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export interface UserImportOptions {
3939
};
4040
}
4141

42+
interface SecondFactor {
43+
uid: string;
44+
phoneNumber: string;
45+
displayName?: string;
46+
enrollmentTime?: string;
47+
factorId: string;
48+
}
49+
4250

4351
/** User import record as accepted from developer. */
4452
export interface UserImportRecord {
@@ -61,13 +69,7 @@ export interface UserImportRecord {
6169
providerId: string,
6270
}>;
6371
multiFactor?: {
64-
enrolledFactors: Array<{
65-
uid: string;
66-
phoneNumber: string;
67-
displayName?: string;
68-
enrollmentTime?: string;
69-
factorId: string;
70-
}>;
72+
enrolledFactors: SecondFactor[];
7173
};
7274
customClaims?: object;
7375
passwordHash?: Buffer;
@@ -143,6 +145,49 @@ export interface UserImportResult {
143145
export type ValidatorFunction = (data: UploadAccountUser) => void;
144146

145147

148+
/**
149+
* Converts a client format second factor object to server format.
150+
* @param multiFactorInfo The client format second factor.
151+
* @return The corresponding AuthFactorInfo server request format.
152+
*/
153+
export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFactor): AuthFactorInfo {
154+
let enrolledAt;
155+
if (typeof multiFactorInfo.enrollmentTime !== 'undefined') {
156+
if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) {
157+
// Convert from UTC date string (client side format) to ISO date string (server side format).
158+
enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString();
159+
} else {
160+
throw new FirebaseAuthError(
161+
AuthClientErrorCode.INVALID_ENROLLMENT_TIME,
162+
`The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` +
163+
`UTC date string.`);
164+
}
165+
}
166+
// Currently only phone second factors are supported.
167+
if (multiFactorInfo.factorId === 'phone') {
168+
// If any required field is missing or invalid, validation will still fail later.
169+
const authFactorInfo: AuthFactorInfo = {
170+
mfaEnrollmentId: multiFactorInfo.uid,
171+
displayName: multiFactorInfo.displayName,
172+
// Required for all phone second factors.
173+
phoneInfo: multiFactorInfo.phoneNumber,
174+
enrolledAt,
175+
};
176+
for (const objKey in authFactorInfo) {
177+
if (typeof authFactorInfo[objKey] === 'undefined') {
178+
delete authFactorInfo[objKey];
179+
}
180+
}
181+
return authFactorInfo;
182+
} else {
183+
// Unsupported second factor.
184+
throw new FirebaseAuthError(
185+
AuthClientErrorCode.INVALID_ENROLLED_FACTORS,
186+
`Unsupported second factor "${JSON.stringify(multiFactorInfo)}" provided.`);
187+
}
188+
}
189+
190+
146191
/**
147192
* @param {any} obj The object to check for number field within.
148193
* @param {string} key The entry key.
@@ -218,40 +263,7 @@ function populateUploadAccountUser(
218263
if (validator.isNonNullObject(user.multiFactor) &&
219264
validator.isNonEmptyArray(user.multiFactor.enrolledFactors)) {
220265
user.multiFactor.enrolledFactors.forEach((multiFactorInfo) => {
221-
let enrolledAt;
222-
if (typeof multiFactorInfo.enrollmentTime !== 'undefined') {
223-
if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) {
224-
// Convert from UTC date string (client side format) to ISO date string (server side format).
225-
enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString();
226-
} else {
227-
throw new FirebaseAuthError(
228-
AuthClientErrorCode.INVALID_ENROLLMENT_TIME,
229-
`The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` +
230-
`UTC date string.`);
231-
}
232-
}
233-
// Currently only phone second factors are supported.
234-
if (multiFactorInfo.factorId === 'phone') {
235-
// If any required field is missing or invalid, validation will still fail later.
236-
const authFactorInfo: AuthFactorInfo = {
237-
mfaEnrollmentId: multiFactorInfo.uid,
238-
displayName: multiFactorInfo.displayName,
239-
// Required for all phone second factors.
240-
phoneInfo: multiFactorInfo.phoneNumber,
241-
enrolledAt,
242-
};
243-
for (const objKey in authFactorInfo) {
244-
if (typeof authFactorInfo[objKey] === 'undefined') {
245-
delete authFactorInfo[objKey];
246-
}
247-
}
248-
result.mfaInfo.push(authFactorInfo);
249-
} else {
250-
// Unsupported second factor.
251-
throw new FirebaseAuthError(
252-
AuthClientErrorCode.INVALID_ENROLLED_FACTORS,
253-
`Unsupported second factor "${JSON.stringify(multiFactorInfo)}" provided.`);
254-
}
266+
result.mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
255267
});
256268
}
257269

src/auth/user-record.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ function parseDate(time: any): string {
4242
return null;
4343
}
4444

45+
interface SecondFactor {
46+
uid: string;
47+
phoneNumber: string;
48+
displayName?: string;
49+
enrollmentTime?: string;
50+
factorId: string;
51+
}
52+
4553
/** Parameters for update user operation */
4654
export interface UpdateRequest {
4755
disabled?: boolean;
@@ -51,6 +59,9 @@ export interface UpdateRequest {
5159
password?: string;
5260
phoneNumber?: string | null;
5361
photoURL?: string | null;
62+
multiFactor?: {
63+
enrolledFactors: SecondFactor[] | null;
64+
};
5465
}
5566

5667
/** Parameters for create user operation */

src/index.d.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,16 @@ declare namespace admin.auth {
639639
*/
640640
tenantId?: string | null;
641641

642+
multiFactor?: {
643+
enrolledFactors: Array<{
644+
uid: string;
645+
phoneNumber: string;
646+
displayName?: string;
647+
enrollmentTime?: string;
648+
factorId: string;
649+
}>;
650+
};
651+
642652
/**
643653
* @return A JSON-serializable representation of this object.
644654
*/
@@ -685,6 +695,16 @@ declare namespace admin.auth {
685695
* The user's photo URL.
686696
*/
687697
photoURL?: string | null;
698+
699+
multiFactor?: {
700+
enrolledFactors: Array<{
701+
uid: string;
702+
phoneNumber: string;
703+
displayName?: string;
704+
enrollmentTime?: string;
705+
factorId: string;
706+
}> | null;
707+
};
688708
}
689709

690710
/**
@@ -1000,6 +1020,16 @@ declare namespace admin.auth {
10001020
* to the tenant corresponding to that `TenantAwareAuth` instance's tenant ID.
10011021
*/
10021022
tenantId?: string | null;
1023+
1024+
multiFactor?: {
1025+
enrolledFactors: Array<{
1026+
uid: string;
1027+
phoneNumber: string;
1028+
displayName?: string;
1029+
enrollmentTime?: string;
1030+
factorId: string;
1031+
}>;
1032+
};
10031033
}
10041034

10051035
/**

test/integration/auth.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,31 @@ describe('admin.auth', () => {
315315

316316
it('updateUser() updates the user record with the given parameters', () => {
317317
const updatedDisplayName = 'Updated User ' + newUserUid;
318+
const now = new Date(1476235905000).toUTCString();
319+
const enrolledFactors = [
320+
{
321+
uid: 'mfaUid1',
322+
phoneNumber: '+16505550001',
323+
displayName: 'Work phone number',
324+
factorId: 'phone',
325+
enrollmentTime: now,
326+
},
327+
{
328+
uid: 'mfaUid2',
329+
phoneNumber: '+16505550002',
330+
displayName: 'Personal phone number',
331+
factorId: 'phone',
332+
enrollmentTime: now,
333+
},
334+
];
318335
return admin.auth().updateUser(newUserUid, {
319336
email: updatedEmail,
320337
phoneNumber: updatedPhone,
321338
emailVerified: true,
322339
displayName: updatedDisplayName,
340+
multiFactor: {
341+
enrolledFactors,
342+
},
323343
})
324344
.then((userRecord) => {
325345
expect(userRecord.emailVerified).to.be.true;
@@ -328,6 +348,31 @@ describe('admin.auth', () => {
328348
expect(userRecord.email).to.equal(updatedEmail);
329349
// Confirm expected phone number.
330350
expect(userRecord.phoneNumber).to.equal(updatedPhone);
351+
// Confirm second factors added to user.
352+
const actualUserRecord: {[key: string]: any} = userRecord.toJSON();
353+
expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2);
354+
expect(actualUserRecord.multiFactor.enrolledFactors).to.deep.equal(enrolledFactors);
355+
// Update list of second factors.
356+
return admin.auth().updateUser(newUserUid, {
357+
multiFactor: {
358+
enrolledFactors: [enrolledFactors[0]],
359+
},
360+
});
361+
})
362+
.then((userRecord) => {
363+
expect(userRecord.multiFactor.enrolledFactors.length).to.equal(1);
364+
const actualUserRecord: {[key: string]: any} = userRecord.toJSON();
365+
expect(actualUserRecord.multiFactor.enrolledFactors[0]).to.deep.equal(enrolledFactors[0]);
366+
// Remove all second factors.
367+
return admin.auth().updateUser(newUserUid, {
368+
multiFactor: {
369+
enrolledFactors: null,
370+
},
371+
});
372+
})
373+
.then((userRecord) => {
374+
// Confirm all second factors removed.
375+
expect(userRecord.multiFactor).to.be.undefined;
331376
});
332377
});
333378

0 commit comments

Comments
 (0)