■ Rails で OpenID を使う (コンシューマ編)

OpenID の中身 (特に Ruby での実装) を調べるために、まずは OpenID のコンシューマを Rails で動かしてみることにした。

インストール

もうみんなOpenIDにを参考に ruby-openid と openid_login_generator を入れる。

$ sudo gem install ruby-openid $ sudo gem install openid_login_generator

バージョンはこんな感じ。（関係あるところだけ抜粋）

$ gem list --local openid_login_generator (0.1) [Rails] OpenID Login generator. rails (1.2.5) Web-application framework with template engine, control-flow layer, and ORM. ruby-openid (1.1.4) A library for consuming and serving OpenID identities. ruby-yadis (0.3.4) A library for performing Yadis service discovery

ジェネレータで OpenID コンシューマを作成

Rails アプリの作成。データベースには SQLite3 を使うことにした。

$ rails -d sqlite3 consumer $ cd consumer

generate openid_login を使って OpenID コンシューマのひな形を作る。 まずは使い方を調べる。

$ ./script/generate openid_login --help SYNOPSIS openid_login [Controller name] Good names are Account Myaccount Security DESCRIPTION This generator creates a general purpose login system. Included: - a User model which stores OpenID authenticated users - a Controller with login, welcome and logoff actions - a mixin which lets you easily add advanced authentication features to your abstract base controller EXAMPLE ./script/generate openid_login Account

引数にコントローラ名を渡せばいいのか。 「Account Myaccount Securityみたいな名前にするといいよ」だって。 EXAMPLE の通りに作ってみる。

$ ./script/generate openid_login Account create lib/openid_login_system.rb create app/controllers/account_controller.rb create app/helpers/account_helper.rb create app/models/user.rb create app/views/layouts/scaffold.rhtml create public/stylesheets/scaffold.css create app/views/account create app/views/account/welcome.rhtml create app/views/account/login.rhtml create app/views/account/logout.rhtml create README_LOGIN

model に user.rb, controller に account_controller.rb が生成された。 view は welcome, login, logout の3つ。 lib に openid_login_system.rb が出来ているけど、これは後で読もう。

まず README_LOGIN を簡単に読んでみる。 データベースに users テーブルを作れって書いてある。 それから、認証を必要とするコントローラには before_filter :login_required を使えと。 これは OpenID に限らず一般的な話か。

とりあえず users テーブルを作る。 ジェネレータを使ってマイグレーションファイルを用意する。 OpenID ログインジェネレータが生成した user.rb を上書きしないように注意。

$ script/generate model user openid_url:string exists app/models/ exists test/unit/ exists test/fixtures/ overwrite app/models/user.rb? [Ynaqd] n skip app/models/user.rb overwrite test/unit/user_test.rb? [Ynaqd] n skip test/unit/user_test.rb overwrite test/fixtures/users.yml? [Ynaqd] n skip test/fixtures/users.yml create db/migrate create db/migrate/001_create_users.rb

rake コマンドで users テーブルを作成。 カラムは id と openid_url の2つ。

$ rake db:migrate (in /home/machu/work/misc/openid/consumer) == CreateUsers: migrating ===================================================== -- create_table(:users) -> 0.0039s == CreateUsers: migrated (0.0051s) ============================================

最後にサーバを起動、と。

$ script/server

使ってみる

本当はアプリ本体のコントローラとかも作るんだけど、とりあえずログイン部分だけを使ってみる。 まずは、ジェネレータが作った account コントローラのメソッドを調査。

$ grep -E '^\s*(class|def|module)' app/controllers/account_controller.rb class AccountController < ApplicationController def login def complete def logout def welcome def consumer def find_user

consumer と find_user はプライベートなメソッドなので、アクションは login, complete, logout, welcome の4つか。

ログイン画面 (loginアクション)

Web ブラウザから login アクションを呼び出す。

http://axela.machu.jp:3000/account/login

account_controller.rb の中身はこんな感じ。 POST じゃない場合（最初に呼び出した場合）は何もせずに login.rhtml を呼び出しているだけ。

def login openid_url = @params[:openid_url] if @request.post? # 中略 end end

