at posts/single.html

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

OpenID comsumer (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 に格納されるんだろう。たぶん。

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

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

OpenID server (login)

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

OpenID server (permit)

ログイン結果の受け取り

はてなから 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 のアカウント名が(なぜかパーセントエンコーディングされて)表示される。

OpenID comsumer (complete)

ここまでのまとめ

説明するとややこしく感じるかもしれないけど、そんなことない。 エラー処理を除けば、実質的にやっていることといえば、以下の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 が何をやっているかを調べようっと。

関連する日記