Skip to content
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

feat(auth): Add multi-factor support for the sign-in flow #6593

Merged
merged 9 commits into from
Oct 26, 2022
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
mikehardy marked this conversation as resolved.
Show resolved Hide resolved
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