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との繋ぎ込み部分に関してのセッション管理をどうするか、認証によるページ制御をどうするかについて説明しようと思います。