いますぐ使えるOpenID

第5回Railsで作るOpenID対応アプリケーション実践(後編)

第4回では、 RailsのOpenID Authenticationプラグインを使って、OpenIDでログインとユーザ登録ができるアプリケーションを作成しました。しかし、現状ではOpenIDの認知率が10%強ということもあり、すべての人が OpenID を使える訳ではありません(参考: 【OpenIDに関する調査】OpenIDの利用率はわずか1.2%/認知率は12%⁠。

そのため、 OpenID認証だけでなくパスワードによる認証も併用したい場合もあります。そこで前回作成したアプリケーションを改造し、OpenID Authenticationプラグインとパスワード認証を行うRESTful Authenticationプラグインを併用して、利用者がOpenID認証とパスワード認証の両方から選べるようにします。

また、最後にはOpenIDでログインできるOPを制限する、ホワイトリスト方式を実装します。

パスワードによる認証機能を提供するRESTful Authentication

第4回でも軽くご紹介しましたが、 RESTful Authentication はパスワードによる認証機能を提供する Rails プラグインです。これまでによく使われていたacts_as_authenticatedプラグインの後継で、Rails 2.0の認証プラグインの標準となっています。今回は説明しませんが、RESTful Authenticationプラグインはパスワードによる認証に加えて、以下のような機能を持っています。

  • salt付きハッシュによるパスワードの暗号化(パスワードを復号できないので、正確には暗号化ではなくハッシュ化です)
  • メールアドレスを用いたユーザのアクティベーション
  • Cookieを用いた「次回から自動的にログイン」機能

RESTful Authenticationプラグインは、以下のコマンドでインストールできます。

$ ./script/plugin install restful_authentication

プラグインをインストールすると、以下のgenerateコマンドでUserモデルとSessionsコントローラのひな形を生成できます。ただし、UserモデルもSessionsモデルも第4回で作成しているので、generateコマンドでそのまま上書きするのではなく、バックアップをとった上で両者のコードをマージしました。

$ ./script/generate authenticated user sessions

generateコマンドを実行すると、認証に必要なライブラリを集めたlib/authentication_system.rbというファイルも生成されます。

パスワード認証機能の組み込み

第4回で作成した画面遷移図を、パスワード認証と併用できるように修正したものが図1になります。

図1 パスワード認証に対応した画面遷移図
図1 パスワード認証に対応した画面遷移図

色がついている画面や矢印が、パスワード認証と併用するために修正を加える箇所になります。修正しない箇所はグレーや点線の矢印で表しています。パスワード認証によるログインの流れは、以下のようになります。

  • (1)利用者がIDとパスワードを入力し、ログインボタンをクリックします
  • (2)利用者から送信されたパラメータを解析し、OpenID認証とパスワード認証のどちらであるかを判定します
  • (2-A)パスワード認証であればデータベースに登録したパスワードと照合し、一致すればログイン成功とみなしサービス画面を表示します
  • (2-B)パスワードが一致しなければログイン画面にエラーメッセージを表示します
  • (2-C)OpenID認証であれば外部のOpenID Providerへ利用者を誘導します(従来の機能)

また、パスワード認証を利用するユーザのためのログイン画面も追加しています。修正箇所の一覧を表1に示します。

表1 修正箇所と修正内容の一覧
コントローラアクション修正内容
SessionsnewIDとパスワードの入力フォームを追加
Sessionscreateパスワード認証を利用するユーザの認証処理を追加
Usersnewパスワード認証を利用するユーザの登録フォーム画面を追加
Userscreateパスワード認証を利用するユーザの登録処理を追加

ログイン処理の修正(Sessionsコントローラ)

まずはログイン画面から修正します。これまでのログイン画面はOpenIDのアカウント名を入れるフォームが存在するだけでした。ここに、IDとパスワードを入力するためのフォームを追加します。

図2 ログイン画面のスクリーンショット
図2 ログイン画面のスクリーンショット

OpenIDでのログインとパスワードでのログインのどちらであっても、フォームに入力したデータはSessionsコントローラのcreateアクションが受け取ります。createアクションはどちらのデータも受け取れるようにします。具体的な修正方法は、OpenID AuthenticationプラグインのREADMEが参考になります。修正後のcreateアクションは以下のようになります。

  def create
    if using_open_id?
      # OpenID による認証
      open_id_authentication
    else
      # パスワードによる認証
      password_authentication
    end
  end

using_open_id?はOpenID Authenticationプラグインが提供するメソッドです。クライアントから送られるクエリストリングやフォームのデータから、OpenID認証のリクエストかどうかを判定します。OpenID認証のリクエストであれば、open_id_authenticationメソッドを呼び出し、そうでなければpassword_authenticationメソッドを呼び出します。それぞれのメソッドはprotectedで宣言し、外部からは直接呼ばれないようにします。

図3 createアクションによる振り分け処理
図3 createアクションによる振り分け処理

open_id_authenticationメソッドは第4回のcreateアクションの処理と同じです。認証に成功してユーザが登録されていればサービス画面に遷移し、登録されていなければユーザ登録画面に遷移します。

  def open_id_authentication
    # OpenID でユーザを認証する (begin, completeの両方に対応)
    authenticate_with_open_id do |result, identity_url|
      # 第4回と同じコードのため省略
    end
  end

password_authenticationメソッドは、RESTful Authenticationプラグインが生成するコードを流用し、以下のように作成しました。

  def password_authentication
    # ID とパスワードでユーザを認証する
    self.current_user = User.authenticate(params[:login], params[:password])
    if logged_in?
      # サービス画面へ遷移する
      successful_login
    else
      failed_login 'ユーザIDまたはパスワードが異なっています'
    end
  end

ちなみに、認証後の処理はこれまでと同様です。

ユーザ登録処理の修正(Usersコントローラ)

パスワード認証の利用者とOpenID認証の利用者では、ユーザ登録に必要な情報も異なります。

  • パスワード認証 … ユーザが入力したパスワードを登録する
  • OpenID認証 … OpenID認証によって確認できたOpenIDアカウント名(User-Supplied Identifier)を登録する

パスワード認証の場合のユーザ登録画面は、以下のようになります。ユーザID, ニックネーム, パスワードの入力を要求します。メールアドレスによるアクティベーションを行う場合は、メールアドレスの入力も必要になるでしょう。

図4 パスワード認証利用時のユーザ登録画面
図4 パスワード認証利用時のユーザ登録画面

OpenID認証の場合のユーザ登録画面は、以下のようになります。こちらはパスワードを入力せずに、ユーザのOpenIDアカウント名を登録します。

図5 OpenID認証利用時のユーザ登録画面
図5 OpenID認証利用時のユーザ登録画面

ホワイトリストを使ってログインを許可するOPを制限する

連載の始めに、OpenIDの特徴は認証サーバが分散していることと言及しました。特にOpenIDはX.509における認証局のような特定の権威を持たない仕組みであるため、誰もがOP(OpenID Provider)になることができます。極端に言えば、OpenIDライブラリに付属するOPのサンプルを起動して、それでログインすることもできるのです。そのため、OPをどうやって信頼するか、というreputation(評価)問題があります。

参考:
OpenID Providerのreputation問題、AOLの方針など - Yet Another Hackadelic

この問題は、RPがOPにどこまでのレベルを期待するかによって、対策が変わってくるように思います。単に、利用者の同一性を確認できればいいのであれば、OPが信頼できるかどうかはあまり重要ではないでしょう。利用者が怪しいOPを使って第三者になりすまされたとしても、それは利用者の責任、ということになります。一方で、botによるユーザ登録を防ぎたいという場合は、それなりに信頼できるOPを選定するか、さもなければRPが自前でbot防止の仕組みを用意することになります。サービスがコミュニティサイトの一面を持つのであれば、後者の場合が多いのではないでしょうか。

OPを識別する方法

今回はOPを制限する一つの方法として、先ほど紹介した参考サイトでも述べられているホワイトリストを用いたログインを実装します。ホワイトリストとは、あらかじめRPが信頼するOPを決めておき、そのOP経由のログインのみを許可する方式です。OpenIDの誰もが自由に使えるIDという理念からは少し外れますが、OPを客観的に審査する方法が無い現状では、ホワイトリストは現実的な解の一つかもしれません。

さて、ホワイトリストを実装するにあたって、ログイン時にOPをどうやって識別すればよいでしょうか。すぐに思いつくのは、利用者のOpenIDアカウント名(User-Claimed Identifier)に含まれているドメイン名で判定することですが、これはあまりいい方法ではありません。なぜなら、OpenIDアカウント名とOPのドメインは必ずしも一致するとは限らないからです。例えば連載の第1回で紹介したyahoo.comの例では、RPへ通知するIDをユーザが選択できました。選択できるURLのドメインは、yahoo.comだけでなくYahoo!が運営する写真共有サイトであるflickr.comも含まれます。つまりOPのドメインがyahoo.comでも、利用者のOpenIDアカウント名のドメインはflickr.comの場合もあるということです。そこでOPの識別にはUser-Claimed Identifierではなく、OP Endpoint URLを用います。OP Endpoint URLは、OpenIDでのログイン時にOPが認証要求を受け付けるURLです。

図6 OP Endpoint URLとUser-Claimed Identifier
図6 OP Endpoint URLとUser-Claimed Identifier

ちなみに、OpenIDアカウント名からOP Endpoint URLを見つける方法については、連載第3回でご紹介したdiscoveryをご覧ください。以下のように、yahoo.comとflickr.comのどちらでログインする場合でも、OP Endpoint URLは以下のように同じURLとなります。

$ ./discover flickr.com
==================================================
Running discovery on flickr.com
 Claimed identifier: http://flickr.com/
 Discovered services:
  1.
     Server URL  : https://open.login.yahooapis.com/openid/op/auth
     Type URIs:
       * http://specs.openid.net/auth/2.0/server
       * http://specs.openid.net/extensions/pape/1.0

$ ./discover yahoo.com
==================================================
Running discovery on yahoo.com
 Claimed identifier: http://www.yahoo.com/
 Discovered services:
  1.
     Server URL  : https://open.login.yahooapis.com/openid/op/auth
     Type URIs:
       * http://specs.openid.net/auth/2.0/server
       * http://specs.openid.net/extensions/pape/1.0

ログイン処理の改造

ホワイトリストを管理するために、TrustedServerというモデルを追加します。TrustedServerにログインを許可するOPのEndpoint URLを登録しておき、ログイン時にホワイトリストに含まれるかどうかをチェックします。Sessionsコントローラのcreateアクションから呼ばれる、open_id_authenticationメソッドを以下のように修正します。先頭の + が追加した行で、 - が削除した行です。

@@ -25,9 +25,13 @@
   protected
   def open_id_authentication
     # OpenID でユーザを認証する (begin, completeの両方に対応)
-    authenticate_with_open_id do |result, identity_url|
+    authenticate_with_open_id do |result, identity_url, sreg, open_id_response|
       if result.successful?
-        if @current_user = User.find_by_identity_url(identity_url)
+        server_url = open_id_response.endpoint.server_url
+        if not TrustedServer.find_by_url(server_url)
+          # 信頼していないOPでのログインは拒否する
+          failed_login "Sorry, untrusted OpenID server: #{server_url}"
+        elsif @current_user = User.find_by_identity_url(identity_url)
           # 認証成功でユーザが登録済みの場合はログイン成功 (A)
           successful_login
         else

OpenIDレスポンスのOP Endpoint URLがTrustedServerに含まれていなければ、エラーメッセージを表示してログインを拒否します。

図7 信頼しないOPでログインした時のスクリーンショット
図7 信頼しないOPでログインした時のスクリーンショット

open_id_responseはruby-openidライブラリが提供するオブジェクトで、OpenIDレスポンスのデータを管理します。このオブジェクトのendpoint.server_urlを呼び出すことで、OPのEndpoint URLを取得できます。RailsのOpenID Authenticationプラグインは、標準ではOpenIDレスポンス(open_id_response)をコントローラへと渡さないため、下記のように引数にopen_id_responseを加えています。

@@ -106,13 +106,13 @@
 
       case open_id_response.status
       when OpenID::Consumer::SUCCESS
-        yield Result[:successful], identity_url, OpenID::SReg::Response.from_success_response(open_id_response)
+        yield Result[:successful], identity_url, OpenID::SReg::Response.from_success_response(open_id_response), open_id_response
       when OpenID::Consumer::CANCEL
-        yield Result[:canceled], identity_url, nil
+        yield Result[:canceled], identity_url, nil, open_id_response
       when OpenID::Consumer::FAILURE
-        yield Result[:failed], identity_url, nil
+        yield Result[:failed], identity_url, nil, open_id_response
       when OpenID::Consumer::SETUP_NEEDED
-        yield Result[:setup_needed], open_id_response.setup_url, nil
+        yield Result[:setup_needed], open_id_response.setup_url, nil, open_id_response
       end
     end

なお、実際の運用ではログイン時にリストボックスなどで許可するOpenIDの一覧を選択できるようにしておいた方がよいでしょう。

まとめ

今回はパスワード認証との併用、ホワイトリストによるOPの制限方法をご説明しました。RPによっては、OpenIDを使う場合にいろいろと検討することがあります。次回は、これまでの連載のまとめとしてOpenIDの課題や使いどころについて整理したいと思います。

おすすめ記事

記事・ニュース一覧