Python で OpenID のサンプルサーバーを動かす。(その2)
前回は、OpenIDの仕様と、python-openidのサンプルサーバを立ち上げるまで を説明しました。今回はサンプルのコードを見ながら実際のコーディングの雰 囲気に慣れたいと思います。
仕組みなどに興味なく OpenIDの認証だけ使いたい場合は、Webフレームワーク
の Pylons で AuthKit を使うと、非常に簡単にアプリケーションに OpenID
の認証を実装させることができます。そのほか、Plone や TurboGears,
Django なども対応しているようです。Webフレームワークを使っていて、とり
あえず使いたい方は参照してみてください。
Python Package Indexで検索
django-openid
RP(コンシューマ)用ライブラリの概要
モジュール openid.consumer.consumer は Relying Parties (別名
Consumers) をサポートします。
RPでは次のことを行います。
- ユーザーがコンシューマサイトでIDを入力してログインします
- discover:コンシューマサイトはYadis プロトコルを使ってプロバイダを見つ ます。
- コンシューマサイトはブラウザをプロバイダにリダイレクトします。
- プロバイダサイトは認証要求に対する応答をつけてブラウザをコンシューマサ イトに戻します。
SESSIONS, STORES, AND STATELESS MODE
consumer は2つのタイプの状態を保持します。
-
ユーザーの現在の認証の試行状態:
見つけられたエンドポイントのURLの リスト,到達できないエンドポイント,すでに試行したエンドポイント。この状 態はConsumer.begin()から Consumer.complete()までの間維持される必要が あります。 -
サーバーとの関係の状態:
サーバとの共有秘密(associations)や一時的な 署名されたメッセージがあります。この情報はあるセッションから次のセッショ ンまで持続し、異常なユーザーエージェントに接続しないようにしなければな りません。
ステートレスモードでは、動作が遅 くなりプロバイダの負荷が増えますが、リプレイ攻撃に対しては安全になりま す。
storeの保持先としては、通常のファイルやSQLデータベースを利用 することができます。 IMMEDIATE MODE
上記のフローでは、プロバイダーはユーザーに対してコンシューマサイトに戻
る前に、IDを確認するページを表示する必要があります。すぐに応答を得たい
場合には、コンシューマサイトはimmediate modeでライブラリを利用すること
ができます。
実際のコーディング手順
- 最初に、Consumerのインスタンスを作成し、IDをつけてbegin()を実行すると、 AuthRequestオブジェクトを返します。
- 引数にOpenIDサーバがIDの確認を試みた後に戻るURL(return_to)とユーザー が認証するときにウェブサイトを確認するURLまたはURLのパターン(realm)を 指定して、AuthRequestオブジェクトのredirectURL()を実行してと、ブラウザ にOPへのリダイレクトを送信します。
ここまでが最初の半分のプロセスです。
-
残りのプロセスは、プロバイダがユーザーのブラウザをredirectURL()で指定
したreturn_toのURLにリダイレクトして戻した後に行われます。
そのリ クエストにはプロバイダによってURLにリクエストを終了するために必要な情 報がリクエストのクエリに付け加えられます。 - 同じsessionとstoreのConsumerインスタンスを選択し、complete()を呼び出し ます。
BaseHTTPServer版のコンシューマサーバ(RP)のコードを見る
それでは、サンプルのコードを見てみます。
Consumerのインスタンスを作成。
このサンプルでは、store は filestore または、memstore が使われてい
ますが、Django版では、PostgreSQLStore,MySQLStore,SQLiteStore が使える
ようになっています。
(以下、examples/consumer.py から抜粋)
from openid.consumer import consumer ... class OpenIDRequestHandler(BaseHTTPRequestHandler): ... def getConsumer(self): # セッションとストアを渡してConsumerのインスタンスを作成します。 # store は コンシューマに必要な情報を保持します。 # store は # openid.store.memstore.MemoryStore() または # openid.store.filestore.FileOpenIDStore(data_path) # # self.getSession() は sessionを戻します。 # sessionは 辞書型変数でsession['id']にクッキーがセットされている。 # session['id'] = sid return consumer.Consumer(self.getSession(), self.server.store) def doVerify(self): ... oidconsumer = self.getConsumer()セッションキーは次のコードで与えられています。
SESSION_COOKIE_NAME = 'pyoidconsexsid' def setSessionCookie(self): sid = self.getSession()['id'] session_cookie = '%s=%s;' % (self.SESSION_COOKIE_NAME, sid) self.send_header('Set-Cookie', session_cookie)
def getSession(self): """Return the existing session or a new session""" if self.session is not None: return self.session # Get value of cookie header that was sent cookie_str = self.headers.get('Cookie') if cookie_str: cookie_obj = SimpleCookie(cookie_str) sid_morsel = cookie_obj.get(self.SESSION_COOKIE_NAME, None) if sid_morsel is not None: sid = sid_morsel.value else: sid = None else: sid = None # If a session id was not set, create a new one if sid is None: sid = randomString(16, '0123456789abcdef') session = None else: session = self.server.sessions.get(sid) # If no session exists for this session ID, create one if session is None: session = self.server.sessions[sid] = {} session['id'] = sid self.session = session return session
ディスカバーの実行
IDを渡して、begin()を実行します。begin()はdiscoverを実行し、失敗すると consumer.DiscoveryFailure例外を発生します。serviceが無い場合はNoneを返 します。
try: request = oidconsumer.begin(openid_url) except consumer.DiscoveryFailure, exc: # 例外処理 ... else: if request is None: ... else: # discover 成功
拡張の設定
requestが正常にリターンされた後、拡張が選択された場合のコードがあります。
openid.extensions.sreg:(Simple registration request)
2.0からサポートされました。
http://openid.net/specs/openid-simple-registration-extension-1_1-01.html
sregはRPとOP両方で使われます。
-
RPではrequestオブジェクトをつくり、SRegRequestを加えます。
auth_request.addExtension(SRegRequest(required=['email']))
-
OPではRPからのrequestからSRegRequest.fromOpenIDRequestを使ってsregを取
り出します。その後,SRegResponseオブジェクトを作成しresponseにユーザー
のデータをつけたsregのレスポンスを加えます。
sreg_req = SRegRequest.fromOpenIDRequest(checkid_request)
# [ get the user's approval and data, informing the user that
# the fields in sreg_response were requested ]
sreg_resp = SRegResponse.extractResponse(sreg_req, user_data)
sreg_resp.toMessage(openid_response.fields)
-
RPは SRegResponse.fromSuccessResponseを使ってresponseのsregを取り出し
ます。
sreg_resp = SRegResponse.fromSuccessResponse(success_response)
openid.extensions.pape:
An implementation of the OpenID Provider Authentication Policy
Extension 1.0
まだドラフトの規格である2.1でサポートされます。OpenID
Provider Authentication Policy Extension - Draft 2
次の認証ポ
リシーが指定できまます。
-
AUTH_PHISHING_RESISTANT:
http://schemas.openid.net/pape/policies/2007/06/phishing-resistant'
潜在的にRPの支配下にある相手にエンドユーザーが共有キーを渡さない認証方 式(潜在的に悪意があるRPによって、ユーザーエージェントがリダイレクトさ れて、その結果それをエンドユーザの実際のOPに送らないように制御されるこ とに注意してください。)
-
AUTH_MULTI_FACTOR:
'http://schemas.openid.net/pape/policies/2007/06/multi-factor'
エ ンドユーザーがひとつの認証要素以上を渡すことによってOPに対して認証する 認証方式。例としては、パスワードとソフトウェアトークンや電子証明書を使っ た認証がある。
-
AUTH_MULTI_FACTOR_PHYSICAL:
'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical'
上記のAUTH_MULTI_FACTORを含み、ハードウェアトークンを使用するような場 合。
オプションを選択したときのコード:
# sregが指定されている場合 if use_sreg: self.requestRegistrationData(request) # papeが指定されている場合 if use_pape: self.requestPAPEDetails(request)sregとpapeのそれぞれ該当する関数のコード
sregを選択した場合は'nickname'、'fullname'、'email'を取得しようとしています。
from openid.extensions import pape, sreg ... def requestRegistrationData(self, request): #required にnickname、optionalに'fullname', 'email'を指定しています。 sreg_request = sreg.SRegRequest( required=['nickname'], optional=['fullname', 'email']) request.addExtension(sreg_request) def requestPAPEDetails(self, request): pape_request = pape.Request([pape.AUTH_PHISHING_RESISTANT]) request.addExtension(pape_request)
リダイレクト
request.shouldSendRedirect()によって、リダイレクトするかPOSTを送信する か判断します。request.redirectURLでリダイレクト先のURLを取得し、ブラウ ザをリダイレクトします。trust_root = self.server.base_url return_to = self.buildURL('process') if request.shouldSendRedirect(): # trust_root はrealmです。 redirect_url = request.redirectURL( trust_root, return_to, immediate=immediate) self.send_response(302) self.send_header('Location', redirect_url) self.writeUserHeader() self.end_headers() else: form_html = request.formMarkup( trust_root, return_to, form_tag_attrs={'id':'openid_message'}, immediate=immediate) self.autoSubmit(form_html, 'openid_message')
ここまででリクエストの前半が終了です。
認証の後半 - return_to での処理
後半です。最初と同じようにConsumerのインスタンスを作成。
def getConsumer(self): return consumer.Consumer(self.getSession(), self.server.store) def doProcess(self): """Handle the redirect from the OpenID server. """ oidconsumer = self.getConsumer()query(辞書型変数)とurl を渡してConsumerのcomplete()を実行。
url = 'http://'+self.headers.get('Host')+self.path info = oidconsumer.complete(self.query, url)complete()は、SUCCESS, CANCEL, FAILURE, または SETUP_NEEDED のステー タスを含むResponseのサブクラスを返します。以下、成功した場合、
display_identifier = info.getDisplayIdentifier() if info.status == consumer.FAILURE and display_identifier: ... elif info.status == consumer.SUCCESS: ... fmt = "You have successfully verified %s as your identity." message = fmt % (cgi.escape(display_identifier),) sreg_resp = sreg.SRegResponse.fromSuccessResponse(info) pape_resp = pape.Response.fromSuccessResponse(info) if info.endpoint.canonicalID: # You should authorize i-name users by their canonicalID, # rather than their more human-friendly identifiers. That # way their account with you is not compromised if their # i-name registration expires and is bought by someone else. message += (" This is an i-name, and its persistent ID is %s" % (cgi.escape(info.endpoint.canonicalID),))以上で終了です。
整理すると
- アプリケーションで入力されたIDを取得する。
- セッションクッキーとstoreを作り、Consumerのインスタンスを作る
- begin()でディスカバーを実行する。
- OPにリダイレクトする。
- OPからreturn_toにレスポンスが帰って来る。
- レスポンスからConsumerのインスタンスを作り、complete()で結果を取得する
- アプリケーションに結果を渡す。
アプリケーション側では、ここからアプリケーションのユーザーのトップペー
ジにアプリケーション用のセッションIDをつけてさらにリダイレクトしたりし
ます。
アプリケーション内の任意のページから認証する場合は、RPのロ
グインページにアプリケーションのページからリダイレクトするとき、クッキー
やクエリ文字列に元ページの情報をセットし、それをRPがreturn_toのクエリ
にセットしておくと、レスポンス時にその情報をクエリから取得することがで
きます。それをもとに、レスポンスが帰ってきたときにアプリケーションの要
求元のページに対し、アプリケーション側から指定されたキーなどやその他の
情報を、クッキーやクエリにつけてアプリケーションの元ページにリダイレク
トして戻すこともできます。
終わりに
OpenIDの仕様はセキュリティ関連の暗号化の話や、ユーザーとRP、OPの3者間 の通信などで理解しがたく、とっつきにくいのですが、調べてみると、すでに Webフレームワークなどで広く対応されているようです。皆さんも是非お試し ください。