Skip to content

Commit

Permalink
feat(auth): Add multi-factor support for the sign-in flow (#6593)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
fzuellich committed Oct 26, 2022
1 parent e6fbf59 commit 3c64bf5
Show file tree
Hide file tree
Showing 18 changed files with 1,621 additions and 14 deletions.
180 changes: 180 additions & 0 deletions docs/auth/multi-factor-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
---
title: Multi-factor Auth
description: Increase security by adding Multi-factor authentication to your app.
next: /firestore/usage
previous: /auth/phone-auth
---

# iOS Setup

Make sure to follow [the official Identity Platform
documentation](https://cloud.google.com/identity-platform/docs/ios/mfa#enabling_multi-factor_authentication)
to enable multi-factor authentication for your project and verify your app.

# Enroll a new factor

> Before a user can enroll a second factor they need to verify their email. See
> [`User`](/reference/auth/user#sendEmailVerification) interface is returned.
Begin by obtaining a [`MultiFactorUser`](/reference/auth/multifactoruser)
instance for the current user. This is the entry point for most multi-factor
operations:

```js
import auth from '@react-native-firebase/auth';
const multiFactorUser = await auth.multiFactor(auth());
```

Request the session identifier and use the phone number obtained from the user
to send a verification code:

```js
const session = await multiFactorUser.getSession();
const phoneOptions = {
phoneNumber,
session,
};

// Sends a text message to the user
const verificationId = await auth().verifyPhoneNumberForMultiFactor(phoneOptions);
```

Once the user has provided the verification code received by text message, you
can complete the process:

```js
const cred = auth.PhoneAuthProvider.credential(verificationId, verificationCode);
const multiFactorAssertion = auth.PhoneMultiFactorGenerator.assertion(cred);
await multiFactorUser.enroll(multiFactorAssertion, 'Optional display name for the user);
```
You can inspect [`User#multiFactor`](/reference/auth/user#multiFactor) for
information about the user's enrolled factors.

# Sign-in flow using multi-factor

Ensure the account has already enrolled a second factor. Begin by calling the
default sign-in methods, for example email and password. If the account requires
a second factor to complete login, an exception will be raised:

```js
import auth from '@react-native-firebase/auth';
auth()
.signInWithEmailAndPassword(email, password)
.then(() => {
// User has not enrolled a second factor
})
.catch(error => {
const { code } = error;
// Make sure to check if multi factor authentication is required
if (code === 'auth/multi-factor-auth-required') {
return;
}
// Other error
});
```

Using the error object you can obtain a
[`MultiFactorResolver`](/reference/auth/multifactorresolver) instance and
continue the flow:

```js
const resolver = auth.getMultiFactorResolver(auth(), error);
```

The resolver object has all the required information to prompt the user for a
specific factor:

```js
if (resolver.hints.length > 1) {
// Use resolver.hints to display a list of second factors to the user
}
// Currently only phone based factors are supported
if (resolver.hints[0].factorId === auth.PhoneMultiFactorGenerator.FACTOR_ID) {
// Continue with the sign-in flow
}
```

Using a multi-factor hint and the session information you can send a
verification code to the user:

```js
const hint = resolver.hints[0];
const sessionId = resolver.session;
auth()
.verifyPhoneNumberWithMultiFactorInfo(hint, sessionId) // triggers the message to the user
.then(verificationId => setVerificationId(verificationId));
```

Once the user has entered the verification code you can create a multi-factor
assertion and finish the flow:

```js
const credential = auth.PhoneAuthProvider.credential(verificationId, verificationCode);
const multiFactorAssertion = auth.PhoneMultiFactorGenerator.assertion(credential);
resolver.resolveSignIn(multiFactorAssertion).then(userCredential => {
// additionally onAuthStateChanged will be triggered as well
});
```

Upon successful sign-in, any
[`onAuthStateChanged`](/auth/usage#listening-to-authentication-state) listeners
will trigger with the new authentication state of the user.

To put the example together:

```js
import auth from '@react-native-firebase/auth';
const authInstance = auth();
authInstance
.signInWithEmailAndPassword(email, password)
.then(() => {
// User has not enrolled a second factor
})
.catch(error => {
const { code } = error;
// Make sure to check if multi factor authentication is required
if (code !== 'auth/multi-factor-auth-required') {
const resolver = auth.getMultiFactorResolver(authInstance, error);
if (resolver.hints.length > 1) {
// Use resolver.hints to display a list of second factors to the user
}
// Currently only phone based factors are supported
if (resolver.hints[0].factorId === auth.PhoneMultiFactorGenerator.FACTOR_ID) {
const hint = resolver.hints[0];
const sessionId = resolver.session;
authInstance
.verifyPhoneNumberWithMultiFactorInfo(hint, sessionId) // triggers the message to the user
.then(verificationId => setVerificationId(verificationId));
// Request verificationCode from user
const credential = auth.PhoneAuthProvider.credential(verificationId, verificationCode);
const multiFactorAssertion = auth.PhoneMultiFactorGenerator.assertion(credential);
resolver.resolveSignIn(multiFactorAssertion).then(userCredential => {
// additionally onAuthStateChanged will be triggered as well
});
}
}
});
```

# Testing

You can define test phone numbers and corresponding verification codes. The
official[official
guide](https://cloud.google.com/identity-platform/docs/ios/mfa#enabling_multi-factor_authentication)
contains more information on setting this up.
2 changes: 1 addition & 1 deletion docs/auth/phone-auth.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Phone Authentication
description: Sign-in users with their phone number.
next: /firestore/usage
next: /auth/multi-factor-auth
previous: /auth/social-auth
---

Expand Down
2 changes: 1 addition & 1 deletion docs/firestore/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Cloud Firestore
description: Installation and getting started with Firestore.
icon: //static.invertase.io/assets/firebase/cloud-firestore.svg
next: /firestore/usage-with-flatlists
previous: /auth/phone-auth
previous: /auth/multi-factor-auth
---

# Installation
Expand Down
2 changes: 2 additions & 0 deletions docs/sidebar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
- '/auth/social-auth'
- - Phone Auth
- '/auth/phone-auth'
- - Multi-factor Auth
- '/auth/multi-factor-auth'
- '//static.invertase.io/assets/firebase/authentication.svg'
- - Cloud Firestore
- - - Usage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ public static void rejectPromiseWithExceptionMap(Promise promise, Exception exce
promise.reject(exception, SharedUtils.getExceptionMap(exception));
}

public static void rejectPromiseWithCodeAndMessage(
Promise promise, String code, String message, ReadableMap resolver) {
WritableMap userInfoMap = Arguments.createMap();
userInfoMap.putString("code", code);
userInfoMap.putString("message", message);
if (resolver != null) {
userInfoMap.putMap("resolver", resolver);
}
promise.reject(code, message, userInfoMap);
}

public static void rejectPromiseWithCodeAndMessage(Promise promise, String code, String message) {
WritableMap userInfoMap = Arguments.createMap();
userInfoMap.putString("code", code);
Expand Down
49 changes: 49 additions & 0 deletions packages/auth/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { describe, expect, it } from '@jest/globals';

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

// @ts-ignore - We don't mind missing types here
import { NativeFirebaseError } from '../../app/lib/internal';

describe('Auth', function () {
describe('namespace', function () {
it('accessible from firebase.app()', function () {
Expand Down Expand Up @@ -69,4 +72,50 @@ describe('Auth', function () {
}
});
});

describe('getMultiFactorResolver', function () {
it('should return null if no resolver object is found', function () {
const unknownError = NativeFirebaseError.fromEvent(
{
code: 'unknown',
},
'auth',
);
const actual = auth.getMultiFactorResolver(auth(), unknownError);
expect(actual).toBe(null);
});

it('should return null if resolver object is null', function () {
const unknownError = NativeFirebaseError.fromEvent(
{
code: 'unknown',
resolver: null,
},
'auth',
);
const actual = auth.getMultiFactorResolver(firebase.app().auth(), unknownError);
expect(actual).toBe(null);
});

it('should return the resolver object if its found', function () {
const resolver = { session: '', hints: [] };
const errorWithResolver = NativeFirebaseError.fromEvent(
{
code: 'multi-factor-auth-required',
resolver,
},
'auth',
);
const actual = auth.getMultiFactorResolver(firebase.app().auth(), errorWithResolver);
// Using expect(actual).toEqual(resolver) causes unexpected errors:
// You attempted to use "firebase.app('[DEFAULT]').appCheck" but this module could not be found.
expect(actual).not.toBeNull();
// @ts-ignore We know actual is not null
expect(actual.session).toEqual(resolver.session);
// @ts-ignore We know actual is not null
expect(actual.hints).toEqual(resolver.hints);
// @ts-ignore We know actual is not null
expect(actual._auth).not.toBeNull();
});
});
});
Loading

1 comment on commit 3c64bf5

@vercel
Copy link

@vercel vercel bot commented on 3c64bf5 Oct 26, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.