猫Rails

ねこー🐈

Wardenの使い方 まとめ

  • 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。

Wardenとは?

  • 認証のフレームワーク。実際の認証は自分で実装する
  • 認証はRackミドルウェアで行う
  • Wardenをアプリで直接使うことはなさそう。deviseのような認証ライブラリのためのライブラリと考えたほうが良さそう
  • Deviseで使われてる。Wardenのstrategy/scope/callback等の知識がないとDeviseのコード読むのつらそう

導入

1. インストール

# Gemfile
gem 'warden'
gem 'bcrypt' # has_secure_passwordのため
$ bundle

2. ベースとなるRailsアプリ

  • routes
# config/routes.rb
Rails.application.routes.draw do
  get 'welcome/index'.
  root 'welcome#index'
end
  • コントローラー
# app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    render text: "Welcome guest, it's #{Time.now}"
  end
end
  • Userのmigration
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :username
      t.string :password_digest
      t.string :authentication_token # 認証にはこれを使う

      t.timestamps null: false

      t.index :authentication_token, unique: true
    end
  end
end
  • Userモデル
# app/models/user.rb
class User < ActiveRecord::Base
  after_create :generate_authentication_token!

  has_secure_password

  private

  # user生成後に一意となるauthentication_tokenを作成する
  def generate_authentication_token!
    self.authentication_token = Digest::SHA1.hexdigest("#{Time.now}-#{self.id}-#{self.updated_at}")
    self.save
  end
end

3. Strategyを定義する

  • strategyは認証の実装。これらを切り替えることで、認証方法を切り替えられる(Strategyパターン)
  • ここで定義するのは認証トークンを利用した認証
  • Railsの場合はlib/strategiesを用意してそこに置くのが良さげ
# lib/strategies/authentication_token_strategy.rb
class AuthenticationTokenStrategy < ::Warden::Strategies::Base
  # ガード
  # params['authentication_token']がある場合のみ、authenticate!を実行する
  def valid?
    params['authentication_token']
  end

  # params['authentication_token']を使い認証
  # userが存在すれば、認証成功
  # コントローラーでenv["warden"].authenticate!の形で利用する
  def authenticate!
    user = User.find_by_authentication_token(params['authentication_token'])

    if user
      # 認証成功
      # ここで渡したuserは、コントローラーからenv['warden'].userで取り出せる
      success!(user)
    else
      # 認証失敗
      # ここで渡したメッセージは、コントローラーからenv['warden'].messageで取り出せる
      fail!('strategies.authentication_token.failed')
    end
  end
end

4. Strategyをwardenに追加する

# config/initializers/warden.rb

require Rails.root.join('lib/strategies/authentication_token_strategy')

# `AuthenticationTokenStrategy`を`:authentication_token`で参照できるようになる
Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)

5. wardenをRackミドルウェアスタックに追加

  • session(cookie)ミドルウェアの後に追加すべき。ここではflashの後に追加しておく。
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  # 先程定義したstrategyをデフォルトのstrategyとする
  manager.default_strategies :authentication_token
end

6. 認証情報をコントローラーから利用する

  • current_user等のメソッドをenv['warden']を使い定義する
  • authenticate!before_filterでリクエストのたびに実行されるようにする
# app/controllers/concerns/warden_helper.rb
module WardenHelper
  extend ActiveSupport::Concern

  included do
    # ビューでも使えるようにする
    helper_method :warden, :signed_in?, :current_user

    # before_filterで認証チェック
    prepend_before_filter :authenticate!
  end

  def signed_in?
    !current_user.nil?
  end

  def current_user
    warden.user
  end

  def warden
    request.env['warden']
  end

  def authenticate!
    warden.authenticate!
  end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include WardenHelper
end

strategy

strategyとは?

  • strategyは認証の実装。これらを切り替えることで、認証方法を切り替えられる(Strategyパターン)

Warden::Strategies::Baseとは?

  • 各strategyの親クラス
  • これのサブクラスを定義することで、独自のstrategyを定義できる

