RailsでOpenIDを使うときにハマったところ
前々から興味のあったOpenIDをRails上で試そうとしたところ, ちょいちょい動かなかったりしたのでそのメモメモ.
とりあえず導入. OpenIDを使うためのRubyGemsとRailsのプラグインを入れる.
$ sudo gem install ruby-openid $ script/plugin install open_id_authentication
そしてプラグインが使うテーブルを追加する.
$ rake open_id_authentication:db:create # これでマイグレーションファイルが作られるので, $ rake db:migrate # 改めてmigrate
そして適当にコントローラーとビューを書く. ちなみに技評さんの記事を参考にしました.
<!-- apps/views/sessions/new.html.erb --> <% form_for :session, :url => {:action => 'create'} do |f| -%> <%= f.label :openid_url, 'OpenID URL' %> <%= f.text_field :openid_url %> <%= f.submit 'Login' %> <% end -%>
# apps/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create openid_url = params[:session][:openid_url] authenticate_with_open_id openid_url do |result, identity_url, sreg| if result.unsuccessful? # ログインに失敗 return end if User.find_by_identity_url identity_url # ログインに成功し, すでにアカウントがある else # ログインには成功したけど, アカウントを所有していない end end end def destroy # 省略 end end
あとルーティングも設定.
# config/routes.rb ActionController::Routing::Routes.draw do |map| map.resource :session, :only => [:new, :create, :destroy] end
"undefined method 'relative_url_root' ~"の問題
プラグインのOpenIdAuthentication#requested_urlで使われているActionController::Request#relative_url_rootがRailsのバージョンによっては存在しない(たぶんActionController::Baseに移された)ので上記の例外が投げられる.
なのでrelative_url_rootの呼び出しの箇所をActionController::Base.relative_url_rootに置き換えればいいかと思ったけど, なぜかnilを返すので, いっそ取り除いてみる.
def requested_url "#{request.protocol + request.host_with_port + request.path}" end
ひとまずおk, かな?
認証後に"Only post and delete requests are allowed."とか言われる問題
とりあえずOpenIdAuthenticationモジュールのコードの一部を載せる.
# vendor/plugins/open_id_authentication/lib/open_id_authentication.rb protected # 略 def authenticate_with_open_id(identity_url = params[:openid_url], options = {}, &block) #:doc: if params[:open_id_complete].nil? begin_open_id_authentication(normalize_url(identity_url), options, &block) else complete_open_id_authentication(&block) end end private def begin_open_id_authentication(identity_url, options = {}) return_to = options.delete(:return_to) open_id_request = open_id_consumer.begin(identity_url) add_simple_registration_fields(open_id_request, options) redirect_to(open_id_redirect_url(open_id_request, return_to)) rescue OpenID::OpenIDError, Timeout::Error => e logger.error("[OPENID] #{e}") yield Result[:missing], identity_url, nil end def complete_open_id_authentication params_with_path = params.reject { |key, value| request.path_parameters[key] } params_with_path.delete(:format) open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) } identity_url = normalize_url(open_id_response.endpoint.claimed_id) if open_id_response.endpoint.claimed_id case open_id_response.status when OpenID::Consumer::SUCCESS yield Result[:successful], identity_url, OpenID::SReg::Response.from_success_response(open_id_response) when OpenID::Consumer::CANCEL yield Result[:canceled], identity_url, nil when OpenID::Consumer::FAILURE yield Result[:failed], identity_url, nil when OpenID::Consumer::SETUP_NEEDED yield Result[:setup_needed], open_id_response.setup_url, nil end end
createアクションで使っているauthenticate_with_open_idはパラメーターにopen_id_completeがあるかどうかで:
- フォームからOpenID URLが送信された場合(begin_open_id_authenticationの実行)
- OPによって認証され, 戻ってきた場合(complete_open_id_authenticationの実行)
を振り分けている. なのでcreateアクションは上記の2つの場合で実行されることになる.
ただし1回目はフォームからのPOSTメソッドでのアクセスだが, 2回目はOPからのリダイレクトによるGETメソッドでのアクセスなので, resourcesを使っている場合, アウトー*1. リダイレクトされるURL(return_to)はこの場合/session?open_id_complete=1になるので, showアクションがあれば問題ないけど, 同じコード書くのは……
んでちょっといろいろ見ていたら(というより他サイトで試していたら)iKnow!改めsmart.fmが良さげな感じに対処してた. smart.fmのreturn_toは以下の通り.
https://smart.fm/openid?_method=post&open_id_complete=1
なるほどー! 以下のようにOpenIdAuthentication#open_id_redirect_urlを書き換えれば_method=postを追加できるのでやってみる.
def open_id_redirect_url(open_id_request, return_to = nil) open_id_request.return_to_args['_method'] = 'post' open_id_request.return_to_args['open_id_complete'] = '1' open_id_request.redirect_url realm, return_to || requested_url end
でもダメー. なんでだろ?
ひとまずボクなりに解決はしてみた. authenticate_with_open_idを用いてひとつのアクションで対応するのではなく, ふたつのアクションで対応してみた.
# apps/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create # authenticate_with_open_idで行われるnormalize_urlが行われなくなるので, # この時点で使っておく openid_url = normalize_url params[:session][:openid_url] begin_open_id_authentication openid_url do |result, identity_url, sreg| # ログインに失敗 # (begin_open_id_authenticationメソッドに与えたブロックは認証に失敗したとき # にしか呼ばれないので, ブロック実行=認証失敗) end end def complete complete_open_id_authentication do |result, identity_url, sreg| if User.find_by_identity_url identity_url # ログインに成功し, すでにアカウントがある else # ログインには成功したけど, アカウントを所有していない end end rescue => ex render :text => ex.message end def destroy # 略 end end
# config/routes.rb ActionController::Routing::Routes.draw do |map| map.resource :session, :only => [:new, :create, :destroy], :member => {:complete => :get} end
# config/initializers/fix_open_id_authentication.rb require 'open_id_authentication' module OpenIdAuthentication private def open_id_redirect_url(open_id_request, return_to = nil) # return_toをコントローラ名/completeにするので, open_id_complete=1で判断する必要なし # よって取り除いた(たぶんその判断にしか使っていないと思う) open_id_request.redirect_url realm, return_to || url_for(:action => 'complete') end def requested_url "#{request.protocol + request.host_with_port + request.path}" end end
コレで一応OK. 用途の違うブロックをひとつのメソッド(authenticate_with_open_id)に与えないのでこっちの方がいいかも?