Firebaseの電話番号認証の代わりにAuthyを使う

[こちらの記事](https://qiita.com/mildsummer/items/1940f430d4191b5c5fd9)でFirebaseの電話番号認証を使用してみましたが、SMSの送信に関してどうにも不安定なので、代替策としてTwilioのAuthyを使ってみます。

Firebase Authenticationでのユーザー管理は別に実装するとして、SMSの送信・トークンの認証までをCloud Functions上で簡単に動作確認してみました。
なお、Cloud Functionsで外部APIを叩くため、FirebaseプロジェクトはFlameプラン（$25/月）かBlazeプラン（従量制）である必要があります。

ソースコードやスクリーンショットはExpo・ReactNativeでのアプリ開発を想定していますが、本質的な部分はなんでも共通なはずなので、画面の構築に関するコードは省略しています。

## AuthyのAPI Keyを取得

[Twilio](https://www.twilio.com/ja/)のアカウント、プロジェクトを作成後、開発者ページの左のメニューからAuthyを選択し、プロジェクトに導入します。
最初は500円の無料枠があり、一回SMSを送るたびに10円前後減っていきます。
<img width="1440" alt="Twilio_Console_-_Authy_Dashboard.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/105608fb-0eb3-b522-3c08-ac3931cc347d.jpeg">
この後チュートリアルのようなページになりますが、APIキーがコピーできた段階で中断してOKです。

## Cloud Functionsに関数を追加

AuthyのAPIを使うためのパッケージはいくつかあり、
公式のドキュメントでは[`authy`](https://www.npmjs.com/package/authy)が使われているのですが、
Cloud FunctionsのPromiseチェーンで扱いやすいため[`authy-client`](https://www.npmjs.com/package/authy-client)を使用してみます。

```console
$ cd functions
$ npm install --save authy-client
```

```js:functions/index.js
const functions = require("firebase-functions");
const admin = require("firebase-admin");
const AuthyClient = require("authy-client").Client;

admin.initializeApp();
const db = admin.firestore();
const authy = new AuthyClient({ key: "ここに APIキー" });
```

Cloud Functionsのセットアップについては省略します。
まずFirestoreを扱うための準備と、`authy-client`にAPIキーを渡してAuthyクライアントを初期化します。

### SMS送信・DBに保存する関数

`verifyPhoneNumber`関数を追加します。国コード、メールアドレス、電話番号からAuthyにユーザーを追加して、AuthyでのユーザーIDを含めてFirestoreの`members`コレクションにユーザー情報を保存します。
クライアント側には`member`ドキュメントのIDを返して、あとから認証コードと照らし合わせるためのIDとして使います。

```js:functions/index.js
exports.verifyPhoneNumber = functions.https.onCall(
  ({ countryCode, email, phoneNumber }) => {
    return authy
      .registerUser({
        countryCode,
        email,
        phone: phoneNumber
      })
      .then(response => {
        return response.user.id;
      })
      .then(authyId => {
        return Promise.all([
          db.collection("members").add({
            authyId,
            email,
            countryCode,
            phoneNumber,
            phoneNumberVerification: false,
            createdAt: admin.firestore.Timestamp.now()
          }),
          authy.requestSms({ authyId })
        ]);
      })
      .then(([docRef]) => {
        return docRef.id;
      });
  }
);
```

### 認証コードと照らし合わせる関数

クライアント側からは`member`ドキュメントのIDと、SMSで送られてきた認証コードを渡して、functionsでこれを照らし合わせます。
認証が成功したら、`member`ドキュメントの`phoneNumberVerification`フラグを`true`にするなど、適宜処理します。

```js:functions/index.js
exports.verifyPhoneNumberToken = functions.https.onCall(({ id, token }) => {
  const docRef = db.collection("members").doc(id);
  return docRef
    .get()
    .then(docSnapshot => {
      return authy.verifyToken({
        authyId: docSnapshot.data().authyId,
        token
      });
    })
    .then(() => {
      return docRef.update({
        phoneNumberVerification: true
      });
    })
    .then(() => {
      return docRef.get();
    });
});
```

## クライアント側の処理

このように電話番号を入力する画面を用意し、

<img width="375" alt="PNGイメージ 12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/aae5f8da-31cb-4322-6ae6-e6f4b0e59055.png">

Cloud Functionsの関数（`verifyPhoneNumber`）を叩きます。`countryCode`や`email`はひとまず固定で。
関数から返ってきたIDはstateに保持しておきます。

```js:App.js
verifyPhoneNumber = async values => {
  const { phoneNumber } = values;
  try {
    const {
      data: verificationId
    } = await firebase.functions().httpsCallable("verifyPhoneNumber")({
      countryCode: "81",
      phoneNumber,
      email: "test@test.com"
    });
    console.log("verificationId", verificationId);
    this.setState({ verificationId });
  } catch (e) {
    console.log(e);
  }
};
```

Authy APIが無事に通ったら、入力した電話番号にSMSが届くはずです。画像の`auth-test`の部分はTwilioで設定したAuthyのプロジェクト名が入ります。
<img width="375" alt="PNGイメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/a4d9b35d-b81a-13d5-5200-8e6570094579.png">

stateに`verificationId`がセットされたら認証コードの入力欄を表示するようにします。

<img width="375" alt="PNGイメージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/6c4f9c6a-65e8-aef1-15d6-39075c621f99.png">

あとは、再びCloud Functionsの関数（`verifyPhoneNumberToken`）を叩き、入力された認証コードを照らし合わせます。

```js:App.js
verifyPhoneNumberToken = async values => {
  const { verificationId } = this.state;
  const { verificationCode } = values;
  try {
    const { data: member } = await firebase
      .functions()
      .httpsCallable("verifyPhoneNumberToken")({
      id: verificationId,
      token: verificationCode
    });
    console.log(member);
    Alert.alert("成功", "認証が完了しました");
  } catch (e) {
    console.log(e);
  }
};
```

Firestoreに`member`ドキュメントが追加されており、`phoneNumberVerification`フラグが`true`になっているのが確認できました。

<img width="519" alt="Test_Project_–_Database_–_Firebase_console.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/2e08b6b3-c9b6-c777-dcf4-34859d7ac8c0.jpeg">