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 も指定できるし。
参考サイト
- OmniAuth と Warden を WardenOmniAuth で連携してみた - present : http://tnakamura.hatenablog.com/entry/20120211/sinatra_warden_omniauth_wardenomniauth
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 がやってくれること
具体的には、
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>