Firebase Authentication + SPA(React/TypeScript) でユーザ登録／ログイン／ログアウト (2:要点の説明)

<font color="Red">※2018/10/29</font>
[Firebase Authentication + SPA(React/TypeScript) でサンプル作り直してみた](https://qiita.com/katsu-o/items/728812f5d7f682e80fc3)としてサンプルを作り直して記事を書きました。

----

[前回](https://qiita.com/katsu-o/items/df1684b98cccd002e041)、説明まで書くと長くなりそうなので 2 回に分けた 2 回目です。

お詫び：
当初は要点処理の説明を書こうと思っていたのですが、いろいろいじってるうちに作り直したくなってきました。
※記事ごとごっそり書き直そうとも思ったのですが、備忘録も兼ねて現段階の内容を書くことにしました。ご了承下さい。

# 処理の要点

Firebase SDK Authentication を使ってユーザーアカウントの作成や Sign-in 処理を実行した場合、```getRedirectResult()``` でリダイレクト系処理の結果の取得、```onAuthStateChanged()``` で認証状態変更の待ち受けを行うことになります[^1]。

今回のサンプルでは、以下の３つの認証関連アクションが存在しますが、それらのアクションからどのように上記の ```getRedirectResult()``` および ```onAuthStateChanged()``` に処理が流れていくかが要点になるかと思いますので、そこのあたり説明します。

###### 3つの認証関連アクション
1. Email & Password で Sign-up(ユーザーアカウント作成 & Sign-in)
2. Email & Passowrd で Sign-in
3. その他プロバイダ認証で Sign-in(ユーザーアカウント作成を伴う場合がある)

以下、それらについて説明します。

###### 1. Email & Password で Sign-up(ユーザーアカウント作成 & Sign-in)

- ```createUserAndRetrieveDataWithEmailAndPassword(email, password)```
  - ユーザーアカウント作成成功 -> ```onAuthStateChanged()```<font color="LimeGreen">(認証完了)</font>
  - ユーザーアカウント作成失敗[^2] -> ```signInWithRedirect(existing_provider)``` 
 [^3] -> ```getRedirectResult()```<font color="Crimson">(認証継続)</font>

###### 2. Email & Passowrd で Sign-in

- ```signInWithEmailAndPassword(email, password)```
  - Sign-in 成功 -> ```onAuthStateChanged()```<font color="LimeGreen">(認証完了)</font>

###### 3. プロバイダ認証で Sign-in(※ユーザーアカウント作成を伴う場合がある)

- ```signInWithRedirect(provider)``` -> ```getRedirectResult()```<font color="Crimson">(認証継続)</font>
- (以下、```getRedirectResult()``` 内にて)：

  - 認証プロバイダで認証された認証情報が既にユーザーアカウントに紐づけられている場合(Sign-in 成功) -> ```onAuthStateChanged()```<font color="LimeGreen">(認証完了)</font>
  - 認証プロバイダで認証された認証情報がどのユーザーアカウントにも紐づけられておらず、且つ、認証プロバイダで認証された Email アドレスがユーザーアカウントに存在しなかった場合(ユーザーアカウント作成を伴って Sign-in 成功) -> ```onAuthStateChanged()```<font color="LimeGreen">(認証完了)</font>
  - 認証プロバイダで認証された認証情報がどのユーザーアカウントにも紐付けられておらず、且つ、認証プロバイダで認証された Email アドレスがユーザーアカウントに既に存在した場合[^2] -> ```signInWithRedirect(existing_provider)```[^3] -> ```getRedirectResult()```<font color="Crimson">(認証継続)</font>
      - さらにこの後、 -> ```linkWithRedirect(provider_to_add)``` -> ```getRedirectResult()```<font color="Crimson">(認証継続)</font> -> ```onAuthStateChanged()```<font color="LimeGreen">(認証完了)</font> となって紐付けされた上で認証が完了する。

(補足)：sessionStorage で管理する情報なども含めてもうちょっと細かいところまで書こう思ってたのですが、もう作り直す気なのでこの程度にしておきます。

# 留意すべきと感じた点
###### 1. プロバイダ認証を使って Sign-in する場合、新規にユーザーアカウントが作成される場合がある

プロバイダ認証を使って Sign-in する場合、```signInWithRedirect(provider)``` 等のメソッドを使用しますが、当該プロバイダによる認証が成功した場合、そのプロバイダで認証された認証情報がどのユーザーアカウントにも紐付けられておらず、且つ、そのプロバイダによって返される Email アドレスがユーザーアカウントとして存在しない場合には、その Email アドレスをユーザーアカウントとする当該プロバイダ認証と紐づけられたユーザーアカウントが新規作成されます。

つまり、例えば GitHub に登録されている Email アドレスが xxx@gmail.com であった場合、ユーザーアカウントに xxx@gmail.com が存在しなければ xxx@gmail.com というユーザーアカウントが新規作成されるケースがあるということです(次に述べますが、これは Google 認証で Sign-in した場合に既存アカウントの上書きが発生しうる状態です)。

###### 2. Google 認証で Sign-in した場合に既存アカウントを上書きする場合がある

xxx@gmail.com が Google 認証以外でユーザーアカウントとして登録されている状態で、xxx@gmail.com を Google 認証で ```signInWithRedirect(provider_google)``` 等としてサインインした場合、xxx@gmail.com は Google 認証のユーザーアカウントとして上書きされてしまいます(この時、それまで登録されていた Google 認証以外の認証方法はクリアされてしまいます)。

つまり、Google 認証で Sign-in した場合、意図せずユーザーアカウントが上書きされるケースがあるということです。

なお、例外ハンドリング等でこの上書きを阻止することはできないようです(最終的に上書きされた後の結果が返されるだけ)。
[Firebase Overwrites Signin with Google Account](https://code.i-harness.com/en/q/26e0b68)
> Since google is the trusted provider for @gmail.com addresses it gets higher priority than other accounts using a gmail as their email. This is why if you sign in with Facebook then Gmail an error isn't thrown, but if you try going Gmail to Facebook then it does throw one.


###### 3. 既存の Firebase ユーザーアカウントに認証プロバイダからの認証の紐付けを行う際、認証プロバイダ側で設定されている Email アドレスと、Firebase 側でユーザーアカウントとして登録されている Email アドレスは違っていてもよい。

そのままです。
(同じでなければいけないと思ってました。。。Orz)
<img width="702" alt="not-gmail.png" src="https://qiita-image-store.s3.amazonaws.com/0/209550/9dd9d21e-d00a-bcbc-aa7c-b62da66cacf6.png">

###### 4. ユーザーアカウントに複数の認証プロバイダをリンクする際、追加したい認証情報が既にユーザーリスト内に存在する場合[^4]、その認証情報はリンクできない。

このこと自体は当たり前ではあるのですが、たとえば複数のアカウントをマージしたいような場合に行うべき処理について留意しておくべきかと思いました。

つまり、ユーザーリスト内に存在しなければ追加できるので、ユーザーリスト内からその認証情報を削除後、紐づけ元となるユーザーアカウントにその認証情報を追加する形をとればよいようです。ただしこの時、データの統合等の処理(ここは各アプリケーションによる)も必要なため、そこは自前で処理を行う必要があります。

# 所感
うーん、ちょっとやってみようかと軽い気持ちでやってみましたが、実運用を想定してあれやこれや考えるとそんなに簡単ではないですね。
いろいろと紹介されているるようにユーザー登録／ログインだけなら簡単ですが、認証プロバイダからの認証の紐付けまで加わると結構いろいろ考えないといけないです。
このあたりまで考慮すると、ユーザーアカウントの作成と認証プロバイダからの認証の紐付けは分けて考えたほうが良さそうな気がしています。

# 今後の方針
最終的にどうなるかは分かりませんが、いろいろ考えた結果以下のような感じで作り直そうかと思っています。

###### 1. 「Sign-up」「Sign-in」「紐付け」は明確に分ける。
- Sign-up 画面(Google, Email & Password (with name))
  - ユーザアカウントの作成。
  - Email の場合、本人確認(verify)も最終的には必要。
- Sign-in 画面(Google, Facebook, Twitter, GitHub, Email & Password)
  - いわゆるログイン。
  - あくまでログインのみ。この画面からの処理でユーザーアカウントが自動生成される場合があるが、そのユーザーアカウントは削除した上でログイン失敗とする(そうしないと 
 @gmail.com アカウントが Google 認証以外で出来てしまう可能性がある)。
- 紐付け画面(ログイン後の設定画面。紐付けと解除を行う)
  - ログイン後、そのユーザーに対してプロバイダ認証の紐付け／解除を行う(パスワード認証の追加／解除も可能)。
  - 現状ではユーザ未定の状態から紐付けまで行ってしまっているため、
      - 処理上のネストが深くてつらい
      - ユーザの意思による能動的な操作としたほうがよいと感じた(いじってるうちにいつのまにか紐付されてるのもなんだか。。。)
  - さらに、紐付け解除機能がそもそも無いのでこれも欲しいところ。

###### 2. Sign-up は、Google および Email & Password のみにする。また、Email & Password による Sign-up 時には @gmail.com は使わせない。

- Firebase が Google のサービスであることを考えると、できれば Google 認証は使いたい。
  - もちろん状況に応じて好きに決めればよいのですが。
- Email & Password も必要。
  - Sign-up は Google 認証だけとする手も考えられますが、実運用考えると柔軟性に欠けるので。
- ただし、ただ単に両者を許容すると、次の問題が発生してしまいます。
  - Email & Password で xxx@gmail.com を登録した後、Google 認証で xxx@gmail.com を使ってサインインした場合、xxx@gmail.com は Google 認証のユーザアカウントとして上書きされてしまう可能性があります(先述しましたが、この時それまで登録されていた Google 認証以外の認証方法はクリアされてしまいます)。
  - これを阻止するためには、xxx@gmail.com というユーザーアカウントを、Google 認証以外で作らせなければよい。
  つまり Email & Password による Sign-up で、@gmail.com を使わせなければよい(もっと言えば、Email & Password によるアカウント作成後、そのアカウントが @gmail.com アカウントであった場合にはそのアカウントを削除した上で Sign-up 失敗としたほうがよいかも)

欲を言えば匿名ログインも欲しいですね。
よーし、時間見つけて作るぞ。いつになるか分かりませんが。。。
※ある程度出来たらまた公開する予定です。

[^1]: コード内では mountFirebaseObservers() (src/containers/ApplicationManager.tsx) で行っています。
[^2]: ユーザーアカウントが既存の場合は紐付け処理のため、一度既存ユーザーアカウントに Sign-in するために signInWithRedirect(registered_provider) しています。
[^3]: コード内では linkToExistingAccount() (src/utilities/linkProviders.ts) で行っています。
[^4]: 「追加したい認証情報が持つ email アドレスがユーザーアカウントとして存在する」「追加したい認証情報が別のユーザーにリンクされている」