Skip to content

Commit 0a6c956

Browse files
feat(auth): Multi-factor Auth support with SMS for Google Cloud Identity Platform (#804)
Defines multi-factor auth administrative APIs for Google Cloud Identity Platform.
1 parent 224f65f commit 0a6c956

15 files changed

+2022
-65
lines changed

package-lock.json

Lines changed: 3 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/auth/auth-api-request.ts

Lines changed: 152 additions & 7 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,
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';
@@ -86,6 +86,16 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
8686
const MAX_LIST_TENANT_PAGE_SIZE = 1000;
8787

8888

89+
/**
90+
* Enum for the user write operation type.
91+
*/
92+
enum WriteOperationType {
93+
Create = 'create',
94+
Update = 'update',
95+
Upload = 'upload',
96+
}
97+
98+
8999
/** Defines a base utility to help with resource URL construction. */
90100
class AuthResourceUrlBuilder {
91101

@@ -180,6 +190,72 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
180190
}
181191

182192

193+
/**
194+
* Validates an AuthFactorInfo object. All unsupported parameters
195+
* are removed from the original request. If an invalid field is passed
196+
* an error is thrown.
197+
*
198+
* @param request The AuthFactorInfo request object.
199+
* @param writeOperationType The write operation type.
200+
*/
201+
function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: WriteOperationType): void {
202+
const validKeys = {
203+
mfaEnrollmentId: true,
204+
displayName: true,
205+
phoneInfo: true,
206+
enrolledAt: true,
207+
};
208+
// Remove unsupported keys from the original request.
209+
for (const key in request) {
210+
if (!(key in validKeys)) {
211+
delete request[key];
212+
}
213+
}
214+
// No enrollment ID is available for signupNewUser. Use another identifier.
215+
const authFactorInfoIdentifier =
216+
request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request);
217+
const uidRequired = writeOperationType !== WriteOperationType.Create;
218+
if ((typeof request.mfaEnrollmentId !== 'undefined' || uidRequired) &&
219+
!validator.isNonEmptyString(request.mfaEnrollmentId)) {
220+
throw new FirebaseAuthError(
221+
AuthClientErrorCode.INVALID_UID,
222+
`The second factor "uid" must be a valid non-empty string.`,
223+
);
224+
}
225+
if (typeof request.displayName !== 'undefined' &&
226+
!validator.isString(request.displayName)) {
227+
throw new FirebaseAuthError(
228+
AuthClientErrorCode.INVALID_DISPLAY_NAME,
229+
`The second factor "displayName" for "${authFactorInfoIdentifier}" must be a valid string.`,
230+
);
231+
}
232+
// enrolledAt must be a valid UTC date string.
233+
if (typeof request.enrolledAt !== 'undefined' &&
234+
!validator.isISODateString(request.enrolledAt)) {
235+
throw new FirebaseAuthError(
236+
AuthClientErrorCode.INVALID_ENROLLMENT_TIME,
237+
`The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` +
238+
`UTC date string.`);
239+
}
240+
// Validate required fields depending on second factor type.
241+
if (typeof request.phoneInfo !== 'undefined') {
242+
// phoneNumber should be a string and a valid phone number.
243+
if (!validator.isPhoneNumber(request.phoneInfo)) {
244+
throw new FirebaseAuthError(
245+
AuthClientErrorCode.INVALID_PHONE_NUMBER,
246+
`The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` +
247+
`E.164 standard compliant identifier string.`);
248+
}
249+
} else {
250+
// Invalid second factor. For example, a phone second factor may have been provided without
251+
// a phone number. A TOTP based second factor may require a secret key, etc.
252+
throw new FirebaseAuthError(
253+
AuthClientErrorCode.INVALID_ENROLLED_FACTORS,
254+
`MFAInfo object provided is invalid.`);
255+
}
256+
}
257+
258+
183259
/**
184260
* Validates a providerUserInfo object. All unsupported parameters
185261
* are removed from the original request. If an invalid field is passed
@@ -244,10 +320,11 @@ function validateProviderUserInfo(request: any): void {
244320
* are removed from the original request. If an invalid field is passed
245321
* an error is thrown.
246322
*
247-
* @param {any} request The create/edit request object.
248-
* @param {boolean=} uploadAccountRequest Whether to validate as an uploadAccount request.
323+
* @param request The create/edit request object.
324+
* @param writeOperationType The write operation type.
249325
*/
250-
function validateCreateEditRequest(request: any, uploadAccountRequest = false): void {
326+
function validateCreateEditRequest(request: any, writeOperationType: WriteOperationType): void {
327+
const uploadAccountRequest = writeOperationType === WriteOperationType.Upload;
251328
// Hash set of whitelisted parameters.
252329
const validKeys = {
253330
displayName: true,
@@ -272,6 +349,9 @@ function validateCreateEditRequest(request: any, uploadAccountRequest = false):
272349
createdAt: uploadAccountRequest,
273350
lastLoginAt: uploadAccountRequest,
274351
providerUserInfo: uploadAccountRequest,
352+
mfaInfo: uploadAccountRequest,
353+
// Only for non-uploadAccount requests.
354+
mfa: !uploadAccountRequest,
275355
};
276356
// Remove invalid keys from original request.
277357
for (const key in request) {
@@ -410,6 +490,23 @@ function validateCreateEditRequest(request: any, uploadAccountRequest = false):
410490
validateProviderUserInfo(providerUserInfoEntry);
411491
});
412492
}
493+
// mfaInfo is used for importUsers.
494+
// mfa.enrollments is used for setAccountInfo.
495+
// enrollments has to be an array of valid AuthFactorInfo requests.
496+
let enrollments: AuthFactorInfo[] | null = null;
497+
if (request.mfaInfo) {
498+
enrollments = request.mfaInfo;
499+
} else if (request.mfa && request.mfa.enrollments) {
500+
enrollments = request.mfa.enrollments;
501+
}
502+
if (enrollments) {
503+
if (!validator.isArray(enrollments)) {
504+
throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS);
505+
}
506+
enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => {
507+
validateAuthFactorInfo(authFactorInfoEntry, writeOperationType);
508+
});
509+
}
413510
}
414511

