Unity + Firebaseによる電話番号認証やってみた

概要

UnityでiOS用のアプリを制作する際に、電話番号認証が必要だったためFirebaseの電話番号認証を利用してみた。

実装方針

1. iOSのネイティブ + Firebaseで電話番号認証を実装する
2. 1の設定をもとにUnityでFirebaseの電話番号認証を実装する

自分自身がWebのフロントエンドを主にやっているので、iOS、Unityともに勘所がいまいちわかっていません。
そのため、一気にUnityで実装するとXCodeの設定が問題なのか、Unityでの実装が問題なのかの区別がつきにくくなりそうだったので、丁寧に順を追って実装していきました。

1. iOSのネイティブ + Firebaseで電話番号認証を実装する

iOSの実装に関しては、世の中に情報がかなり存在していたので苦労する点はそれほど多くありません。
ただ、電話番号を有効にするまでの手順がおおくあります。

iOSでFirebaseの電話番号認証を有効にする手順
・FirebaseAuth SDKを導入する
・電話番号ログインを有効にする
・Xcodeで、プッシュ通知設定を有効にする
 ・バックグラウンドモードをオン
 ・プッシュ通知設定をオン
・APNs認証キーをアップルデベロッパーアカウント上で生成する
・Firebaseに生成したAPNs認証キーをアップロード
・Xcode上で、reCAPTCHA検証の設定を有効化する

この手順に関しては、以下のサイトでかなり具体的に説明されていたのでそちらを参考にしました。
特に認証キー周りは手順として何をしたらいいのかがわかりにくいところなので、とても助かりました。

テスト電話番号の設定

Firebaseでは、開発用にテスト用の電話番号を登録することができます。

画像1

テスト用の電話番号を使うことで開発時に以下のようなメリットがあります。

・使用量の割り当てを消費することなく電話番号認証をテストできます。
・実際の SMS メッセージを送信することなく電話番号認証をテストできます。
・同じ電話番号で連続してテストを実施してもスロットルが発生しません。このため、レビュー担当者が偶発的にテストに同じ電話番号を使用しても、アプリストアのレビュー プロセス中に拒否されるリスクが最小限に抑えられます。
・追加の作業を必要とせずに開発環境で簡単にテストできます。たとえば、Google Play 開発者サービスを使用せずに iOS シミュレータや Android Emulator で開発できます。
・セキュリティ チェックによるブロックが生じない統合テストを作成できます。通常、本番環境の実際の電話番号にはセキュリティ チェックが適用されます。

実際の電話番号を使ったテストだと、一定時間に送信できるSMSの数が制限されているので、できる限りテスト番号を使って開発をしていきます。

実装
公式にはポイントポイントのコードだけが記載されていたので、全文を載せます。
これを実行すると以下のようにテスト用の電話番号でFirebaseにユーザーが作成されます。

AppDelegate.swift

import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

   var window: UIWindow?

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       // Override point for customization after application launch.
       FirebaseApp.configure()
       return true
   }

   // MARK: UISceneSession Lifecycle
   func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
       // Called when a new scene session is being created.
       // Use this method to select a configuration to create the new scene with.
       return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
   }

   func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
       // Called when the user discards a scene session.
       // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
       // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
   }


}

ViewController.swift

import UIKit
import FirebaseAuth
class ViewController: UIViewController {
   var verificationID = ""
   override func viewDidLoad() {
       super.viewDidLoad()

				//テスト用の電話番号で登録したもの本来はUIからの入力を取得するべきだが、テストなのでハードコーディング
       let phoneNumber = "+819000000000"
       let testVerificationCode = "198304"
       
       Auth.auth().settings?.isAppVerificationDisabledForTesting = true
       
       PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:nil) {
                                                                   verificationID, error in
           if (error != nil) {
               print("電話番号のバリデーションエラー")
               print(error.debugDescription)
             return
           }
           //verificationIDはアプリで保持して、ユーザーの入力する認証コードと一緒に投げる
           self.verificationID = verificationID!
           let credential = PhoneAuthProvider.provider().credential(withVerificationID: self.verificationID ?? "",
                                                                      verificationCode: testVerificationCode)
           Auth.auth().signInAndRetrieveData(with: credential) { authData, error in
             if (error != nil) {
               // Handles error
               print("ログインエラー")
               print(error.debugDescription)
               return
             }
             print("LOGIN")
               
               //jwt取得
               authData!.user.getIDTokenForcingRefresh(true) { idToken, error in
               if let error = error {
                 // Handle error
                   print("トークン取得エラー")
                   print(error.localizedDescription)
                 return;
               }

               print("トークン取得成功")
               print(idToken)
             }
               
           };
           
       };
   }
}

