ククログ

株式会社クリアコード > ククログ > Rails 3.0 beta4でDeviseを使ってOpenID認証

Rails 3.0 beta4でDeviseを使ってOpenID認証

とあるRails 3を使っているたいやき用のCMSでDeviseを使ってOpenID認証をするようにしたので、そのやり方を紹介します。RubyはRuby 1.9.2 RC2も出ていますが、今回はRuby 1.9.1を使います。

Deviseとは

DeviseRackベースの認証システムです。バックエンドにWardenを利用しているため、Basic認証やOpenID、OAuthなど認証方法を切り替えることができます。

ただ、以下の説明を読んでみてもらってもわかる通り、動き出すまでにそこそこの作業が必要になります。機能は豊富なので、動き出したらカスタマイズしてアプリケーションの要求に合わせていくことができるでしょう。日本語での情報もあまりありませんが、探せばいくつかはあるので、試してみてはいかがでしょうか。

とはいえ、今回はDeviseのデフォルトの認証方法ではなく、OpenIDでのみ認証することにします。また、未登録のユーザがログインしようとしたときは自動的に新規ユーザを作成することにします。このようにも使えるという例ということで読むとよいかもしれません。

インストール

まず、Rails 3.0 beta4をインストールします。

% sudo gem1.9.1 install rails --pre

サンプル用のアプリケーションを作ります。

% ruby1.9.1 rails new taiyaki
% cd taiyaki

次にDeviseをインストールします。

% sudo gem1.9.1 install devise --version=1.1.rc2

インストールしたDeviseを利用するため、以下のようにGemfileに追記します。

Gemfile:

gem 'devise', "1.1.rc2"

アプリケーションにDeviseが動作するために必要なファイルをインストールします。config/initializers/devise.rbなど主に設定ファイルです。

% ruby1.9.1 script/rails generate devise:install

いくつかは手動で設定する必要があります。それぞれ以下の通りです。

Deviseはパスワードの再設定をする機能もあり、そのときはユーザにメールを送信します。そのような機能を使うときはActionMailerのURL生成オプションを設定する必要があります。例えば、開発時のホスト情報を設定する場合は以下のようになります。

config/environments/development.rb:

config.action_mailer.default_url_options = {:host => 'localhost:3000'}

Deviseはリダイレクト先のURLを生成するときなどにデフォルトではroot_pathを使うので、rootパスへのマッピングを追加します。以下の例ではwelcome#indexを指定しているので、後でWelcomeControllerを作ります。

config/routes.rb:

root :to => "welcome#index"

Deviseはnoticeとalertのflashを設定するので、レイアウトに追加しておくとよいでしょう。例えば、以下のようにyieldの前に追加します。

app/views/layouts/application.html.erb:

<%# ... %>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

<%= yield %>
<%# ... %>

作成

これでインストールは完了したので、コントローラーやモデルを作成します。

まず、ユーザー用のモデルを作成します。

% ruby1.9.1 script/rails generate devise User

このとき生成されるスキーマは、以下のようにデータベース上にパスワードのダイジェストなどの情報を持ち、それを利用して認証することになります。

db/migrate/XXXX_devise_create_users.rb:

class DeviseCreateUsers < ActiveRecord::Migration
  def self.up
    create_table(:users) do |t|
      t.database_authenticatable :null => false
      t.recoverable
      t.rememberable
      t.trackable

      # t.confirmable
      # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both

      # t.token_authenticatable

      t.timestamps
    end

    add_index :users, :email,                :unique => true
    add_index :users, :reset_password_token, :unique => true
    # add_index :users, :confirmation_token,   :unique => true
    # add_index :users, :unlock_token,         :unique => true
  end

  def self.down
    drop_table :users
  end
end

しかし、今回は自分では認証情報を持たずにOpenIDで認証するので、データベース上に認証情報を持たないようにします。代わりにOpenID用のカラムを追加します。

db/migrate/XXXX_devise_create_users.rb(変更後):

class DeviseCreateUsers < ActiveRecord::Migration
  def self.up
    create_table(:users) do |t|
      t.string :email
      t.string :nickname
      t.string :identity_url
      t.string :fullname
      t.string :birth_date
      t.integer :gender
      t.string :postcode
      t.string :country
      t.string :language
      t.string :timezone

      t.rememberable
      t.trackable

      t.confirmable
      t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
      t.token_authenticatable

      t.timestamps
    end

    add_index :users, :identity_url,         :unique => true
    add_index :users, :email,                :unique => true
    add_index :users, :confirmation_token,   :unique => true
    add_index :users, :unlock_token,         :unique => true
  end

  def self.down
    drop_table :users
  end
end

変更したらスキーマを反映させます。

% rake1.9.1 db:migrate

モデルのコードにもデータベースで認証するためのコードが入っています。今回は必要のないユーザ登録用の:registerableオプションやパスワードの入力チェックなどをする:validatableオプションなどは外します。password_required?メソッドをオーバーライドしているのは、OpenIDで認証するためパスワードが必要がないからです。

app/models/user.rb:

class User < ActiveRecord::Base
  ...
  # devise :database_authenticatable, :registerable,
  #        :recoverable, :rememberable, :trackable, :validatable
  devise :database_authenticatable, :rememberable, :trackable
  ...

  def password_required?
    false
  end
end

Deviseではログイン画面などデフォルトのビューも提供してくれますが、今回はOpenIDを使った認証にするためビューをカスタマイズします。ビューをカスタマイズする場合は、コントローラーごとカスタマイズする方法と、ビューだけカスタマイズする方法がありますが、今回はコントローラーごとカスタマイズする方法にします。

