Firebase Authentication を利用した認証

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

今日は Firebase Authentication 認証について書きます。

選定理由

まず、なぜ今回 Firebase Authentication を利用することに決めたかを説明します。検討した選択肢としては

  • Firebase Authentication
  • Amazon Cognito
  • Auth0
  • 自前でがんばる

あたりです。このうち自前でがんばるのは大変なので最後の手段とし、可能な限りマネージドなサービスを検討しました。Rails 時代はセッションを Cookie ストアにしていたので、サーバー側でデータストアが不要だったりしてそんなに面倒ではなかったのですが、安全面を考えると Cookie ストアはやめたいと思っていたし、JWT にするにしても、結局安全面を考えると expire を短くしてリフレッシュトークンを使うことになりますが、リフレッシュトークンの管理をするのが面倒だったりします。なのでそのあたりをマネージドでできるのであればマネージドがいい。

マネージドサービスを検討するにあたり、考えないといけなかったのは過去のデータとの互換性です。ログインシステムを変更することで過去に作ったカレンダーや投稿を管理できなくなるのは避けたいところです。

Adventar ではこれまで、認証には omniauth という Gem を利用していました。omuniauth は色々な OAuth プロバイダを利用して認証システムを作ることができるライブラリです。Adventar では Google, Twitter, Facebook, GitHub をプロバイダとして利用していました。

なので、前提条件として、Google, Twitter, Facebook, GitHub の OAuth には絶対に対応している必要がありました。それを考えると Amazon Cognito は Twitter, GitHub のログインに対応していないので採用を見送りました。

Auth0 と Firebase Authentication はどちらも要件を満たしていましたが、Firebase Authentication はなんと無料で使えるということだったので、Firebase Authentication が今回のユースケースで使えるかを検証し、問題なさそうだったので今回利用を決めました。

Auth0 も Adventar ぐらいの規模だと無料か一番安いプラン($23)ぐらいで利用できそうではありましたが、個人開発にとって $23 はかなりでかいのです。

アーキテクチャ

Firebase Authentication は Firebase のその他の機能と連携すると、おそらくそんなに面倒なことはないのですが、今回は Firebase Authentication 以外に Firebase の機能は使わないので、Firebase Authentication を使った認証の仕組みを自前のサーバーに組み込む必要があります。

サーバー側が必要な情報は、ログインプロバイダと OAuth の uid 、名前とプロフィールアイコンの4つです。旧システムではログインプロバイダと OAuth の uid ユーザーの組み合わせを一意なユーザーの識別子として利用していたので、これがないとユーザーの識別ができません。名前とプロフィールアイコンは表示に使うだけなので、あれば嬉しいという感じです。これらを Firebase Authentication の API から取得してサーバーに送ることができればいいわけです。

Firebase Authentication ではまず最初に、クライアント側で認証の処理を行います。要点だけ説明していきますので、詳細はドキュメントを見てください。

https://firebase.google.com/docs/auth/web/firebaseui

まず、ユーザーが Adventar から、「Google でログイン」を押すと以下の処理が実行されます(API Key などは別途設定しています)。

firebase.auth().signInWithRedirect(new firebase.auth.GoogleAuthProvider());

これでユーザーは Firebase Authentication のサイトを経由して Google 認証の画面に飛びます。ユーザーが Google の認証情報を入力して成功すると Adventar に戻ってくるわけですが、このとき以下のようなコードで JWT のトークンを取得することができます。

firebase.auth().onAuthStateChanged(async user => {
  const token = await user.getIdToken();
});

実際のコードはこのあたりです。

https://github.com/adventar/adventar/blob/f580de20510f9debe6356a5ad193c4532d8f6a0d/frontend/lib/Auth.ts#L105-L127

この token の実態は JWT で、前述したログインプロバイダ、uid、名前、プロフィールアイコンなどのほしかった情報が入っています。

なので、これをサーバー側に投げれば Adventar が持っているデータと突き合わせてユーザーを識別できるのですが、この token が改ざんされていないかを確認するため、JWT の検証が必要です。

https://firebase.google.com/docs/auth/admin/verify-id-tokens

今回はアプリケーションサーバーに Go を使っていますが、Firebase Authentication の Go SDK に JWT の検証機能があるのでこれを使います。

json := os.Getenv("FIREBASE_CREDENTIAL_JSON")
if json == "" {
    return nil, fmt.Errorf("FIREBASE_CREDENTIAL_JSON is empty")
}
opt := option.WithCredentialsJSON([]byte(json))
app, err := firebase.NewApp(context.Background(), nil, opt)
if err != nil {
    return nil, xerrors.Errorf("Failed to initialize firebase app: %w", err)
}

client, err := app.Auth(context.Background())
if err != nil {
    return nil, xerrors.Errorf("Failed to get auth client: %w", err)
}

token, err := client.VerifyIDToken(context.Background(), idToken)
if err != nil {
    return nil, xerrors.Errorf("Failed to verify token: %w", err)
}

こんな感じです。実際のコードは以下にあります。

https://github.com/adventar/adventar/blob/f580de20510f9debe6356a5ad193c4532d8f6a0d/api-server/grpc-server/util/auth.go#L24-L83

検証が済んだらこの token に含まれるデータを信頼できるので、それを使ってユーザーの認証を行います。

まとめ

Firebase Authentication を選んだ理由や基本的な認証の仕組みについて解説しました。

明日は Firebase Authentication を実際のアプリケーションに組み込んだときに工夫した点や苦労した点について書きたいと思います。