2. 1の設定をもとにUnityでFirebaseの電話番号認証を実装する

iOSで実装したことにより、XCodeでの設定やFirebaseでの設定などが正しくできたことが確認できたので、Unityの実装に移ります。
Unityによる電話番号認証については、公式情報をもとに実装していきました。

UnityでFirebaseの電話番号認証を有効にする手順
・ FirebaseAuth SDKを導入する(※)
・ 電話番号ログインを有効にする
・Xcodeで、プッシュ通知設定を有効にする(※)
・ バックグラウンドモードをオン
・プッシュ通知設定をオン
・ APNs認証キーをアップルデベロッパーアカウント上で生成する
・ Firebaseに生成したAPNs認証キーをアップロード
・ Xcode上で、reCAPTCHA検証の設定を有効化する

基本はiOSのときと変わりませんが、※の部分だけが違います。

FirebaseAuth SDKを導入する
・ FirebaseSDKをダウンロードして、FirebaseAuth.unitypackageをUnityにインポートする
・ GoogleService-Info.plistをFirebaseのプロジェクトからダウンロードしてAssetsフォルダ配下の任意の場所に設置する

Xcodeで、プッシュ通知設定を有効にする
Unityの場合はビルドしたときにXCodeのプロジェクトファイルが生成されるため、UnityのPostProcessBuildでXCodeの設定を自動で行うようにします。

Assets/Editor/PostBuildProcess.cs

using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public class PostBuildProcess
{
   [PostProcessBuildAttribute(1)]
   public static void OnPostProcessBuild(BuildTarget target, string path)
   {

#if UNITY_IPHONE

       var separator = Path.DirectorySeparatorChar;

       string projectPath = PBXProject.GetPBXProjectPath(path);
       PBXProject project = new PBXProject();
       project.ReadFromFile(projectPath);
       string targetName = PBXProject.GetUnityTargetName();
       string targetGUID = project.TargetGuidByName(targetName);
       var entitlementPath = path + separator + targetName + separator + targetName + ".entitlements";
       var entitlementFileName = Path.GetFileName(entitlementPath);
       var unityTarget = PBXProject.GetUnityTargetName();
       var relativeDestination = unityTarget + "/" + entitlementFileName;
 

       var capabilityManager = new ProjectCapabilityManager(projectPath, relativeDestination, unityTarget);
       capabilityManager.AddPushNotifications(development: Debug.isDebugBuild);
       capabilityManager.AddBackgroundModes(BackgroundModesOptions.RemoteNotifications);
       capabilityManager.WriteToFile();


#endif
   }
}

実装
UIは簡易的に以下のようにしています。

画像2

電話番号を入力してprovider.VerifyPhoneNumberを実行すると確認idが返ってくるのですが、確認コードと一緒にこのidを使用する必要があります。

確認 ID を保存し、アプリの読み込み時に復元します。これにより、ユーザーがログインフローを完了する前にアプリが終了した場合でも(たとえば SMS アプリへの切り替え時など)、有効な確認 ID を残すことができます。

とFirebaseの説明にもあるので、PlayerPrefsでローカルに保存しておき、確認コードの認証とともにこのidを投げるようにしています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Firebase.Auth;
using UnityEngine.UI;

public class App : MonoBehaviour
{
  
   uint timeout = 60*1000;

   [SerializeField] Button next = null;
   [SerializeField] Button login = null;
   [SerializeField] InputField phoneNumberInput = null;
   [SerializeField] InputField verifyCodeInput = null;

   void Start()
   {
      
   }

