行動すれば次の現実

テック中心の個人ブログ

Rails7 APIモードの認証機能をdevise_token_authで実装する

Rails7 APIモードの認証機能を実装するにあたり、Deviseのトークン認証を可能にするdevise_token_authというライブラリを使用することにしました。

導入に少しハマったところなどもありましたので記事にまとめました。同じような構成を検討している方の参考になれば幸いです。

環境構成

フロントエンド:Next.js 12.1
バックエンド:Rails 7.0
開発環境:Docker、docker-compose

devise_token_authの準備

Gemのインストール

Gemfileに以下を記載してbundle installします。

**Gemfile**

gem 'devise'
gem 'devise_token_auth'
gem 'devise-i18n'
gem 'rack-cors'

rack-corsはCORS(Cross-Origin Resource Sharing)を実現させるために必要となりますので、一緒にインストールします。 devise-i18nはメッセージの日本語化のために必要となるパッケージです。

インストールタスクの実行

deviseとdevise_token_authのインストールタスクを実行します。 今回はUserモデルに対してDeviseを適用したいと思います。

rails g devise:install
rails g devise_token_auth:install User auth

作成されるconfig/initializers/devise_token_auth.rbというファイルに各種設定を記述します。

私の場合config.change_headers_on_each_request = falseのみ変更しました。 これ設定をすることでリクエストの都度トークンが変更されなくなるので、トークンの管理が楽になります。

routesとmigrationファイルの修正

今回は/api/v1/authというパス上に認証用のエンドポイントを作成したいと思うので以下のようにroutes.rbを修正します。

**config/routes.rb**

namespace :api do
  namespace :v1 do
    mount_devise_token_auth_for 'User', at: 'auth'
  end
end

また、devise_token_auth:installではログイン日時などの情報をトラッキングするためのマイグレーション情報が作成されないようなので、以下のように手動で修正を加えます。

**db/migrate/xxxxx_devise_token_auth_create_users.rb**

# Trackable用のカラムを追加します
t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string  :current_sign_in_ip
t.string  :last_sign_in_ip

メッセージの日本語化

devise-i18nを使用してデフォルト英語のメッセージを日本語に変換させます。

rails g devise:i18n:locale ja

を実行すると/config/locales/devise.views.ja.ymlというファイルが作成されます。

このままでは日本語化されませんので、application_controller.rbに以下の定義を記載します。

**app/controllers/application_controller.rb**

before_action do
  I18n.locale = :ja
end

CORSの設定

originsにフロントのホスト(localhost:8000)を記載して以下のようにCORSの設定ファイルを作成します。

exposeには%w[access-token uid client]を記述しています。これによりクロスドメインでもヘッダー情報が公開されるようになります。

**config/initializers/cors.rb**

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "localhost:8000"

    resource "*",
             headers: :any,
             expose: %w[access-token uid client],
             methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

各メソッドの動作検証

ここまで準備が完了しましたらrails db:migrateを実行してdeviseの動作検証をしてきます。

devise_token_authはHTTPリクエストのヘッダーにuid、access-token、clientを付与することで認証が行われます。 ちなみにcurlに-iオプションをつけるとHTTPレスポンスのヘッダー情報が出力されますので便利です。

ユーザ登録

  • / にPOSTでアクセスします
  • email、password、password_confirmationをparamsに設定して送信することでユーザが作成されます
# コマンド実行例
curl localhost:3000/api/v1/auth -X POST -d '{"email":"test@example.com", "password":"password", "password_confirmation": "password"}' -H "content-type:application/json" -i

ユーザ削除

  • /にDELETEでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与することで、該当のユーザが削除されます
# コマンド実行例
curl http://localhost:3000/api/v1/auth -H "uid: test@example.com" -H "client: hpHyxPAgFmVxFA75uJOhYA" -H "access-token: glXm3iysPovJSi4q_ffpwQ" -X DELETE
ActionDispatch::Request::Session::DisabledSessionErrorについて

ちなみにRails7の場合、そのままの状態だと以下のエラーが発生します

ActionDispatch::Request::Session::DisabledSessionError (Your application has sessions disabled. To write to the session you must first configure a session store):

Rails7より、APIモードのようにセッションを使用しない場合にセッションへのアクセスがあるとエラーを発生させる機構が備わったことによるエラーです。

deviseでは内部でセッションにアクセスしている箇所があるので当該エラーが発生する模様です。

私の場合、ワークアラウンドとして以下の処理を入れることで一旦回避させました。 根本対応されましたら、きちんと修正しておくことにします。

**app/controllers/application_controller.rb**

 class ApplicationController < ActionController::API
    include DeviseTokenAuth::Concerns::SetUserByToken
    include DeviseHackFakeSession # この部分を追加します
 end
