Deviseのベースになっているgem "Warden"のwikiを翻訳してみました。(2022/12/18時点)
誤訳のご指摘があれば修正します。
Warden
Warden wikiへようこそ。WardenはRackベースのRubyアプリケーションでの認証メカニズムを提供します。同じRackインスタンス内で複数のアプリケーションを共有することを念頭に作られています。
ここに長いドキュメントがあります。ドキュメントは最新を保つように努めますが、正確な詳細についてはソースコードやソースドキュメントを参照してください
Overview
The What
WardenはRubyWebアプリケーションの認証メカニズムを提供するために設計されたRackベースのミドルウェアです。Rackの構成要素に合う一般的なメカニズムで、認証用に強力な選択肢を提供します。
Wardenは怠惰に設計されています。つまり、使わないときには何もせず、使うときには突然動き出して、どんなRackベースのアプリケーションでも認証を許可する基本的メカニズムを提供するのです。
The Why
Rackアプリケーションを使うようにすると機会が大きく広がります。同じプロセスやサブアプリケーション、そのまたサブアプリケーションで複数のアプリケーションを実行できるようになります。
複数からなるアプリケーションは多くの人にとって魅力的ですが、問題はどうやってこのような状況で認証を管理するかということです。それぞれのアプリケーションは認証、もしくは「ユーザー」を必要とすることがあります。全体として、Rackミドルウェアツリーの中にあるすべてのアプリケーションで、認証とはシステムへのログインを許可されている同じ「ユーザー」ということになりそうです。承認はさておき。
Wardenは下流のミドルウェアやエンドポイントが共通の認証メカニズムを共有しつつ、アプリケーション全体で管理することを可能にします。それぞれのアプリケーションはRackミドルウェアツリー全体を通して同じロジックを使い、認証されたユーザーにアクセスしたり、同じ方法で認証を要求することができます。それぞれのアプリケーションはどんなAPIでも上に重ねることができますし、それでいて基礎的なシステムは機能するのです。
The How
WardenはRackスタックの中で、セッションミドルウェア(env['rack.session']
にハッシュライクなオブジェクトであるセッションを格納するもの)の後に位置します。(注: RailsユーザーはRack環境にはrequest.envでアクセスできるので、env = request.env
と考えてください。)
Wardenは遅延オブジェクトをenv['warden']
でRack環境に注入します。この遅延オブジェクトとのやりとりで、認証されているか問い合わせたり、Rackの構成要素のうち下流のマシンで認証させたりすることができます。もしリクエストが認証されていればWardenは邪魔をせず、そうでなければ"fail"させて反応させることができます。
# リクエストが前に認証されたか問い合わせる
env['warden'].authenticated?
# リクエストが :foo スコープに対して認証されているか問い合わせる
env['warden'].authenticated?(:foo)
# :password ストラテジーによって認証を試みる。失敗しても続行する
env['warden'].authenticate(:password)
# :password ストラテジーによる認証を保証する。失敗したら脱出する
env['warden'].authenticate!(:password)
もしリクエストを認証したくないなら、env['warden']
に認証するように頼まなければいいだけです。
認証が行われて成功したら、「ユーザー」オブジェクトにアクセスできるようになります。オブジェクトはnil以外なら何でもかまいません。
env['warden'].authenticate(:password)
env['warden'].user # ユーザーオブジェクト
Wardenをセッションミドルウェアの直後に置くことで、すべての下流ミドルウェアとアプリケーションが認証オブジェクトにアクセスできるようになります。そうすると、別のソースから立ち寄られても、すべてのアプリケーションが認証への結合され一貫したアプローチを持つことができるのです。さまざまなAPIを重ねていても、すべてのRackミドルウェアとエンドポイントは基礎となる同じ認証システムを使うことができます。
「ストラテジー」は認証ロジックが実際に実行される場所です。詳細はStrategiesをご覧ください。
Failing Authentication
認証が失敗しているようなら、Wadenミドルウェアの下流の任意のポイントで単に :warden
シンボルを投げてください。また、オプションのハッシュに任意の情報を含めて投げることができます。
throw(:warden) # 認証失敗により脱出する
throw(:warden, :stuff => "foo") # オプションで状況を示して脱出する
ここでしていることは"failure application"への脱出で、セットアップが必要です。標準的なRackアプリケーションの"failure application"は認証が失敗した場合に処理のためにあります。例えばログインフォームをレンダリングするために使うかことがあるでしょう。
Callbacks
認証サイクルのキーポイントでたくさんのコールバックが定義されています。詳しくはCallbacksをご覧ください。
Scopes
Wardenで複数のユーザーが同時にログインできるようになります。Scopesをご覧ください。
Setup
残念ながらWardenは少々セットアップが必要です。フレームワークでプラグインが使えるならいくらか楽にできます。
Rack Setup
Wardenは何らかのセッションミドルウェアの下流にある必要があります。"failure application" を宣言しなければならず、デフォルトでどのストラテジーを使うかの宣言も必要です。
Rack::Builder.new do
use Rack::Session::Cookie, :secret => "replace this with some secret key"
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = BadAuthenticationEndsUpHere
end
run SomeApp
end
Session Setup
何らかのオブジェクトをユーザーオブジェクトとして使うということは、Wardenにセッション内外でユーザーをシリアライズする方法を伝える必要があるということです。
以下の設定が必要です。
Warden::Manager.serialize_into_session do |user|
user.id
end
Warden::Manager.serialize_from_session do |id|
User.get(id)
end
もちろん、必要なら複雑に設定することもできます。
Declare Some Strategies
実際にWardenに認証させるにはいくつかストラテジーを宣言する必要があります。
詳しくはStratediesのページをご覧ください。
Use it in your application
env['warden'].authenticate! # デフォルトの :password と :basic を使用
env['warden'].authenticate! :password # :password ストラテジーのみ使用
Advanced Setup (with scopes)
もしユーザーが1種類だけでスコープを使う必要がないなら、おそらくこのパートは飛ばした方がいいかもしれません。(もしくはあまり心配しすぎないでください。)
Wardenでは異なるスコープをセットアップして、さまざまなふるまいをさせることができます。これはとても強力な技術です。実際に例を見てもらったほうが早いでしょう。
# 動作中のRailsアプリケーションから抽出
use Warden::Manager do |config|
config.failure_app = ->(env) do
env['REQUEST_METHOD'] = 'GET'
SessionsController.action(:new).call(env)
end
config.default_scope = :user
config.scope_defaults :user, :strategies => [:password]
config.scope_defaults :account, :store => false, :strategies => [:account_by_subdomain]
config.scope_defaults :membership, :store => false, :strategies => [:membership, :account_owner]
config.scope_defaults(
:api,
:strategies => [:api_token],
:store => false,
:action => "unauthenticated_api"
)
end
ここではいろいろなことが行われています。
Default Scope
デフォルトではWardenはデフォルトスコープを設定しますが、上書きすることができます。
ここでは:user
に上書きしています。これはenv['warden'].authenticate!
が:user
スコープ内で認証することを意味しています(何も指定されていないので)。
Scope Defaults
それぞれのスコープはデフォルトのふるまいのリストと一緒に設定できます。ストラテジーのリスト、セッションで結果を永続化するかどうか、"Failure application"のアクションまでスコープごとに設定できます。
定義された方法でこれらのスコープのうちのどれかを使うには、そのスコープに対して認証するだけです。
env['warden'].authenticate! :scope => :api
この呼び出しは:api_token
ストラテジーによって認証を行い、セッションで永続化せず、その「ユーザー」を:api
スコープで使えるようにします。(例)env['warden'].user(:api)
Strategies
Wardenはリクエストを認証する必要があるかどうか判断するのにカスケードストラテジーの概念を用います。Wardenは以下のいずれかに行きつくまで次々にストラテジーを試していきます。
- ストラテジーが成功する
- 関連するストラテジーが見つからない
- ストラテジーが失敗する
What is a Strategy
概念的にはストラテジーとはリクエスト認証用のロジックを置く場所のことです。実際にはWarden::Strategies::Base
の子孫にあたります。
多くのストラテジーを定義して選択的に使うことができます。定義を見てみましょう。
Warden::Strategies.add(:password) do
def valid?
params['username'] || params['password']
end
def authenticate!
u = User.authenticate(params['username'], params['password'])
u.nil? ? fail!("Could not log in") : success!(u)
end
end
ここでは:password
ストラテジーを宣言しています。二点気を付けることとして、#authenticate!
と#valid?
メソッドがあります。
#valid?
#valid?
メソッドはストラテジーのガードとして動作します。#valid?
メソッドを宣言するかどうかは選択でき、宣言しない場合はストラテジーは常に実行されます。宣言しているならストラテジーは#valid?
の評価がtrueの場合のみ試されます。
上記のストラテジーはパラメータに'username'
か'password'
のどちらかがあればユーザーがログインしようとしていると推測します。どちらかしかなければUser.authenticate
の呼び出しは失敗しますが、望ましい(有効な)ストラテジーです。
#authenticate!
#authenticate!
メソッドで実際にリクエストの認証ステップが行われます。リクエストを認証するためのロジックはここにあります。
リクエスト関連のたくさんのメソッドを使用できます。
-
request
- Rack::Request オブジェクト -
session
- リクエスト用のセッションオブジェクト -
params
- リクエストのパラメータ -
env
- Rack env オブジェクト
ストラテジーで使えるアクションもたくさんあります。 -
halt!
- ストラテジーのカスケードを停止します。最後に処理されます。 -
pass
- ストラテジーを無視します。わかりやすくするためのもので、このアクションを呼び出す必要はありません。 -
success!
- ユーザーをログインさせるためにsuccess!
にユーザーオブジェクトを渡します。halt!
を発生させます。 -
fail!
- ストラテジーをfailにセットします.halt!
を発生させます。 -
redirect!
- 別のURLにリダイレクトします。redirect!
にエンコードされるパラメータとオプションを渡せます。halt!
を発生させます。 -
custom!
- カスタムのRack配列をそのまま返します。halt!
を発生させます。
他にも -
headers
- ストラテジーに関連するヘッダーをセットします。 -
errors
- エラーオブジェクトにアクセスできます。認証に関するエラーを入れることができます。
Using Strategies
ストラテジーを使うには宣言時に設定したラベルで参照してください。
:password
ストラテジーを使う場合:
env['warden'].authenticate(:password)
複数のストラテジーを使うことができ、どれかひとつが停止するか何もなくなるまで一つずつ順番に試されます。
:password
ストラテジーを使用し、失敗したら:basic
ストラテジーを使う場合:
env['warden'].authenticate(:password, :basic)
Sharing Strategies
Wardenを使用している他のアプリケーションとストラテジーを共有することができます。この利点はもちろん個々の状況によります。
OpenID、Facebook、Googleのような柔軟に対応できるものにとってはストラテジーを共有することはおそらく役立つでしょう。しかし、このようなコミュニティの共有はあなたのニーズにぴったりフィットしないかもしれません。いいストラテジーが書かれれば、上手くいけば多くのストラテジーが知られるようになるでしょう。
ストラテジーの宣言は柔軟性を保ちながらも可能な限りシンプルにしました。
アプリケーションインスタンス間でストラテジーを共有することの最も明確な利益は、同じ会社に実行される複数のアプリケーションによるものです。多くの小さいアプリケーションは同じ認証を使うことができます。例えば、Sinatraや素のRackで簡単なテストアプリケーションを立ち上げたいとします。Wardenを使うとメインのアプリケーションから同じストラテジーを共有することができ、一貫した会社全体の認証要件を提供することができます。
Failures
Wardenでレスポンスの認証に失敗すると、Rackエンドポイントが呼び出されます。このRackエンドポイントは"failure application"と呼ばれます。
ミドルウェアをスタックに追加する際に、認証失敗時に呼び出されるRackエンドポイントを提供する必要があります。
Failing Authentication
認証を失敗させるにはシンプルに:warden
シンボルを投げます。そのままシンボルを投げることもハッシュと投げることもできます。
# failure applicationに脱出する:
throw(:warden)
# failure applicationに脱出して env['warden.options'] にオプションのハッシュを置く:
throw(:warden, :some => :option)
これはどんな下流のミドルウェアやエンドポイントからでも投げられます。
認証に失敗して:warden
が投げられたら以下のことが起きます。
- リダイレクトやカスタムRackレスポンスなどがないか遅延認証オブジェクトをチェックする。失敗しているか何も起きていない場合は"failure application"が呼び出される
-
env['PATH_INFO']
が"/unauthenticated"
に書き換えられる - 渡されたオプションは全て
env['warden.options']
に含まれる - 全ての
before_failure
コールバックが呼び出される - "failure application"が呼び出される
"failure application"で呼び出されるアクションを変えたい場合は、単純にthrowのオプションに:action
シンボルを渡してください。以下のやり方があります。
throw(:warden, :action => "different_action")
# 認証時
env['warden'].authenticate! :action => "different_action"
スコープごとに別の失敗時のアクションを投げるように設定できます。Setupをご覧ください。
Users
Wardenの仕事は結局はリクエストを認証することです。それには2つの側面があります。
- リクエストを送る人(ユーザー)
- アプリケーション
Accessing a user
ユーザーにアクセスするには、単に認証オブジェクトのユーザーを呼び出すだけです。
env['warden'].user # 現在ログインしているユーザーを取得する
もしスコープ付きの認証を使用しているなら、どのスコープのユーザーにアクセスしたいのか伝える必要があります。
env['warden'].user(:sudo)
Setting the User
認証に成功したらユーザーが設定されます。手動でユーザーを設定したいときは、以下のように設定できます。
env['warden'].set_user(@user)
スコープを使用しているとき:
env['warden'].set_user(@user, :scope => :admin)
特定のリクエストでユーザーを設定して、セッションには保存したくない場合は:store
をfalseに設定できます。
env['warden'].set_user(@user, :store => false)
Callbacks
認証サイクルの中ではさまざまなポイントで多くのコールバックを利用できます。
- after_set_user
- after_authentication
- after_fetch
- before_failure
- after_failed_fetch
- before_logout
- on_request
使いたい分だけコールバックは追加でき、宣言された順番に実行されます。先頭に追加したい場合はprepend_before_failure
やprepend_before_logout
のように、コールバック名の先頭に"prepend_"
をつける必要があります。そして後述の同じ引数を渡します。
after_set_user
ユーザーが設定されるたびに呼び出されます。ユーザーは
-
env['warden'].user
によって初めてアクセスされる各リクエストで - ユーザーが最初に認証されたときに
-
set_user
メソッドで
設定されます。
例
Warden::Manager.after_set_user do |user, auth, opts|
unless user.active?
auth.logout
throw(:warden, :message => "User not active")
end
end
after_authentication
ユーザーが認証されるたびに実行されます。(各セッションでの初回)
例
Warden::Manager.after_authentication do |user,auth,opts|
user.last_login = Time.now
end
before_failure
"failure application"が呼び出される直前に実行されます。
使用されるRackエンドポイントで必要な場合にenvを変更するのに役立ちます。例えば、エンドポイントがrequest.params[:action]
をメソッド名に設定する必要がある場合です。
例
Warden::Manager.before_failure do |env, opts|
request = Rack::Request.new(env)
env['SCRIPT_INFO'] =~ /\/(.*)/
request.params[:action] = $1
end
before_logout
このコールバックは各ユーザーがログアウトする前に実行されます。ユーザーからremember_me
トークンを削除する際に役立ちます。
例
Warden::Manager.before_logout do |user,auth,opts|
user.forget_me!
auth.response.delete_cookie "remember_token"
end
Scopes
Wardenは複数のユーザーが同時にログインすることを可能にしますが、よく注意する必要があります。
Sudoアクセスや、パブリッシャーが他のユーザーとして閲覧するとどう見えるかをチェックすること、また、チェックアウトするときの安全な認証ステップなどを行う場合です。特定のアカウントへのユーザーのアクセスを認証するためにスコープを使用できます。
デフォルトではスコープは:default
になっています。:default
スコープは何もスコープが指定されていない時に使われます。
Using Scopes
スコープはオブジェクトによって識別されます。(通常シンボルを使用します)
Authenticating
# :sudo スコープのチェック
env['warden'].authenticated?(:sudo)
# :sudo スコープを :pgp ストラテジーで認証
env['warden'].authenticate(:pgp, :scope => :sudo)
# #authenticate と #authenticate! で同じオプションを使えます
Scoped User Access
env['warden'].user(:sudo)
Logout
env['warden'].logout # セッションを消去。全ユーザーをログアウトさせる
env['warden'].logout(:default) # :default ユーザーをログアウトさせる
env['warden'].logout(:admin) # :admin ユーザーをログアウトさせる
Keeping Each User’s Data Separate
各ユーザーのデータを別々に保持するにはAuthenticated Session Data機能を使用します。
Authenticated session data
Wardenのスコープは複数の認証済みユーザーが1つのセッション内に存在できるようにするメカニズムを提供します。
例として2つのスコープ、:admin
と:user
を使用する場合を考えてみましょう。:user
スコープは一般ユーザーがログインする際にアプリケーションにアクセスするために使われます。:admin
スコープは管理者ユーザーがログインする際に使われますが、実のところ認証ではなくセッションを整理する方法です。ストラテジーにおいてこれらのスコープの差異を決めるのはあなた自身ということです。
では、:admin
ユーザーがログインして、特定のユーザーとしてサイトを閲覧するケースを考えてみましょう。adminユーザーが一般ユーザーのふりをしてサイトを訪問できるように、両方のユーザーを同じセッション内でログインさせることができます。
warden = env['warden']
if warden.authenticated?(:admin)
warden.authenticated?(:user) && warden.logout(:user)
warden.set_user(@user, scope: :user)
end
今は@user
としてログインしてサイトを訪問しています。デフォルトの:user
スコープでサイトにいる間、何かをセッションに保存することがあります。
env['warden'].session(:user)[:redirect_back] = "/some/url"
ここでは{redirect_back: "/some/url"}
を保存します。このデータは:user
スコープに指定されています。この技術を利用するために上記の「ユーザーのふり」の例を少し拡張します。
warden = env['warden']
if warden.authenticated?(:admin)
warden.authenticated?(:user) && warden.logout(:user)
warden.session(:admin)[:redirect_back] = "/admin/path/to/somewhere"
warden.set_user(@user, scope: :user)
end
これでセッション中に2つの:redirect_back
キーを持っていることになります。1つがadmin用、もう1つが一般ユーザー用です。
warden.session(:admin)[:redirect_back] # "/admin/path/to/somewhere"
warden.session(:user)[:redirect_back] # "/some/url"
2セットのセッションデータはスコープ指定されていますが、同じセッション内に存在します。では、「ふり」をやめたらどうなるでしょうか。
warden = env['warden']
warden.authenticated?(:admin) && warden.authenticated?(:user) # 両方のセッションを有効にする
warden.logout(:user) # 一般ユーザーのみログアウトし、セッションデータも一般ユーザーのものだけ削除する
redirect_to warden.session(:admin)[:redirect_back] || "/admin"
上記の例で一般ユーザーからログアウトすると、一般ユーザーはセッション全体から取り除かれますが(両方ログアウトしています)、スコープ指定されたセッションデータも消去されます。ただ、adminユーザーのスコープ指定されたデータだけはそのまま残ります。
全セッションからログアウトして、セッションデータも全て消去したい場合:
env['warden'].logout
スコープが渡されていない場合は全ての既知のスコープがログアウトされ、データも消去されます。上記を呼び出す前に少なくとも
env['warden'].authenticated?(scope)
をそれぞれのスコープに対して呼び出す必要があり、これによってWardenが各スコープを認識します。
Testing
WardenはRackアプリケーションであるため、テストする際はフルスタックテストを行うことをお勧めします。Wardenに実際にテストに参加してもらうにはスタックを通す必要があります。
私はテストにはrack-testを使うことを好んでおり、バージョン0.9.5の時点でテストヘルパーをいくつか同梱しています。
例は全てRack::Testを想定しています。
Requirements
ヘルパーを提供するにはアプリケーションのスコープにWarden::Test::Helpers
をincludeしてください。
さらに重要なこととして、テストフレームワークのクリーンアップの段階で、WardenをテストモードではWarden.test_reset!
を用いてリセットする必要があります。
include Warden::Test::Helpers
after { Warden.test_reset! }
Login
# テストスコープでincludeしてセットアップ
include Warden::Test::Helpers
# アクションの前に"A User"としてログイン
login_as "A User"
get "/foo"
# "An Admin"としてログイン
login_as "An Admin", :scope => :admin
get "/foo"
# adminと一般ユーザーの両方でログイン
login_as "A User"
login_as "An Admin", :scope => :admin
get "/foo"
Logout
# ヘルパーのセットアップ
include Warden::Test::Helpers
# リクエストを受け取る前に全てのユーザーからログアウト
logout
get "/foo"
# リクエストを受け取る前にadminユーザーのみログアウト
logout :admin
get "/foo"
Custom
# ヘルパーセットアップ
include Warden::Test::Helpers
# on_next_requestを追加するときはリクエストがWardenに到達したら実行される
# 一度到達したら処理は使い切られ以降のリクエストに影響しない
#
# 一般ユーザーとしてログインしている場合:
Warden.on_next_request do |proxy|
proxy.set_user("Some User", :scope => :foo)
end