Warden でログイン管理
Warden でログイン管理
概要
前回 omni-twitter を見てみましたが、 これは単に認証しかしないので、ログイン状態の保持やログアウト、などはsession を使うなりして自分でやらなければなりません。面倒だし穴があるといろいろ面倒なので、既存のツール、warden に頼ります。
使うのはいいんですが、一応
- strategy の理解
- セッションとのやりとり
は理解しておかないとエラーなどがおこった場合つらいです。
strategy とは、簡単にいってしまうと、「どのように認証するか」です。ユーザー名とパスワードをつきあわせるのか、OAUTH 使うのか。前者ならユーザー名とパスワードが両方ともあたえられて、パスワードがデータベースに入ってるのと合ってるのか確認しなきゃだし、後者ならサービスプロバイダとやりとりしてtoken, secret をもらわなきゃいけません。
session に関しては後述。
ひな形の作成
とりあえずひな形だけ作ってみましょう。
使うのは warden なので require 'warden' します。必要なら bundle install も。セッションも使うので enable :sessions で(secret key がアレとかはまた別の話)。
require 'sinatra/base' require 'sinatra/reloader' require 'warden' module YourApp class Application < Sinatra::Base configure do register Sinatra::Reloader enable :sessions end
そして Warden::Manager を use します。この時、どの strategy を使うのか、認証失敗した時のコールバックなどを指定します。
... use Warden::Manager do |manager| manager.scope_defaults :default, :strategies => [:test], :action => '/auth/failure' manager.failure_app = self end
:test という strategy を使い、失敗した時は /auth/failure に戻り、自クラスで処理をする、と。
実際にどう認証するかは、 Warden::Strategies.add(:test) { &block } で定義します。 valid?() と authenticate!() という関数を定義します。前者は認証のための情報が渡されているか、後者は実際の認証を受け持ちます。後者では、成功した時 success!(user_obj)、失敗したとき fail!(message) を呼びます。この user_obj が、env['warden'].user に入り、自アプリでユーザ情報の取得、操作などで使っていくことになります。
(scope などについては略)
Warden::Strategies.add(:test) do def valid? true end def authenticate! success!({}) end end
ここでは簡単のため常に valid, 認証成功、としています。ユーザーオブジェクトとしては、単純にハッシュ。
ちなみに valid?, authenticated!を定義したクラスを作り、それを Warden::Strategies.add(:test, YourStrategy) で渡すこともできます。
あとはルーティングを書きます。env['warden'] に認証などをさせるための proxy が入ってますので、これに対して操作を指示します。
get '/' do erb "hello world" end get '/auth/login' do env['warden'].authenticate! redirect '/' get '/auth/failure' do erb "login failed" end get '/auth/logout' do env['warden'].logout redirect '/' end end
view は省略。
簡単な basic 認証
username, password で認証させます。といってもちゃんとデータベース作って暗号化させるのは面倒なので、 とりあえずここでは
class User attr_accessor :username, :password def initialize(username, password) @username, @password = username, password end end Users = {"foo" => User.new("foo", "foopw"), "bar" => User.new("bar", "barpw")}
のように簡略化してしまいます。
認証の部分は、
Warden::Strategies.add(:test) do def valid? params['username'] && params['password'] end def authenticate! user = Users[params['username']] if !user.nil? and user.password = params['password'] success!(user) else fail!("auth failed") end end end
とクエリで username と password を渡すとパスワードとつきあわせます。ちなみにここでの paramsはあくまで warden の実装なので、sinatra での params のように :symbol は使えないし、 /get/:id のルーティングでの params[:id] は引っ張ってくれません。あくまでクエリのです。
ログインフォームを作って post してそこで env['warden'].authenticate! させるべきなのですが、面倒なので get /protected で authenticate させます。
get '/protected' do env['warden'].authenticate! erb "protected page" end
これで /protected?username=foo&password=foopw でアクセスすると認証ができます。
シリアライズ
、と、そもそも暗号化もしてないのでアレですが、session.inspect すると、
{"session_id"=>"62ae3dxxxxxx", ..... "warden.user.default.key"=>#<User:70041300015360 id='' name='foo' password='foopw'>}
ともろパスワード(暗号化されていたとしても)などが乗っちゃってます。またユーザー情報がながくなった場合バッファあふれを起こすことがあります。なので、session に載せるのはあくまで key (=username) のみにして、必要に応じてユーザ情報をアプリ内で取得したいところ。これをやるのがシリアライズ。
use Warden::Manager の前あたりに、
Warden::Manager.serialize_into_session{|user| user.username} Warden::Manager.serialize_from_session{|username| Users[username]}
とします。セッションに入れるときは key のみ (user.username)、セッションからユーザ情報を取得するときは username を key として Users[username] でひっぱる、ということをしています。こうするとセッションに乗るのは username のみになります。
{"session_id"=>"49753dxxxxxx", ..., "warden.user.default.key"=>"foo"}
サンプルコード
と、いままでのサンプル。
require 'sinatra/base' require 'sinatra/reloader' require 'warden' ################################################################ module YourApp class User attr_accessor :username, :password def initialize(username, password) @username, @password = username, password end end Users = {"foo" => User.new("foo", "foopw"), "bar" => User.new("bar", "barpw")} class Application < Sinatra::Base configure do register Sinatra::Reloader enable :sessions end Warden::Manager.serialize_into_session{|user| user.username} Warden::Manager.serialize_from_session{|username| Users[username]} use Warden::Manager do |manager| manager.scope_defaults :default, :strategies => [:test], :action => 'auth/failure' manager.failure_app = self end Warden::Strategies.add(:test) do def valid? params['username'] && params['password'] end def authenticate! user = Users[params['username']] if !user.nil? and user.password = params['password'] success!(user) else fail!("auth failed") end end end helpers do include ERB::Util enable :inline_templates end ################ get '/' do erb "hello world" end get '/protected' do env['warden'].authenticate! erb "protected page" end get '/auth/login' do env['warden'].authenticate! redirect '/' end get '/auth/twitter/callback' do env['warden'].authenticate! redirect '/' end get '/auth/failure' do erb "login failed" end get '/auth/logout' do #session[:user] = nil env['warden'].logout redirect '/' end end end __END__ @@layout <html> <head> <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.0.min.js"></script> <script type="text/javascript" src="http://getbootstrap.com/dist/js/bootstrap.min.js"></script> <link href="http://getbootstrap.com/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="navbar navbar-default"> <ul class="nav navbar-nav"> <li><a href="/">top</a></li> <li><a href="/auth/twitter">login</a></li> <li><a href="/protected">protected</a></li> <li><a href="/auth/logout">logout</a></li> </ul> </div> <div class="container"> <div class="panel panel-primary"> <div class="panel-heading">title</div> <div class="panel-body"> <%= yield %> <hr> <%= erb :user_info %> </div> </div> </div> </body> </html> @@user_info <%= h env['warden'].user.inspect %> <hr> <% if !session[:user].nil? %> <hr> <% end %> <div><h2>warden</h2><%= h env['warden'].inspect %></div> <div><h2>warden.user</h2><%= h env['warden'].user.inspect %></div> <div><h2>omniauth.auth</h2><%= h env['omniauth.auth'].inspect %></div> <div><h2>session</h2><%= h session.inspect %></div> <div><h2>rack.session</h2><%= h env['rack.session'].inspect %></div> <div><h2>env</h2><%= h env.inspect %></div>