Firebase Authenticationの認証とアカウントリンク

[PONOS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ponos)の3日目の記事です。

昨日は[@e73ryo](https://qiita.com/e73ryo)さんのScriptableWizardからコンポーネントとプレハブを同時に作成する例でした。

# はじめに
Firebase Authenticationを利用すると簡単にアプリに認証機構を導入できます。
一時的な匿名認証の他、一般的なパスワード認証、さらにGoogleアカウントやFacebookログイン等を簡単に導入することができます。
Firebase Authenticationでは大まかに三つの挙動を上げることができます。
1. サインアップ（アカウント作成）
2. サインイン
3. アカウントを他の認証にリンクする
今回はこれらの認証を呼び出した時の状態変化がどうなるかを検証してみたいと思います。

また、ここでは具体的な実装方法は省略いたします（Firebaseのドキュメントをみてね）。

# この記事の対象者
- Firebase/Firebase Authenticationの基本的な知識がある

# 検証環境
今回はお手軽に試すためにFirebaseSDKのWEBクライアントを使用しますが、基本的な考え方は例えばUnity版のSDKなどでも同様だと思います。
FirebaseSDKのバージョンは7.23.0

# 試してみたいこと
Firebaseで任意の手段でサインインする場合、`signInXXXX`というメソッドを使用します。
ログイン中のアカウントに対して任意の手段の認証方法を紐付ける（Googleログインやパスワード）場合は、`linkWithXXXX`というメソッドを使用します。

一方やや特殊に見えるのが、新しいアカウントを作成する場合です。

例えば匿名認証を使用する場合は単に`signInAnonymously`を呼び出します。匿名アカウントに対して明示的な作成を行うメソッドはなく、ログインしていなければ新たなアカウントが作成され、そのアカウントにログインした状態になります。
一方パスワード認証を使用する場合、最初から入力したメールアドレス/パスワードのアカウントを作成する場合は`createUserWithEmailAndPassword`を使用し、サインインする場合には`signInWithEmailAndPassword`を使用します。

ではGoogleログインを使用してアカウントがない状態からログインしようとした場合や、すでにログインしている場合などに他の`signInXXXX`を呼び出したりした場合にどのような挙動になるかを検証してみたいと思います。

# 検証する認証の種類
- 匿名認証
- パスワード認証
- Googleログイン

の代表的な三つの組み合わせを使って試してみます。

# 検証
## ベーシックな操作
### 1. 匿名認証でアカウントの利用を開始する
もっともベーシックな部分のおさらいです。
匿名認証を使う場合、明確なアカウント作成操作はなく、サインイン呼び出しでOKです。

    auth.signInAnonymously().catch(function(error) {
      // ...error
    });

|事前状態|実行操作|結果|
|---|---|---|
|サインアウト|匿名認証サインイン|匿名アカウントサインイン|

以降は匿名アカウントにサインインした状態となり、次回アクセス時（FirebaseAuthenticationSDK初期化時）に、ローカルの情報が消えていなければ自動的に同じ匿名アカウントとしてサインインした状態になります。

### 2. Googleログインでアカウントの利用を開始する
匿名認証を永久アカウントにするのではなく、最初からGoogleログインで永久アカウントを作ります。

    var provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(function(result) {
      // ...success
    }).catch(function(error) {
      // ...error
    });

※ ここではポップアップを使用していますが、リダイレクト版もあります。

|事前状態|実行操作|結果|
|---|---|---|
|サインアウト|パスワード認証サインアップ|永久アカウントサインイン<br>(Googleログイン)|

Googleログインを利用して`signInXXXX`する事で、自動的にFirebase側のアカウントが作成されるようです。
ちなみに当然、すでにそのGoogleアカウントに連携したアカウントがあれば、そちらにログインすることになります。

### 3. パスワード認証でアカウントの利用を開始する
最初からパスワード認証でアカウントを利用する場合、`signInXXXX`ではなく`createUserWithEmailAndPassword`を使います。

匿名認証はアカウントが存在しないということがあり得ず（誰でも始められる）、Googleログインなどの場合はサードパーティ側のアカウントがあればよいため、`signInXXXX`を呼び出すことで自動的にFirebase側のアカウントが作られるという事だと思います。
一方パスワード認証の場合はFirebase側にアカウントがなければ当然"アカウントが見つかりません"となるため、明示的なアカウント作成操作を伴うという流れなのだろうなと解釈。

（ちなみに本題とややずれますが、カスタム認証を使用する場合もいきなりカスタム認証から始める事で、こちら側が指定したIDのアカウントを作成できました。）

    firebase.auth().createUserWithEmailAndPassword(email, password).catch(function(error) {
      // ...error
    });

|事前状態|実行操作|結果|
|---|---|---|
|サインアウト|パスワード認証サインアップ|永久アカウントサインイン<br>(PASSWORD)|

このメソッド呼び出しでアカウント作成からサインイン状態になります。別途サインイン呼び出しは不要なようです。

主なエラーとして、そのメールアドレスを使用したアカウントがすでに作成されていた場合はエラーコード`auth/email-already-in-use`が返されます。
また、それがGoogleログイン認証を使ったアカウントであっても、メールアドレスが同一であればエラーコードが返されるようです。
つまり認証方法によらずメールアドレスが共通であれば同一の人物であるべきということだと思います。

### 4. 匿名認証アカウントにパスワード認証手段をリンクする
匿名アカウントを永続化する一般的な手段１。

    var credential = firebase.auth.EmailAuthProvider.credential(email, password);
    auth.currentUser.linkWithCredential(credential).then(function(usercred) {
      // ...success
    }, function(error) {
      // ...error
    });


|事前状態|実行操作|結果|
|---|---|---|
|匿名アカウントサインイン|パスワード認証リンク|永続アカウントサインイン<br>(PASSWORD)|

`createUserWithEmailAndPassword`と同様、メールアドレスがすでに他アカウントに対して使用されていた場合などはエラーが返されます。
ただし、自身のアカウントに同じメールアドレスのGoogleアカウント認証がすでに紐づいていた場合については問題ありませんでした。

### 5. 匿名認証アカウントにGoogleログイン認証手段をリンクする
匿名アカウントを永続化する一般的な手段2。

    var googleProvider = new firebase.auth.GoogleAuthProvider();
    auth.currentUser.linkWithPopup(provider).then(function(result) {
      // ...success
    }).catch(function(error) {
      // ...error
    });

※ ここではポップアップを使用していますが、リダイレクト版もあります。

|事前状態|実行操作|結果|
|---|---|---|
|匿名アカウントサインイン|Googleログイン認証リンク|永続アカウントサインイン<br>(Googleログイン)|

すでにそのGoogleアカウントにリンクしている別のアカウントが存在していた場合はエラーコードが返されます。

### 6. パスワード認証アカウントにGoogleログイン認証手段をリンクする
複数の認証手段をアカウントに紐付ける場合です。
コードは5のケースと同様です。

|事前状態|実行操作|結果|
|---|---|---|
|永続アカウントサインイン<br>(PASSWORD)|Googleログイン認証リンク|永続アカウントサインイン<br>(PASSWORD,Googleログイン)|

こちらも特に違和感はなく想定した通りの結果になりました。

## ややイレギュラー気味な操作

### 1. 匿名アカウントでサインイン中に匿名認証でサインインを実行する
やりがちのがこのケースではないかと思います。
匿名アカウントへのログインは`signInAnonymously`メソッドで行いますが、2回目以降の利用時は（ローカルにトークンがある限り）自動的にログインしてくれます。
しかし利用開始時に単純に`signInAnonymously`呼び出しを行うように実装していると、実は意図せずこの操作をしてしまっていることもあると思いますので、動作をみてみました。

ドキュメントでは次のようにコメントされています。
> Asynchronously signs in as an anonymous user.
> 
> If there is already an anonymous user signed in, that user will be returned; otherwise, a new anonymous user identity will be created and returned.

つまり、すでに匿名ユーザでログインしている場合、同じユーザを返すとのことです。それなら安心！
気になったのでiOS版のSDKのコードの中身をみたところ、現在匿名ユーザとしてログインしているかどうかをチェックする条件分岐がありましたので、同じユーザを返すというのはサーバAPIの動作ではなくSDKでの動作ということになりそうです。
（7.23.0ではこの様に動作しましたが、以前のバージョンではSDKに不具合があり、この操作をすると必ず異なる匿名アカウントにログインしなおしが発生していたようですので、バージョンにも注意が必要です。）

しかし注意が必要な点もあります。
アプリをスタートしてFirebaseSDKを初期化してから自動的に前回の匿名アカウントにログインするまでは少しタイムラグがあるようです。
そのため直後に`signInAnonymously`を呼び出すと、前回とは異なる匿名アカウントにサインインしてしまいます。
Unityを例にとると、

    this.auth = Firebase.Auth.FirebaseAuth.DefaultInstance;
    this.auth.StateChanged += AuthStateChanged;

を実行した直後に呼び出してしまうと、別の匿名アカウントになることが確認できました。

### 2. 匿名アカウントでサインイン中にGoogleログインでサインインを実行する
この操作は想定通りで、現在の匿名アカウントからサインアウトし、Googleアカウントにリンクしたアカウントにサインインしなおします。

ただ、前述で試したとおりパスワード認証を除いて`signInWithXXXX`は自動的にアカウント生成も行うことから、
そのGoogleアカウントにリンクしたアカウントがない場合でも、Firebase側の新しいアカウントが生成されてそちらにサインインします。

### 3. 永久アカウントでサインイン中に匿名認証でサインインを実行する
`signInAnonymously`は同じユーザを返すと書いてありましたが、それはあくまでも匿名アカウントであればの場合です。
この場合では、永久アカウント（例えばGoogleログイン済みなど）からサインアウトし、新たに匿名アカウントが作成されてそちらにサインインします。

### 4. パスワード認証で設定したメアドと同じメールアドレスを持つGoogleアカウントでサインインを実行する
どういう意味かというと、

- メールアドレスAを設定したアカウントAがすでに存在する
- そのアカウントはGoogleログインのリンクはしていない
- そのアカウントへサインインもしていない

この状態で、同じメールアドレスAを持つGoogleアカウントでサインインを実行します。
これまでにあったように`signInWithXXXX`は自動的にアカウントを生成しますが、一方で同じメールアドレスを持つアカウントを複数作ることはできないようです。なのでこの場合は一体どうなるのか。。。

結果、アカウントAに対してGoogleログインのリンクが設定された状態になり、アカウントAへサインイン状態になりました。

### 5. パスワード認証アカウントでサインイン中にGoogleログインでサインインを実行する１
上記のケースと同じですが、これをサインイン中に実行してみます。

結果としては、アカウントAに対してGoogleアカウントがリンクした状態になりました。
しかし、WEBクライアントでリダレクトを使った方法（`signInWithRedirect(provider)`）だと、うまく動作しなくなることがたびたびありました。実験コードがおかしかったのか検証しきれなかったのではっきり断言できませんが、うまくいく時といかない時が。。

本来意図としては`linkWithXXXX`を呼ぶべきところだと思いますので、この呼び出し自体はやらないほうがよいかと考えますが。

### 6. パスワード認証アカウントでサインイン中にGoogleログインでサインインを実行する２
上記のケースを、異なるメールアドレスを持つGoogleアカウントでやってみます。

こちらの結果は想像のとおり、Firebase側に別のアカウントが作成され、そちらにGoogleアカウントがリンクされました。

# まとめ
FirebaseSDKを利用することで簡単に認証を導入できますが、認証状態をブラックボックスにまかせることになります。
`signInWithXXXX`と`linkWithXXXX`などのルールを理解していれば、ほとんどのケースにおいては問題はないと思いますが、暗黙的に利用していると思わぬ動作をすることもあると思います。

特に現在の状態に対して行うべきは`signIn`なのか`link`なのかといった部分や、ここでは言及していませんがGoogleログインなどを連携させる場合の状態に対するエラーハンドリングは、こちらが意識せずにできてしまうからこそ、気を付けるべきポイントだと感じます。

※ 検証結果に間違い等ございましたらご指摘ください。

明日は[@e73ryo](https://qiita.com/e73ryo)さんです！