Rails で OpenID を使う (コンシューマ編)
2007-11-24
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 が何をやっているかを調べようっと。