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


こちらの記事でFirebaseの電話番号認証を使用してみましたが、SMSの送信に関してどうにも不安定なので、代替策としてTwilioのAuthyを使ってみます。

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

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

AuthyのAPI Keyを取得

Twilioのアカウント、プロジェクトを作成後、開発者ページの左のメニューからAuthyを選択し、プロジェクトに導入します。
最初は500円の無料枠があり、一回SMSを送るたびに10円前後減っていきます。

この後チュートリアルのようなページになりますが、APIキーがコピーできた段階で中断してOKです。

Cloud Functionsに関数を追加

AuthyのAPIを使うためのパッケージはいくつかあり、
公式のドキュメントではauthyが使われているのですが、
Cloud FunctionsのPromiseチェーンで扱いやすいためauthy-clientを使用してみます。

$ cd functions
$ npm install --save authy-client
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として使います。

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にするなど、適宜処理します。

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();
    });
});

クライアント側の処理

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

Cloud Functionsの関数(verifyPhoneNumber)を叩きます。countryCodeemailはひとまず固定で。
関数から返ってきたIDはstateに保持しておきます。

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

Authy APIが無事に通ったら、入力した電話番号にSMSが届くはずです。画像のauth-testの部分はTwilioで設定したAuthyのプロジェクト名が入ります。

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

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

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になっているのが確認できました。