個人開発で、フロントをFlutter、APIをRailsを使って開発しており、認証情報管理はFirebase Authenticationに任せることにしたので、その周りの実装方針についてメモです。
実現したいこと
FirebaseAuthenticationで認証 → Railsで認証して、認証成功時のみAPIコールできるようにしたい。
やること
大枠の流れとしては以下記事の10スライド目のイメージ。 https://www.slideshare.net/TomoeTeshima/firebase-auth-nuxt-rails
- フロントからFirebaseに認証情報(今回はメール・パスワード)を送る
- 認証成功したらidTokenを取得
- フロントからidTokenをRails(API)に送り、内容が適切か検証(検証がOKであれば各API内部処理へ)
※ フロント実装未済のためPostmanを使って動作検証をしました。
事前準備
事前に以下を実施済の前提で書きます。
- Firebaseにプロジェクト作成 - Firebase Authenticationを有効化(メール・パスワードプロバイダ) - Firebase Authenticationのコンソールからユーザを追加 - Railsでusersテーブル作成(最低限string型のuidカラムを用意) - Firebase Authenticationに追加したユーザのユーザIDをusersテーブルの任意のレコードのuidカラムに格納する - 認証が成功したかどうかを確認するために、適当なAPI1つ作成しておく
フロントからFirebaseに認証情報を送り、idTokenを取得(1・2)
Postmanで検証する場合、1.2は一緒に実施。
Using Postman to Fetch a Firebase Tokenを参照し、以下のように設定し、Send。 → kind・localId・idTokenなどなどが返ってきます。
フロントからidTokenをRails(API)に送り、内容が適切か検証(3)
本件の核。
firebaseのSDKが使えれば、基本的には以下のとおりにやればできるのですが・・・ https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja
2022年3月時点ではfirebaseのSDKでRubyがサポートされていません:(
取り得る選択肢は2つ。
- この部分だけサポートされている言語でコーディングする
- 検証部分を自前で実装する(上記firebase公式のサードパーティのJWTライブラリを使用してID トークンを確認する箇所)
今回は2で進めることに!(認証まわりの理解を深めるよいきっかけだと思ったことが大きな理由です)。
やりたいことを分割すると以下のとおりです。
- 各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.rbclass ApplicationController < ActionController::API include FirebaseAuthenticator before_action :authenticate 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
適切な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/