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>