-
Notifications
You must be signed in to change notification settings - Fork 992
Authentication Emulator Supports MFA for Simple Read/Write User Operations (Fixes #3170) #3173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@wokkaflokka thanks for the super clear issue and the PR! Assigning to @yuchenshi who does most of the Auth emulator things around here. (also 👋 we met back in the Firestore alpha days) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR. Unfortunately, this creates a confusing situation where a user with mfaInfo
can still sign in without MFA. I understand that you won't need to full implementation for you use case, but would you mind making it so that users with mfaInfo
cannot sign in at all with a NotImplementedError
?
@yuchenshi thanks for the review. I'll get to work on those changes. |
…Authentication Emulator
… to the `updateUser` path + add test cases for validation of `signUp` MFA flows
@yuchenshi appreciate all of your help to date. I've completed a second pass, addressing your initial review comments and extending support to the update case. My personal schedule should be less chaotic for the foreseeable future, so I should also be able to respond to any and all feedback on a quicker cycle at this time. Summary of Changes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great and I've left a few comments inline. The big blockers are the uniqueness checks and consistency for the maps in state.
And as of this PR, mfaInfo
will be lost when you export and re-import users. The scope of the PR is already quite big and I think we should leave a TODO and address it in a follow-up PR.
@yuchenshi once again, thanks for the thorough review. I'll have another pass this evening or tomorrow morning addressing the points you raise. To summarize what I gleaned from review/what I intend to change in my follow-up:
|
2. match SDK behavior with respect to MFA ID uniqueness constraints 3. stylistic updates to test code 4. add TODOs regarding import/export MFA support
…A Enrollment IDs on create and update
@yuchenshi -- well, overpromised and underdelivered, but I've completed another pass of this PR. Changes are in two commits. I believe all existing comments should be addressed; the changes ended up being larger than expected, but I think it was a net positive. One of the benefits of this change is that I think we achieved a simplified state management for MFA data. In the first commit, I implemented the points 1-4 I mentioned I would previously fix. In a final testing pass, I spent a little bit of time testing against my real project to see how create/update work when duplicate enrollment ID's and duplicate phone numbers are specified. Based on the behaviors I observed, I implemented some changes in a second commit. These behaviors match the SDK for the following:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're pretty close here. The only real blocker is using some constants for phone numbers instead of real phone numbers.
const secondMfaFactor = { | ||
displayName: "Second MFA Factor", | ||
phoneInfo: TEST_PHONE_NUMBER, | ||
phoneInfo: "+12813308004", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate the reference, but let's not put Mike's number in the source code. If you need a different phone number, please create a constant like TEST_PHONE_NUMBER_2
and stick to the fictional blocks (e.g. the next number after TEST_PHONE_NUMBER
). I think we'd need three or four of these.
src/emulator/auth/operations.ts
Outdated
function getMfaEnrollmentsFromRequest( | ||
state: ProjectState, | ||
request: MfaEnrollments, | ||
getEnrollmentId: (enrollment: MfaEnrollment, enrollmentIds: Set<string>) => string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd actually suggest just options: {generateEnrollmentIds: boolean}
instead. Less code, less abstractions and more readable.
const savedMfaInfo = savedUserInfo.mfaInfo![0]; | ||
expect(savedMfaInfo?.mfaEnrollmentId).to.be.a("string").and.not.empty; | ||
savedMfaInfo.displayName = "New Display Name"; | ||
savedMfaInfo.phoneInfo = "+15555550101"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly this could use a constant like TEST_PHONE_NUMBER_2
const user = { | ||
email: "[email protected]", | ||
password: "notasecret", | ||
mfaInfo: [mfaInfo, mfaInfo, mfaInfo], | ||
mfaInfo: [TEST_MFA_INFO, { ...TEST_MFA_INFO, phoneInfo: "+12813308004" }], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
|
||
const newMfaInfo = { | ||
displayName: "New New", | ||
phoneInfo: "+12813308004", | ||
phoneInfo: "+12813308005", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use a test phone number please
const user = { | ||
email: "[email protected]", | ||
password: "notasecret", | ||
mfaInfo: [mfaInfo, mfaInfo, mfaInfo], | ||
mfaInfo: [TEST_MFA_INFO, { ...TEST_MFA_INFO, phoneInfo: "+12813308004" }], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish we had more test phone numbers Back Then.
expect(info.mfaInfo).to.have.length(2); | ||
for (const savedMfaInfo of info.mfaInfo!) { | ||
if (savedMfaInfo.phoneInfo !== TEST_MFA_INFO.phoneInfo) { | ||
expect(savedMfaInfo.phoneInfo).to.eq("+12813308004"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I were you I'd probably do a search and replace
TEST_MFA_INFO, | ||
TEST_MFA_INFO, | ||
TEST_MFA_INFO, | ||
{ ...TEST_MFA_INFO, phoneInfo: "+12813308004" }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
25,000 calls per day
expect(bobInfo.mfaInfo).to.have.length(2); | ||
for (const savedMfaInfo of bobInfo.mfaInfo!) { | ||
if (savedMfaInfo.phoneInfo !== TEST_MFA_INFO.phoneInfo) { | ||
expect(savedMfaInfo.phoneInfo).to.eq("+12813308004"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we stress testing the phone carrier?
phoneInfo: TEST_PHONE_NUMBER, | ||
mfaEnrollmentId: randomId(28), | ||
it("should ignore if multi factor enrollment ID is specified on create", async () => { | ||
const mfaEnrollmentId1 = randomId(28); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's hard code some IDs like thisShouldBeIgnored1
-- more readable, and the test won't randomly fail if we had really really bad (good?) luck with the RNG.
@yuchenshi all very reasonable. Can't blame ya!
Thanks very much for all your time and help so far. Much appreciated. |
@wokkaflokka One last thing -- I meant that we should use |
@yuchenshi roger that, pushed another one. |
@wokkaflokka Merged! Thanks a ton for the PR and I've filed #3221 to track the remaining work. |
@wokkaflokka thank you so much for this contribution and thanks @yuchenshi for the super thorough reviews! |
…tions (Fixes firebase#3170) (firebase#3173) * Authentication Emulator Supports MFA Info for Simple Read/Write User Operations (firebase#3170) * throw NotImplementedError if an MFA user attempts to login using the Authentication Emulator * bring validation into operations layer and extend MFA related support to the `updateUser` path + add test cases for validation of `signUp` MFA flows * 1. simplify state handling for MFA 2. match SDK behavior with respect to MFA ID uniqueness constraints 3. stylistic updates to test code 4. add TODOs regarding import/export MFA support * match the SDK behavior for duplicated phone numbers and duplicated MFA Enrollment IDs on create and update * change import for brevity * update constants and IDs used in tests + simplify ID generation * update variables in test * Update CHANGELOG.md Co-authored-by: Yuchen Shi <[email protected]>
Description
This PR proposes an initial resolution for #3170 by implementing rudimentary support for "mutli-factor" information during admin SDK operations (e.g.,
admin.auth().createUser(...)
).Note, deletes, client operations, and advanced scenarios are intentionally excluded from this change at this time. It seems like there is large potential scope of support for MFA in general, and possibly some changes going on under the hood, and I wanted to keep this as focused as possible to start.
Scenarios Tested
This change is focused on supporting read/write operations from the admin SDK while using the Authentication Emulator. It has been tested using the test case provided on #3170 as well as a modified variant of the test case from #3170, provided here.
Sample Commands
To run the test case above, configure a minimal typescript project with the implied dependencies and execute something like:
firebase emulators:exec --debug 'npm run test firebaseRepro.test.ts'
Fixes #3170.