@@ -25,7 +25,7 @@ import {
2525import { CreateRequest , UpdateRequest } from './user-record' ;
2626import {
2727 UserImportBuilder , UserImportOptions , UserImportRecord ,
28- UserImportResult ,
28+ UserImportResult , AuthFactorInfo , convertMultiFactorInfoToServerFormat ,
2929} from './user-import-builder' ;
3030import * as utils from '../utils/index' ;
3131import { ActionCodeSettings , ActionCodeSettingsBuilder } from './action-code-settings-builder' ;
@@ -86,6 +86,16 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
8686const 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. */
90100class 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.
0 commit comments