Skip to content

feat(auth): add support for multi-factor auth with SMS code #5372

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

Closed
wants to merge 12 commits into from

Conversation

temaput
Copy link

@temaput temaput commented May 28, 2021

Description

Hi guys! Thanks for this wonderful library. I needed to use MFA on my project so I decided to go ahead and add the implementation. I only needed it for IOS so it is IOS only for now.
I can confirm that it works in my project, providing most of the functionality that Firebase provides (including unenrolling and user second factor status reporting).

This is my first experience working with native modules so obviously this implementation is not perfect and I have many doubts on different decisions I had to make. Hence it is only a draft of PR.

Here goes my list of questions if you don't mind:

  • (/auth/ios/RNFBAuth/RNFBAuthModule.m) Why do you store verificationId in users defaults and not just send it back from JS? Should I store some similar things in defaults?
  • (/auth/ios/RNFBAuth/RNFBAuthModule.m) Am I doing the right thing with the way I store multiple pointers to MFAResolver in the global dictionary mfaResolvers? Perhaps just use global var? See signInWithMultiFactorInfo. Should I store opaque session the same way to achieve complete accordance with firebase-js-sdk?
  • (/auth/ios/RNFBAuth/RNFBAuthModule.m) There is some kind of a bug when I read mfa factors in FIRUser enrolled factors: each of them suppose to have some value in factorID by which I should know to cast into phone auth factor and fetch the phone number. This doesn’t work and factorID is nil. It works however when factors are coming from MFA resolver (during sign in)
  • (/auth/ios/RNFBAuth/RNFBAuthModule.m) Multifactor-not-found error gets stuck somewhere after successful unenroll attempt and then it throws on the next enroll attempt unless the app is restarted. I didn’t really work with this, perhaps it is just another Firebase SDK bug, but most likely it is my implementation. Perhaps you know something that could explain this behaviour?
  • (/auth/package.json) How should I deal with package versioning? Not touch it? Update all? I decided to bump up the versions of the packages that I updated, but since I had to update the App package (it has the main FirebaseError structure in it) all other packages started complaining about them being outdated.
  • (/auth/lib/User.js) I feel like multiFactor property inside the user that I put as a getter function looks foreign in your code, perhaps you could advise on how to do it better?
  • Should the SMS handling part be designed with another kind of PhoneAuthListeners similar to different ConfirmationResults that I added (/auth/lib/ConfirmationResultMFAEnroll.js)? Currently I’m using a promise-based approach, but I suppose it wouldn’t work on Android?
  • Typescript typings are not done yet. That's not a problem to do for me later.
  • Tests are not written yet. Perhaps you could advise what kind of testing would be crucial here? I mean should I concentrate on e2e with simulator or just some simple unit tests first?
  • Can I modify .eslint?

Related issues

Fixes #4166

Release Summary

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan


Think react-native-firebase is great? Please consider supporting the project with any of the below:

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@vercel
Copy link

vercel bot commented May 28, 2021

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/invertase/react-native-firebase/9aLg4UN6wQetv2gYRjXcmCsRtVgv
✅ Preview: https://react-native-f-git-fork-temaput-implement-mfa-ios-in-135d3b.vercel.app

Copy link
Collaborator

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thank you!
I have family visiting this week so I may not be as responsive as normal but I have a few surface-level things I'll try to comment on now

This is a big work, for sure. One general comment I immediately noticed was that it may not have fidelity with firebase-js-sdk which would be a showstopper as noted with links in line comments

A second thing I noticed was that I'm not sure how you're developing this, but there are no jest tests for argument validation, and even though MFA may be hard to set up in our test project for actual MFA validation there are no E2E tests that at least probe the error cases and make sure the code in general is exercised - those are really important for our ability to maintain the code and know future releases are good, so I'll need at least the starts of entries for MFA in the auth jest / e2e area - luckily that's not that hard in fact it makes feature development easier in my experience, to develop that way https://github.com/invertase/react-native-firebase/blob/master/tests/README.md

@@ -16,6 +16,7 @@ module.exports = {
version: '16.1.0',
},
},
env: { node: true },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm dubious on need for any formatting changes as part of a feature implementation or bug fix, these should be a separate PR as they'll just confound review of real functionality. Please revert lint changes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove that