**app/controllers/concerns/devise_hack_fake_session.rb**

module DeviseHackFakeSession
  extend ActiveSupport::Concern

  class FakeSession < Hash
    def enabled?
      false
    end

    def destroy
    end
  end

  included do
    before_action :set_fake_session

    private

    def set_fake_session
      if Rails.configuration.respond_to?(:api_only) && Rails.configuration.api_only
        request.env['rack.session'] ||= ::DeviseHackFakeSession::FakeSession.new
      end
    end
  end
end

参考: https://github.com/heartcombo/devise/issues/5443

ユーザ変更

  • /にPUTでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与して該当のユーザを変更します
  • デフォルトはemailとpasswordのみ変更可能なので、必要に応じてconfigure_permitted_parametersにパラメータを追記します
# コマンド実行例
curl http://localhost:3000/api/v1/auth -d '{"password":"password", "name":"taro"}'  -H "content-type:application/json" -H "uid: test@example.com" -H "client: fy5z545K8TwgunV_D2LJXg" -H "access-token: B5z0QZ8jro5zZbqcuHAtow" -X PUT
**app/controllers/application_controller.rb**

  before_action :configure_permitted_parameters, if: :devise_controller?

  # nameを変更可能にする例
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end

ログイン

  • /sign_inにPOSTでアクセスします
  • 認証が成功するとHTTPレスポンスヘッダーにuid、access-token、clientが付与されてきます。認証が必要なアクションに対してリクエストヘッダーそれらを付与することでアクセス可能になります
curl localhost:3000/api/v1/auth/sign_in -X POST -d '{"email":"test@example.com", "password":"password"}' -H "content-type:application/json" -i

# レスポンス一部抜粋
access-token: OtzRYGvcy-W3zvXzQijB4Q
token-type: Bearer
client: r7zZ0MBaFSnNcGsiRZQetQ
expiry: 1657875966
uid: test@example.com

ログアウト

  • /sing_outにDELETEでアクセスします
  • HTTPヘッダーにuid、access-token、clientを付与して該当のユーザがログアウトされます
curl http://localhost:3000/api/v1/auth/sign_out -H "content-type:application/json" -H "uid: test@example.com" -H "client: fy5z545K8TwgunV_D2LJXg" -H "access-token: B5z0QZ8jro5zZbqcuHAtow" -X DELETE

パスワードリセット

  • /auth/passwordにPOSTでアクセスします
  • emailとredirect_urlをパラメータに付与することで、該当のアドレスに対してパスワードリセットメールが送信されます。メール内に記載されたパスワードリセットフォームのURLにredirect_urlが設定されます
  • devise_token_auth(v1.2.2)時点でなぜかrails g devise_token_auth:install_viewsでメールテンプレートファイルを作成しなければredirect_urlが動作しない事象が発生しています。
curl http://localhost:3000/api/v1/auth/password -H "content-type:application/json" -d '{"email":"hello@example.com", "redirect_url":"http://localhost:3000/pages"}' -X POST

パスワードリセットメール

疎通確認

では実際に、認証が必要なエンドポイントを設けて実際に疎通確認をしてみます。

/api/v1/homeというエンドポイントを設けます。

ログインしている場合のみアクセスできるようにbefore_action :authenticate_api_v1_user!を定義しています。authenticate_api_v1_userのメソッド名はdeviseがマウントされているpathによって動的に命名されますので、api/v1という構成でない場合は読み替えてください。

Rails.application.routes.draw do
  resources :pages, only: [:index]
  namespace :api do
    namespace :v1 do
      resources :home, only: [:index]

      mount_devise_token_auth_for 'User', at: 'auth'
    end
  end
end
class Api::V1::HomeController < ApplicationController
  before_action :authenticate_api_v1_user!

  def index
    render json: { message: 'hello' }
  end
end

tokenが正しい場合

curl http://localhost:3000/api/v1/home -H "content-type:application/json" -H "uid: hello@example.com" -H "client: NdQxhsz2PiRdBR25xIfzJg" -H "access-token: Zv5thbTxDEWEyn7Nhl_SLg" -X GET

{"message":"hello"}

tokenが正しくない場合

curl http://localhost:3000/api/v1/home -H "content-type:application/json" -H "uid: hello@example.com" -H "client: NdQxhsz2PiRdBR25xIfzJg" -H "access-token: hoge" -X GET

{"errors":["ログインもしくはアカウント登録してください。"]}

終わりに

今回の記事ではdevise_token_authのインストール方法と使い方をメインに説明しました。 次回はNext.jsとの繋ぎ込み部分に関してのセッション管理をどうするか、認証によるページ制御をどうするかについて説明しようと思います。

Next.jsとdevise_token_authを使って認証周りを実装する