アルパカログ

カスタマーサポート (CS) とエンジニアリングを掛け算したい CRE (Customer Reliability Engineer) が気になる技術や思ったことなど。

【Ruby on Rails】API認証とRSpecの書き方

仕事で Ruby on Rails 5 を触る機会があり、API 認証まわりを調べたのでテストの書き方もあわせてまとめておきます。

認証として JWT を使った例を記載しています。JWT を扱うにあたって ruby-jwt という Gems を使っています。

ちなみに JWT はジョットと発音するそうです。

ApplicationController による認証

ApplicationController に認証のメソッドを追加し、before_action に設定します。

認証に失敗した場合、ステータスコード403 (Forbidden) を返すようにしています。

ただし、実際にはもっと細かく JWT のエラー(claim)をハンドリングしなければならないことに注意してください。ここでは説明のシンプルさを優先しています。

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  before_action :authenticate

  private

  def authenticate
    _, token = request.headers['Authorization'].split(' ')
    @payload, = JWT.decode token, 'api secret', true, algorithm: 'HS256'
  rescue JWT::DecodeError
    head :forbidden
  end
end

authenticate が認証用のメソッドです。このように、action として呼ばれることを想定していないメソッドはプライベートにしておきましょう。

Only public methods are callable as actions. It is a best practice to lower the visibility of methods (with private or protected) which are not intended to be actions, like auxiliary methods or filters.

https://guides.rubyonrails.org/action_controller_overview.html#methods-and-actions

これで ApplicationController を継承した Controller は before_action で認証が課されるになります。

RSpec によるテストの書き方

テストのポイントは、認証のテストをどう書くかということと、Controller のテストでどうやって認証を回避するかということでしょう。

1つずつ見ていきましょう。

認証のテスト

認証のテストは Controller のテスト(controller spec) としてではなく、Request のテスト(request spec) として書きます。

request spec にも action が必要になるので、先に適当な Controller と action, routes を追加しておきましょう。

app/controllers/cats_controller.rb

class CatsController < ApplicationController
  def index
    render json: []
  end
end

config/routes.rb

Rails.application.routes.draw do
  resources :cats, only: [:index]
end

request spec は次のようになります。type: :request とすることで request spec になります。

spec/requests/authentication_spec.rb

require 'rails_helper'

RSpec.describe 'Authentication', type: :request do
  let(:payload) { { 'data' => 'test' } }
  let(:api_secret) { 'api secret' }
  let(:algorithm) { 'HS256' }
  let(:token) { JWT.encode payload, api_secret, algorithm }
  let(:headers) { { 'Authorization' => "Bearer #{token}" } }

  describe 'ApplicationController#authenticate' do
    describe 'response status' do
      subject do
        get '/cats', headers: headers
        response
      end

      context 'invalid token' do
        let(:token) { 'invalid token' }
        it { is_expected.to have_http_status(:forbidden) }
      end

      context 'valid token' do
        it { is_expected.to have_http_status(:ok) }
      end
    end
  end
end

request spec と controller spec とでは、get が取り得る引数に違いがあるので注意しましょう。

Controller のテストで認証を回避する

controller spec で認証を回避するには、ApplicationController で定義した authenticate メソッドを stub してあげれば良いです。

しかし Controller のテストを書くたびに authenticate を stub するというのはかなり煩雑です。

そこで、authenticate メソッドを stub するための Helper モジュールを作り include するようにします。

まず Helper を次のように定義します。

spec/support/authentication_helper.rb

module AuthenticationHelper
  extend ActiveSupport::Concern

  included do
    before do
      allow_any_instance_of(ApplicationController).to receive(:authenticate)
    end
  end
end

ActiveSupport::Concern がわからない方は、良い解説記事がたくさんあるので調べてみてください。

このモジュールのポイントは、include されたときに authenticate メソッドを stub していることです。これにより、モジュールを include するだけで認証がスキップされるため、使い勝手が良くなっています(自画自賛)。

次に、このモジュールを rails_helper.rb で include します。

spec/rails_helper.rb

...
RSpec.configure do |config|
  ...
  config.include AuthenticationHelper, type: :controller, authentication: :skip
end

これで type: :controller, authentication: :skip オプションのある controller spec では全て認証がスキップされるようになります。

つまり、controller spec で認証を回避するには次のように書きます。

RSpec.describe CatsController, type: :controller, authentication: :skip do
  ...
end

まとめ

Ruby on Rails 5 で API 認証とその RSpec の書き方を説明しました。

Ruby on RailsAPI を作ることはケースとしてかなり多いのではないかと思います。

ぜひみなさまの参考になれば幸いです。