@@ -1,6 +1,6 @@
{
"name": "@react-native-firebase/app",
"version": "12.0.0",
"version": "12.0.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Versions are managed entirely by lerna and should never be touched in PRs, can you revert all version changes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood

@@ -1,6 +1,6 @@
{
"name": "@react-native-firebase/auth",
"version": "12.0.0",
"version": "12.0.3",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Versions are managed entirely by lerna and should never be touched in PRs, can you revert all version changes?

Note that if you need the results of these changes before the PR is approved / merged / released you may pull the "patch-package" artifact from the CI patches check, and incorporate it into your project. That was created to make it easier to close the loop between contributors here and their consuming projects since it's otherwise really difficult to incorporate change from our monorepo structure

Comment on lines +84 to +93
[FIRAuthErrorCodeSecondFactorRequired] = @"multi-factor-auth-required",
[FIRAuthErrorCodeMissingMultiFactorSession] = @"missing-multi-factor-session",
[FIRAuthErrorCodeMissingMultiFactorInfo] = @"missing-multi-factor-info",
[FIRAuthErrorCodeInvalidMultiFactorSession] = @"invalid-multi-factor-session",
[FIRAuthErrorCodeMultiFactorInfoNotFound] = @"multi-factor-info-not-found",
[FIRAuthErrorCodeSecondFactorAlreadyEnrolled] = @"second-factor-already-in-use",
[FIRAuthErrorCodeMaximumSecondFactorCountExceeded] = @"maximum-second-factor-count-exceeded",
[FIRAuthErrorCodeUnsupportedFirstFactor] = @"unsupported-first-factor",

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting nitpick - do you have a link for these error codes? That would allow me to cross-check for completeness/correctness

Suggested change
[FIRAuthErrorCodeSecondFactorRequired] = @"multi-factor-auth-required",
[FIRAuthErrorCodeMissingMultiFactorSession] = @"missing-multi-factor-session",
[FIRAuthErrorCodeMissingMultiFactorInfo] = @"missing-multi-factor-info",
[FIRAuthErrorCodeInvalidMultiFactorSession] = @"invalid-multi-factor-session",
[FIRAuthErrorCodeMultiFactorInfoNotFound] = @"multi-factor-info-not-found",
[FIRAuthErrorCodeSecondFactorAlreadyEnrolled] = @"second-factor-already-in-use",
[FIRAuthErrorCodeMaximumSecondFactorCountExceeded] = @"maximum-second-factor-count-exceeded",
[FIRAuthErrorCodeUnsupportedFirstFactor] = @"unsupported-first-factor",
[FIRAuthErrorCodeSecondFactorRequired] = @"multi-factor-auth-required",
[FIRAuthErrorCodeMissingMultiFactorSession] = @"missing-multi-factor-session",
[FIRAuthErrorCodeMissingMultiFactorInfo] = @"missing-multi-factor-info",
[FIRAuthErrorCodeInvalidMultiFactorSession] = @"invalid-multi-factor-session",
[FIRAuthErrorCodeMultiFactorInfoNotFound] = @"multi-factor-info-not-found",
[FIRAuthErrorCodeSecondFactorAlreadyEnrolled] = @"second-factor-already-in-use",
[FIRAuthErrorCodeMaximumSecondFactorCountExceeded] = @"maximum-second-factor-count-exceeded",
[FIRAuthErrorCodeUnsupportedFirstFactor] = @"unsupported-first-factor",

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +96 to 97
[mfaResolvers removeAllObjects];
[credentials removeAllObjects];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick - just for consistency, the rest of the code appears to work on mfaResolvers right after credentials, makes it easier to scan for consistent handling

Suggested change
[mfaResolvers removeAllObjects];
[credentials removeAllObjects];
[credentials removeAllObjects];
[mfaResolvers removeAllObjects];

Comment on lines +998 to +1002
FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *verificationId = [defaults stringForKey:@"authVerificationID"];
if (user)
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same fast return suggestion here

Suggested change
FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *verificationId = [defaults stringForKey:@"authVerificationID"];
if (user)
{
FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
if (!user) {
[self promiseNoUser:resolve rejecter:reject isError:YES];
return;
}
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *verificationId = [defaults stringForKey:@"authVerificationID"];

Comment on lines +954 to +956
if (error) {
[self promiseRejectAuthException:reject error:error];
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could replicate the "fast return" idea here as well on error conditions. Check for error, reject and return immediately, un-nest the remaining logic so all the un-happy paths are out of the way and we can focus on the happy path and making sure it makes sense

Comment on lines +1058 to +1063
} else {
[RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:(NSMutableDictionary *) @{
@"code": @"unsupported-second-factor",
@"message": @"This second factor is not supported",
}];
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same fast-error suggestion here - do this error handling first up top and return, then rest of the code is clean mentally from the extra indent / mental load

return @{
@"code": code,
@"message": message,
@"nativeErrorMessage": nativeErrorMessage,
@"authCredential" : authCredentialDict != nil ? (id) authCredentialDict : [NSNull null],
@"resolver" : mfaResolverDict != nil ? (id) mfaResolverDict : [NSNull null]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trialing comma to match prior line

Suggested change
@"resolver" : mfaResolverDict != nil ? (id) mfaResolverDict : [NSNull null]
@"resolver" : mfaResolverDict != nil ? (id) mfaResolverDict : [NSNull null],

*
*/

export default class ConfirmationResultMFAEnroll {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one and the SignIn class also need to be in index.d.ts exported as types I believe?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - this will complicate matters a bit but in all ways our types and APIs must match firebase-js-sdk, the stated goal of the project is to be a drop-in replacement for people migrating from firebase-js-sdk to us so we need 100% type/API compliance including when and how errors are thrown unless native means we must do something different

Here's a general hook to the batch of auth classes that should drive the definitions here, the APIs are linked around near this:
https://firebase.google.com/docs/reference/js/firebase.auth#classes

Cases where we might deviate are for instance when we are implementing a synchronous firebase-js-sdk API but it might throw an error, and in our implementation we have to go from JS to native. The only way to throw an error in that case for us since the react-native bridge is async is for us to make the API async in our implementation so we can appropriately bubble the native error up to JS and throw it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with you. I actually started that way first and even put all the appropriate types from corresponding firebase-js-sdk here here https://github.com/temaput/react-native-firebase/blob/implement-mfa/packages/auth/lib/index.d.ts, but then I got stuck having no Idea on how to pass opaque multifactor session https://firebase.google.com/docs/reference/ios/firebaseauth/api/reference/Classes#/c:objc(cs)FIRMultiFactorSession
across the bridge and decided to go with a shortcut.

But now, having some experience and your support I believe I could return to that path. I marked the most important questions for me to start going this way.

@mikehardy mikehardy added the Workflow: Needs Review Pending feedback or review from a maintainer. label May 28, 2021
@mikehardy
Copy link
Collaborator

with apologies I want to specifically note you have asked real questions in your description and I have addressed none of them, please have patience and I'll think through things more than surface level as soon as I can 🙏

@mikehardy mikehardy added the Workflow: Waiting for User Response Blocked waiting for user response. label May 28, 2021
@edkahara
Copy link

edkahara commented Aug 6, 2021

@mikehardy @temaput Is there an update on this feature? I'm currently trying to implement SMS-based MFA in react native but I don't see this functionality mentioned in the documentation.

@mikehardy
Copy link
Collaborator

All the development is in the open here, the status of this pr (draft, unmerged, no activity for a while) is the status that exists. If you'd like to take it up you can use it to base work on and I'll happily collaborate with you to get it ready for merge and released

@mikehardy
Copy link
Collaborator

apparently abandoned - happy to collaborate with any one that uses this directly so they can test it and assert it's working - just pull the branch and fix it up and we are in business

@mikehardy mikehardy closed this Nov 13, 2021
@mikehardy mikehardy removed the Workflow: Needs Review Pending feedback or review from a maintainer. label Nov 13, 2021
@mikehardy
Copy link
Collaborator

multi-factor released in version 11.3.0!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Workflow: Waiting for User Response Blocked waiting for user response.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[📚] Does this module support MFA with Firebase?
4 participants