Skip to content

Commit 3c64bf5

Browse files
authored
feat(auth): Add multi-factor support for the sign-in flow (#6593)
* feat(auth): adds iOS/Web support for multi-factor sign-in flow Adds code required to complete the sign-in flow for users that have enrolled second factors. Due to a difference in the implementation of the PhoneAuthProvider it is not possible to follow the implementation of the Web API. * docs(auth): update documentation for multi-factor authentication Provide general setup and usage information for multi-factor authentication flows. * fix(docs): Change prev link for Firestore docs to multi-factor auth * feat(auth): Adds Android support for multi-factor sign-in flow Implement the Android part required to support the multi-factor sign-in flow. Makes the `multiFactor` property for the Firebase user object available as well. Known issues: - The enrollmentInfo for a MultiFactorInfo is currently reported with 0 on Android. * feat(auth): multi-factor enroll feature for Android and e2e-tests * feat(auth): Implement multi-factor enrollment for iOS * fix(tests): More robust matchers for invalid-verification-code error * Fix error message for invalid phone numbers to match the error message produced by the Web. * Add test and fix error message for unknown multi-factor hint to match the error message produced by the Web.
1 parent e6fbf59 commit 3c64bf5

File tree

18 files changed

+1621
-14
lines changed

18 files changed

+1621
-14
lines changed

docs/auth/multi-factor-auth.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
---
2+
title: Multi-factor Auth
3+
description: Increase security by adding Multi-factor authentication to your app.
4+
next: /firestore/usage
5+
previous: /auth/phone-auth
6+
---
7+
8+
# iOS Setup
9+
10+
Make sure to follow [the official Identity Platform
11+
documentation](https://cloud.google.com/identity-platform/docs/ios/mfa#enabling_multi-factor_authentication)
12+
to enable multi-factor authentication for your project and verify your app.
13+
14+
# Enroll a new factor
15+
16+
> Before a user can enroll a second factor they need to verify their email. See
17+
> [`User`](/reference/auth/user#sendEmailVerification) interface is returned.
18+
19+
Begin by obtaining a [`MultiFactorUser`](/reference/auth/multifactoruser)
20+
instance for the current user. This is the entry point for most multi-factor
21+
operations:
22+
23+
```js
24+
import auth from '@react-native-firebase/auth';
25+
const multiFactorUser = await auth.multiFactor(auth());
26+
```
27+
28+
Request the session identifier and use the phone number obtained from the user
29+
to send a verification code:
30+
31+
```js
32+
const session = await multiFactorUser.getSession();
33+
const phoneOptions = {
34+
phoneNumber,
35+
session,
36+
};
37+
38+
// Sends a text message to the user
39+
const verificationId = await auth().verifyPhoneNumberForMultiFactor(phoneOptions);
40+
```
41+
42+
Once the user has provided the verification code received by text message, you
43+
can complete the process:
44+
45+
```js
46+
const cred = auth.PhoneAuthProvider.credential(verificationId, verificationCode);
47+
const multiFactorAssertion = auth.PhoneMultiFactorGenerator.assertion(cred);
48+
await multiFactorUser.enroll(multiFactorAssertion, 'Optional display name for the user);
49+
```
50+
51+
You can inspect [`User#multiFactor`](/reference/auth/user#multiFactor) for
52+
information about the user's enrolled factors.
53+
54+
# Sign-in flow using multi-factor
55+
56+
Ensure the account has already enrolled a second factor. Begin by calling the
57+
default sign-in methods, for example email and password. If the account requires
58+
a second factor to complete login, an exception will be raised:
59+
60+
```js
61+
import auth from '@react-native-firebase/auth';
62+
63+
auth()
64+
.signInWithEmailAndPassword(email, password)
65+
.then(() => {
66+
// User has not enrolled a second factor
67+
})
68+
.catch(error => {
69+
const { code } = error;
70+
// Make sure to check if multi factor authentication is required
71+
if (code === 'auth/multi-factor-auth-required') {
72+
return;
73+
}
74+
75+
// Other error
76+
});
77+
```
78+
79+
Using the error object you can obtain a
80+
[`MultiFactorResolver`](/reference/auth/multifactorresolver) instance and
81+
continue the flow:
82+
83+
```js
84+
const resolver = auth.getMultiFactorResolver(auth(), error);
85+
```
86+
87+
The resolver object has all the required information to prompt the user for a
88+
specific factor:
89+
90+
```js
91+
if (resolver.hints.length > 1) {
92+
// Use resolver.hints to display a list of second factors to the user
93+
}
94+
95+
// Currently only phone based factors are supported
96+
if (resolver.hints[0].factorId === auth.PhoneMultiFactorGenerator.FACTOR_ID) {
97+
// Continue with the sign-in flow
98+
}
99+
```
100+
101+
Using a multi-factor hint and the session information you can send a
102+
verification code to the user:
103+
104+
```js
105+
const hint = resolver.hints[0];
106+
const sessionId = resolver.session;
107+
108+
auth()
109+
.verifyPhoneNumberWithMultiFactorInfo(hint, sessionId) // triggers the message to the user
110+
.then(verificationId => setVerificationId(verificationId));
111+
```
112+
113+
Once the user has entered the verification code you can create a multi-factor
114+
assertion and finish the flow:
115+
116+
```js
117+
const credential = auth.PhoneAuthProvider.credential(verificationId, verificationCode);
118+
119+
const multiFactorAssertion = auth.PhoneMultiFactorGenerator.assertion(credential);
120+
121+
resolver.resolveSignIn(multiFactorAssertion).then(userCredential => {
122+
// additionally onAuthStateChanged will be triggered as well
123+
});
124+
```
125+
126+
Upon successful sign-in, any
127+
[`onAuthStateChanged`](/auth/usage#listening-to-authentication-state) listeners
128+
will trigger with the new authentication state of the user.
129+
130+
To put the example together:
131+
132+
```js
133+
import auth from '@react-native-firebase/auth';
134+
135+
const authInstance = auth();
136+
137+
authInstance
138+
.signInWithEmailAndPassword(email, password)
139+
.then(() => {
140+
// User has not enrolled a second factor
141+
})
142+
.catch(error => {
143+
const { code } = error;
144+
// Make sure to check if multi factor authentication is required
145+
if (code !== 'auth/multi-factor-auth-required') {
146+
const resolver = auth.getMultiFactorResolver(authInstance, error);
147+
148+
if (resolver.hints.length > 1) {
149+
// Use resolver.hints to display a list of second factors to the user
150+
}
151+
152+
// Currently only phone based factors are supported
153+
if (resolver.hints[0].factorId === auth.PhoneMultiFactorGenerator.FACTOR_ID) {
154+
const hint = resolver.hints[0];
155+
const sessionId = resolver.session;
156+
157+
authInstance
158+
.verifyPhoneNumberWithMultiFactorInfo(hint, sessionId) // triggers the message to the user
159+
.then(verificationId => setVerificationId(verificationId));
160+
161+
// Request verificationCode from user
162+
163+
const credential = auth.PhoneAuthProvider.credential(verificationId, verificationCode);
164+
165+
const multiFactorAssertion = auth.PhoneMultiFactorGenerator.assertion(credential);
166+
167+
resolver.resolveSignIn(multiFactorAssertion).then(userCredential => {
168+
// additionally onAuthStateChanged will be triggered as well
169+
});
170+
}
171+
}
172+
});
173+
```
174+
175+
# Testing
176+
177+
You can define test phone numbers and corresponding verification codes. The
178+
official[official
179+
guide](https://cloud.google.com/identity-platform/docs/ios/mfa#enabling_multi-factor_authentication)
180+
contains more information on setting this up.

docs/auth/phone-auth.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Phone Authentication
33
description: Sign-in users with their phone number.
4-
next: /firestore/usage
4+
next: /auth/multi-factor-auth
55
previous: /auth/social-auth
66
---
77

docs/firestore/usage/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Cloud Firestore
33
description: Installation and getting started with Firestore.
44
icon: //static.invertase.io/assets/firebase/cloud-firestore.svg
55
next: /firestore/usage-with-flatlists
6-
previous: /auth/phone-auth
6+
previous: /auth/multi-factor-auth
77
---
88

99
# Installation

docs/sidebar.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
- '/auth/social-auth'
3939
- - Phone Auth
4040
- '/auth/phone-auth'
41+
- - Multi-factor Auth
42+
- '/auth/multi-factor-auth'
4143
- '//static.invertase.io/assets/firebase/authentication.svg'
4244
- - Cloud Firestore
4345
- - - Usage

packages/app/android/src/reactnative/java/io/invertase/firebase/common/ReactNativeFirebaseModule.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ public static void rejectPromiseWithExceptionMap(Promise promise, Exception exce
4343
promise.reject(exception, SharedUtils.getExceptionMap(exception));
4444
}
4545

46+
public static void rejectPromiseWithCodeAndMessage(
47+
Promise promise, String code, String message, ReadableMap resolver) {
48+
WritableMap userInfoMap = Arguments.createMap();
49+
userInfoMap.putString("code", code);
50+
userInfoMap.putString("message", message);
51+
if (resolver != null) {
52+
userInfoMap.putMap("resolver", resolver);
53+
}
54+
promise.reject(code, message, userInfoMap);
55+
}
56+
4657
public static void rejectPromiseWithCodeAndMessage(Promise promise, String code, String message) {
4758
WritableMap userInfoMap = Arguments.createMap();
4859
userInfoMap.putString("code", code);

packages/auth/__tests__/auth.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { describe, expect, it } from '@jest/globals';
22

33
import auth, { firebase } from '../lib';
44

5+
// @ts-ignore - We don't mind missing types here
6+
import { NativeFirebaseError } from '../../app/lib/internal';
7+
58
describe('Auth', function () {
69
describe('namespace', function () {
710
it('accessible from firebase.app()', function () {
@@ -69,4 +72,50 @@ describe('Auth', function () {
6972
}
7073
});
7174
});
75+
76+
describe('getMultiFactorResolver', function () {
77+
it('should return null if no resolver object is found', function () {
78+
const unknownError = NativeFirebaseError.fromEvent(
79+
{
80+
code: 'unknown',
81+
},
82+
'auth',
83+
);
84+
const actual = auth.getMultiFactorResolver(auth(), unknownError);
85+
expect(actual).toBe(null);
86+
});
87+
88+
it('should return null if resolver object is null', function () {
89+
const unknownError = NativeFirebaseError.fromEvent(
90+
{
91+
code: 'unknown',
92+
resolver: null,
93+
},
94+
'auth',
95+
);
96+
const actual = auth.getMultiFactorResolver(firebase.app().auth(), unknownError);
97+
expect(actual).toBe(null);
98+
});
99+
100+
it('should return the resolver object if its found', function () {
101+
const resolver = { session: '', hints: [] };
102+
const errorWithResolver = NativeFirebaseError.fromEvent(
103+
{
104+
code: 'multi-factor-auth-required',
105+
resolver,
106+
},
107+
'auth',
108+
);
109+
const actual = auth.getMultiFactorResolver(firebase.app().auth(), errorWithResolver);
110+
// Using expect(actual).toEqual(resolver) causes unexpected errors:
111+
// You attempted to use "firebase.app('[DEFAULT]').appCheck" but this module could not be found.
112+
expect(actual).not.toBeNull();
113+
// @ts-ignore We know actual is not null
114+
expect(actual.session).toEqual(resolver.session);
115+
// @ts-ignore We know actual is not null
116+
expect(actual.hints).toEqual(resolver.hints);
117+
// @ts-ignore We know actual is not null
118+
expect(actual._auth).not.toBeNull();
119+
});
120+
});
72121
});

0 commit comments

Comments
 (0)