Rails: Deviseを徹底理解する(2)応用編(翻訳)
原注
本記事は、Rubyの隠れたgem: Deviseシリーズ記事の一部です。
- Rails: Deviseを徹底理解する(1)基礎編(翻訳)
- Rails: Deviseを徹底理解する(2)応用編(翻訳)-- 本記事
本シリーズ記事のパート1では、サンプルアプリを用いてDeviseを導入し、モジュール、ヘルパー、ビュー、コントローラー、ルーティングを探りました。
このパート2では、Deviseのさらに高度な利用法として、OmniAuthやAPI認証、Authtrailの使い方を探求します。
さっそく始めましょう!
🔗 OmniAuthで認証する
最近のWebアプリケーションは、TwitterやFacebookなどのSNSからGoogleやGitHubなどに至るさまざまな認証プロバイダを利用してログインするオプションを提供していることがほとんどです。
この便利なマルチプロバイダ認証機能の多くは、OmniAuthというライブラリで提供されています。
OmniAuthは、Ruby向けの柔軟で強力な認証ライブラリであり、さまざまな外部認証プロバイダと統合できるようになります。
OmniAuthは、さまざまなOAuthプロバイダーに接続するためのシンプルで統一されたAPIを提供します。OmniAuthは特に、SNSアカウントでサインアップまたはログインするオプションをユーザーに提供したい場合に便利です。OmniAuthを使うことで、RailsアプリケーションにSNSログイン機能を手軽に追加できます。
Devise gemでOmniAuthを併用すると、ユーザーの認証と権限管理がさらに簡単になります。Deviseの組み込み認証機能を活用しながら、外部認証プロバイダのサインインオプションにはOmniAuthを利用できます。
🔗 OmniAuthをDeviseで利用する
前述したように、OmniAuthを使えばさまざまなサードパーティの認証プロバイダと統合できるようになります。本記事ではGitHubを認証プロバイダとして利用することにします。
🔗 OmniAuthをインストールする
アプリのGemfile
に以下の行を追加します。
# Gemfile
gem 'omniauth'
gem 'omniauth-github' # OmniAuthでは、認証プロバイダごとに独自のgemがある
OmniAuth 2.0以降を使う場合は、以下も追加する必要があります。
# Gemfile
gem 'omniauth-rails_csrf_protection'
omniauth-rails_csrf_protection
gemは、OAuthフローへのあらゆるGET
リクエストを無効化します。また、OAuthリクエストの直前にRailsのCSRFトークン検証を挿入します。この2つの操作は、OAuth認証フローを標的とするクロスサイトリクエストフォージェリ攻撃を軽減するためのものです。
参考: §3 クロスサイトリクエストフォージェリ(CSRF)-- Rails セキュリティガイド - Railsガイド
次にbundle install
を実行して、このgemをインストールします。
🔗 新規GitHub OAuthアプリを作成する
今度は、GitHub上で新しいOAuthアプリを作成する必要があります。このアプリは認証権限を持つユーザーとして振る舞い、必要に応じて簡単に取り消せます。
最初の手順として、GitHubアカウントのプロフィールの「Settings」ページに移動します。
次に、左側のメニューで「Developer settings」をクリックします。以下のような画面が表示されるので、そこで新しいOAuthアプリを作成できます。
「Register new application」をクリックすると、以下のような画面が表示されます。
以下の要領で項目に入力します。
- Application name
- 新規OAuthアプリに適切な名前を指定します。
- Homepage url
- 今はさしあたって
http://localhost:3000/
を入力します。production環境では実際のホームページURLを入力します。 - Application description
- 必須ではありませんが、OAuthアプリが増えたときに区別できるように入力しておくとよいでしょう。
- Authorization callback url
- これは必須の入力項目です。通常は、
http://<アプリのURL>/users/auth/<アプリケーションプロバイダ名>/callback
のような形式のOAuthコールバックURLに従います。ただし、Googleなど一部のOAuthプロバイダはこの形式に沿っていないので注意が必要です。
入力が終わったら、「Register application」をクリックします。次の画面で、新しいアプリのsecret(秘密情報)を生成し、安全な場所にメモしておいてください(注: secretは一度しか表示されません)。
🔗 Deviseイニシャライザを設定する
Deviseのイニシャライザファイルconfig/initializers/devise.rb
を開き、GitHubに関連するOmniAuthセクションに移動します。おそらくコメントアウトされているので、コメントを解除して、新しいGitHub OAuthアプリのIDとsecretを使うように編集します。
# config/initializers/devise.rb
...
config.omniauth :github, ENV['GITHUB_APP_ID'], ENV['GITHUB_APP_SECRET'], scope: 'user,public_repo'
...
🔗 OmniAuthのコールバックコントローラを作成する
Deviseのコントローラーを既に生成済みであれば、OmniauthCallbacksController
がカスタマイズ用に準備されています。このコントローラがない場合は、手動で作成し、以下のように編集します。
# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# OmniAuthプロバイダはここで手軽に追加できる
...
def github
@user = User.from_omniauth(request.env["omniauth.auth"])
sign_in_and_redirect @user # sign_in_and_redirectはOAuthのメソッド
end
...
end
上のコードについていくつか補足します。
from_omniauth
:User
モデル内で実装するメソッドです。sign_in_and_redirect
: OAuth内部のメソッドです。
🔗 Userモデルを変更するマイグレーションを追加
今度は、Userモデルにカラムをいくつか追加する必要があります。具体的にはprovider
カラムとuid
カラムです。
bundle exec rails g migration AddOmniauthToUsers provider:string uid:string
bundle exec rails db:migrate
を実行して手順を完了します。
🔗 DeviseのモデルをOmniauthable
でOmniAuthに対応する
ここでは、User
モデルにDeviseのOmniauthable
モジュールを追加する必要があります。
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable
...
end
それが終わったら、from_omniauth
メソッドを追加します。これは、さきほど設定したUsers::OmniauthCallbacksController
から呼び出されます。
# app/models/user.rb
class User < ApplicationRecord
...
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user |
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
end
end
...
end
残る作業はあと1つ、Deviseのビューにログインリンクを追加する作業です。
🔗 ログインリンクのセットアップ
Deviseは、ユーザー登録用ビューやログイン用ビューに、デフォルトで適切な認証プロバイダのログインリンクを自動的に追加します。このリンクはGET
メソッドを使いますが、OmniAuth 2.0以降ではPOST
リクエストが好ましいことがわかっています。
そういうわけで、現在のリンクを無効にして、独自のPOST
リクエストを挿入する必要があります。
<!-- app/views/users/sessions/new.html.erb -->
<!-- デフォルトでPOSTリクエストを行うbutton_toを使っている点に注目 -->
<%= button_to "Sign in with GitHub", user_github_omniauth_authorize_path %>
以上で、Ruby on Rails 7アプリにDeviseとGitHub OAuth認証をセットアップ完了しました。対応するアプリの完全なソースコードはtasker_appリポジトリで入手可能です。
次は、別の高度なユースケースである「API呼び出しをDeviseで認証する方法」について解説します。
🔗 DeviseでAPI認証を行う
昨今、API経由でアプリに接続可能であることをユーザーから期待されることは珍しくありません。このセクションでは、そうしたユーザーリクエストをDeviseで安全に認証する方法について見ていきます。
ブラウザベースの認証は一般にCookieベースで行われますが、API認証はほとんどの場合JWT(JSON Web Token)と呼ばれるトークンを用いて行われます。これらのトークンはHTTPヘッダー内でやり取りされます。
原注
このセクションでは、RailsのAPI専用アプリケーションを使うことが前提となっています。この前提に沿いたい場合は、新しいアプリケーションをrails new app_name --api
で作成してください。
🔗 JWTベースの認証フロー
既に述べたように、API認証はJWTトークンに基づいているので、JWTベースの認証フローがどのように行われるかを理解しておくことが重要です。基本的に、以下にまとめた一般的な手順の流れで行われます。
- ユーザークライアントがAPIアプリに呼び出しを行う。
- APIアプリはJWT(JSON Web Token)と呼ばれる認証トークンを返します。この認証トークンはcookieの代わりに利用可能です。
- 以後、ユーザークライアントから送信されるリクエストは、このトークンをHTTP
Authorization
ヘッダーで利用する形で行われます。 - 以後のユーザーは、Deviseの「セッション破棄(session destroy)」操作を実行可能になります。この操作を行うと、認証トークンが破棄され、ユーザーはログアウトします。
それでは、実際にこのフローを実行してみましょう。
最初は、CORS(cross-origin resource sharing: オリジン間リソース共有)と呼ばれるものから始まります。
参考: オリジン間リソース共有 (CORS) - HTTP | MDN
🔗 CORSをセットアップする
CORSは、外部からのリクエストを許可するためにAPIアプリをセットアップします。CORSはHTTPベースのセキュリティポリシーであり、外部からのリクエストがアプリケーションでどのように処理されるかを定義します。
デフォルトのCORSは、初期リクエストを行ったドメインと異なるドメインからのリクエスト(つまり、異なる「origin」からのリクエスト)をすべてブロックします。
CORSを適切に処理するために、rack-cors
という便利なgemを使います。
Gemfile
で以下の行をコメント解除してから、bundle install
コマンドを実行します。
# Gemfile
gem 'rack-cors'
また、対応するCORSイニシャライザファイルを開き、以下のように変更します。
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*"
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
expose: %w[Authorization Uid]
end
end
ここで行った内容について重要な点をいくつか指摘しておきます。
origins "*"
- 「このAPIアプリは任意のソースからのリクエストを受信可能になる」という意味です。
expose: %w[Authorization Uid]
rack-cors
gemは、デフォルトではAuthorization
ヘッダーやUid
ヘッダーを公開しませんが、認証トークンが通過するにはこれらのヘッダーが必要です。
それが終わったら、次はDevise gemとそれに対応するDevise-JWT gemをインストールします。
🔗 RailsアプリにDeviseとDevise-JWTを追加する
devise-jwt gemは、JWTトークンを利用可能にするDeviseの拡張機能です。
これらのgemをGemfile
に追加してからbundle install
を実行します。
# Gemfile
gem 'devise'
gem 'devise-jwt'
Deviseをインストールするジェネレータbundle exec rails g devise:install
を実行します。
🔗 モデルの生成と設定
ここでは、2つのモデルをセットアップする必要があります。
通常のDeviseユーザーモデル(bundle exec rails g devise User
)に加えて、無効化戦略(revocation storategy: 言い換えればユーザーがAPIからログアウトする方法)に利用するモデルも必要です。
bundle exec rails g model jwt_revocation
以下のように、通常のDeviseユーザーモデルをAPI認証用に変更し、JWTトークンを認証可能にするJwtAuthenticatable
モジュールを追加し、トークンの無効化戦略に使うJwtDenylist
モデルを定義します。
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist
end
次に、第2のJwtDenylist
モデルを以下のように設定して、利用する無効化戦略と無効化用テーブル名を参照します。
# app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = 'jwt_denylist'
end
次のセクションでは、認証トークンの無効化とその必要性について説明します。
🔗 トークンの無効化が重要な理由
トークンの無効化が重要な理由とは何でしょうか?JWTトークンは、状態を持たないステートレス(stateless)だからです。サーバーは、トークンに署名すること以外にそのトークンについて一切関知しません。このようなシナリオでは、対応するトークンを無効化してユーザーをログアウトする方法がサーバーにはありません。個別のトークンを無効化する手段が存在しないため、そのための手段を作成し、サーバーにそれを使うよう指示する必要があります。
トークンを無効化するときに実際に行われているのは、トークンの一意の部分であるjti
(JWT ID)を抽出して、定義済みの無効化戦略に沿ってjti
を利用することです。
もちろん、これは「トークンの無効化戦略とはそもそも何なのか」という別の疑問を呼び起こします。トークンの無効化戦略とは、要するに、サーバーがどのような方法でトークンを無効化するかを定義したものです。基本的な無効化戦略は以下の3種類です。
- JTIMatcher戦略
- この戦略では、ユーザーモデルに
jti
という一意のカラムを追加します。このカラムは、無効化用テーブルとしても振る舞います。ユーザーがリクエストを行うたびに、ヘッダー内のjti
を、保存済みのトークンと照合し、両者が一致する場合にのみアクセスが許可されます。 - Denylist戦略
- この戦略では、
jti
とトークンの失効(expiry: つまり無効化されたトークンのexp
)をデータベースのテーブルに保存します。ユーザーがリクエストを行うたびに、現在のトークンのjti
を、データベース内で無効化済みのものと比較するチェックが行われます。両者が一致した場合、そのユーザーのリクエストは拒否されます。 - Allowlist戦略
- この戦略は、ある意味で最初のJTIMatcher戦略と似ていますが、JWT IDを保存するテーブルと、ユーザーのトークンを保存する別のテーブルが「一対多のリレーションシップ」になる点が異なります。リクエストが行われるたびに、Allowlistテーブルに保存されているユーザーの
jti
が、トークンを持つマッチング用テーブルに保存されているものと一致するかどうかをチェックし、両者が一致した場合にのみアクセスが許可されます。
このトークン無効化の概要は、明らかに相当簡略化したものなので、詳しくは以下を参照してください。
🔗 JWTトークンの秘密鍵に署名する設定を行う
ユーザーとそのリクエストを認証するためにセキュアなトークンを利用したいので、それらに署名する方法が必要です。ここで秘密鍵(secret key)が登場します。ここではRailsの秘密鍵であるsecret_key_base
を使わず、別の秘密鍵を新たに生成することが強く推奨されています。
bundle exec rake secret
を実行して一意の鍵を生成してから、Deviseのイニシャライザを編集して、その鍵を使うようにします。
# config/initializers/devise.rb
Devise.setup do |config|
...
config.jwt do |jwt|
jwt.secret = ENV['DEVISE_JWT_SECRET']
end
...
end
最後に、コントローラをセットアップします。
🔗 コントローラのセットアップ
DeviseでAPI認証を実装する最後のステップは、コントローラのセットアップです。話を簡単にするため、「ユーザー登録を担当するコントローラ」と「セッション用のコントローラ」の2つのコントローラをセットアップします。
コントローラを手作りすることにしましょう。最初はRegistrationsController
です。
# app/controllers/users/registrations/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
private
def respond_with(resource, _opts={})
successful_registration && return if resource.persisted?
failed_registration
end
def successful_registration
render json: { message: "You've registered successfully", user: current_user }, status: :ok
end
def failed_registration
render json: { message: "Something went wrong. Please try again" }, status: :unprocessable_entity
end
end
このコントローラでは以下を行います。
- リクエストに対してJSONをレスポンスとして返すように設定する。
- 登録の成功または失敗の結果を返す
respond_with
アクションを指定する。
次はSessionsController
です。
# app/controllers/users/sessions/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(resource, _opts={})
render json: { message: "Welcome, you're in", user: current_user }, status: :ok
end
def respond_to_on_destroy
successful_logout && return if current_user
failed_logout
end
def successful_logout
render json: { message: "You've logged out" }, status: :ok
end
def failed_logout
render json: { message: "Something went wrong." }, status: :unauthorized
end
end
登録用のコントローラーと同様に、このコントローラーもJSONでレスポンスを返すように指定します。また、ユーザーがログインに成功した場合のrespond_with
アクションと、ユーザーがサインアウトするためのrespond_to_on_destroy
を定義します。
以上で、DeviseとJWTトークンを利用する、実際に動くAPI認証フローが完成しました!
最後のセクションでは、DeviseとAuthtrailでユーザーログインをトラッキングする方法について簡単に説明します。
🔗 DeviseのログインをAuthtrailでトラッキングする
アプリのユーザーがアカウントにログインするたびに、ユーザーのIPアドレスやログインのタイムスタンプなどの詳細情報を含む通知メールを送信したいとしたら、どうすれば実現できるでしょうか?
そのためには、ユーザーログインをトラッキングして、その情報をユーザーに送信する通知メールで利用する必要があります。ただし、最初にユーザーログインをトラッキングする方法が必要です。これは、Authtrailという便利なgemで実現できます。このgemはDeviseとの相性も優れています。
🔗 Authtrailをインストールする
最初のステップは、bundle add authtrail
を実行してAuthtrail gemをインストールすることです。
さらに、アプリケーションのデータベースにメールアドレスやIPアドレスなどのユーザー識別情報を保存する場合は、Lockbox gemとBlind Index gemを組み合わせる形で、このデータをproduction環境で暗号化しておくことを強く推奨します。
次に、Authtrailジェネレータを実行して、イニシャライザファイルと、ログインデータを保存するテーブルのマイグレーションを作成します。
# ここではencryptionフラグにlockboxを指定してジェネレータを実行しているが、
# `--encryption=none`を指定すれば暗号化なしにもできる
bundle exec rails g authtrail:install --encryption=lockbox
bundle exec rails db:migrate
🔗 Authtrailのしくみ
ユーザーがログインしようとするたびに、以下の重要な詳細情報を含む新しいAuthtrailレコードが作成されます。
- ログインで使われたメールアドレス
- ログインの成功/失敗
- ログインが失敗した場合の理由
- ユーザーのIPアドレスや
referrer
などの多くの情報
この情報は必要に応じてさまざまな形で利用できます。
たとえば、ユーザーのアカウントに対してログインが試行されたことを通知するメールをユーザーに送信するときに、ログインに使われたメールアドレスやIPアドレスの情報もメールに含められます。
Authtrailでできることをすべて知るには、Authtrailのドキュメントをお読みください。
🔗 まとめ
本シリーズでは、Devise gemについて詳しく解説しました。
パート1では、Deviseの基礎を理解し、Deviseの「モジュール」「ヘルパー」「ビュー」「コントローラ」「ルーティング」のしくみについても学びました。
最後のパート2では、DeviseをOAuthやAuthtrailと併用する方法や、API認証で利用する方法を詳しく見てきました。
本シリーズが、皆さんにとってDevise認証のすべてを学ぶうえで有用なガイドとなることを願っています。
Happy coding!
P.S. Ruby Magicの記事をいち早くお読みになりたいのであれば、ぜひRuby Magicニュースレターに登録して、記事を見逃さずに読めるようにしましょう!
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。