UnityでFirebase AuthenticationとFirestoreのサンプル

# はじめに

最近、個人でミニゲームを作成していて裏側でFirebaseを使用しています。
それで認証周りとFirestoreの疎通を検証したのでそのサンプルコードを載せておきます。
あと、エラーの遭遇とかも多々あったのでその辺りも記載しています。


基本ボタンをポチッとするだけで認証通ってゲームデータにアクセスできる作りを想定して組んでいます。
なのでメール認証とか電話番号認証とかはスルーしています。


~~Androidも合わせて作るつもりでしたが、iOSが終わって続けてAndroidもやろうとしたらビルドでこけて解消したときには心折れていたので、匿名（ゲストユーザー）の実装しかしていません。
週末あたりに対応して記事更新しておきます。~~
**※2020/10/04追記
Androidも認証追加しました。**


以下の認証フローで確認しました。

ゲストから他の認証行った場合にユーザーをリンクさせる処理は実装しています。
複数アカウントの結合は今のところ必要な機能ではなかったので実装していません。

* iOS
    * 匿名（ゲストユーザー）
    * Sign In with Apple
    * GameCenter

* Android
    * 匿名（ゲストユーザー）
    * Google
    * Playゲーム


# サンプルコード

先にコードと動きを確認できる動画を貼っておきます。

