@@ -41,6 +41,19 @@ const FIREBASE_AUTH_HEADER = {
4141const FIREBASE_AUTH_TIMEOUT = 10000 ;
4242
4343
44+ /** List of reserved claims which cannot be provided when creating a custom token. */
45+ export const RESERVED_CLAIMS = [
46+ 'acr' , 'amr' , 'at_hash' , 'aud' , 'auth_time' , 'azp' , 'cnf' , 'c_hash' , 'exp' , 'iat' ,
47+ 'iss' , 'jti' , 'nbf' , 'nonce' , 'sub' , 'firebase' ,
48+ ] ;
49+
50+ /** Maximum allowed number of characters in the custom claims payload. */
51+ const MAX_CLAIMS_PAYLOAD_SIZE = 1000 ;
52+
53+ /** Maximum allowed number of users to batch download at one time. */
54+ const MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE = 1000 ;
55+
56+
4457/**
4558 * Validates a create/edit request object. All unsupported parameters
4659 * are removed from the original request. If an invalid field is passed
@@ -64,6 +77,7 @@ function validateCreateEditRequest(request: any) {
6477 deleteProvider : true ,
6578 sanityCheck : true ,
6679 phoneNumber : true ,
80+ customAttributes : true ,
6781 } ;
6882 // Remove invalid keys from original request.
6983 for ( let key in request ) {
@@ -127,9 +141,67 @@ function validateCreateEditRequest(request: any) {
127141 // disabled externally. So the error message should use the client facing name.
128142 throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_DISABLED_FIELD ) ;
129143 }
144+ // customAttributes should be stringified JSON with no blacklisted claims.
145+ // The payload should not exceed 1KB.
146+ if ( typeof request . customAttributes !== 'undefined' ) {
147+ let developerClaims ;
148+ try {
149+ developerClaims = JSON . parse ( request . customAttributes ) ;
150+ } catch ( error ) {
151+ // JSON parsing error. This should never happen as we stringify the claims internally.
152+ // However, we still need to check since setAccountInfo via edit requests could pass
153+ // this field.
154+ throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_CLAIMS , error . message ) ;
155+ }
156+ const invalidClaims = [ ] ;
157+ // Check for any invalid claims.
158+ RESERVED_CLAIMS . forEach ( ( blacklistedClaim ) => {
159+ if ( developerClaims . hasOwnProperty ( blacklistedClaim ) ) {
160+ invalidClaims . push ( blacklistedClaim ) ;
161+ }
162+ } ) ;
163+ // Throw an error if an invalid claim is detected.
164+ if ( invalidClaims . length > 0 ) {
165+ throw new FirebaseAuthError (
166+ AuthClientErrorCode . FORBIDDEN_CLAIM ,
167+ invalidClaims . length > 1 ?
168+ `Developer claims "${ invalidClaims . join ( '", "' ) } " are reserved and cannot be specified.` :
169+ `Developer claim "${ invalidClaims [ 0 ] } " is reserved and cannot be specified.` ,
170+ ) ;
171+ }
172+ // Check claims payload does not exceed maxmimum size.
173+ if ( request . customAttributes . length > MAX_CLAIMS_PAYLOAD_SIZE ) {
174+ throw new FirebaseAuthError (
175+ AuthClientErrorCode . CLAIMS_TOO_LARGE ,
176+ `Developer claims payload should not exceed ${ MAX_CLAIMS_PAYLOAD_SIZE } characters.` ,
177+ ) ;
178+ }
179+ }
130180} ;
131181
132182
183+ /** Instantiates the downloadAccount endpoint settings. */
184+ export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings ( 'downloadAccount' , 'POST' )
185+ // Set request validator.
186+ . setRequestValidator ( ( request : any ) => {
187+ // Validate next page token.
188+ if ( typeof request . nextPageToken !== 'undefined' &&
189+ ! validator . isNonEmptyString ( request . nextPageToken ) ) {
190+ throw new FirebaseAuthError ( AuthClientErrorCode . INVALID_PAGE_TOKEN ) ;
191+ }
192+ // Validate max results.
193+ if ( ! validator . isNumber ( request . maxResults ) ||
194+ request . maxResults <= 0 ||
195+ request . maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE ) {
196+ throw new FirebaseAuthError (
197+ AuthClientErrorCode . INVALID_ARGUMENT ,
198+ `Required "maxResults" must be a positive non-zero number that does not exceed ` +
199+ `the allowed ${ MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE } .`
200+ ) ;
201+ }
202+ } ) ;
203+
204+
133205/** Instantiates the getAccountInfo endpoint settings. */
134206export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings ( 'getAccountInfo' , 'POST' )
135207 // Set request validator.
@@ -185,6 +257,13 @@ export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('setAccountInfo',
185257export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings ( 'signupNewUser' , 'POST' )
186258 // Set request validator.
187259 . setRequestValidator ( ( request : any ) => {
260+ // signupNewUser does not support customAttributes.
261+ if ( typeof request . customAttributes !== 'undefined' ) {
262+ throw new FirebaseAuthError (
263+ AuthClientErrorCode . INVALID_ARGUMENT ,
264+ `"customAttributes" cannot be set when creating a new user.` ,
265+ ) ;
266+ }
188267 validateCreateEditRequest ( request ) ;
189268 } )
190269 // Set response validator.
@@ -275,6 +354,40 @@ export class FirebaseAuthRequestHandler {
275354 return this . invokeRequestHandler ( FIREBASE_AUTH_GET_ACCOUNT_INFO , request ) ;
276355 }
277356
357+ /**
358+ * Exports the users (single batch only) with a size of maxResults and starting from
359+ * the offset as specified by pageToken.
360+ *
361+ * @param {number= } maxResults The page size, 1000 if undefined. This is also the maximum
362+ * allowed limit.
363+ * @param {string= } pageToken The next page token. If not specified, returns users starting
364+ * without any offset. Users are returned in the order they were created from oldest to
365+ * newest, relative to the page token offset.
366+ * @return {Promise<Object> } A promise that resolves with the current batch of downloaded
367+ * users and the next page token if available. For the last page, an empty list of users
368+ * and no page token are returned.
369+ */
370+ public downloadAccount (
371+ maxResults : number = MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE ,
372+ pageToken ?: string ) : Promise < { users : Object [ ] , nextPageToken ?: string } > {
373+ // Construct request.
374+ const request = {
375+ maxResults,
376+ nextPageToken : pageToken ,
377+ } ;
378+ // Remove next page token if not provided.
379+ if ( typeof request . nextPageToken === 'undefined' ) {
380+ delete request . nextPageToken ;
381+ }
382+ return this . invokeRequestHandler ( FIREBASE_AUTH_DOWNLOAD_ACCOUNT , request )
383+ . then ( ( response : any ) => {
384+ // No more users available.
385+ if ( ! response . users ) {
386+ response . users = [ ] ;
387+ }
388+ return response as { users : Object [ ] , nextPageToken ?: string } ;
389+ } ) ;
390+ }
278391
279392 /**
280393 * Deletes an account identified by a uid.
@@ -293,6 +406,41 @@ export class FirebaseAuthRequestHandler {
293406 return this . invokeRequestHandler ( FIREBASE_AUTH_DELETE_ACCOUNT , request ) ;
294407 }
295408
409+ /**
410+ * Sets additional developer claims on an existing user identified by provided UID.
411+ *
412+ * @param {string } uid The user to edit.
413+ * @param {Object } customUserClaims The developer claims to set.
414+ * @return {Promise<string> } A promise that resolves when the operation completes
415+ * with the user id that was edited.
416+ */
417+ public setCustomUserClaims ( uid : string , customUserClaims : Object ) : Promise < string > {
418+ // Validate user UID.
419+ if ( ! validator . isUid ( uid ) ) {
420+ return Promise . reject ( new FirebaseAuthError ( AuthClientErrorCode . INVALID_UID ) ) ;
421+ } else if ( ! validator . isObject ( customUserClaims ) ) {
422+ return Promise . reject (
423+ new FirebaseAuthError (
424+ AuthClientErrorCode . INVALID_ARGUMENT ,
425+ 'CustomUserClaims argument must be an object or null.' ,
426+ ) ,
427+ ) ;
428+ }
429+ // Delete operation. Replace null with an empty object.
430+ if ( customUserClaims === null ) {
431+ customUserClaims = { } ;
432+ }
433+ // Construct custom user attribute editting request.
434+ let request : any = {
435+ localId : uid ,
436+ customAttributes : JSON . stringify ( customUserClaims ) ,
437+ } ;
438+ return this . invokeRequestHandler ( FIREBASE_AUTH_SET_ACCOUNT_INFO , request )
439+ . then ( ( response : any ) => {
440+ return response . localId as string ;
441+ } ) ;
442+ }
443+
296444 /**
297445 * Edits an existing user.
298446 *
0 commit comments