コントローラーをカスタマイズするにはconfig/routes.rbに追加されたdevise_for:controllersオプションを指定します。以下のように指定するとUsers::SessionsControllerコントローラーを使います。

config/routes.rb:

devise_for(:users,
           :controllers => {:sessions => "users/sessions"})

コントローラーを作成します。

% ruby1.9.1 script/rails generate controller Users::Sessions

コントローラーをカスタマイズする場合は、ApplicationControllerではなくDevise::SessionsControllerを継承します。

app/controllers/users/sessions_controller.rb:

class Users::SessionsController < Devise::SessionsController
end

ログインフォームではOpenID用の識別子を入力してもらうようにします。

app/views/users/sessions/new.html.erb:

<%= form_for(resource,
             :as => resource_name,
             :url => session_path(resource_name)) do |f| %>
  <p>
    <label for="openid_identifier" >OpenID URL:</label>
    <%= text_field_tag :openid_identifier %>
  </p>
  <p><%= f.label :remember_me %> <%= f.check_box :remember_me %></p>
  <p><%= f.submit "Login" %></p>
<% end %>

あとは、トップページを準備すれば画面を確認することができます。

トップページ用のコントローラーを生成します。

% ruby1.9.1 script/rails generate controller welcome index
% rm public/index.html

トップページではログインページに移動できるようにします。ログイン時はログイン中のユーザ情報を表示します。

app/views/welcome/index.html.erb:

<h1>Welcome#index</h1>

<% if user_signed_in? %>
  <p>ようこそ<%= current_user.nickname %>さん</p>
  <%= link_to("ログアウト", destroy_user_session_path) %>
<% else %>
  <%= link_to("ログイン", new_user_session_path) %>
<% end %>

サーバを起動します。

% ruby1.9.1 script/rails server

http://localhost:3000/にアクセスすると以下のような画面になります。

トップページ

ログインページに行くと以下のようなフォームになります。

ログインフォーム

OpenID対応

それではOpenIDに対応します。DeviseからOpenIDを使うために、warden-openidを使います。

% sudo gem1.9.1 install warden-openid

Gemfileにも追記します。

Gemfile:

gem 'warden-openid'

WargenでOpenIDを使うようにします。

config/initializers/devise.rb:

Devise.setup do |config|
  ...
  config.warden do |manager|
    manager.default_strategies(:openid, :scope => :user)
  end
end

OpenIDの設定をします。warden-openidではOpenIDの認証が成功した時にコールバックが実行され、そこで認証情報に対応したアプリケーション用のユーザを返すことになります。今回は、ここで、ユーザが存在しない場合は自動的に新規ユーザを作成することにします。

config/initializers/openid.rb:

Rails.application.config.middleware.insert(Warden::Manager, Rack::OpenID)

Warden::OpenID.configure do |config|
  config.required_fields = User.required_open_id_fields
  config.optional_fields = User.optional_open_id_fields
  config.user_finder do |response|
    user = User.find_by_identity_url(response.identity_url)
    if user.nil?
      user = User.new
      user.extract_open_id_values(response)
      unless user.save
        message = "failed to create user: "
        message << "#{users.errors.full_messages.inspect}: "
        message << user.inspect
        Rails.logger.error(message)
        user = nil
      end
    end
    user
  end
end

OpenIDの情報とアプリケーションのユーザ情報をマッピングする処理はモデルで行います。

app/models/user.rb:

class User < ActiveRecord::Base
  REQUIRED_FIELDS = {
    :nickname => "nickname",
  }

  OPTIONAL_FIELDS = {
    :email => "email",
    :fullname => "fullname",
    :birth_date => "dob",
    :gender => "gender",
    :postcode => "postcode",
    :country => "country",
    :language => "language",
    :timezone => "timezone"
  }

  class << self
    def required_open_id_fields
      REQUIRED_FIELDS.values
    end

    def optional_open_id_fields
      OPTIONAL_FIELDS.values
    end
  end

  def password_required?
    false
  end

  def extract_open_id_values(response)
    profile_data = {}
    [OpenID::SReg::Response, OpenID::AX::FetchResponse].each do |response_class|
      data_response = response_class.from_success_response(response)
      profile_data.merge!(data_response.data) if data_response
    end
    [REQUIRED_FIELDS, OPTIONAL_FIELDS].each do |fields|
      fields.each do |model_key, profile_key|
        unless profile_data[profile_key].blank?
          self.send("#{model_key}=", profile_data[profile_key])
        end
      end
    end
    self.identity_url = response.identity_url
    self.nickname ||= identity_url
  end
end

一応、ニックネーム情報は欲しいとリクエストしますが、もらえなくてもなんとなく動くようになっています。この状態でログインページにOpenID識別子を入力して、認証に成功するとアプリケーションにログインする事ができます。

ログイン成功

ただし、現在リリースされているruby-openidはRuby 1.9のEncodingに対応していないため、認証中にASCII以外の文字列を含むページにアクセスすることになると失敗します。これを修正する方法はRuby 1.9.1 supportで報告済みですが、まだ取り込まれていません。

まとめ

Rails 3.0 beta4でDeviseを使ってOpenID認証する方法を紹介しました。Rails 3で認証まわりはどうしようか、と考えていている人は試してみるとよいかもしれません。ただ、betaやrcのものを使っているので、これから使い方は変わっていく可能性が高いと考えられます。注意してください。

そういえば、トップページにある会社紹介資料PDFを更新しました。エンジニア紹介ページなどが更新されています。