feat(auth): Adds support for importUsers API.#220
Conversation
This API can be used for bulk account import to Firebase Auth user database from external Auth systems using different hashing algorithms as well as other Firebase projects. fix(auth): Fixes quota exceeded errors thrown in auth integration tests due to backend throttling.
hiranya911
left a comment
There was a problem hiding this comment.
Code looks correct. Most of my comments are about style and readability.
src/auth/auth.ts
Outdated
| } | ||
|
|
||
| /** | ||
| * Imports the list of users provided to Firebase Auth users database. This is useful when |
There was a problem hiding this comment.
to Firebase Auth. sounds good enough I think. The word database in there might confuse somebody.
src/auth/auth.ts
Outdated
| * When importing a list of password users, UserImportOptions are required to be specified. | ||
| * | ||
| * @param {UserImportRecord[]} users The list of user records to import to Firebase Auth | ||
| * database. |
There was a problem hiding this comment.
Remove database from here too?
src/auth/user-import-builder.ts
Outdated
| this.indexMap = {}; | ||
|
|
||
| this.validateAndPopulateUsers(); | ||
| this.validateAndPopulateOptions(); |
There was a problem hiding this comment.
Can we refactor the code so the above two lines end up looking like:
this.validatedUsers = this.validateUsers(users);
this.validatedOptions = this.validateOptions(options);
There was a problem hiding this comment.
This is not as simple as you think, there are other private variables computed in the process of these. I prefer not to refactor this.
There was a problem hiding this comment.
Ok, i made that change but it contains private variables within.
src/auth/user-import-builder.ts
Outdated
| passwordHash: undefined, | ||
| salt: undefined, | ||
| lastLoginAt: undefined, | ||
| createdAt: undefined, |
There was a problem hiding this comment.
Can we not list these undefined fields here? Then you don't have to remove them later in the code.
There was a problem hiding this comment.
I add them so i don't have to define this interface. anyway, i can remove them but will need to define this interface. In addition, other fields could be undefined as passed from developer and still need to be removed. Anyway, i will remove and define the interface even though there is little gained from this.
src/auth/user-import-builder.ts
Outdated
| } | ||
|
|
||
|
|
||
| /** Callback function to validate an object. */ |
There was a problem hiding this comment.
Document the contract of this function in more details. What should it do when an invalid user is encountered?
src/auth/auth-api-request.ts
Outdated
| } | ||
|
|
||
| /** | ||
| * Imports the list of users provided to Firebase Auth users database. This is useful when |
There was a problem hiding this comment.
Lets drop database from the text. Here and elsewhere.
src/auth/user-import-builder.ts
Outdated
| * Returns the corresponding constructed uploadAccount request. | ||
| * @return {UploadAccountRequest} The constructed uploadAccount request. | ||
| */ | ||
| public generateRequest(): UploadAccountRequest { |
There was a problem hiding this comment.
I think buildRequest is a better name for this.
test/integration/auth.spec.ts
Outdated
| return safeDelete(randomUid); | ||
| }); | ||
|
|
||
| it('successfully imports users with HMAC_SHA256 hashed passwords', () => { |
There was a problem hiding this comment.
Is it possible to organize these tests by creating an array of test fixtures? For example:
interface UserImportTest {
name string
importOptions UserImportOptions
passwordHash string
}
const fixtures: Array<UserImportTest> = [
{
name: 'hmac',
importOptions: {...},
passwordHash: crypto.createHmac(...),
},
...
];
fixtures.forEach((fixture) => {
it(...)
});
There was a problem hiding this comment.
Ok, changing it as described. The passwordHash will be a function so i don't need to hardcode the parameters.
| photoURL: 'http://www.example.com/' + newUserUid + '/photo.png', | ||
| disabled: false, | ||
| }; | ||
| let deleteQueue = Promise.resolve(); |
There was a problem hiding this comment.
I overwrite this at the bottom every time I queue a single delete:
// Suppress errors in delete queue to not spill over to next item in queue.
deleteQueue = deletePromise.catch((error) => {
// Do nothing.
});
return deletePromise;
src/auth/user-import-builder.ts
Outdated
| * @return {UserImportResult} The user import result based on the returned failed | ||
| * uploadAccount response. | ||
| */ | ||
| public generateResponse( |
|
We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for the commit author(s). If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google. |
hiranya911
left a comment
There was a problem hiding this comment.
Looks pretty good. Just a few nits to address, and then I can LGTM.
src/auth/auth.ts
Outdated
| /** | ||
| * Imports the list of users provided to Firebase Auth. This is useful when | ||
| * migrating from an external authentication system without having to use the Firebase CLI SDK. | ||
| * A maximum list of 1000 users are allowed to be imported one at a time. |
There was a problem hiding this comment.
Nit: At most 1000 users are....
src/utils/error.ts
Outdated
| }; | ||
| public static INVALID_HASH_ALGORITHM = { | ||
| code: 'invalid-hash-algorithm', | ||
| message: 'The hash algorithm must be a string matching the list of supported algorithms.', |
There was a problem hiding this comment.
Nit: The hash algorithm must match one of the strings in the list of...
test/integration/auth.spec.ts
Outdated
| computePasswordHash: (userImportTest: UserImportTest): Buffer => { | ||
| return Buffer.from(scryptPasswordHash, 'base64'); | ||
| }, | ||
| rawPassword: scryptRawPassword, |
There was a problem hiding this comment.
Can these two also be password and NaCl like the other cases? In that case you can extract these fields out of the fixture and just make them shared constants.
There was a problem hiding this comment.
I modified the tests to all use the same rawPassword and rawSalt constants. However, it is not easy to generate the scrypt (internal version) password hash on the fly. The only way to do it is via: https://github.com/firebase/scrypt
test/integration/auth.spec.ts
Outdated
| } | ||
|
|
||
| /** | ||
| * Safe deletes a specificed user identified by uid. This API chains all delete |
| @@ -77,27 +101,12 @@ export interface UploadAccountRequest { | |||
| parallelization?: number; | |||
| blockSize?: number; | |||
| dkLen?: number; | |||
There was a problem hiding this comment.
I missed this in the previous review. Shall we spell this out like all the others?
There was a problem hiding this comment.
This is the backend name. We call this derivedKeyLength (the developer facing one). Check UserImportOptions inteface.
src/auth/user-import-builder.ts
Outdated
| return; | ||
| private populateOptions( | ||
| options: UserImportOptions, requiresHashOptions: boolean): UploadAccountOptions { | ||
| let populatedOptions: UploadAccountOptions = {}; |
There was a problem hiding this comment.
Can this be just let populatedOptions: UploadAccountOptions;?
There was a problem hiding this comment.
I actually return this when hash options are required but that is fine, i will return {} directly.
| } | ||
| index++; | ||
| }); | ||
| return populatedUsers; |
There was a problem hiding this comment.
If you wanted, you can just return a single array from this method containing both users and errors:
const populatedUsers = [];
users.forEach((user, index) => {
try {
const result = populateUploadAccountUser(user, userValidator);
populatedUsers.push(result);
} catch (error) {
populatedUsers.push({index, error});
}
})
return populatedUsers;
Then in the caller (constructor) you can make all the state changes:
const results = populateUsers(...);
this.validatedUsers = // filter out the users from results
this.indexMap = // construct the index map
this.requireHashOptions = // check if at least one user requires hash
I don't feel strongly about this, so I'll leave it to you.
There was a problem hiding this comment.
It is not easy constructing the indexMap after this completes. I have to remember the map original locations of the users to their new locations in the request sent to backend.
src/auth/user-import-builder.ts
Outdated
| let index = 0; | ||
| this.users.forEach((user) => { | ||
| const populatedUsers: UploadAccountUser[] = []; | ||
| users.forEach((user) => { |
There was a problem hiding this comment.
You could do users.forEach((user, index) => { here and avoid having to maintain you own index variable.
|
If you wish to amend the author of old commits, here's a useful guide: https://www.git-tower.com/learn/git/faq/change-author-name-email You basically has to perform an interactive rebase, and amend each commit with the incorrect email. |
OAuth providers. This helps test provider emails are correctly set.
|
Related to #158 |
This API can be used for bulk account import to Firebase Auth user database from external Auth systems using different hashing algorithms as well as other Firebase projects.
Unit tests and integration tests also added for this new API.
fix(auth): Fixes quota exceeded errors thrown in auth integration tests due to backend throttling.