@@ -25,7 +25,7 @@ import {
25
25
import { CreateRequest , UpdateRequest } from './user-record' ;
26
26
import {
27
27
UserImportBuilder , UserImportOptions , UserImportRecord ,
28
- UserImportResult ,
28
+ UserImportResult , AuthFactorInfo , convertMultiFactorInfoToServerFormat ,
29
29
} from './user-import-builder' ;
30
30
import * as utils from '../utils/index' ;
31
31
import { ActionCodeSettings , ActionCodeSettingsBuilder } from './action-code-settings-builder' ;
@@ -86,6 +86,16 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
86
86
const MAX_LIST_TENANT_PAGE_SIZE = 1000 ;
87
87
88
88
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
+
89
99
/** Defines a base utility to help with resource URL construction. */
90
100
class AuthResourceUrlBuilder {
91
101
@@ -180,6 +190,72 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder {
180
190
}
181
191
182
192
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
+
183
259
/**
184
260
* Validates a providerUserInfo object. All unsupported parameters
185
261
* are removed from the original request. If an invalid field is passed
@@ -244,10 +320,11 @@ function validateProviderUserInfo(request: any): void {
244
320
* are removed from the original request. If an invalid field is passed
245
321
* an error is thrown.
246
322
*
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 .
249
325
*/
250
- function validateCreateEditRequest ( request : any , uploadAccountRequest = false ) : void {
326
+ function validateCreateEditRequest ( request : any , writeOperationType : WriteOperationType ) : void {
327
+ const uploadAccountRequest = writeOperationType === WriteOperationType . Upload ;
251
328
// Hash set of whitelisted parameters.
252
329
const validKeys = {
253
330
displayName : true ,
@@ -272,6 +349,9 @@ function validateCreateEditRequest(request: any, uploadAccountRequest = false):
272
349
createdAt : uploadAccountRequest ,
273
350
lastLoginAt : uploadAccountRequest ,
274
351
providerUserInfo : uploadAccountRequest ,
352
+ mfaInfo : uploadAccountRequest ,
353
+ // Only for non-uploadAccount requests.
354
+ mfa : ! uploadAccountRequest ,
275
355
} ;
276
356
// Remove invalid keys from original request.
277
357
for ( const key in request ) {
@@ -410,6 +490,23 @@ function validateCreateEditRequest(request: any, uploadAccountRequest = false):
410
490
validateProviderUserInfo ( providerUserInfoEntry ) ;
411
491
} ) ;
412
492
}
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
+ }
413
510
}
414
511
415
512
@@ -508,7 +605,7 @@ export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('/accounts:update'
508
605
AuthClientErrorCode . INVALID_ARGUMENT ,
509
606
'"tenantId" is an invalid "UpdateRequest" property.' ) ;
510
607
}
511
- validateCreateEditRequest ( request ) ;
608
+ validateCreateEditRequest ( request , WriteOperationType . Update ) ;
512
609
} )
513
610
// Set response validator.
514
611
. setResponseValidator ( ( response : any ) => {
@@ -545,7 +642,7 @@ export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST
545
642
AuthClientErrorCode . INVALID_ARGUMENT ,
546
643
'"tenantId" is an invalid "CreateRequest" property.' ) ;
547
644
}
548
- validateCreateEditRequest ( request ) ;
645
+ validateCreateEditRequest ( request , WriteOperationType . Create ) ;
549
646
} )
550
647
// Set response validator.
551
648
. setResponseValidator ( ( response : any ) => {
@@ -867,7 +964,7 @@ export abstract class AbstractAuthRequestHandler {
867
964
// No need to validate raw request or raw response as this is done in UserImportBuilder.
868
965
const userImportBuilder = new UserImportBuilder ( users , options , ( userRequest : any ) => {
869
966
// Pass true to validate the uploadAccount specific fields.
870
- validateCreateEditRequest ( userRequest , true ) ;
967
+ validateCreateEditRequest ( userRequest , WriteOperationType . Upload ) ;
871
968
} ) ;
872
969
const request = userImportBuilder . buildRequest ( ) ;
873
970
// Fail quickly if more users than allowed are to be imported.
@@ -1014,6 +1111,28 @@ export abstract class AbstractAuthRequestHandler {
1014
1111
request . disableUser = request . disabled ;
1015
1112
delete request . disabled ;
1016
1113
}
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
+ }
1017
1136
return this . invokeRequestHandler ( this . getAuthUrlBuilder ( ) , FIREBASE_AUTH_SET_ACCOUNT_INFO , request )
1018
1137
. then ( ( response : any ) => {
1019
1138
return response . localId as string ;
@@ -1078,6 +1197,32 @@ export abstract class AbstractAuthRequestHandler {
1078
1197
request . localId = request . uid ;
1079
1198
delete request . uid ;
1080
1199
}
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
+ }
1081
1226
return this . invokeRequestHandler ( this . getAuthUrlBuilder ( ) , FIREBASE_AUTH_SIGN_UP_NEW_USER , request )
1082
1227
. then ( ( response : any ) => {
1083
1228
// Return the user id.
0 commit comments