warden_omniauth-jonrowe

hassox の warden_omniauth (0.1.0: https://github.com/hassox/warden_omniauth/)より jonrowe の(https://rubygems.org/gems/warden_omniauth-jonrowe)の方がいいっぽい。

hassox のだと gem に上がってる奴だと env['omniauth.auth'] を見ないのでコールバーっくループが起こるけど jonroweのは見る。 またredirect_after_callback にブロックが渡せるので

    use WardenOmniAuth do |config|
      config.redirect_after_callback {|env|
        logger.debug(env['rack.session']['redirect_after_callback'])
        env['rack.session']['redirect_after_callback']
      }
    end

みたいなことが出来る。

    helpers do
      def warden
        env['warden']
      end
      def authenticate!(redirect_after_callback="/")
        session['redirect_after_callback'] = redirect_after_callback
        warden.authenticate!
        session['redirect_after_callback'] = "/"
      end
    end
    ...
    get :protected do
      authenticate!(url_for(:protected))
      ...

ってな感じで動的に戻してもらいたいところを指定。

以前の hassox を uninstall してから % sudo gem install warden_omniauth-jonrowe

Padrino 覚書き

前準備

インストール

ふつーに % sudo gem install padrino で。

プロジェクト作成

padrino のgenerate project コマンドで。-d で ORM、-e でレンダリングエンジンを指定。-b をつけると bundle までやってくれる。

% padrino g project sample -d activerecord -e erb
...

作った後に設定変えたい場合は .components をいじる。

ファイル構成

こんなかんじ。rails よりだいぶシンプル。

+ app/
| + controllers/
| + helpers/
| + views/
| | + layouts/
| + app.rb
+ config/
| + apps.rb
| + boot.rb
| + database.rb
+ public/
| + (snip)
+ config.ru
+ Gemfile
+ Rakefile

Hello world

まずはapp/app.rb に sinara と同じような感じで。

module SampleBlog
  class App < Padrino::Application
    ...
    get '/' do
        erb "hello world"
    end

これで rackup すれば hello world できるはず。ちなみにセッションは使わなくても必須なので enable :sessions は消しちゃダメ。

admin

先に admin を作っておくと db の CRUD ができるとかいろいろいいらしいので入れる。 % padrino g admin で、後はメッセージにあるよう、rake db:migrate, db:seed する。db:create は省略しても db:migrate で db を作ってくれる。 rackup して localhost:9292/admin にアクセスし db:seed で入力したメール、パスワードを入れると管理画面に入れる。 この時点では特にあまりうれしいことはなさそうだが、後で使う。

コントローラー

sinatra ではルーティングが増えるとワケワカになりがちだが、 padrino だと namespace で分けたりまとめたり出来る。 また、実urlとは別に概念としてのアクションが実装できる。 例えば sinatra だと get '/show' で /show のアクセスで show のアクションを記述するが、 padrino だと get :show としてアクションを記述した上、:map => "/showvalue" などと実際のルーティングと別にできる。

def :show, :map => "/showvalue", :with => :id
  params[:id]
end

実際のルーティングは、% rake routes で表示できる。

helper

link_to, url_for とか。他にもあるけど。

  link_to("show value", url_for(:show, :id => 5))

グルーピング

Padrina::Application のクラスメソッド controllers を呼び出して作る。

  class App < Padrina::Application
    self.controllers :admin to
      get :index do
      end
      get :create do
      end
    end
  end

コントローラーの作成

g controller で。引数を渡すとメソッドを作ってくれる。

% padrino g controller posts get:index get:show

で app/controllers/app/controllers/posts.rb にテンプレートを作ってくれる。 単に Sample::App.controllers do; end と引数で指定した def get :index; end の枠組みを作ってくれるだけ。

モデル

g project -d activerecord としていれば、% padrino g create model post で models/post.rb(中身は class Post < ActiveRecord::Base; end だけ。)と db/migrate/001_create_posts.rb(いわゆるマイグレーションファイル)を作成。 必要に応じて migration ファイルにcreate_table したり model ファイルにhas_many したりして、 db:migrate でおk。

データベースファイルは特に指定しないと db/sample_development.db に。establish_connection() する必要がないので楽。

コンソール

% padrino c でコンソールに入れるので、> post Post.new(title: "foo", text: "asdf") とかして model をいじくることができる。

ビュー

app/views/layouts/application.erb がレイアウトテンプレート。

.emacs

;;;; -*- mode: emacs-lisp; coding: iso-2022-7bit -*-
;;;;
;;;; Copyright (C) 2001 The Meadow Team

(global-set-key "\C-h" 'delete-backward-char)

(add-to-list 'load-path "~/.emacs.d/lisp/")

;; haml-mode
(autoload 'haml-mode "haml-mode" "Mode for editing haml files" t)
(setq auto-mode-alist  (append '(("\\.haml$" . haml-mode)) auto-mode-alist))


;; ruby-mode
(autoload 'ruby-mode "ruby-mode" "Mode for editing ruby source files" t)
(setq auto-mode-alist  (append '(("\\.rb$" . ruby-mode)) auto-mode-alist))
(setq auto-mode-alist  (append '(("\\.cgi$" . ruby-mode)) auto-mode-alist))
(setq interpreter-mode-alist (append '(("ruby" . ruby-mode)) interpreter-mode-alist))
;(autoload 'run-ruby "inf-ruby" "Run an inferior Ruby process")
;(autoload 'inf-ruby-keys "inf-ruby" "Set local key defs for inf-ruby in ruby-mode")
  
'(w32-ime-initialize)
(global-set-key [M-kanji] 'ignore) 
(global-set-key [kanji] 'ignore)  ; See more at: http://yohshiy.blog.fc2.com/blog-entry-169.html#sthash.P4hJnxxH.dpuf

; ibuffer
(global-set-key (kbd "C-x C-b") 'ibuffer)

(cd "~/")
(setq default-directory "~/")
(setq command-line-default-directory "~/vagrant/dev/source/")

;; 初期フレームの設定
(setq default-frame-alist
      (append (list '(foreground-color . "black")
            '(background-color . "LemonChiffon")
            '(background-color . "gray")
            '(border-color . "black")
            '(mouse-color . "white")
            '(cursor-color . "black")
;;          '(ime-font . (w32-logfont "MS ゴシック"
;;                        0 16 400 0 nil nil nil
;;                        128 1 3 49)) ; TrueType のみ
;;          '(font . "bdf-fontset")    ; BDF
;;          '(font . "private-fontset"); TrueType
            '(width . 90)
            '(height . 47)
            '(top . 20)
            '(left . 20))
          default-frame-alist))

sinatra で書く markdown viewer

markdown viewer

ローカルで書いた markdown ファイルをすぐ見たいですね。Ctrl-S, Alt-Tab, Ctrl-r で。

require 'sinatra/base'
require 'rdiscount'

module MarkdownViewer
  class Application < Sinatra::Base
    include ERB::Util
    get '/:file.md/?:opt?' do
      filename = params[:file] + ".md"
      if File.exists?(filename)
        File.open(filename){|f|
          if params[:opt] == 'raw'
            erb "<pre>#{h f.read}</pre>"
          else
            erb RDiscount.new(f.read, :autolink, :filter_html).to_html
          end
        }
      else
        erb "no such file: #{h filename}"
      end
    end
  end
end

warden-omniauth でログイン管理

warden-omniauth でログイン管理

概要

omniauth-twitter, warden と見てきましたが、この2つをつなげてみたいですね。 というかこれが最初はやりたかったこと。

gem にそのものズバリ warden_omniauth というのがありますが、これが結構曲者でapi.twitter.com にリクエストループを起こしてしまったりします。 なので、そのソースを追いながら、自分で両者を組み合わせていく、ということをやっていきます。

処理の流れ

  • GET /auth/twitter で OAUTH をし /auth/twitter/callback に(認証成功なら)帰る (omniauth-twitter)
  • そのコールバックから strategy で anthenticate! させる (warden)

いってみればそれだけです。

/auth/twitter/callback の実装

/auth/twitter => OAUTH は OmniAuth::Builder すればやってくれますから、コールバックのところを書いてみます。

    get '/auth/twitter/callback' do
      env['warden'].authenticate!
      redirect '/'
    end

まずはここから。

strategy の定義

   use Warden::Manager do |manager|
      manager.scope_defaults :default, :strategies => [:o_twitter], :action => 'auth/failur
      manager.failure_app = self
    end
    Warden::Strategies.add(:o_twitter, WardenOmniTwitter::Strategy)

strategy として:o_twitterを指定し(:omni_twitter は WardenOmniAuth で使ってるから)、 valid? と authenticate! を実装するクラス WardenOmniTwitter::Strategy (次に作る)を :o_twitter に対応するものとして追加します。

まず strategy の実装を見ると、

class WardenOmniTwitter
  class Strategy < Warden::Strategies::Base
    def authenticate!
      if user = _auth2user(env['omniauth.auth'])
        success!(user)
      else
        redirect! "/auth/twitter"
      end
    end

    def _auth2user(auth)
      return if auth.nil?
      user = {
        :provider => auth.provider,
        :uid => auth.uid,
        :credentials => auth.credentials,
        :info=>auth.info
      }
      return user
    end

Warden::Strategies::Base から派生させ、 authenticate!() で、env['omniauth.auth'] から必要な情報を抽出し、ハッシュに落とします(_auth2user())。env['omniauth.auth'] が空だったら nil が返り失敗、成功すればそのハッシュが success!() 経由で env['warden'].user にセットされます。

まずここまでで動作確認します。動くはず。

コールバックの処理をミドルウェア

一応これで動くは動くのですが、/auth/twitter/callback の処理を吸収したいところ。 それにはミドルウェアを一枚かませて routing 処理をします。rackミドルウェアにするには、

  • initialize(app) で app を取る
  • call(env) で env をうけ、[code, {header}, [body]] を返す

を守ればいいです。

class WardenOmniTwitter
  ...
  def initialize(app)
    @app = app
  end
  def call(env)
    @app.call(env)
  end
end

これを作っておいて、自アプリ内で use するだけです。

    ...
    Warden::Strategies.add(:o_twitter, WardenOmniTwitter::Strategy)
    use WardenOmniTwitter

ただし、use OmniAuth::Builder の後にしてください。じゃないとenv['omniauth.auth'] がうまくとれなくてうまくうごきません。

で、call(env)をいじくっていきます。基本的には、request.path_info が /auth/twitter/callback だったら認証をみて、されていればてきとーなところにリダイレクト、されてなかったら /auth/twitter に飛ぶ、ということをします。

  def call(env)
    req = Rack::Request.new(env)
    res = Rack::Response.new
    if req.path =~ /^\/auth\/twitter\/callback$/
      if env['warden'].authenticate?
        res.redirect("/")
      else
        res.redirect("/auth/twitter")
      end
      res.finish
    else
      @app.call(env)
    end

env['warden'].authenticate? と ? がついてるのは、既に認証されてたら true を返し、 されてなかったら認証をしにいきます(winnning strategy の authenticate! を走らす)。

なので、認証されてなかったら /auth/twitter に認証しにいき、oauth が終わって /auth/twitter/callback に返ってきたらここで拾って authenticate! を走らせて env['omniauth.auth'] から env['warden'].user にユーザー情報をセットしsuccess!(user)、 で / にリダイレクト、と。 ほんとはリダイレクト先は redirect_after_callback= で指定できるようにすべき。

ここまでで同じく動くはず。

Warden-OmniAuth

gem に登録されてるのと、git で見られるは、微妙に違うんですね。git の方だと、

  class Strategy < Warden::Strategies::Base
    ...
    def authenticate!
      if user = (env['omniauth.auth'] || env['rack.auth'] || request['auth']) # TODO: Fix..  Completely insecure... do not use this will look in params for the auth.  Apparently fixed in the new gem

とあるけど、gem のほうだと 最初の env['omniauth.auth'] || 外されており、 いつも空が返ってしまいループします。修正して require すればなんとか動くんですが、 いいのかよくわかりません。 request['auth'] を見るのはヤバいと思うけど、env['omniauth.auth'] なら大丈夫だと思うんだけど。。。

warden-omniauth を使うには、strategy を :omni_twitter にして、use WardenOmniAuth すれば動きます。redirect_after_callback も指定できるし。

参考サイト

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>

omniauth-twitter を使う

omniauth-twitter がやってくれること

  • /auth/twitter で認証しにいく
  • 成功なら /auth/twitter/callback に戻ってくる
  • 失敗なら /auth/failure に戻ってくる

具体的には、

use OmniAuth::Builder すると、get /auth/twitter のルーティングを拾い、api.twitter.com やユーザとのやりとりの後、認証できれば /auth/twitter/callback、失敗すれば /auth/failure に帰ってきます。ユーザ情報その他は env['omniauth.auth'] に入ってます。いろいろ入っていますが、

  • provider: "twitter"
  • uid: "1000xxxxxxxxxx"
  • credentials
    • secret: "xxxx"
    • token: "xxxx"
  • info
    • name: "ataru_kodaka"
    • nickname: "小高 あたる"

などが重要なところかと。たとえば、token = env['omniauth.auth'].credentials.token で取れます。

こちらがやることは、

  • twitter でログイン などのリンクをはる
  • get '/auth/twitter/callback' で env['omniauth.auth'] からid, name, token, secret などを拾う
  • /auth/failure で認証失敗処理

をやればいいですね。

サンプルコード

require 'sinatra/base'
require 'sinatra/reloader'

require 'omniauth'
require 'omniauth-twitter'

module TwitterAuth
  class Application < Sinatra::Base
    configure do
      register Sinatra::Reloader
      enable :sessions
    end

    helpers do
      include ERB::Util
      enable :inline_templates
    end

    consumer_key = "key"
    consumer_secret = "sekret"
    
    use OmniAuth::Builder do
      provider :twitter, consumer_key, consumer_secret
    end  

    ################
    get '/' do
      erb "hello world"
    end

    get '/auth/twitter/callback' do
      auth = env['omniauth.auth']
      erb auth.inspect
    end

    get '/auth/failure' do
      erb "login failed"
    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>
        <a href="/auth/twitter">login</a>
    </ul>
  </div>
  <div class="container">
    <div class="panel panel-primary">
      <div class="panel-heading">title</div>
      <div class="panel-body">
        something here
        <%= yield %>
        <hr>
        <%= env['omniauth.auth'].inspect %>
      </div>
    </div>
  </div>
</body>
</html>