-
entry001353
comments
Ruby on Rails を使い OpenID で認証を行うアプリケーションを作ってみました。 「認証を行う」というのもあいまいな表現ですね。 正確には RP です。 OpenID の諸々はこちら。
Web アプリケーションを OpenID に対応させるための諸々はこちらが参考になります。
今回は既存アプリケーションへの機能追加ではなく、新規に OpenID 対応アプリケーションを作成します。
前提として、以下のパッケージがそろっていることとします。
- Ruby 1.8.7
- RubyGems 1.2.0
- Ruby on Rails 2.1.1
最初にライブラリーをインストールします。
- ruby-openid 2.1.2
- openid_login_generator 0.1
ruby-opendid は Ruby 用の OpenID ライブラリーです。 OpenID 2.0 に対応し、RP だけではなく OP も構築可能です。 これは特定のフレームワークに依存したものではなく、もちろん Ruby on Rails 専用というわけではありません。
Ruby on Rails で ruby-openid を使った認証メカニズムを簡単に構築するためのジェネレーターが openid_login_generator です。 ただ、2008-09-15 時点では多少古くなっているようで、Ruby on Rails 2.x や ruby-openid 2.1.2 に完全には対応していません。 後ほど手で修正を加えます。
この二つのライブラリーを gem でインストールします。
sudo gem install ruby-openid --include-dependencies sudo gem install openid_login_generator --include-dependencies
以上で下準備完了。 ここからアプリケーションの作成に入ります。
まずはお決まりの
rails openid_app cd openid_app
アプリケーション名はお好みでどうぞ。 続いて openid_login_generator で認証メカニズムを生成したいのですが、その前にちょっと修正が必要です。 Ruby on Rails では 2.x でビューのテンプレートの拡張子が変更されました。 しかし openid_login_generator は古い拡張子を指しているのでそのままでは動きません。 生成前にこれを修正しておきます。 修正対象は openid_login_generator.rb です。 ぼくの環境では /usr/local/lib/ruby/gems/1.8/gems/openid_login_generator-0.1 にありました。
*** openid_login_generator.rb.org 2008-09-15 00:41:13.000000000 +0900 --- openid_login_generator.rb 2008-09-15 00:41:28.000000000 +0900 *************** *** 14,20 **** m.template "users.yml", "test/fixtures/users.yml" # Layout and stylesheet. ! m.template "scaffold:layout.rhtml", "app/views/layouts/scaffold.rhtml" m.template "scaffold:style.css", "public/stylesheets/scaffold.css" # Views. --- 14,20 ---- m.template "users.yml", "test/fixtures/users.yml" # Layout and stylesheet. ! m.template "scaffold:layout.html.erb", "app/views/layouts/scaffold.html.erb" m.template "scaffold:style.css", "public/stylesheets/scaffold.css" # Views.この修正が済んだら晴れて生成です。
./script/generate openid_login authn
第二引数はコントローラー名です。 お好みでどうぞ。
ここで生成されたファイル群はそのままでは動かず、次の修正が必要になります。
まずはビューです。
*** app/views/authn/login.rhtml.org 2008-09-15 00:45:46.000000000 +0900 --- app/views/authn/login.rhtml 2008-09-15 00:45:59.000000000 +0900 *************** *** 1,4 **** ! <%= start_form_tag :action=> "login" %> <div title="Account login" id="loginform" class="form"> <h3>Please login</h3> --- 1,4 ---- ! <% form_tag :action=> "login" do %> <div title="Account login" id="loginform" class="form"> <h3>Please login</h3> *************** *** 11,15 **** </div> ! <%= end_form_tag %> --- 11,15 ---- </div> ! <% end %>続いてコントローラーの修正。 これは ruby-openid に同梱されたサンプル "consumer_controller.rb" が参考になります。 主な修正点は次のとおりです。
- ライブラリーの読み込み (require) のお作法
- response.status が無くなったので例外捕捉に
- params, request, session がインスタンス変数からローカル変数に変更
- response.status は健在だがクラスを OpenID::Consumer::SUCCESS に変更
- ストアーのクラスを OpenID::Store::Filesystem に変更
実際の差分はこう。
*** app/controllers/authn_controller.rb.org 2008-09-15 00:43:51.000000000 +0900 --- app/controllers/authn_controller.rb 2008-09-15 06:09:31.000000000 +0900 *************** *** 2,13 **** require "cgi" # load the openid library ! begin ! require "rubygems" ! require_gem "ruby-openid", ">= 1.0.2" ! rescue LoadError ! require "openid" ! end class AuthnController < ApplicationController layout 'scaffold' --- 2,9 ---- require "cgi" # load the openid library ! require "openid" ! require "openid/store/filesystem" class AuthnController < ApplicationController layout 'scaffold' *************** *** 15,27 **** # process the login request, disover the openid server, and # then redirect. def login ! openid_url = @params[:openid_url] ! ! if @request.post? ! request = consumer.begin(openid_url) ! case request.status ! when OpenID::SUCCESS return_to = url_for(:action=> 'complete') trust_root = url_for(:controller=>'') --- 11,21 ---- # process the login request, disover the openid server, and # then redirect. def login ! openid_url = params[:openid_url] ! if request.post? ! begin ! request = consumer.begin(openid_url) return_to = url_for(:action=> 'complete') trust_root = url_for(:controller=>'') *************** *** 29,41 **** redirect_to(url) return ! when OpenID::FAILURE escaped_url = CGI::escape(openid_url) flash[:notice] = "Could not find OpenID server for #{escaped_url}" - - else - flash[:notice] = "An unknown error occured." - end end --- 23,31 ---- redirect_to(url) return ! rescue => e escaped_url = CGI::escape(openid_url) flash[:notice] = "Could not find OpenID server for #{escaped_url}" end end *************** *** 43,54 **** # handle the openid server response def complete ! response = consumer.complete(@params) case response.status ! when OpenID::SUCCESS ! @user = User.get(response.identity_url) # create user object if one does not exist if @user.nil? --- 33,46 ---- # handle the openid server response def complete ! current_url = url_for(:action => 'complete') ! parameters = params.reject{|k,v|request.path_parameters[k]} ! response = consumer.complete(parameters, current_url) case response.status ! when OpenID::Consumer::SUCCESS ! @user = User.find_by_openid_url(response.identity_url) # create user object if one does not exist if @user.nil? *************** *** 58,71 **** # storing both the openid_url and user id in the session for for quick # access to both bits of information. Change as needed. ! @session[:user_id] = @user.id flash[:notice] = "Logged in as #{CGI::escape(response.identity_url)}" redirect_to :action => "welcome" return ! when OpenID::FAILURE if response.identity_url flash[:notice] = "Verification of #{CGI::escape(response.identity_url)} failed." --- 50,63 ---- # storing both the openid_url and user id in the session for for quick # access to both bits of information. Change as needed. ! session[:user_id] = @user.id flash[:notice] = "Logged in as #{CGI::escape(response.identity_url)}" redirect_to :action => "welcome" return ! when OpenID::Consumer::FAILURE if response.identity_url flash[:notice] = "Verification of #{CGI::escape(response.identity_url)} failed." *************** *** 97,105 **** # create the OpenID store for storing associations and nonces, # putting it in your app's db directory store_dir = Pathname.new(RAILS_ROOT).join('db').join('openid-store') ! store = OpenID::FilesystemStore.new(store_dir) ! return OpenID::Consumer.new(@session, store) end # get the logged in user object --- 89,97 ---- # create the OpenID store for storing associations and nonces, # putting it in your app's db directory store_dir = Pathname.new(RAILS_ROOT).join('db').join('openid-store') ! store = OpenID::Store::Filesystem.new(store_dir) ! return OpenID::Consumer.new(session, store) end # get the logged in user object最後にユーザー情報を扱うための model を作りましょう。 本来は一人のユーザーが複数の Identifier を持つことができるように、1:n の関係にしなければならないのですが、今回はシンプルに、ユーザー名とそれに紐付くひとつの Identifier とします。 model 直接ではなくて scaffold で生成しました。
./script/generate scaffold user name:string openid_url:string
スキーマを作成します。
rake db:migrate
以上で完了です。 動作を確認してみましょう。 まずはサーバーの起動。
./script/server
ログイン・ページにアクセスしましょう。
- http://example.com:3000/authn/login
認証ダイアログに OpenID の Identifier を入力します。 今回は、はてなの OpenID を使ってみます。
"Login" ボタンをクリックすると、はてなのログイン画面に遷移します。 はてなのユーザーIDとパスワードを使ってログインします。
外部サイトへのログイン許可を求められます。 URI を確認し、当該 ID でのログインを許可してもよければ "今回のみ許可"、"常に許可" をクリックします。
ログイン成功画面が表示されます。
既に はてな にログインしていた場合は はてな へのログイン画面は表示されず、既に許可したアプリケーションの場合は外部サイトへのログイン許可画面は表示されません。 もっともシンプルな場合は、(見かけ上) はてなのページは一度も表示されません。
ログインに成功して以降はアプリケーション側でセッションを維持します。
この仕組みの最大の利点は、アプリケーション側でユーザーのパスワードを保持する必要がない点です。 ユーザー側はあちこちのアプリケーションにパスワードを預けなくても良いので、管理の手間、漏洩のリスクを低減することができます。 アプリケーション側もパスワードを管理するコストを削減することができます (ユーザー管理テーブルは (:name, :openid_url) でしたよね)。
次は拡張属性 SREG を試してみます。
MTEntryMore