🚗

Firebase Authentication × Rails API構成での認証

2022/03/20に公開

はじめに

フロントをFlutter、APIをRailsで個人開発をしており、諸々の事情でFirebaseとRailsを使うことになったため、備忘も兼ねての記録です。

実現したいこと

メールアドレス・パスワードの管理をFirebaseに任せたいため、FirebaseAuthenticationで認証→Railsで認証して、認証成功時のみAPIコールできるようにする。

やること

流れとしては以下記事の10スライド目のイメージです。
https://www.slideshare.net/TomoeTeshima/firebase-auth-nuxt-rails

  1. フロントからFirebaseに認証情報(今回はメール・パスワード)を送る
  2. 認証成功したらidTokenを取得
  3. フロントからidTokenをRails(API)に送り、内容が適切か検証(検証がOKであれば各API内部処理へ)

※ フロント実装未済のためPostmanを使って動作検証をしました。

事前準備

事前に以下を実施ください。
- Firebaseにプロジェクト作成
- Firebase Authenticationを有効化(メール・パスワードプロバイダ)
- Firebase Authenticationのコンソールからユーザを追加
- Railsでusersテーブル作成(最低限string型のuidカラムを用意)
- Firebase Authenticationに追加したユーザのユーザIDをusersテーブルの任意のレコードのuidカラムに格納する
- 認証が成功したかどうかを確認するために、適当なAPI1つ作成しておく

1. フロントからFirebaseに認証情報(今回はメール・パスワード)を送る ・ 2. 認証成功したらidTokenを取得

Postmanで検証する場合、1.2は一緒に実施します。

Using Postman to Fetch a Firebase Tokenを参照し、以下のように設定し、Sendします。
kindlocalIdidTokenなどなどが返ってくればOKです。

3.フロントからidTokenをRails(API)に送り、内容が適切か検証

本件の核はここです。

firebaseのSDKが使えれば、基本的には以下のとおりにやればできると思います。
https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja

ただ、2022年3月時点ではfirebaseのSDKでRubyがサポートされていないのです:(

取り得る選択肢は2つです。

  1. この部分だけサポートされている言語でコーディングする
  2. 検証部分を自前で実装する(上記firebase公式のサードパーティのJWTライブラリを使用してID トークンを確認する箇所)

今回は2で進めることにしました(特に深い意味はなく、認証まわりの理解を深めるよいきっかけだと思ったことが1番の理由です)。

やりたいことは細かくすると以下のとおりです。

  • 各APIコールされたタイミングで、AuthorizationヘッダーのBearerに指定されるidTokenを取得
  • jwt形式のidTokenをデコード
  • ヘッダー、ペイロード、署名の内容がサードパーティのJWTライブラリ~~~にある制限を満たすことを確認

【JWTライブラリ】
https://github.com/jwt/ruby-jwt

以前はknockが使われることも多かったようですが、knockのREADMEにはDISCLAIMERとして、メンテンナンスしてないからruby-jwt使うことをオススメしますと記載があります。

【実装】
jwt形式のidTokenをデコードするためのモジュールを作成。
基本的に以下を参照し実装。
https://satococoa.hatenablog.com/entry/2018/10/05/210933
ruby-jwtの使い方を把握し、firebase公式に記載のある検証すべき内容を網羅できていればOK。
  
次にapplication_controller.rbにて本丸の認証部分のコード書きます。
簡単に内容を下記します。
上記モジュール(FirebaseAuthenticatorと命名)をApplicationControllerでincludeし、定義したauthenticateメソッド内でFirebaseAuthenticatorモジュールのdecodeメソッドを呼び、モジュール側で内容の検証をします。

検証が失敗したらモジュール側でraiseするようにしています。
検証が成功したらpayloadが取得できるので、そこからuser_idを拾い、登録があるユーザかどうかを検索し、いればcurrent_userとして扱えるようにします。

(補足)
decodeメソッドの引数にrequest.headers["Authorization"]ではなく、request.headers["Authorization"]&.split&.lastを渡している点について。
AuthorizationヘッダーのBearer tokenタイプで指定していると、request.headers["Authorization"]Bearer exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxの形となり、ruby-jwtで期待している形式ではないため、デコードできずエラーになるため、「Bearer」部分を除外する意図でsplitしています。

application_controller.rb



class ApplicationController < ActionController::API
  include FirebaseAuthenticator
  before_action :authenticate
  class AuthenticationError < StandardError; end
  rescue_from AuthenticationError, with: :not_authenticated

  def authenticate
    payload = decode(request.headers["Authorization"]&.split&.last)
    raise AuthenticationError unless current_user(payload["user_id"])
  end

  def current_user(user_id = nil)
    @current_user ||= User.find_by(uid: user_id)
  end

  private
  def not_authenticated
    render json: { error: { messages: ["ログインしてください"] } }, status: :unauthorized
  end
end


【動作検証】

これで準備完了なので、
AuthorizationヘッダーのBearer Tokenタイプで先程取得したidTokenを設定し、適当なエンドポイントを指定しsendします。(事前にrailsのローカルサーバを起動するのを忘れずに!)

適切なidTokenを指定した場合には正しくAPIの内部処理が行われ、
不適切なそれを指定した場合にはエラーになることが確認できればOKです。

あとは認証が不要なAPI(登録用APIなど)においてはskip_before_action :authenticate_userを指定するなど、適宜最適化を図ってください。

おわりに

初めてこのあたりに深く入ったので非常に勉強になりました。
間違いやより良い方法などあればぜひコメントお寄せいただけると幸いです。

他に参考にさせていただいた情報

https://blog.shimar.me/2017/02/10/ruby-jwt
https://techblog.yahoo.co.jp/advent-calendar-2017/jwt/

Discussion