使い方1. Warden::Strategies::Baseを継承する場合

  • authentiate!は必須
  • valid?はなくてもOK。ない場合はtrueで、必ず実行される
# lib/strategies/authentication_token_strategy.rb
class AuthenticationTokenStrategy < ::Warden::Strategies::Base
  # ガード
  # params['authentication_token']がある場合のみ、authenticate!を実行する
  def valid?
    params['authentication_token']
  end

  # params['authentication_token']を使い認証
  # userが存在すれば、認証成功
  # コントローラーでenv["warden"].authenticate!の形で利用する
  def authenticate!
    user = User.find_by_authentication_token(params['authentication_token'])

    if user
      # 認証成功
      # ここで渡したuserは、コントローラーからenv['warden'].userで取り出せる
      success!(user)
    else
      # 認証失敗
      # ここで渡したメッセージは、コントローラーからenv['warden'].messageで取り出せる
      fail!('strategies.authentication_token.failed')
    end
  end
end

# 追加
Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)

使い方2. 定義 + 追加

  • addする際に、同時に定義することも可能
  • 内部でWarden::Strategies::Baseのサブクラスを定義してる
Warden::Strategies.add(:authentication_token) do
  def valid?
    ...
  end

  def authenticate!
    ...
  end
end

使い方3. 復数strategyを使う場合

  • sample1のトークン認証strategyに加えて、Basic認証strategyも使う
# lib/strategies/basic_auth_strategy.rb
class BasicAuthStrategy < ::Warden::Strategies::Base
  def auth
    @auth ||= Rack::Auth::Basic::Request.new(env)
  end

  def valid?
    auth.provided? && auth.basic? && auth.credentials
  end

  def authenticate!
    user = User.find_by_username(auth.credentials[0])
    if user && user.authenticate(auth.credentials[1])
      success!(user)
    else
      fail!('strategies.basic_auth.failed')
    end
  end
end
  • WardenのStrategyに追加
# config/initializers/warden.rb
require Rails.root.join('lib/strategies/authentication_token_strategy')
require Rails.root.join('lib/strategies/basic_auth_strategy')

Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)
Warden::Strategies.add(:basic_auth, BasicAuthStrategy)
  • デフォルトのstrategyに指定。デフォルトstrategyは復数指定可能
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  manager.default_strategies :authentication_token, :basic_auth
end
  • 順番に実行されて、halt!されるまでcascadeされる
env['warden'].authenticate

scope

scopeとは?

  • :user以外にも、:adminのようなユーザーを作ることができる。それぞれにstrategyを適用することができる
  • デフォルトのscopeは:default。scope指定がない場合は常に:default
  • deviseでも使われてる。current_userとかをcurrent_adminとして扱えるやつ

使い方

scopeを設定する

use Warden::Manager do |config|
  # default_stragetiesの代わりに、default_scopeを指定する
  config.default_scope = :user

  # 各scopeに、strategiesを指定する
  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,         :store => false  ,:strategies => [:api_token], :action => "unauthenticated_api"
end

scopeを使用する

# 認証
env['warden'].authenticate(:scope => :admin)

# 認証済みか?
env['warden'].authenticated?(:admin)

# userへのアクセス
env['warden'].user(:admin)

# ログアウト
env['warden'].logout(:admin)

callback

callbackとは?

  • env['warden'].user等にcallbackを仕掛けられる

使い方

  • callbackは宣言順に実行される
Warden::Manager.after_set_user do |user, warden, opts|
  unless user.active?
    warden.logout
    throw(:warden, :message => "User not active")
  end
end

種類

Warden::Manager.after_set_user: userをset後

基本

  • trigger
    • env['warden'].userを初めて呼ぶ時
    • env['warden'].authenticate
    • env['warden'].set_uesr(user)
Warden::Manager.after_set_user do |user, warden, opts|
  unless user.active?
    warden.logout
    throw(:warden, :message => "User not active")
  end
end

only: 指定のtriggerだけ実行

