Firebase Authentication の苦労話

Adventarを支える技術 Advent Calendar 2019 の8日目です。

今日は Firebase Authentication で苦労した点や工夫した点について書きます。

ログインの判定が遅い問題

Firebase Authentication を使って、現在のセッションがログインしているかどうかを検出するには、firebase.auth().onAuthStateChanged ()を呼び出します。

https://firebase.google.com/docs/auth/web/manage-users

firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    // User is signed in.
  } else {
    // No user is signed in.
  }
});

しかし、この処理は呼び出してから初回のイベントが発火するのに数秒ぐらいレイテンシがあります。なので、愚直にこの機能を使うと、ページがロードされて、ログインしていれば数秒後にログインしている状態(自分のアイコンがヘッダに表示されたり、自分の投稿に編集ボタンが出たり)に切り替わる、ということになり、いい体験ではありません。

そこでログイン状態は localStorage にキャッシュすることにしました。キャッシュしているコードはこのへんです。

キャッシュした情報は plugin でリストアしています。

https://github.com/adventar/adventar/blob/f580de20510f9debe6356a5ad193c4532d8f6a0d/frontend/plugins/auth.ts

この処理は初期描画の直前に一回だけ実行されるので、描画と同時にログイン状態がわかります。機密情報をlocalStorageに保存するのはセキュリティ的なアレがありますが、今回保存するのは token そのものでなく、表示に必要なデータだけにしているのでそのあたりは安心です。

これでほぼよさそうに見えますが、キャッシュの状態が不整合になる可能性を考えてみましょう。

  1. 別タブでログアウトした/別のアカウントでログインした
  2. ログイン処理を終えてリダイレクトされて戻ってきた

1 のケースは問題にならなくて、firebase.auth().onAuthStateChanged()のイベントはユーザーのログイン状態が変わったら発火するイベントリスナーなのですが、別タブでログイン/ログアウトしたような場合でもイベントが発火します。なので、別タブでログイン/ログアウトしてもユーザーの状態は同期されます。

2のケースは、ログインプロバイダのログイン画面から戻ってきたときの話しです。ログイン画面から戻ってきた瞬間なので、キャッシュの状態は未ログイン状態のはずですが、実際にはプロバイダでのログインが成功していればログイン状態となるはずです。なので、キャッシュと実際の状態が不整合になるのは実はこの場合だけです。

このケースは、ログイン処理中であるというローディングを出すことで対応しました。最初は画面全体をローディングにしていましたが、状態が変わってから再描画するところはそんなに多くなく、ログイン処理中というのがわかればいいので、ヘッダのアイコンのところだけ局所的にローディングすることにしました。

f:id:hokaccha:20191205123241p:plain

ただ、プロバイダのログイン画面から戻ってきた、という状態を取る方法がなさそうだったので、リダイレクト前に sessionStorage に適当な値をセットして、次に初期化処理が走った場合はリダイレクトから帰ってきたとみなして、firebase.auth().onAuthStateChanged()が発火するまでローディングを出します。

唯一不自然になるパスは、ログインを押してログイン画面に遷移後、ログインを完了せずに戻ってくるパターンですが、まあそんなに致命的な問題でもないので許容しています。

アイコン URL が更新されない問題

Adventar ではユーザーアイコンに OAuth でとってきた画像を使っています。例えば Twitter でユーザーがアイコンを変えた場合には Adventar でも新しいアイコンになってほしいので、ログインの処理のときにアイコンをアップデートする処理をいれています。コードはこのあたりです。

https://github.com/adventar/adventar/blob/b73af145638e9f4d4da6655015786bdac54510eb/api-server/grpc-server/service/user.go#L47-L57

しかしこれではダメで、Firebase Authentication は最初にログインしたときの情報を Firebase Authentication 側にストアしてもっているらしく、ユーザーが Twitter 側でアイコンを更新しても、Firebase Authentication 側のデータは更新されないようです。

いくつか方法はありそうですが、一旦今はログアウト時に Firebase Authentication 側のユーザーを消すという方法を取っています。

https://github.com/adventar/adventar/blob/b73af145638e9f4d4da6655015786bdac54510eb/frontend/lib/Auth.ts#L56

export async function logoutWithFirebase() {
  const user = firebase.auth().currentUser;
  if (!user) return;

  try {
    await user.delete();
  } catch (err) {
    console.error(err);
  }

  await firebase.auth().signOut();
}

Adventar 側では特に Firebase Authentication の User ID などは使ってないので、ログアウト時に消しても問題ないためです。ただ、ユーザーとしてはアイコンを更新するためにログアウトが必要、というのは非常にわかりにくいので、もう少しいい方法を考えたいところです。

Firebase Authentication でログイン時にプロバイダの API のアクセストークンが取れるので、それで API をこちらで叩いて最新のアイコンを取得して更新、という感じになるかなと思っています(が、けっこうめんどくさいので後回しになっている)。

third-patry cookie が無効な場合にログインできない問題

これはそのままなのですが、Firebase Authentication は third-patry cookie が無効な場合はログインが失敗するという問題があります。

https://github.com/firebase/firebase-js-sdk/issues/934

これはもう仕方ないので、ログイン時のエラーをハンドリングして、Cookie によるエラーの場合は third-party cookie が無効な可能性がある、というメッセージを表示することにしました。

https://github.com/adventar/adventar/blob/b73af145638e9f4d4da6655015786bdac54510eb/frontend/lib/Auth.ts#L88-L92

const COOKIE_ERROR_MSG =
  "third-party cookie の設定が無効になってる可能性があります。ブラウザの設定をご確認ください。";
const msg = err.code === "auth/web-storage-unsupported" ? COOKIE_ERROR_MSG : err.message;
alert(`ログインに失敗しました。\n${msg}`);
console.error(err);

GitHub 認証の場合にユーザー名が空のケースがある問題

Firebase Authentication から取得できる JWT の中には名前やプロフィールアイコンのURLなどが以下のような形で格納されています。

{
  "name": "Kazuhito Hokamura",
  "picture": "https://lh3.googleusercontent.com/a-/AAuE7mDtNB98Nu2WHMoeBUs1x3XrNqCrav4GnZRTbwMB8g",
  "user_id": "xxx",
  "sub": "xxx",
  "iat": 1234567890,
  "exp": 1234567890,
  "firebase": {
    "identities": {
      "google.com": [
        "xxx"
      ]
    },
    "sign_in_provider": "google.com"
  }
}

この namepicture がそうです。GoogleTwitter などのプロバイダに設定してある名前やアイコン画像が取れるのですが、GitHub の場合、name が含まれない場合がありました。

GitHub は名前の入力が必須ではないので、入力してないユーザーの場合はこのフィールドがない状態の token になります。名前は必ず存在する、という前提でコードを書いていたので、そのような状態のユーザーがログインしようとするとエラーになってログインできない状態になっていました。

空の場合は user_id (github.com/xxxxxx)の部分が取れればいいのですが、Firebase Authentication 経由だとその情報は取れなさそうだった(もちろん自前でアクセストークン使って GitHubAPI を自前で呼び出せばとれますが)ので諦めて"No Name"のような名前でユーザーを作ることで対応しました。

https://github.com/adventar/adventar/blob/b73af145638e9f4d4da6655015786bdac54510eb/api-server/grpc-server/util/auth.go#L47-L51

まとめ

Firebase Authentication でハマった点などをいくつか紹介しました。慣れてないので色々とハマりましたが、やはり自前で認証の仕組みを用意するのに比べると格段に楽でよかったです。

明日は Server Side Rendering のについて書きます。