415512

@@ -508,7 +605,7 @@ export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('/accounts:update'
508605
AuthClientErrorCode.INVALID_ARGUMENT,
509606
'"tenantId" is an invalid "UpdateRequest" property.');
510607
}
511-
validateCreateEditRequest(request);
608+
validateCreateEditRequest(request, WriteOperationType.Update);
512609
})
513610
// Set response validator.
514611
.setResponseValidator((response: any) => {
@@ -545,7 +642,7 @@ export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST
545642
AuthClientErrorCode.INVALID_ARGUMENT,
546643
'"tenantId" is an invalid "CreateRequest" property.');
547644
}
548-
validateCreateEditRequest(request);
645+
validateCreateEditRequest(request, WriteOperationType.Create);
549646
})
550647
// Set response validator.
551648
.setResponseValidator((response: any) => {
@@ -867,7 +964,7 @@ export abstract class AbstractAuthRequestHandler {
867964
// No need to validate raw request or raw response as this is done in UserImportBuilder.
868965
const userImportBuilder = new UserImportBuilder(users, options, (userRequest: any) => {
869966
// Pass true to validate the uploadAccount specific fields.
870-
validateCreateEditRequest(userRequest, true);
967+
validateCreateEditRequest(userRequest, WriteOperationType.Upload);
871968
});
872969
const request = userImportBuilder.buildRequest();
873970
// Fail quickly if more users than allowed are to be imported.
@@ -1014,6 +1111,28 @@ export abstract class AbstractAuthRequestHandler {
10141111
request.disableUser = request.disabled;
10151112
delete request.disabled;
10161113
}
1114+
// Construct mfa related user data.
1115+
if (validator.isNonNullObject(request.multiFactor)) {
1116+
if (request.multiFactor.enrolledFactors === null) {
1117+
// Remove all second factors.
1118+
request.mfa = {};
1119+
} else if (validator.isArray(request.multiFactor.enrolledFactors)) {
1120+
request.mfa = {
1121+
enrollments: [],
1122+
};
1123+
try {
1124+
request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => {
1125+
request.mfa.enrollments.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
1126+
});
1127+
} catch (e) {
1128+
return Promise.reject(e);
1129+
}
1130+
if (request.mfa.enrollments.length === 0) {
1131+
delete request.mfa.enrollments;
1132+
}
1133+
}
1134+
delete request.multiFactor;
1135+
}
10171136
return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request)
10181137
.then((response: any) => {
10191138
return response.localId as string;
@@ -1078,6 +1197,32 @@ export abstract class AbstractAuthRequestHandler {
10781197
request.localId = request.uid;
10791198
delete request.uid;
10801199
}
1200+
// Construct mfa related user data.
1201+
if (validator.isNonNullObject(request.multiFactor)) {
1202+
if (validator.isNonEmptyArray(request.multiFactor.enrolledFactors)) {
1203+
const mfaInfo: AuthFactorInfo[] = [];
1204+
try {
1205+
request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => {
1206+
// Enrollment time and uid are not allowed for signupNewUser endpoint.
1207+
// They will automatically be provisioned server side.
1208+
if (multiFactorInfo.enrollmentTime) {
1209+
throw new FirebaseAuthError(
1210+
AuthClientErrorCode.INVALID_ARGUMENT,
1211+
'"enrollmentTime" is not supported when adding second factors via "createUser()"');
1212+
} else if (multiFactorInfo.uid) {
1213+
throw new FirebaseAuthError(
1214+
AuthClientErrorCode.INVALID_ARGUMENT,
1215+
'"uid" is not supported when adding second factors via "createUser()"');
1216+
}
1217+
mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));
1218+
});
1219+
} catch (e) {
1220+
return Promise.reject(e);
1221+
}
1222+
request.mfaInfo = mfaInfo;
1223+
}
1224+
delete request.multiFactor;
1225+
}
10811226
return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SIGN_UP_NEW_USER, request)
10821227
.then((response: any) => {
10831228
// Return the user id.

src/auth/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export interface DecodedIdToken {
7070
[key: string]: any;
7171
};
7272
sign_in_provider: string;
73+
sign_in_second_factor?: string;
74+
second_factor_identifier?: string;
7375
[key: string]: any;
7476
};
7577
iat: number;

0 commit comments

Comments
 (0)