サーバーサイドなんもわからんアナタに向けたハンズオン　Unity × gRPC × Go × Firebase × Docker 〜第五章、FirebaseSDKを用いたアカウント作成とログイン実装〜

# 最初に

本記事の内容は、サーバーサイドにほとんど触れてこなかったUnityエンジニアに向けて、最近の主流構成を学びながら少しずつサーバーの知識を身につけられるハンズオン形式で進めます。この記事が、サーバーサイドに対する理解を深める一助になれば幸いです。

本記事の対象者は以下の通りです。

- A Tour of Goを一通り終わらせたが次に何をするべきかわからない初心者サーバーエンジニア
- gRPCについて学びたいが、Unityとの関連性も知りたい初学者
- なんとなくGoやgRPCについては理解できるけど、具体的な仕組みについて知りたいUnityエンジニアの方

また、今回のハンズオン記事は全７章が存在します。

こちらから他の章も見ていただけると嬉しいです！
[「第一章、環境構築」](https://qiita.com/4_mio_11/items/08e972ae361c55b317f0)
[「第二章、Cloud Runへのデプロイ」](https://qiita.com/4_mio_11/items/2027978feb5f131fdd48)
[「第三章、CI/CDパイプライン構築」](https://qiita.com/4_mio_11/items/1148aa1603f4acb32088)
[「第四章、Firebaseプロジェクトの作成とFirebaseSDK導入」](https://qiita.com/4_mio_11/items/6a9b61fe2f1db2b5c38a)
[「第五章、FirebaseSDKを用いたアカウント作成とログイン実装」](https://qiita.com/4_mio_11/items/aabefeac56e700afc6e4)


### **今回の環境**

- OS : MacOS
- IDE : GoLand
- Gitクライアント : Fork

## 前提

以下の記事を終えているか、チャットアプリケーションを作るにあたって、必要な設定をGCP上で行う準備ができていることを前提としています。

[サーバーサイドなんもわからんアナタに向けたハンズオン　Unity × gRPC × Go × Firebase × Docker 〜第四章、Firebaseプロジェクトの作成とFirebaseSDK導入〜 - Qiita](https://qiita.com/4_mio_11/private/6a9b61fe2f1db2b5c38a)

また、記事の内容を自由に学習できるよう、GitHub上に公開リポジトリを作成しました。各ステップに対応したバージョンをReleasesとして登録しているので、興味のあるステップから学習を始められます。

https://github.com/rossam/grpc-chat-app-sample/releases

上記リポジトリ内容を使用して特定のステップから始める場合はリポジトリをForkするか、手元にDownloadなどをしてから進めるようお願いします。

<br>

# 1. ユーザー登録機能の実装

それでは実際に処理を作ってみましょう。

`LoginOrSingUpView.cs`というスクリプトを`Assets > grpc-chat-app > Scripts`下に作成します。

コードの中身は以下のようにしました。

```csharp
using Firebase.Auth;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace grpc_chat_app.Scripts
{
    public class LoginOrSingUpView : MonoBehaviour
    {
        [SerializeField] private TMP_InputField signUpEmail = default!;
        [SerializeField] private TMP_InputField signUpPassword = default!;
        [SerializeField] private Button signUpButton = default!;

        [SerializeField] private TMP_Text signUpFeedbackText = default!;

        private FirebaseAuth _auth;

        private void Start()
        {
		    // Firebaseプロジェクトにリンクされたデフォルトの認証インスタンスを取得します。
            // このインスタンスを使ってユーザー認証操作を行います。
            _auth = FirebaseAuth.DefaultInstance;
            
            // ボタンにユーザー登録処理を紐づける
            signUpButton.onClick.AddListener(RegisterButton);
        }

        // メールアドレスとパスワードで新規登録
        private void RegisterButton()
        {
            var email = signUpEmail.text;
            var password = signUpPassword.text;

			// FirebaseAuthのCreateUserWithEmailAndPasswordAsyncメソッドを使って、新しいユーザーを作成します。
            _auth.CreateUserWithEmailAndPasswordAsync(email, password).ContinueWithOnMainThread(task =>
            {
                if (task.IsCanceled)
                {
                    Debug.LogError("CreateUserWithEmailAndPasswordAsync was canceled.");
                    signUpFeedbackText.text = "ユーザー登録がキャンセルされました。";
                    return;
                }

                if (task.IsFaulted)
                {
                    Debug.LogError("CreateUserWithEmailAndPasswordAsync encountered an error: " + task.Exception);
                    signUpFeedbackText.text = "エラー: " + task.Exception!.InnerExceptions[0].Message;
                    return;
                }

                // ユーザーの作成に成功
                var newUser = task.Result;
                
                // 成功した場合、task.Resultから新しいユーザー情報が得られます。
                Debug.LogFormat("User created successfully: {0} ({1})", newUser.User.DisplayName, newUser.User.UserId);
                signUpFeedbackText.text = "ユーザー登録成功！";
            });
        }
    }
}
```

コード自体に難しい箇所はないかと思いますが、一点、ContinueWithOnMainThreadについて細く説明を入れておきます。

<br>

### ContinueWithOnMainThreadについて

`ContinueWithOnMainThread`は、FirebaseがUnity用に提供している特別な機能で、非同期処理の続きを**Unityのメインスレッド**上で実行できるようにするものです。

:::note

**メインスレッドという言葉に馴染みがない方向けに…**

- Unityでは、**UI操作**や**GameObjectの変更**などの処理は、必ずメインスレッド上で実行する必要があります。
- 例えば、ボタンのテキストを変更したり、ゲームキャラクターの動きをプログラムで制御したりするとき、メインスレッド以外でこれを行うとエラーや意図しない挙動が発生してしまいます。

Firebaseのような非同期処理では、データベースの通信や認証といった処理は通常メインスレッド以外で行われます。そのため、Firebaseの処理が完了した後に**UIやゲームの状態を更新したい場合**、Unityのメインスレッドに戻す必要があります。

:::

`ContinueWithOnMainThread`を使えば、メインスレッドへの戻り処理を自動で行ってくれるため、安心して非同期処理の続きを実装できます。

例えば上記コードだと、以下の流れとなります。

1. **非同期処理の呼び出し**：
`CreateUserWithEmailAndPasswordAsync` メソッドを呼び出して、Firebaseで新しいユーザーを登録するリクエストを送信します。このメソッドは非同期で実行されるので、Firebaseからの応答を待ちながら、他の処理を続けられるようになります。
2. **`ContinueWithOnMainThread`で処理の続き**：
Firebaseからの応答が返ってきたとき、`ContinueWithOnMainThread`を使用して結果を処理します。これにより、Unityのメインスレッド上で続きを実行できるので、UIやゲームのオブジェクトを安全に操作できます。
3. **キャンセルやエラー時の処理、成功時の処理**：
`task`の`IsCanceled`や`IsFaulted`を参照してキャンセルやエラー時のUIが関連する処理を行ったり、`task.Result`から新しいユーザー情報を取得して「ようこそ！」のようなメッセージを出す処理をここで行います。

<br>

コードの流れがわかったところで、実際にPrefabにコードを追加して、各コンポーネントをアタッチしていきましょう。

今回は`Canvas`下の`LoginOrSignUp`に追加して、`SignUp`下にある各`GameObject`をアタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/7d75b306-dce2-df08-82c3-a3a182054637.png)


この状態で実行してみましょう。

もしも、実行時に以下のようなダイアログが出た場合は、MacOSのシステム設定 > プライバシーとセキュリティ 画面へ進み、画面下部にあるFirebaseCppAppに対する案内の「このまま許可」をクリックしてください。その後、Unityに戻り、ダイアログの「キャンセル」をクリックして進めてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/7b24110f-e7ff-6e55-327f-2e04026631ef.png)


実行後に適当なメールアドレスとパスワードを入力して、「SignUp with Mail address and Password」ボタンをクリックしてください。

その際に、発生しうる代表的なエラーと、その内容についての説明も記載しておきます。

## 新規アカウント作成を試行した際に発生し得るエラー

**無効なメールアドレス**

- **エラーコード：**`FirebaseAuthInvalidCredentialsException`
- 入力されたメールアドレスの形式が正しくない場合に発生します（例: `example@com` や `example.com`）。

**短すぎるパスワード**

- **エラーコード：**`FirebaseAuthWeakPasswordException`
- Firebaseではパスワードの長さは6文字以上が必要です。それ以下のパスワードを入力するとこのエラーが発生します。

**メールアドレスが既に登録されている**

- **エラーコード：**`FirebaseAuthUserCollisionException`
- 入力されたメールアドレスがすでにFirebaseに登録済みの場合に発生します。

**不安定なインターネット接続**

- **エラーコード：**`FirebaseNetworkException`
- ネットワーク接続が切れている、または非常に不安定な場合に発生します。

**Firebase側の問題**

- **エラーコード：**`FirebaseAuthInvalidCredentialsException` または `FirebaseAuthInvalidUserException`
- Firebaseプロジェクトの認証設定が正しく構成されていない場合や、クライアントアプリのAPIキーが無効になっている場合に発生します。

**サーバー側エラー**

- **エラーコード：**`FirebaseException`
- Firebaseのサーバーで一時的な問題が発生した場合（例: サーバーがダウンしている、リクエスト数が多すぎる）に発生します。

<br>

問題なく新規アカウントが作成されたかどうか、再度FirebaseのAuthentication画面を確認してみましょう。

以下のように、自身で指定したメールアドレスをIDに持つユーザーが追加されていたら成功です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/5b2b0504-6c76-3933-b1a1-62ca22c5cbbb.png)


<br>

# 2.ユーザー機能の実装

続いてログイン機能も作ってみましょう。

先ほどのコードにログイン用のフィールドとメソッドを用意します。

```csharp
using Firebase.Auth;
using Firebase.Extensions;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace grpc_chat_app.Scripts
{
    public class LoginOrSingUpView : MonoBehaviour
    {
        ...

        [SerializeField] private TMP_InputField loginEmail = default!;
        [SerializeField] private TMP_InputField loginPassword = default!;
        [SerializeField] private Button loginButton = default!;
        [SerializeField] private TMP_Text loginFeedbackText = default!;

        private FirebaseAuth _auth;

        private void Start()
        {
            _auth = FirebaseAuth.DefaultInstance;

            signUpButton.onClick.AddListener(RegisterButton);
            loginButton.onClick.AddListener(LoginButton);
        }

        // メールアドレスとパスワードで新規登録
        private void RegisterButton()
        {
            ...
        }

        // メールアドレスとパスワードでログイン
        private void LoginButton()
        {
            var email = loginEmail.text;
            var password = loginPassword.text;

            // Firebase Authenticationを使ってログイン処理
            _auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWithOnMainThread(task =>
            {
                if (task.IsCanceled)
                {
                    Debug.LogError("SignInWithEmailAndPasswordAsync was canceled.");
                    loginFeedbackText.text = "ログインがキャンセルされました。";
                    return;
                }

                if (task.IsFaulted)
                {
                    Debug.LogError("SignInWithEmailAndPasswordAsync encountered an error: " + task.Exception);
                    loginFeedbackText.text = "エラー: " + task.Exception!.InnerExceptions[0].Message;
                    return;
                }

                // ログイン成功
                var user = task.Result;
                Debug.LogFormat("User signed in successfully: {0} ({1})", user.User.DisplayName, user.User.UserId);
                loginFeedbackText.text = "ログイン成功！";
            });
        }
    }
}
```

基本的な処理の流れは、新規でアカウント作成する場合とほとんど変わりません。

それでは、各コンポーネントをアタッチしていきましょう。`Login`下にある各`GameObject`をアタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/d778873f-3e35-97a9-c7a3-5d4996cb4a0e.png)


アタッチが完了したら、先ほど新規アカウント作成時に用意したメールアドレスとパスワードを打ち込んで、ログインができているかどうかを確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/f597cf04-5dbf-8967-a37e-b85cf49ac6f8.png)

ログイン時に発生し得るエラーについても、新規アカウント作成時のものとは少しだけ異なるので、確認しておきましょう。

<br>

## ログインを試行した際に発生し得るエラー

**無効なメールアドレス**

- **エラーコード：**`FirebaseAuthInvalidCredentialsException`
- 入力されたメールアドレスの形式が正しくない場合に発生します（例: `example@com` や `example.com`）。

**間違ったパスワード**

- **エラーコード：**`FirebaseAuthInvalidCredentialsException`
- 入力されたパスワードが登録されているものと一致しない場合に発生します。

 **アカウントが存在しない**

- **エラーコード：**`FirebaseAuthInvalidUserException`
- 入力されたメールアドレスがFirebaseに登録されていない場合に発生します。詳細メッセージには、「There is no user record corresponding to this identifier」といった内容が含まれます。

**アカウントが無効化されている**

- **エラーコード：**`FirebaseAuthInvalidUserException`
- 管理者によってアカウントが無効化されている場合に発生します。詳細メッセージには、「The user account has been disabled by an administrator」といった内容が含まれます。

**不安定なインターネット接続**

- **エラーコード：**`FirebaseNetworkException`
- ネットワーク接続が切れている、または非常に不安定な場合に発生します。

**Firebase側の問題**

- **エラーコード：**`FirebaseAuthInvalidCredentialsException` または `FirebaseAuthInvalidUserException`
- Firebaseプロジェクトの認証設定が正しく構成されていない場合や、クライアントアプリのAPIキーが無効になっている場合に発生します。

**サーバー側エラー**

- **エラーコード：**`FirebaseException`
- Firebaseのサーバーで一時的な問題が発生した場合（例: サーバーがダウンしている、リクエスト数が多すぎる）に発生します。

:::note
ここまでで、アカウント新規作成・ログイン試行時で発生する可能性のあるエラーを見てきましたが、より詳細にエラーコードを見たい場合は、以下の公式ドキュメントを参照してください。

[**A**dmin Authentication API Errors](https://firebase.google.com/docs/auth/admin/errors?hl=ja)
:::

<br>

# 3.ログイン状態のチェック

ここまでで、新規アカウントの作成とログイン処理が実装できました。

しかし、アプリを開くたびにログインを行うのはとても面倒です。実は、「Firebase Authentication」を使えば、アプリが起動したときに、既にユーザーがログインしているかどうかを確認することもできます。

`LoginOrSingUpView.cs`を編集し、Startメソッドにて現在のユーザーのログイン状態をチェックするように変更してみます。
合わせて、以下の項目の対応もしておきましょう。

- ログイン済みであれば、即座にチャット画面を表示
- Chat Startボタンのイベント追加
    - ログイン状態ならチャット画面を表示
    - 未ログイン状態なら無反応（ログにエラーを表示）

```csharp
...

[SerializeField] private GameObject chatScreen = default!;
[SerializeField] private Button chatStartButton = default!;


private void Start()
{
    _auth = FirebaseAuth.DefaultInstance;
    
    // アプリ起動時に現在のユーザーをチェック
    var currentUser = _auth.CurrentUser;
    if (currentUser != null)
    {
        // ユーザーがログインしている場合、情報を取得
        Debug.Log($"User is logged in: {currentUser.DisplayName} ({currentUser.UserId})");
        loginFeedbackText.text = $"ログイン中: {currentUser.DisplayName} ({currentUser.UserId})";
        signUpFeedbackText.text = $"ログイン中: {currentUser.DisplayName} ({currentUser.UserId})";

        chatScreen.SetActive(true);
        gameObject.SetActive(false);
    }
    else
    {
        // ユーザーが未ログインなら、ログイン画面を表示
        Debug.Log("No user is logged in.");
    }

    ...
    
    chatStartButton.onClick.AddListener(() =>
    {
        if (_auth.CurrentUser == null)
        {
            Debug.LogError("User is not logged in.");
            return;
        }
        chatScreen.SetActive(true);
        gameObject.SetActive(false);
    });
}
```

上記コード実装後、以下のようにアタッチしておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36899/67a1fe3a-2de4-9dd0-0a48-0c6b3899039c.png)


`_auth.CurrentUser`には現在ログインしているユーザー情報が入ります。このプロパティは、ユーザーがログインしていない場合には`null`を返します。

ログインしている場合、`DisplayName`や`UserId`などの情報を取得できます。これらをUIに反映させることで、ユーザーにログイン状態を表示することもできます。

これでログインが継続されているかチェックして、継続されていればそのままその情報が使えるようになりました。

<br>
では、認証状態はどれくらいの期間維持されるのでしょう？

<br>


## **Firebaseの認証状態の維持について**

Firebase Authenticationでは、認証状態が通常は長期的に保持されます。これにより、ユーザーがアプリを再起動しても、アプリ側はログイン状態を維持することができます。

ただし、以下のケースでは再認証が必要になることがあります。

- トークンの有効期限が切れた場合
- アカウントが無効化された場合
- パスワードが変更された場合

アカウントやパスワードに関しては理解しやすいと思いますが、ここでいうトークンとは何でしょうか？

<br>

## トークンとは

**アプリケーションなどでユーザーの認証・認可に使われる、電子的な「鍵」や「証明書」のようなもの**です。特にFirebaseのようなクラウドサービスでは、認証やセッション管理に利用されます。

Firebaseを含む認証システムでよく使用されるトークンには、以下のような種類があります。

### **IDトークン (Identity Token)**

ユーザーの認証情報（ユーザーIDやメールアドレスなど）を含むトークンです。

Firebase Authenticationでは、このトークンを使用してユーザーの識別が可能です。

特徴：

- **短期間有効**（通常は1時間）。有効期限が切れると再取得が必要になります。
- JSON形式でエンコードされており、アプリやサーバーがトークンを解析して必要な情報を取り出すことが可能です。

使用例：

- Firebase Realtime DatabaseやFirestoreなどのデータベースへのアクセス時にユーザーの認証を行う。
- ユーザーのログインセッションを確認する。

<br>

### 更新トークン (Refresh Token)

Dトークンやアクセストークンの有効期限が切れた際に**新しいトークンを取得するためのトークン**です。

長期間有効（無期限に近い）で、ユーザーのログインセッションを維持します。

特徴：

- クライアント端末に安全に保存され、バックグラウンドでFirebase SDKが自動的に管理します。
- Firebase Authenticationでは、ユーザーが再ログインすることなく、IDトークンの自動更新を可能にします。

使用例：

- ユーザーの認証セッションを継続する。

<br>

### アクセストークン (Access Token)

**APIや特定のリソースへのアクセスを許可するためのトークン**です。

Firebaseのカスタムサービスや、外部のAPIと連携する場合に使用されることがあります。

特徴：

- アクセス権限（スコープ）や有効期限が含まれます。
- Firebase Authentication自体では直接管理されない場合が多いですが、外部サービスと連携する際に使用することがあります

使用例：

- カスタムバックエンドAPIへのアクセス制御。
- 特定のリソース（例: 特定のデータやファイル）への操作を制限。

このように複数種類のトークンを用途によって使い分けることで、トークンのセキュリティ性を高め、セッションが安全に維持できるようになります。

<br>

### **Firebaseにおけるトークンの役割と流れ**

1. ログイン時にIDトークン発行します。
2. クライアントアプリがIDトークンをサーバーに送信し、ユーザーの認証を行います。
サーバー側ではトークンを検証して、ユーザーが認証済みかどうかを判断します。
3. IDトークンの有効期限が切れると、Firebase SDKは更新トークンを使って新しいIDトークンを取得します。これにより、ユーザーが再ログインすることなく、認証状態を維持できます。

<br>

# まとめ

第五章では、Firebase Authenticationを活用して、メールアドレスとパスワードを用いたユーザー認証の実装をしました。新規アカウント作成からログイン機能、そしてログイン状態の確認や維持まで、基礎的な認証フローを構築することができたと思います。

今回のハンズオンでは以下のことを学びました。

- Firebase Authenticationの使用方法
- `ContinueWithOnMainThread`について
- 新規登録実装方法
- ログイン処理実装方法
- トークンの役割について

次回は、Firestoreを活用したユーザーデータ管理を解説します。

もしも記事の中で進められない箇所があったら、Xなどでご連絡ください。