ログイン画面で OpenID のアカウント名 (URL) を入力して Login ボタンを押す。 今回は試しに、はてなのOpenIDアカウントを使ってみた。 今度は POST リクエストが送られるので、 login メソッドの中略部分が実行される。 return_to に complete アクションを指定している。 これは、はてなでのログイン後には complete アクションに帰ってきてねってこと。

request = consumer.begin(openid_url) case request.status when OpenID::SUCCESS return_to = url_for(:action=> 'complete') trust_root = url_for(:controller=>'') url = request.redirect_url(trust_root, return_to) redirect_to(url) return when OpenID::FAILURE # 略 else # 略 end

consumer.begin がたぶんポイント。 この時点でコンシューマ (Rails アプリ) がはてなのサーバにアクセスして、鍵交換などをやっているはず。 はてなサーバとの通信に成功すれば (OpenID::SUCCESSが返ってくれば) はてなのログイン画面へWebブラウザをリダイレクトさせている。 リダイレクト先は OpenID ライブラリの request.redirect_url で生成している。

はてなサーバとの鍵交換のデータは、 db/openid-store に保存されてるっぽい。 associations に鍵交換情報。nonces にノンスが入ってる。 ノンスについてはOpenIDをとりまくセキュリティ上の脅威とその対策 − ＠ITが参考になる。

$ ls db/openid-store/** db/openid-store/associations: http-www.hatena.ne.jp_2Fopenid_2Fserver-BOrhJFCxctkhbPb5ULxJlm_h88w db/openid-store/nonces: YHQYIZtx db/openid-store/temp:

associations に入っているデータは以下の通り。 はてなサーバとの通信用の HMAC キーなどが入ってる。

version: 2 handle: 1195960051:uB4vs2DTjZ8i7Wlog16l:29256cf179 secret: 5uUyv35I/eibJD+KJfPwdp8mzR8= issued: 1195960089 lifetime: 1198349 assoc_type: HMAC-SHA1

もうみんなOpenIDにに書かれている active_record_openid_store を使えば、この情報が ActiveRecord に格納されるんだろう。たぶん。

ログイン処理 (はてなサーバ)

はてなサーバにリダイレクトされたあとは、はてなにログインしていなければログイン画面が表示される。

ログイン後に、IDを教えてもいいかどうかの確認画面が表示される。 この辺ははてなの認証APIと同じような感じ。 とりあえず「今回のみ許可」を選ぶ。

ログイン結果の受け取り

はてなから Rails 側へと戻ってくる。 login アクションで指定したとおり、 complete アクションが呼ばれる。 ポイントは consumer.complete メソッド。このメソッド内でログインに成功したかどうかを確認している。 ログインに成功していれば (OpenID::SUCCESSが返っていれば) 、データベースにユーザを追加し、セッションにユーザIDを格納している。 その後、 welcome アクションへと転送する。

def complete response = consumer.complete(@params) case response.status when OpenID::SUCCESS @user = User.get(response.identity_url) if @user.nil? @user = User.new(:openid_url => response.identity_url) @user.save end @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 # 略 else # 略 end when OpenID::CANCEL # 略 else # 略 end redirect_to :action => 'login' end

この時点で DB にユーザIDが格納される。

$ sqlite3 db/development.sqlite3 sqlite> select * from users; 1|http://www.hatena.ne.jp/kmachu/

welcome アクションは何もやっていない。welcome.rhtmlが呼ばれるだけ。 OpenID のアカウント名が（なぜかパーセントエンコーディングされて）表示される。

ここまでのまとめ

説明するとややこしく感じるかもしれないけど、そんなことない。 エラー処理を除けば、実質的にやっていることといえば、以下の2つだけだから。

(1) ログイン画面の呼び出し

ユーザが入力した OpenID アカウントを元に認証サーバへと誘導する。

request = consumer.begin(openid_url) url = request.redirect_url(trust_root, return_to) redirect_to(url)

(2) ログイン結果の受け取り

認証サーバでのログイン結果を受け取る。

response = consumer.complete(@params) # response.identity_url でユーザIDを取得できる

と言うわけで、次は OpenID ライブラリの begin, redirect_url, complete が何をやっているかを調べようっと。