   public void clickNext() {

       if(phoneNumberInput.text[0] == '0')
       {
           //Firebase認証のため日本の国コード+81を付与する
           System.Text.StringBuilder sb = new System.Text.StringBuilder(phoneNumberInput.text);
           string replacePhoneNumber = sb.Replace("0", "+81", 0, 1).ToString();
           SendPhoneNumber(replacePhoneNumber);
       }
   }

   public void clickLogin() {
       SendVerificationCode(verifyCodeInput.text);
   }

   void SendPhoneNumber(string phoneNumber) {
       FirebaseAuth firebaseAuth = FirebaseAuth.DefaultInstance;
       PhoneAuthProvider provider = PhoneAuthProvider.GetInstance(firebaseAuth);

       provider.VerifyPhoneNumber(phoneNumber, timeout, null,
         verificationCompleted: (credential) =>
         {
             
         },
           verificationFailed: (error) =>
           {
               
           },
           codeSent: (id, token) =>
           {
               //アプリを閉じてしまった後でも認証を続けられるようにidをPlayerPrefsに保持する
               UnityEngine.PlayerPrefs.SetString("credentialID", id);
               UnityEngine.PlayerPrefs.Save();
           });
   }

   void SendVerificationCode(string verificationCode) {
       FirebaseAuth firebaseAuth = FirebaseAuth.DefaultInstance;
       PhoneAuthProvider provider = PhoneAuthProvider.GetInstance(firebaseAuth);

       Debug.Log("SendVerificationCode");
       string credentialID = UnityEngine.PlayerPrefs.GetString("credentialID");
       Credential credential = provider.GetCredential(credentialID, verificationCode);

       firebaseAuth.SignInWithCredentialAsync(credential).ContinueWith(task => {
           if (task.IsFaulted)
           {
               Debug.LogError("SignInWithCredentialAsync encountered an error: " +
                              task.Exception);
               return;
           }

           FirebaseUser newUser = task.Result;
       });

   }

}

これでUnityでのFirebase電話番号認証をすることができました。

つまづいたところ

設定もれによるエラー
手順としてiOSのネイティブで最小構成の実装をした上でUnityの実装を行ったのですが、Unityでビルドしたところ invalid Token とだけエラーメッセージがでてVerifyPhoneNumberでverificationFailedが呼び出されてしまいました。
最初の設定から見直していたところ「Firebaseに生成したAPNs認証キーをアップロード」の手順を飛ばしていたことが原因でした。
エラーメッセージからは推測することが難しいため、原因がわからない時にはいろいろな設定が正しくされているかを確認することが重要です。
iOSでは


Auth.auth().settings?.isAppVerificationDisabledForTesting = true

としてバックグラウンドで APN トークンを要求したりサイレント プッシュ通知を送信することなく処理できるようにしていたため、うまくいっていたのだと思います。

Unityだとテスト電話番号が使えない
iosネイティブでログインに成功したテスト電話番号が、Unityだと

SignInWithCredentialAsync encountered an error: System.AggregateException: One or more errors occurred. ---> System.AggregateException: One or more errors occurred. ---> Firebase.FirebaseException: The SMS verification code used to create the phone auth credential is invalid. Please resend the verification code SMS and be sure to use the verification code provided by the user.`

とエラーが出て認証されませんでした。
試しに自分の電話番号を入れたところ、SMSが届き認証に成功しました。 ただし、同じ端末から4時間に5回までしかSMS送ることができないみたいなのでテストは慎重に行う必要がありました。
もしかしたら自分の設定がおかしいだけかもしれないので、まだ少し調査は必要そうです。

まとめ

UnityによるFirebaseの電話認証というのはとてもレアなのか、公式以外の情報がほとんどないという状態でした。
もちろん公式の手順を丁寧にこなしていけば実装できるのですが、つまづくポイントなどがいくつかあり、簡単にできたとは言えませんでした。
iOSのネイティブでの実装と、Unityによる実装をしっかり分けて行うことで、原因を特定できやすくなるので、今回のように小さくステップを進めて行うことをおすすめします。
同じような境遇にいる人が、どれだけいるかわかりませんが、その人たちの参考になれば嬉しいです。

この記事が気に入ったらサポートをしてみませんか?