- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
Wardenとは?
- 認証のフレームワーク。実際の認証は自分で実装する
- 認証はRackミドルウェアで行う
- Wardenをアプリで直接使うことはなさそう。deviseのような認証ライブラリのためのライブラリと考えたほうが良さそう
- Deviseで使われてる。Wardenのstrategy/scope/callback等の知識がないとDeviseのコード読むのつらそう
導入
- Railsアプリ(APIモード)にwardenを使い認証機能を追加する
- deviseなし。生warden
- 参考
1. インストール
gem 'warden'
gem 'bcrypt'
$ bundle
2. ベースとなるRailsアプリ
Rails.application.routes.draw do
get 'welcome/index'.
root 'welcome#index'
end
class WelcomeController < ApplicationController
def index
render text: "Welcome guest, it's #{Time.now}"
end
end
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
class User < ActiveRecord::Base
after_create :generate_authentication_token!
has_secure_password
private
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
を用意してそこに置くのが良さげ
class AuthenticationTokenStrategy < ::Warden::Strategies::Base
def valid?
params['authentication_token']
end
def authenticate!
user = User.find_by_authentication_token(params['authentication_token'])
if user
success!(user)
else
fail!('strategies.authentication_token.failed')
end
end
end
4. Strategyをwardenに追加する
require Rails.root.join('lib/strategies/authentication_token_strategy')
Warden::Strategies.add(:authentication_token, AuthenticationTokenStrategy)
5. wardenをRackミドルウェアスタックに追加
- session(cookie)ミドルウェアの後に追加すべき。ここではflashの後に追加しておく。
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
manager.default_strategies :authentication_token
end
6. 認証情報をコントローラーから利用する
- current_user等のメソッドを
env['warden']
を使い定義する
authenticate!
はbefore_filter
でリクエストのたびに実行されるようにする
module WardenHelper
extend ActiveSupport::Concern
included do
helper_method :warden, :signed_in?, :current_user
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
class ApplicationController < ActionController::API
include WardenHelper
end
strategy
strategyとは?
- strategyは認証の実装。これらを切り替えることで、認証方法を切り替えられる(Strategyパターン)
Warden::Strategies::Baseとは?
- 各strategyの親クラス
- これのサブクラスを定義することで、独自のstrategyを定義できる
使い方1. Warden::Strategies::Baseを継承する場合
authentiate!
は必須
valid?
はなくてもOK。ない場合はtrueで、必ず実行される
class AuthenticationTokenStrategy < ::Warden::Strategies::Base
def valid?
params['authentication_token']
end
def authenticate!
user = User.find_by_authentication_token(params['authentication_token'])
if user
success!(user)
else
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も使う
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
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.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|
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, :store => false ,:strategies => [:api_token], :action => "unauthenticated_api"
end
scopeを使用する
env['warden'].authenticate(:scope => :admin)
env['warden'].authenticated?(:admin)
env['warden'].user(:admin)
env['warden'].logout(:admin)
callback
callbackとは?
env['warden'].user
等に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だけ実行
env['warden'].user
env['warden'].authenticate
env['warden'].set_uesr(user)
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: ?
Warden::Manager.before_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|
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. クラスを使う場合
class FailureApp
def call(env)
error_message = env["warden"].message
status = 401
headers = { "Content-Type" => "application/json"}
body = [error_message]
[stauts, headers, body]
end
end
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
manager.default_strategies :authentication_token
manager.failure_app = FailureApp
end
使い方3. Railsのコントローラーを使う場合
Rails.application.config.middleware.use Warden::Manager do |manager|
manager.default_strategies :authentication_token
manager.failure_app = ->(env) { SessionsController.action(:new).call(env) }
end
def new
flash.now.alert = warden.message if warden.message.present?
end
userをsessionに保存する
- userインスタンスをそのままsessionに突っ込むのではなく、user.idだけ突っ込む
Warden::Manager.serialize_into_session do |user|
user.id
end
Warden::Manager.serialize_from_session do |id|
User.find(id)
end
未ログイン時のルーティングを指定する
- constraintとwardenを利用することで、未ログイン時のルーティングを指定できる
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
テスト
ヘルパーを追加
include Warden::Test::Helpers
login_as: ログイン
login_as "A User"
login_as "An Admin", :scope => :admin
login_as "A User"
ログアウト
logout
logout :admin
使いそうなメソッドまとめ
Warden::Strategies::Base
request: Rack::Requestオブジェクト
session: sessionオブジェクト
params: パラメータオブジェクト
env: Rackのenv
succes!: 認証成功 + halt!
halt!
でstrategyのcascadingを止める
success! user
fail: 認証失敗 + halt!
halt!
でstrategyのcascadingを止める
!
なしだとhalt!しないっぽい。次のstrategyに移り実行する
fail "Invalid email or password"
redirect!: リダイレクト + halt!
custom!: カスタムのRackアプリ配列 + halt!
halt!: strategyのcascadingを止める
pass: このstrategyを飛ばす
Warden::Manager
manager.default_strategies: デフォルトの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)
manager.default_scope = :user
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
env['warden'].user(:api)
env['warden'].message: 認証失敗時にsetしたエラーメッセージ
env['warden'].authenticated?: 認証済みなら、true
env['warden'].authenticated?
env['warden'].authenticated?(:foo)
env['warden'].authenticate!: 認証する
env['warden'].authenticate
env['warden'].authenticate!
env['warden'].authenticate(:password)
env['warden'].authenticate(:password, :basic)
env['warden'].authenticate!(:scope => :api)
env['warden'].valid?: ガード
env['warden'].set_user(@user): userを自分でセットする
env['warden'].set_user(@user)
env['warden'].set_user(@user, :scope => :admin)
env['warden'].set_user(@user, :store => false)
env['warden'].logout: ログアウト
env['warden'].logout
env['warden'].logout(:sudo)
ざっくりコードリーディング
warden.gemspec
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