サンプルコードはこちらの[gist(FirebaseAuthDebugUI.cs)](https://gist.github.com/bu-tokumitsu/751b7b6054552b7271462872f36e9082)に全文載せていますが、[quickstart-unity](https://github.com/firebase/quickstart-unity/blob/master/auth/testapp/Assets/Firebase/Sample/Auth/UIHandler.cs)をベースに作り替えたものになります。
画面上部に認証やFirestoreへの投稿などのUIを表示して、画面下部にはログ表示しています。

GUILayoutで記載してあるのでGameObjectにスクリプトを充てて、Firebaseの設定をしていただければ動作確認できると思います。
(GameCenterなどは確認するために開発者アカウント必要ですがその辺はすでに持っている前提で進めます)

表示はこんな感じになります。匿名でもなんでもいいのでアカウント作らないとFirestoreにはアクセスできないようにルールを設定しています。
（サンプルなので見た目やエラー表示がちゃんとできていないところもありますがご容赦ください、あとAppleID隠すためのぼかし入れるの下手すぎました）

![fbsample-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/424b51d7-19be-a192-9edd-5e889997a4a8.gif)

# 動作確認環境

* Unity (ver 2019.4.5f1)
* Xcode (ver 12.0 12A7209)
    * iPhone X (OS ver 14.0)
* Android Studio (ver 4.0.1)
    * Nexus5x (OS ver 8.0.0)
* Firebase Unity SDK (ver 6.15.2)


# Firebase consoleのプロジェクト設定

Unityプロジェクトを追加して、iOS・AndroidともにBundleID, PackageNameを登録して、plistとjsonファイルをUnityのAssetsディレクトリ配下に設置します。

その後にUnity起動してfirebase_unity_sdkのunitypackage（バージョンが2019.4.5f1なのでdotnet4の方）をインポートしていきます。

~~今回は認証でFirebaseAuth、疎通確認の用にFirebaseFirestoreのパッケージを取り込みますが、
ここが後々、[一番ハマったポイント](https://qiita.com/mittsu/items/f2a7d86271748bacd733#%E3%82%A8%E3%83%A9%E3%83%BC%EF%BC%91ios-firebasefirestoreunitypackage%E5%8F%96%E3%82%8A%E8%BE%BC%E3%82%93%E3%81%A7%E3%81%8B%E3%82%89ios%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%A8podfile%E3%81%8C%E6%B6%88%E3%81%88%E3%82%8B)になったので僕はインポートした後に結局パッケージを削除して別の方法で取り込めるようにしました。~~

**※2020/10/04追記**
その後、原因を色々と調べていくうちに.unitypackageでインポートする際に余計なファイルをインポートしていることが原因でした。
[Firebase 既知の問題](https://firebase.google.com/docs/unity/setup?hl=ja#known_issues)にある通り`Unity.Compat.dll`、`Unity.Tasks.dll`の有効・無効を対応しないとダメでした。
最初の頃に流し読みして完全にスルーしてました。すみません。

今回は.NET 4.xを使用しているので
こちらは削除もしくは取り込まない
`Parse/Plugins/Unity.Compat.dll`
`Parse/Plugins/Unity.Tasks.dll`

こちらだけインポートするようにしたら問題は起きませんでした。
`Parse/Plugins/dotNet45/Unity.Compat.dll`
`Parse/Plugins/dotNet45/Unity.Tasks.dll`

挙動がおかしくなっていた時は**どちらも取り込んでいた**のでそれが原因のようです。


# Firebase Authenticationの初期化

AndroidでGooglePlay開発者サービスアプリが最新状態かチェックする処理を通過した後に初期化処理を行います。
iOSにはアプリはありませんが、iOSでもこの実装を呼んでも初期化まで処理は進むので問題ありません。

```FirebaseAuthDebugUI.cs
    // Firebase認証
    private Firebase.Auth.FirebaseAuth auth;
    // Android向け 最新のGooglePlay開発者サービスアプリがインストールされているかチェックに使用する
    private Firebase.DependencyStatus dependencyStatus = Firebase.DependencyStatus.UnavailableOther;

    public virtual void Start() {
        // GooglePlay開発者サービスアプリがインストールされているかチェックしてInitializeFirebase()を呼ぶ
        Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWithOnMainThread(task => {
            dependencyStatus = task.Result;
            if(dependencyStatus == Firebase.DependencyStatus.Available)
            {
                InitializeFirebase();
            }
            else
            {
                Debug.LogError("Could not resolve all Firebase dependencies: " + dependencyStatus);
            }
        });
    }

    // Firebase認証の初期化処理
    protected void InitializeFirebase() {
        DebugLog("Setting up Firebase Auth");
        auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
        // Firebase認証の状態監視を追加
        auth.StateChanged += AuthStateChanged;
        auth.IdTokenChanged += IdTokenChanged;
        AuthStateChanged(this, null);
    }
```


# 各認証に必要な対応と実装

Firebase consoleのAuthentication Sign-in methodで必要なプロバイダを有効にしていきます。


## 匿名（ゲストユーザー）
トグルで有効に切り替えていきます。
![ss1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/30884c16-1197-fc83-a63c-a0cf5d715545.png)

#### 匿名（ゲストユーザー）の実装

```FirebaseAuthDebugUI.cs
    // 匿名(ゲスト)ユーザー認証
    public Task SigninAnonymouslyAsync() {
        DebugLog("Attempting to sign anonymously...");
        DisableUI();
        return auth.SignInAnonymouslyAsync().ContinueWithOnMainThread(HandleSignInWithUser);
    }
```


## Sign In with Apple
iOSのみで動作させる場合はトグルで有効に切り替えるだけです。
AndroidやWebで対応が必要な場合は他の情報も入力する必要があります。
![ss02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/b6ac3155-88ad-0190-595d-0ea9610a3dea.png)

### Sign In with Appleの実装

こちらは最初ネイティブコードのプラグイン作る必要があるのかなとか考えていましたが、便利なライブラリがありましたのでそちらを使えば簡単に実装できました。

[lupidan/apple-signin-unity](https://github.com/lupidan/apple-signin-unity#implement-sign-in-with-apple)

Firebaseに認証通す時にはNonceを加える必要もあるのですがwikiにその辺りまでC#のコード付きで丁寧に記載されているので助かりました。

[Working with Firebase](https://github.com/lupidan/apple-signin-unity/wiki/Sign-in-with-Apple-Unity-Plugin:-Working-with-Firebase)

```FirebaseAuthDebugUI.cs
    // Start()で呼び出す初期化処理
    private void StartWithSIWA() {
        // Sign In with Apple認証がサポートされている端末なら認証用のインスタンスを初期化する
        if (AppleAuthManager.IsCurrentPlatformSupported)
        {
            var deserializer = new PayloadDeserializer();
            this.appleAuthManager = new AppleAuthManager(deserializer);   
        }
    }

    // Update()で呼び出す処理
    private void UpdateWithSIWA() {
        // SignInwithAppleを成功させる為にAppleAuthManagerのUpdate()をUpdate()で呼び続ける必要がある
        if (this.appleAuthManager != null)
        {
          this.appleAuthManager.Update();
        }
    }

    private static string GenerateRandomString(int length)
    {
        if (length <= 0)
        {
            throw new Exception("Expected nonce to have positive length");
        }

        const string charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
        var cryptographicallySecureRandomNumberGenerator = new RNGCryptoServiceProvider();
        var result = string.Empty;
        var remainingLength = length;

        var randomNumberHolder = new byte[1];
        while (remainingLength > 0)
        {
            var randomNumbers = new List<int>(16);
            for (var randomNumberCount = 0; randomNumberCount < 16; randomNumberCount++)
            {
                cryptographicallySecureRandomNumberGenerator.GetBytes(randomNumberHolder);
                randomNumbers.Add(randomNumberHolder[0]);
            }

            for (var randomNumberIndex = 0; randomNumberIndex < randomNumbers.Count; randomNumberIndex++)
            {
                if (remainingLength == 0)
                {
                    break;
                }

                var randomNumber = randomNumbers[randomNumberIndex];
                if (randomNumber < charset.Length)
                {
                    result += charset[randomNumber];
                    remainingLength--;
                }
            }
        }
        return result;
    }

    private static string GenerateSHA256NonceFromRawNonce(string rawNonce)
    {
        var sha = new SHA256Managed();
        var utf8RawNonce = Encoding.UTF8.GetBytes(rawNonce);
        var hash = sha.ComputeHash(utf8RawNonce);

        var result = string.Empty;
        for (var i = 0; i < hash.Length; i++)
        {
            result += hash[i].ToString("x2");
        }
        return result;
    }

    // Sign In with Apple認証の実行
    private void SignInWithApple() {
        var rawNonce = GenerateRandomString(32);
        var nonce = GenerateSHA256NonceFromRawNonce(rawNonce);
        var loginArgs = new AppleAuthLoginArgs(LoginOptions.IncludeEmail | LoginOptions.IncludeFullName, nonce);
        // iOSクライアントでSIWA認証を実行し、成功時のcredentialとNonceを使用してFirebaseAuthを実行する
        this.appleAuthManager.LoginWithAppleId(loginArgs,
            credential =>
            {
                var appleIdCredential = credential as IAppleIDCredential;
                DebugLog(String.Format("appleIdCredential:{0} | rawNonce:{1}", appleIdCredential, rawNonce));

                if (appleIdCredential != null)
                {
                    DebugLog(String.Format("appleIdCredential:{0} | rawNonce:{1}", appleIdCredential, rawNonce));
                    var identityToken = Encoding.UTF8.GetString(appleIdCredential.IdentityToken);
                    var authorizationCode = Encoding.UTF8.GetString(appleIdCredential.AuthorizationCode);
                    Credential firebaseCredential = OAuthProvider.GetCredential("apple.com", identityToken, rawNonce, authorizationCode);
                    SignInOrLinkCredentialAsync(firebaseCredential);
                }
            },
            error =>
            {
                // Something went wrong
                var authorizationErrorCode = error.GetAuthorizationErrorCode();
                DebugLog("SignInWithApple error:" + authorizationErrorCode);
            }
        );
    }
```



## GameCenter
こちらもFirebase consoleではトグルで有効に切り替えるだけです。
![ss3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/fbc023fc-0687-8941-1089-a9f10c8cd01a.png)

動作確認にはApp Store Connectの設定なども必要です。

### App Store Connectの設定について

現在はApple Developer Site(Certificates, Identifiers & Profiles)で証明書を追加すればGameCenterはデフォルトで有効になっており、Firebaseのプロジェクト追加も正常にできていればFrameworkやCapabilityなどの追加も不要になります。

App Store Connectでアプリ登録時にGameCenterを有効にする必要があります。
![app_store_gamecenter_bo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/bd8730b9-c70e-7868-fe14-256a8a09f0e8.png)

動作確認にはTestFlightにアップする必要があり、そこでストアに反映されるまで時間が掛かります。
配布されたアプリ経由で認証の確認が取れます。

### GameCenterの実装

```FirebaseAuthDebugUI.cs
    // GameCenter認証
    private void SignInGameCenter() {
        Social.localUser.Authenticate(success => {
            if (success)
            {
                var credentialTask = Firebase.Auth.GameCenterAuthProvider.GetCredentialAsync();
                var continueTask = credentialTask.ContinueWithOnMainThread(task => {
                    if(!task.IsCompleted)
                    return;
                    if(task.Exception != null)
                    DebugLog("GC Credential Task - Exception: " + task.Exception.Message);

                    Credential credential = task.Result;
                    SignInOrLinkCredentialAsync(credential);
                });
            }
            else
            {
                DebugLog("GameCenter authenticate error");
            }
        });
    }
```


## Playゲーム

Firebase consoleでトグルで有効にして、Google API consoleでクライアントIDとクライアントシークレットを取得して保存する必要があります。
あと、Firebase consoleのAndroidプロジェクトにビルド証明書のフィンガープリントの追加も必要になります。
そしてGoogle Play Consoleにも設定が必要なのでPlayゲームの認証が個人的には一番面倒に感じました。

<details><summary>ここの設定だけ本当に長かったので折り畳んでおきます</summary><div>

最初にフィンガープリントの登録になります。
Androidのビルド証明書が必要になるのでUnityのPlayerSettingのAndroidの項目でKeystore Managerから証明書を追加していきます。

![ss0050.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/14613ed8-2db7-2873-676c-7e7b1241157e.png)

コマンドで追加したKeystoreのフィンガープリントを確認します。

```
$ keytool -exportcert -list -v \
    -alias {証明書作成時に決めたエイリアス名} -keystore {キーストアファイルを指定}
```

表示された内容の内、SHA1の部分をAndroidプロジェクトの項目に追加していきます。

![ss0052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/87bf1fc9-112f-1607-77e3-d1a06bd91ea5.png)

---

次にFirebase consoleでPlayゲームを有効する設定を進めていきます。

Google API consoleの認証情報でOAuthクライアントIDを選択し

![ss0041.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/8373b24b-eb5c-c346-08c0-ee8b4458f488.png)

赤枠のWeb clientを選択します。
こちらはFirebase consoleでプロジェクト作成していれば自動で作られているはずです。

![ss0042.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/b3f8b4f2-8c5f-6413-504a-01bef0e8b1f1.png)

こちらに記載されているクライアントIDとクライアントシークレットをFirebase consoleの箇所に登録し保存します。

![ss0043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/c150724d-dcf6-980d-f5e1-6be78e410eba.png)

![ss0040.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/11d7f66d-f3bf-79f4-6bf2-cb4df41fbbe5.png)

---


そして、ここからGoogle Play Consoleの設定をしていきます。
**2020/11/02にGoogle Play Console画面がリニューアル**されるそうですが、新しい設定画面ではゲームサービスの新規追加の方法が分からなかったので一旦既存の設定画面で設定した流れになります。

最初にアプリのページを追加してテスト版などにapkをアップロードしていきます。
何かしらのapkをアップロードしないとゲームサービス内でアプリとリンクすることができないようになっています。
またFirebaseの認証と紐づけるのでFirebaseに登録したパッケージ名のapkでなければいけません。
apkはUnityのBuildで作られたapkをそのままアップロードしていきます。

今はAPIレベルを29以上にしないとエラーで弾かれるのでご注意ください。（僕は弾かれました）
正常にアップロードが完了すればストアの設定などは何もしないで大丈夫です。

ここからPlayゲームサービスとアプリを紐づけていきます。
ゲームサービス -> 新しいゲームの追加を選択します。

![ss0060.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/dfa0216d-f8e5-c44a-1169-d6e07a7917bb.png)

**ゲームでGoogle APIを既に使用しています** のタブを選択してリンクするプロジェクトにFirebaseのプロジェクトを選択します。
下部にゲームのジャンル選択がありますが適宜設定してください。

![ss0061.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/562ebb2f-272f-9617-fa51-78a7e2e3046d.png)

追加が終わったらゲームを選択し、ゲームの詳細から`基本的なゲームサービスが動作するために必要な API`が有効になっているかチェックします。

![ss0062.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/a6da58cd-d43e-b6ed-9d21-ba6c04017d25.png)

次にリンク済みアプリからAndroidを選択し、apkをアップロード済みだと選択できるようになっているので対象のアプリを選択します。

![ss0064.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/a18b750f-9466-0e66-390f-7d436ecdcc85.png)

次にリンク済みアプリからウェブを選択していきます。

![ss0065.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/d6f73a90-0915-c4fe-87a5-f0868138b247.png)

ウェブでは起動URLの入力して保存しますが、URLはGoogle API Consoleに記載されています。

![ss0066.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/cec2756a-fb0e-2ee3-f41e-1aea6222894b.png)

ここに起動URLが記載されています。
Playゲームの設定の最初の方でOAuthクライアントID・クライアントシークレットを取得したページと同じです。
`https://********.firebaseapp.com`のURLになります。

![ss0067.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/4ec68fa4-d8c4-ce41-6aeb-da2e7a1e53cc.png)

次に`イベント`、`実績`、`リーダーボード` のどれかに項目を追加していきます。
僕は適当にイベントでtestという新しいイベントを追加しました。
何かしら追加すると**リソースを取得**の項目が表示されるのでここで取得できるxmlを次のUnityの実装の時に使用していきます。

![ss0068.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/9499080c-8f7f-61ba-a617-378e1a78870d.png)
![ss0069.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/8a14e546-13eb-e565-bf96-66d56492e970.png)

最後にテスター登録をすれば設定は完了です。
僕は**Playゲームのgmailアカウント**をテスター登録しました。

![ss0070.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/67b8f2d3-1ba0-08cb-9320-8661edfc0f8e.png)

Google Play Consoleでの設定はこれで完了です。
長かったですね、お疲れ様でした。

これからようやくUnity側の実装に入ります。

</div></details>


### play-games-plugin-for-unityのパッケージインポート

[github play-games-plugin-for-unity/releases](https://github.com/playgameservices/play-games-plugin-for-unity/releases)
こちらからパッケージが取得できるのでダウンロードして中のcurrent-buildディレクトリ内の.unitypackageをインポートしていきます。

インポートに成功するとUnityのWindowの項目にGooglePlayGamesの設定ページが追加されるのでこちらに情報を追加していきます。

![ss0080.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/280301c3-494a-8bfe-038e-ae3b14031c00.png)

**Resources Definition**は先ほどGoogle Play Consoleで取得したxml情報
**ClientID**はFirebase consoleで登録したものと同じWebのClientIDになります。

![ss0081.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/dcfc76ac-2032-09ec-ddb1-fc2f94821b35.png)

この時取得したIDです。

![ss0043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/c150724d-dcf6-980d-f5e1-6be78e410eba.png)


### Playゲームの実装

```FirebaseAuthDebugUI.cs
// Playgames認証で使用(Android)
using GooglePlayGames;
using GooglePlayGames.BasicApi;
using UnityEngine.SocialPlatforms;

// Start()で呼び出す初期化処理
private void StartWithPlayGames() {
    #if UNITY_ANDROID
    // playgames設定、初期化
    PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
        .RequestServerAuthCode(false /* Don't force refresh */)
        .Build();
    PlayGamesPlatform.InitializeInstance(config);
    PlayGamesPlatform.Activate();
    #endif
}

// Playゲーム認証（iOSだとエラーになるのでAndroidでのみ呼び出す）
#if UNITY_ANDROID
private void SignInPlayGames() {
    Social.localUser.Authenticate(success => {
        if (success)
        {
            var authCode = PlayGamesPlatform.Instance.GetServerAuthCode();
            Firebase.Auth.FirebaseAuth auth = Firebase.Auth.FirebaseAuth.DefaultInstance;

            Credential credential = Firebase.Auth.PlayGamesAuthProvider.GetCredential(authCode);
            SignInOrLinkCredentialAsync(credential);
        }
        else
        {
            DebugLog("Playgames authenticate error");
        }
    }); 
}
#endif
```


## Google認証

Firebase consoleでトグルで有効にして、プロジェクトのサポートメールを受けるGmailアカウントを追加します。
Firebase consoleのAndroidプロジェクトにビルド証明書のフィンガープリントの追加も必要になりますが、既にPlayゲームの設定が完了しているなら追加済みのはずなので追加で登録の必要はありません。

![ss0090.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/d589e70c-9c81-df86-fca0-d7336837b141.png)

### google-signin-pluginのインポート

[github google-signin-unity](https://github.com/googlesamples/google-signin-unity)

こちらでGoogle認証用のUnityPackgeが配布されているのでこちらを使用していきますが、プロジェクトが古いので一手間必要になります。

普通にインポートするとこちらのエラーが出続けます。

```
ArgumentException: Requested value 'X86' was not found.
System.Enum+EnumResult.SetFailure (System.Enum+ParseFailureKind failure, System.String failureMessageID, System.Object failureMessageFormatArgument) (at <fb001e01371b4adca20013e0ac763896>:0)
System.Enum.TryParseEnum (System.Type enumType, System.String value, System.Boolean ignoreCase, System.Enum+EnumResult& parseResult) (at <fb001e01371b4adca20013e0ac763896>:0)
System.Enum.Parse (System.Type enumType, System.String value, System.Boolean ignoreCase) (at <fb001e01371b4adca20013e0ac763896>:0)
System.Enum.Parse (System.Type enumType, System.String value) (at <fb001e01371b4adca20013e0ac763896>:0)
GooglePlayServices.AndroidAbis.EnumValueStringToULong
```

こちらの[issue](https://github.com/googlesamples/google-signin-unity/issues/139)にあるように解決するには`PlayServicesResolver`を一度削除してから[unity-jar-resolver](https://github.com/googlesamples/unity-jar-resolver/releases)を取得してパッケージを更新してから再起動することで直りました。

実装でGoogleSignIn.Configurationの`WebClientId`にクライアントIDを設定する必要がありますが、ここと同じクライアントIDになります。

![ss0043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/c150724d-dcf6-980d-f5e1-6be78e410eba.png)

ちなみにAssets内のgoogle-service.json内のここにも同じクライアントIDが入っているようです。

```google-service.json
      "oauth_client": [
        {
          "client_id": "{ウェブのクライアントID}",
          "client_type": 3
        }
      ],
```

### Google認証の実装

```FirebaseAuthDebugUI.cs
// Google認証
using Google;

// Google認証で使用するクライアントID(プロジェクト毎に取得する)
private const string GoogleClientId = "{ウェブのクライアントID}";

// Start()で呼び出す初期化処理
private void StartWithSignInGoogle() {
    // Google SignInの設定
    GoogleSignIn.Configuration = new GoogleSignInConfiguration {
        RequestIdToken = true,
        // Copy this value from the google-service.json file.
        // oauth_client with type == 3
        WebClientId = GoogleClientId
    };
}

// Google認証
private void SignInGoogle() {
    Task<GoogleSignInUser> signIn = GoogleSignIn.DefaultInstance.SignIn();

    signIn.ContinueWith (task => {
        if (task.IsCanceled) {
            DebugLog("GoogleSignIn was canceled.");
        } else if (task.IsFaulted) {
            DebugLog("GoogleSignIn was error.");
        } else {
            Credential credential = Firebase.Auth.GoogleAuthProvider.GetCredential(((Task<GoogleSignInUser>)task).Result.IdToken, null);
            SignInOrLinkCredentialAsync(credential);
        }
    });
}
```

## 匿名(ゲスト)アカウントと認証をリンクさせる

ちょいちょいコードの中でSignInOrLinkCredentialAsyncていう関数を呼んでいましたが内部で匿名（ゲスト）かチェックしてリンクさせるかどうか処理を分けています。

すでにSign In with AppleやGameCenterでアカウントが登録されている場合はエラーになってしまうので、その場合は別にアカウントをマージする処理が必要になります。（今回は実装していません）

```FirebaseAuthDebugUI.cs
    // ゲストユーザーで認証済みの時はリンクさせる、新規の場合はそのまま認証完了
    private void SignInOrLinkCredentialAsync(Credential firebaseCredential) {
        if(auth.CurrentUser != null && auth.CurrentUser.IsAnonymous)
        {
            auth.CurrentUser.LinkWithCredentialAsync(firebaseCredential).ContinueWith(task => {
                if (task.IsCanceled)
                {
                    DebugLog("LinkWithCredentialAsync was canceled.");
                    return;
                }
                if (task.IsFaulted)
                {
                    DebugLog("LinkWithCredentialAsync encountered an error: " + task.Exception.Message);
                    return;
                }
                Firebase.Auth.FirebaseUser newUser = task.Result;
                DebugLog(String.Format("Credentials successfully linked to Firebase user: {0} ({1})", newUser.DisplayName, newUser.UserId));

                signedInGuest = newUser.IsAnonymous;
                signedInUser = !newUser.IsAnonymous;
            });
        }
        else 
        {
            var task = auth.SignInWithCredentialAsync(firebaseCredential);
            if(task.Exception != null)
                DebugLog("GC Credential Task - Exception: " + task.Exception.Message);

            task.ContinueWithOnMainThread(HandleSignInWithUser);
        }
    }
```

### リンク前と後の表示

リンクさせた場合はユーザーUIDが同じで識別子が匿名から変更されていることが確認できます。

![gesut_register_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/432cc804-44a6-7978-6807-fd9ee433cd26.png)
![siwa_user_link.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64426/2d09d18a-2853-ea67-a8ec-3879a521248d.png)



## サインアウト 

サンプル内にコードはありますが、UIの実装していません。
実装的にはこちらになります。

```FirebaseAuthDebugUI.cs
    // サインアウト
    protected void SignOut() {
        DebugLog("Signing out.");
        auth.SignOut();
    }
```



# Firestoreの設定

### ルール設定

Cloud Firestoreで読み込み、書き込みできるルールを決めておきます。
今回は認証済みのユーザーならOKというルールにしておきます。

```
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.auth.uid != null;
    }
  }
}
```

### Firestoreの実装

今回は認証有無しでアクセスの確認がメインの目的だったのでFirestoreはドキュメントルートの一覧読み込みとサンプルでメッセージを送るだけのコードになります。

```FirebaseAuthDebugUI.cs
    private string collectionPath = "send_test";
    private string documentId = "";

    protected FirebaseFirestore db {
      get {
        return FirebaseFirestore.DefaultInstance;
      }
    }

    private CollectionReference GetCollectionReference() {
      return db.Collection(collectionPath);
    }

    private DocumentReference GetDocumentReference() {
      if (documentId == "") {
        return GetCollectionReference().Document();
      }
      return GetCollectionReference().Document(documentId);
    }

    // 読み込み
    private IEnumerator ReadDoc(Query query) {
        Task<QuerySnapshot> getTask = query.GetSnapshotAsync();
        yield return new WaitForTaskCompletion(this, getTask);
        if (!(getTask.IsFaulted || getTask.IsCanceled))
        {
            QuerySnapshot allCitiesQuerySnapshot = getTask.Result;
            foreach (DocumentSnapshot documentSnapshot in allCitiesQuerySnapshot.Documents)
            {
                Console.WriteLine("read document data for {0} document:", documentSnapshot.Id);
                Dictionary<string, object> city = documentSnapshot.ToDictionary();
                IDictionary<string, object> resultData = documentSnapshot.ToDictionary();
                foreach (KeyValuePair<string, object> kv in resultData)
                {
                    DebugLog(String.Format("read key = {0} : value = {1}", kv.Key, kv.Value));
                }
            }
        }
    }

    // 書き込み
    private IEnumerator WriteDoc(DocumentReference doc, IDictionary<string, object> data) {
        Task setTask = doc.SetAsync(data);
        yield return new WaitForTaskCompletion(this, setTask);
        if (!(setTask.IsFaulted || setTask.IsCanceled))
        {
            foreach (KeyValuePair<string, object> kv in data)
            {
                DebugLog(String.Format("write key = {0} : value = {1}", kv.Key, kv.Value));
            } 
        }
    }

    // テスト送信
    private void FirestoreSendTest() {
        var msg = "test";
        if (sendMessage != "")
        {
            msg = sendMessage;
        }
        var data = new Dictionary<string, object>{
            {"message", msg}
        };
        StartCoroutine(WriteDoc(GetDocumentReference(), data));
    }

    class WaitForTaskCompletion : CustomYieldInstruction {
        Task task;
        FirebaseAuthDebugUI debugUI;

        public WaitForTaskCompletion(FirebaseAuthDebugUI debugUI, Task task) {
            debugUI.previousTask = task;
            this.debugUI = debugUI;
            this.task = task;
        }

        public override bool keepWaiting {
            get {
                if (task.IsCompleted)
                {
                    debugUI.cancellationTokenSource = new CancellationTokenSource();
                    if (task.IsFaulted)
                    {
                        Debug.Log("WaitForTaskCompletion exception:" + task.Exception.Message);
                    }
                    return false;
                }
                return true;
            }
        }
    }
```

---

<details><summary>初期の検証中に起こったエラー一覧（今となってはただの実装間違いが原因、もしくは気にする必要が無いエラーだったので折り畳みました）</summary><div>

## 開発中の起こったエラー一覧 

### エラー１：(iOS) FirebaseFirestore.unitypackage取り込んでからiOSプロジェクトを作るとPodfileが消える

ドキュメントに沿うとfirebase_unity_sdkのunitypackage（dotnet4の方）をインポートしていきます。
FirebaseAuth.unitypackageだけならiOSプロジェクト作った時にPodfileも作られていて問題無く動作するのですが、FirebaseFirestore.unitypackageをインポートした後にプロジェクトを作成するとPodfileが作られなくなりました。

static libraryは存在しているようなのでその辺でごにょごにょしたり、Podfile自前で追加したりで対応していたのですが、他プロジェクトでインポートした時はうまくいってたりして、よく分からない状態に陥っていました。

最終的に上手くいっているプロジェクトと差分見た時に`Packages/manifest.json`に記述の違いがあったので以下をmanifest.jsonに記載してunitypackageはインポートしていません。

```manifest.json
{
  "dependencies": {
    "com.google.firebase.auth": "6.15.2",
    "com.google.firebase.firestore": "6.15.2",

  ~~ 省略 ~~
  }
}
```

Unity経験が浅いので、ここに記載するのは良くないのかどうかはちゃんと分かっていないのですが、これでiOSはPodfileが生成され取り込みもできました。
AndroidもGradleにauth, firestoreともに記述が加わっていたので大丈夫でした。

正直、unitypackageインポートと取り込んだファイルを削除を何回も繰り返した結果の後なのでこれで解決してるのも、たまたまなんじゃないかと思いますが、

ゲーム開発しているプロジェクトの方でも正常に動いているので一旦はこれでいいかなと考えています。


### エラー２：(Android) Multidexエラー

久々に出会った気がしたMultidexエラー。
Firebase SDKは大きいので取り込むとすぐにこのエラーがでてきます。
UnityではTemplete用意してそこにMultidexの記述する対応方法があるようです。

今回はそもそもMinSDKバージョンを5.0に上げても問題無いのでデフォルト4.4のバージョンでプロジェクトを生成していたところを5.0に変更して済ませました。

### エラー３：(Android) ビルドエラー LibraryのAndroidManifestにminSDKの記述がある

最近ではbuild.gradleにminSDKの記述をしないと怒られるようです。
`Plugin/Firebase/AndroidManifest.xml` の中のminSDKVersionの記述を削除しました。

### エラー４：(Android) Firebase Libraryのbuild.gradleでエラー

Android Studioのバージョンが高くてgradleのバージョンが高すぎたのが原因か記述が古いままのbuild.gradleでエラーが出てました。
以下差分になります。

```build.gradle
- apply plugin: 'android-library'
+ apply plugin: 'com.android.library'


dependencies {
    implementation fileTree(dir: 'bin', include: ['*.jar'])
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

android {
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            //java.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
            jniLibs.srcDirs = ['libs']
        }
    }

    compileSdkVersion 28
    buildToolsVersion '28.0.3'
    defaultConfig {
-        targetSdkVersion 9
+        targetSdkVersion 28
    }

    lintOptions {
        abortOnError false
    }
}
```

</div></details>



## まとめ

とりあえず、認証通してFireStoreの確認までできたので良かったです。

途中iOSの部分でかなりハマってしまいましたが、Firebaseの認証周りを実装してみてconsoleの作業も少なく、サンプルも充実していたのでハマったところ以外は楽な印象がありました。

~~といってもAndroidはゲストの確認して放り出したので、また対応して記事更新します。~~

**※2020/10/04追記**
AndroidもPlayゲームとGoogle認証もなんとか追加しました。
Androidの方が実装以外の設定が多く、特にPlayゲームはいくつもConsole設定を見ながら進めるので苦労しました。

今回試した4つの認証設定の中ではPlayゲームの設定が一番面倒でした。