Rails と Firebase Authentication でJWT認証実装してみた

最近スマホアプリの開発は大体firebaseを使うようになっていて、認証もfirebase authenticationを使っておけば、実装がすごく楽になっていると感じます。

既にRuby on railsでwebアプリケーションを開発していて後からJWT認証を付けようとするとちょっと面倒かと思いますが、 firebase-auth-railsというGemを使って比較的簡単にJWT認証を行えるようになったので設定方法を書いてみます。

github.com

基本的にREADME通りで動いたのですが、どんな設定が必要なのかチョット補足します。 スマホアプリのバックエンドにこのGemを使ったので、Rails APIモードで実装したコードを記載しています。

環境

概要図

ユーザ登録時、以下のような仕組みでユーザを作ります。 f:id:moritamorie:20200115222155j:plain

ユーザ登録後、例えばマイ書籍一覧画面のような特定のユーザにしか見れない画面を開く時は以下のような仕組みで特定のユーザを判別します。 f:id:moritamorie:20200115223710j:plain

ディレクトリ/ファイル構成

├── app
│     └── controllers
│           └── api
│                ├── v1
│                │   └── auth
│                │        └── registrations_controller.rb
│                ├ application_controller.rb
│                └ books_controller.rb
└── config
      └── initializers
            └── firebase_id_token.rb

セットアップ

Redis

Redisをサーバにインストールしておきます。

$ sudo apt install redis-server

内部でfirebase_id_tokenというgemを使っていて、このgemはRedisが必須のためインストールしておきます。 google x509 証明書とその期限を管理するためにRedisを使っているようです。

Gem追加

Gemfileに以下を追記し、

gem 'firebase-auth-rails'

Gemをインストールしておきます。

$ bundle

実装

イニシャライザーの追加

config/initializers/firebase_id_token.rbにRedisとfirebabaseプロジェクトの設定を記載します。

FirebaseIdToken.configure do |config|
  config.redis = Redis.new
  config.project_ids = ['firebase_project_id']
end

上記概要図④、⑨でRailsサーバ側でfirebaseにJWT認証するので設定しておきます。

ユーザのuidカラムを追加

firebaseのUIDを保存しておくカラムを追加します。(上記、概要図⑤、⑩で使うカラム)

$ rails g migration AddUidToUsers uid:string

できたマイグレーションを実行しておきます。

class AddUidToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :uid, :string
  end
end
$ rails db:migrate

ApplicationControllerへ追記

以下のようにApplicationControllerにinclude Firebase::Auth::Authenticable, before_action :authenticate_userの2行を追加しておきます。

# app/controllers/api/v1/application_controller.rb
module Api
  class V1::ApplicationController < ActionController::API
    include Firebase::Auth::Authenticable
    before_action :authenticate_user
  end
end

こうすることでV1::ApplicationControllerを継承したコントローラのメソッドにアクセス時、HTTPリクエストヘッダに、「Authorization: “Bearer *****************(トークン)”」を入れるだけで、勝手にJWT認証してくれるようになります。(上記、概要図⑨、⑩の箇所)

ユーザ登録用のコントローラを作成

ユーザ登録用のコントローラ(上記概要図④のRailsサーバ側の処理)はこんな感じで作ってみました。

# app/controllers/api/v1/auth/registrations_controller.rb
require_dependency 'api/v1/application_controller'
module Api
  module V1
    module Auth
      class RegistrationsController < V1::ApplicationController
        skip_before_action :authenticate_user

        def create
          FirebaseIdToken::Certificates.request
          raise ArgumentError, 'BadRequest Parameter' if payload.blank?
          @user = User.find_or_initialize_by(uid: payload['sub'])
          if @user.save
            render json: @user, status: :ok
          else
            render json: @user.errors, status: :unprocessable_entity
          end
        end

        private

        def token_from_request_headers
          request.headers['Authorization']&.split&.last
        end

        def token
          params[:token] || token_from_request_headers
        end

        def payload
          @payload ||= FirebaseIdToken::Signature.verify token
        end
      end
    end
  end
end

ログインが必要な画面のコントローラを作成

Api::V1::ApplicationControllerを継承しているので、indexアクセス時勝手にJWT認証(上記、概要図⑨、⑩の箇所)してくれます。

# app/controllers/api/v1/books_controller.rb
module Api
  module V1
    class BookssController < Api::V1::ApplicationController
      def index
        render json: current_user.books, status: 200
      end
    end
  end
end

Routes追加

routes.rbには以下のように記載しました。

Rails.application.routes.draw do
  〜〜〜
  namespace 'api' do
    namespace 'v1' do
      resources :books, only: %i(index)
      namespace 'auth' do
        post 'registrations' => 'registrations#create'
      end
    end
  end
end

デプロイ後のオペレーション

サーバにコードデプロイ後は、firebase_id_tokenのREADME に記載してある以下をrails consoleで実行し、Googleのx509証明書をダウンロード・確認することで、JTW認証が動くようになりました。

FirebaseIdToken::Certificates.request
FirebaseIdToken::Certificates.present?
=> true

補足

スマホアプリはReact Nativeで実装したのですが、以下のようなコードになりました。 firebaseパッケージを使っています。

[サインアップ時の処理]

// SignupScreen.js サインアップ画面のコード
export default class SignupScreen extends React.Component {
  〜〜〜
  onSignupPress = () => {
    firebase.auth().createUserWithEmailAndPassword(this.state.email, this.state.password)
        .then(apiClient.authenticate, authError);
    // => サインアップ後、apiClient.authenticate 認証処理を実行
  }
  〜〜〜
}

[ログイン時の処理]

// LoginScreen.js ログイン画面のコード
export default class LoginScreen extends React.Component {
  〜〜〜
  onLoginPress = () => {
    firebase.auth().signInWithEmailAndPassword(this.state.email, this.state.password)
      .then( apiClient.authenticate , authError);
    // => ログイン後、apiClient.authenticate 認証処理を実行
  }
  〜〜〜
}

[ログイン処理(ログイン時と、サインアップ時で共通)のコード]

class ApiClient {
  async authenticate() {
    const token = await firebase.auth().currentUser.getIdToken(true)
    const data = { token }
    postRequest('/api/v1/auth/registrations', data,);
  }
}