RESTful なコントローラーにOpenID認証をくっつける
OpenID自体は、1.POST(consumer) => 2.Redirect-GET(consumer -> provider) => 3.POST(provider) => 4.Redirect-GET(provider -> consumer) となるので、Consumer側は、全然RESTfulになれない罠。とりあえず実装は完了したけど、どうだろう。1 と 4 のURIとその挙動を考えてみる。理想と実装の現実の狭間(笑。
1.POST(consumer) => 2.Redirect-GET(consumer -> provider) => 3.POST(provider) => 4.Redirect-GET(provider -> consumer)
1. は /accounts/current にした。なぜならローカルアカウント(メールアドレスとパスワードが登録されているユーザー) は /accounts/current に自分の認証情報をPOSTして、ログイン済みのユーザーを作成する、という動作にしたので。本当は /accounts/{login_name}/current のほうが正しいと思うけど。
ということで、/accounts/current に user[open_id_uri] をPOSTすると consumer が ID Providerに問い合わせるようになっている。
begin req = consumer.begin(uri) return redirect_to(req.redirect_url(open_id_realm, open_id_return_to)) rescue OpenID::OpenIDError => e flash[:error] = _("OpenID Error #{e.message}") valid = false end
問題はこのときの、open_id_return_to のURLで、とりあえず、/accounts/open_id にしてみた。
def open_id_return_to open_id_accounts_url() # map.resources :accounts, :collection => {:open_id} end
ちなみにconsumerは OpenID::Consumer。
def consumer if @consumer.nil? dir = File.join(RAILS_ROOT, "tmp", "webjourney", "idstore") store = OpenID::Store::Filesystem.new(dir) @consumer = OpenID::Consumer.new(session, store) end @consumer end
1.POST(consumer) => 2.Redirect-GET(consumer -> provider) => 3.POST(provider) => 4.Redirect-GET(provider -> consumer)
ということで、/accounts/open_id のアクションを考えてみる。まず、GET
# Resource for open id users # GET POST PUT DELETE /accounts/open_id # GET : the end point redirected from open id provider # POST : Create a new open id user # PUT : Not Supported. # Delete : Not Supported def open_id case request.method
いきなりだが、PUT/DELETEはスルー。URIは単なるRedirectされるためのエンドポイントとして実装されればいいはず。で、そのGET
when :get uri = params["openid.identity"] raise WebJourney::ForbbidenError.new unless uri res = consumer.complete(params.reject{|k,v|request.path_parameters[k]}, open_id_return_to)
OpenIDのプロトコル上、ID Providerからリダイレクトされるときは openid.identity={認証に使用したOpenID} というのが含まれるらしい。というわけで、これがない場合の動作で迷う。
- リダイレクトじゃないから、consumer.begin が行われていない。つまりconsumerとしてのcurrentが作られていないんだから、NotFoundじゃないのか?
- いや、とりあえず「そんなアクセスだめよ」ってことでForbiddenか。
ここでは後者を採用。で、先に認証が失敗したときをみる。
case res.status when OpenID::Consumer::FAILURE session[:wj_current_user_open_id_idp_authenticated] = false flash[:error] = "OpenID Authentication failed." redirect_to login_system_accounts_url(:_layout => "page") end
りだーいれくと。/accounts/login に戻されることで、ログイン処理に失敗したことにする(4XXを返してもいいんだけど、とりあえずはブラウザ相手ってことで)。次に成功したとき。
when OpenID::Consumer::SUCCESS # see vendor/plugin/webjourney/lib/webjourney/security.rb session[:wj_current_user_open_id_idp_authenticated] = uri @user = do_open_id_login(uri) if @user go_to_current_user_start_page else # try to create user @user = WjOpenIdUser.new(:open_id_uri => uri) @user.login_name = WjOpenIdUser.suggest_login_name(uri) flash[:info] = _("You need to create your profile associated with your OpenID at first time.") # Render create User page end
OpenIDユーザーが登録されていれば*1、通常のログイン処理を行って、ユーザーのホームページにとばす。登録されていなければ、新しくユーザー登録を促すことになる。つまり /accounts/open_id というURIは、GETされたときは、
- 登録済みユーザーにとっては認証処理用エンドポイント
- 未登録ユーザーにとってはOpenIDプロファイル登録作成ページ
ということになってしまう。 そして最大の問題は、
session[:wj_current_user_open_id_idp_authenticated] = uri
つまり、OpenID認証されたことを記憶しておく必要があることだ。これは、POSTのための処理で、
POSTされたときのコードは次の通り。
when :post uri = params[:user][:open_id_uri] if uri == session[:wj_current_user_open_id_idp_authenticated] @user = WjOpenIdUser.new(:open_id_uri => uri) @user.login_name http://d.hatena.ne.jp/images/admin/markup_url.gif= params[:user][:login_name] @user.disable = false @user.wj_role_ids = WjRole.defaults.map(&:id) if @user.save do_open_id_login(@user.open_id_uri) # Goto User start page go_to_current_user_start_page end else flash[:error] = _("You have not authorized by the OpenId provider yet.") redirect_to login_system_accounts_url() end end
つまり、POST時にクライアントから渡される登録用のOpenIDURIと、サーバー側の認証済みOpenIDのURIをマッチさせる必要がある。こうしないと、無制限にOpenIDユーザー作られてしまうし。IDPからRedirectされたときのパラメーターを全部HTML hiddenに埋め込んで、再度POSTするようにしても良さそうな気がするんだけれど。IDP側の設定で「一度だけ認証する」にしてしまうと、もう一度completeを発行することができないのでだめ。
とりとめがなくなってしまったが、OpenIDを使うときに、ステートレスなアクションを作るのは難しいってことで。
とりあえず、ソースは[http://project.webjourney.org/webjourney/trac/browser/trunk/components/system/accounts_controller.rb
=この辺]。