# triggerは以下の3つ
env['warden'].user           # fetch
env['warden'].authenticate   # authentication
env['warden'].set_uesr(user) # set_user

# fetch時だけ実行
Warden::Manager.after_set_user only: :fetch

except: 指定のtriggerでは実行しない

Warden::Manager.after_authentication: 認証後

  • Warden::Manager.after_set_user only: :authentication のalias
Warden::Manager.after_authentication do |user,warden,opts|
  user.last_login = Time.now
end

Warden::Manager.after_fetch: user取得後

  • Warden::Manager.after_set_user only: :fetchのalias

Warden::Manager.before_failure: failure_app呼び出し前

Warden::Manager.before_failure do |env, opts|
  request = Rack::Request.new(env)
  env['SCRIPT_INFO'] =~ /\/(.*)/
  request.params[:action] = $1
end

Warden::Manager.after_failed_fetch: ?

  • trigger
    • env['warden'].user

Warden::Manager.before_logout: ログアウト前

  • trigger
    • env['warden'].logout
Warden::Manager.before_logout do |user,warden,opts|
  user.forget_me!
  warden.response.delete_cookie "remember_token"
end

Warden::Manager.on_request: リクエスト毎(wardenインスタンス生成時)

Warden::Manager.on_request do |warden|
  # do_something
end

Warden::Manager.prepend_xxxxx: prepend

  • prependしたい場合は、prepend_xxxxとする
  • prepend_before_failureとか

failure_app

  • 認証失敗時の処理をRackアプリとして実装できる
  • failure_appとしてセットされたRackアプリは、user.authenticate! で全ての認証が失敗した時に起動される

使い方1. lambdaを使う場合

  • RackアプリならなんでもOK。lambdaでもOK
config.middleware.use Warden::Manager do |manager|
  manager.default_strategies :authentication_token
  manager.failure_app = ->(env) { ['401', {'Content-Type' => 'application/json'}, { error: 'Unauthorized', code: 401 }] }
end

使い方2. クラスを使う場合

# app/controllers/unauthorized_controller.rb
class FailureApp
  def call(env)
    # `fail!`のメッセージを取得できる
    error_message = env["warden"].message

    status = 401
    headers = { "Content-Type" => "application/json"}
    body = [error_message]

    [stauts, headers, body]
  end
end
# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  manager.default_strategies :authentication_token
  manager.failure_app = FailureApp
end

使い方3. Railsのコントローラーを使う場合

# config/initializers/warden.rb
Rails.application.config.middleware.use Warden::Manager do |manager|
  manager.default_strategies :authentication_token

  # アクションを直接指定してもよいが開発環境でキャッシュされてしまうようなので、lambdaにしておくのが良さそう
  manager.failure_app = ->(env) { SessionsController.action(:new).call(env) }
end

# app/controllers/sessions_controller.rb
def new
  flash.now.alert = warden.message if warden.message.present?
end

userをsessionに保存する

  • userインスタンスをそのままsessionに突っ込むのではなく、user.idだけ突っ込む
# /config/initializers/warden.rb
# sessionにはuser.idを保存する
Warden::Manager.serialize_into_session do |user|
  user.id
end

# sessionからuser.idを取り出して、userを取得する
Warden::Manager.serialize_from_session do |id|
  User.find(id)
end

未ログイン時のルーティングを指定する

  • constraintとwardenを利用することで、未ログイン時のルーティングを指定できる
# config/routes.rb
# 未ログインの場合は、このルーティングが適用される
scope constraints: ->(request) { request.env['warden'].user.nil? } do
  get "signup", to: "users#new", as: "signup"
  get "login", to: "sessions#new", as: "login"
end

resources :users
resources :sessions

テスト

ヘルパーを追加

  • login_aslogoutを追加してくれる
include Warden::Test::Helpers

login_as: ログイン

# "A User"として、ログインする
login_as "A User"

# "An Admin"として、ログインする
login_as "An Admin", :scope => :admin

# 両方で、ログインする
login_as "A User"

ログアウト

# ログアウトする
logout

# adminとして、ログアウトする
logout :admin

