7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby開発Advent Calendar 2022

Day 18

Wardenのwikiを翻訳してみた

Last updated at Posted at 2022-12-18

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が投げられたら以下のことが起きます。

  1. リダイレクトやカスタムRackレスポンスなどがないか遅延認証オブジェクトをチェックする。失敗しているか何も起きていない場合は"failure application"が呼び出される
  2. env['PATH_INFO']"/unauthenticated"に書き換えられる
  3. 渡されたオプションは全てenv['warden.options']に含まれる
  4. 全てのbefore_failureコールバックが呼び出される
  5. "failure application"が呼び出される

"failure application"で呼び出されるアクションを変えたい場合は、単純にthrowのオプションに:actionシンボルを渡してください。以下のやり方があります。

throw(:warden, :action => "different_action")

# 認証時
env['warden'].authenticate! :action => "different_action"

スコープごとに別の失敗時のアクションを投げるように設定できます。Setupをご覧ください。

Users

Wardenの仕事は結局はリクエストを認証することです。それには2つの側面があります。

  1. リクエストを送る人(ユーザー)
  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_failureprepend_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
7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?