使いそうなメソッドまとめ

Warden::Strategies::Base

request: Rack::Requestオブジェクト

session: sessionオブジェクト

params: パラメータオブジェクト

env: Rackのenv

succes!: 認証成功 + halt!

  • halt!でstrategyのcascadingを止める
# このuserはenv['warden'].userでアプリから取り出せる
success! user

fail: 認証失敗 + halt!

  • halt!でstrategyのcascadingを止める
  • !なしだとhalt!しないっぽい。次のstrategyに移り実行する
# このメッセージはenv['warden'].messageでアプリから取り出せる
fail "Invalid email or password"

redirect!: リダイレクト + halt!

custom!: カスタムのRackアプリ配列 + halt!

halt!: strategyのcascadingを止める

  • 後続のstrategyは実行されない

pass: このstrategyを飛ばす

Warden::Manager

manager.default_strategies: デフォルトのstrategy

# passwordとbasicの2つのstrategyをデフォルトとしてセット
manager.default_strategies :password, :basicp

manager.failure_app=: failure_app設定

manager.failure_app = FailureApp

manager.default_scope: デフォルトのscope

use Warden::Manager do |manager|
  manager.failure_app = Public::SessionsController.action(:new)

  # default_stragetiesの代わりに、default_scopeを指定する
  manager.default_scope = :user

  # 各scope*4に、strategiesを指定する
  manager.scope_defaults :user,        :strategies => [:password]
  manager.scope_defaults :account,     :store => false,  :strategies => [:account_by_subdomain]
  manager.scope_defaults :membership,  :store => false,  :strategies => [:membership, :account_owner]
  manager.scope_defaults :api,         :store => false  ,:strategies => [:api_token], :action => "unauthenticated_api"

end

manager.scope_defaults: デフォルトのstrategy(scope)

Warden::Manager.serialize_into_session: sessionに保存する

Warden::Manager.serialize_into_session do |user|
  user.id
end

Warden::Manager.serialize_from_session: sessionから取り出す

Warden::Manager.serialize_from_session do |id|
  User.find(id)
end

Railsコントローラー

env['warden']: wardenを取得

env['warden'].user: 認証成功時にsetしたuser

env['warden'].user # デフォルトscope
env['warden'].user(:api) # scope指定

env['warden'].message: 認証失敗時にsetしたエラーメッセージ

env['warden'].authenticated?: 認証済みなら、true

env['warden'].authenticated? # デフォルトscope
env['warden'].authenticated?(:foo) # scope指定

env['warden'].authenticate!: 認証する

env['warden'].authenticate # 認証(失敗時に、処理を継続する)
env['warden'].authenticate! # 認証(失敗時に、例外を投げる)
env['warden'].authenticate(:password) # passwordストラテジーを利用する
env['warden'].authenticate(:password, :basic) # strategyは復数指定可能
env['warden'].authenticate!(:scope => :api) # scopeを指定する

env['warden'].valid?: ガード

env['warden'].set_user(@user): userを自分でセットする

env['warden'].set_user(@user) # デフォルトscope
env['warden'].set_user(@user, :scope => :admin) # scope指定
env['warden'].set_user(@user, :store => false) # 今回のリクエストだけで、sessionには保存しない

env['warden'].logout: ログアウト

env['warden'].logout # デフォルトscope
env['warden'].logout(:sudo) # scope指定

ざっくりコードリーディング

  • v1.2.8
  • 難しい...あんまりわからんかった

warden.gemspec

  • 依存gemはrackのみ

lib/warden/proxy.rb

  • env['warden']Warden::Proxy.new
  • env['warden']のメソッド群はここに定義されてる

lib/warden/strategies

  • Warden::Strategies.addが定義されてる

lib/warden/strategies/base.rb

  • Warden::Strageties::Baseは全てのStrategyの親クラス
  • Warden::Strategies.addを使えば、WArden::Strageties::Baseを継承した自前のstrategyを簡単に定義できる
  • succsess!等のメソッドはここに定義されてる

参考URL