Python で OpenID の認証サーバーを動かしてみました
あるサイトで OpenID の使用を検討していて、備忘録をかねてブログにまとめててみようと思い記事にしてみました。
OpenID の基本的な用語説明と、OpenID 2.0 で新たに取り入れられた仕様についてまとめてみました。また、Python OpenID Library で用意されているサンプルのサーバーを動作させる方法について紹介してみたいと思います。
OpenIDは、ユーザーの管理や認証を既存の他の認証サーバに任せることができる仕組みです。
OpenIDに対応したサイトでユーザー登録したユーザーには、OpenID用のID(identifier:識別子)が割り当てられます。
ユーザー登録やパスワード管理などの機能を持たないサイトでは、ユーザーにこのOpenID用のIDを入力してもらい、
OpenIDの仕組みを使って安全にIDの認証と使用の認可の確認を行うことができます。
ユーザーは、認証サーバ側で認証機能を持たないサイトで、自分のアカウントが使用されることを許可する必要があります。
最新の規格はOpenID2.0で07/12/05にリリースされました。
OpenID Authentication 2.0,
OpenID Attribute Exchange 1.0
また、以下のURLから始まる記事は非常に参考になります。
http://www.atmarkit.co.jp/fsecurity/rensai/openid01/openid01.html
用語
OpenIDは、ユーザーのブラウザ(User-Agent)、認証サーバを信頼する側(RP)、認証サーバ(OP)の3者間でHTTPによるやり取りを行うことによって
ユーザーのIDが正しいこと確認します。
OpenIDの仕組みの中で使用される用語は次のようなものがあります。
- Identifier:(ID 識別子)
- 識別子は "http" や "https" で始まるURIです。 2.0ではXRI “Extensible Resource Identifier (XRI) Syntax V2.0,” もサポートします。
- User-Agent:(ユーザエージェント)
- HTTP/1.1が動作するユーザーのウェブブラウザ
- Relying Party:(信頼する側)
- 以下、RPで示す。IDの確認を認証サーバーに問い合わせる側。 OpenID1.1ではConsumer(コンシューマ)と言われていた。
- OpenID Provider:(OpenID プロバイダ)
- 以下、OPで示す。 RPが信頼するOpenIDの認証サーバ。
- OP Endpoint URL:(エンドポイントURL)
- OpenID認証要求を受け付けるURL。これはユーザーのIDから、そのIDの示すURLのページを RPが取得し、その内容を読み込んで見つけられる。
- OP Identifier:(プロバイダ識別子)
- 2.0から使用できるプロバイダ側の識別子
- User-Supplied Identifier:(ユーザーが供給する識別子)
- ユーザーによってウェブアプリケーションに示されるか、または、プロバイダ上でユーザーによって選ばれるID。 プロトコルのイニシエーションフェーズでエンドユーザーがウェブアプリケーション上で入力するユーザーのID、 もしくはプロバイダのID。 もし、プロバイダのIDが使われると、OPはエンドユーザーにウェブアプリケーションで利用するIDを選択させる。
- Claimed Identifier:(要求された識別子)
- ユーザーが所有することを要求する識別子。OpenIDのプロトコルは このIDが正しいことを調べることを目的としている。
- これはIDがURLであれば、URLが正規化されたもの。
- IDがXRIであれば、CanonicalIDになる。生体認証などで使うことができる。
- OP-Local Identifier:プロバイダローカル識別子
- OPで局所的に使用されるユーザーのID。ユーザのコントロール化にある必要は無い。
プロトコルの概要
OpenIDで使用される通信はHTTPで行われますが、メッセージのやり取りの方法には直接通信(Direct Communication)と間接通信(Indirect Communication) の2種類があります。
直接通信
直接通信(ダイレクトコミュニケーション)はRPとOPの間でやり取りされ、RPがOPに対してPOSTによりダイレクトリクエスト投げて、OPから text/plain のダイレクトレスポンスを受け取ることで通信を行います。
間接通信
間接通信(インダイレクトコミュニケーション)はRPとOPの間で認証リクエストと認証レスポンスを交換するときに使用されます。
間接通信の方法は、ユーザーエージェントをHTTPでリダイレクトする方法と、HTMLでのフォームサブミッションを行う方法があります。
いずれの場合も直接的には、ユーザーエージェントがRPまたはOPと通信します。
手順の概要
手順は次のようになります
- イニシエーション(Initiation):
ユーザーがIDをRPに渡す。 - ディスカバリ(discovery):
ユーザーから示されたIDを正規化し、RPはOPのエンドポイントURLに対し通信を行う。
- アソシエーションの確立(Establishing Associations):
OPとRPがDiffie-Hellmanの鍵交換を使用して共有秘密鍵を生成し、これを利用してアソシエーション(Association)を確立する。 OPはアソシエーションを使用して、その後のメッセージに署名し、RPはそれらのメッセージを確認する。 これは、続く認証の要求と応答の後に、署名を確認するために行う直接要求の必要性を取り除く。 (これは仕様ではOptionalになっている。行わない場合は、判定の検証時に行う。)
- 認証要求(Requesting Authentication):
RPがユーザーのブラウザをOPに認証要求をもってリダイレクトする。
- OPで、エンドユーザーがOpenIDでの認証を行うことを認可するか確認する。
エンドユーザーのOPでの認証の方法と、認証に関するポリシーは、このOpenIDの規定範囲外である。
- 肯定判定、否定判定(Positive Assersion,Negative Assersion):
OPはブラウザを認証の結果を含めてRPにリダイレクトして戻す。
- 判定の検証(Verifying Assertion):
RPは Return URL、ディスカバリ時に得られた情報、nonceと署名の検証を行う。署名の検証は、アソシエーションの間に確立された共有キーを使うか、 または直接要求をOPに送ることによって行われる。
イニシエーションとディスカバリ
後でOpenIDのライブラリを使用したサンプルコードを見ますが、ディスカバリとアソシエーションの確立については
コードだけでは中身がわかりにくいので、ここで仕様を簡単に説明しておきます。
イニシエーション
RPはユーザーがIDを入力するフォームを持ちます。IDのフォームフィールドの"name"属性は"openid_identifier"でなければいけません。
ディスカバリ
ディスカバリは従来のHTMLベースのディスカバリのほか、2.0になってXRIやYadisプロトコルを使用したXRDSベースのディスカバリがサポートされるようになりました。
- IDがXRI(xri://で始まる)の場合、XRI Resolutin V2 によってXRDSドキュメントを取得します。
- IDがURLの場合、最初にYadisプロトコルが試みられ、成功すると結果がXRDS ドキュメントに収められます。
- Yadisプロトコルが失敗するか、XRDSドキュメントが正しくなかったり、サービス(Service)エレメントが見つからない場合は、 URLを使ってHTMLベースのディスカバリが行われます。
アソシエーションの確立
アソシエーションセッション要求
アソシエーションセッションでは、はじめにRPからOPに直接要求を送ります。要求はPOSTメッセージとして送られ、次のようなパラメータを含みます。
(詳細な説明は省きます)
一般的なパラメータ
- openid.ns
-
値 http://specs.openid.net/auth/2.0
(http://openid.net/signon/1.1" または "http://openid.net/signon/1.0の場合は、1.1互換モードになります)
- openid.mode:
- 値 associate
- openid.assoc_type:
- 値 HMAC-SHA1 または HMAC-SHA256
- openid.session_type:
- 値 no-encryption, DH-SHA1 または DH-SHA256
- openid.dh_modulus:
- 値 base64(btwoc(p))
- openid.dh_gen:
- 値 base64(btwoc(g))
- Default: g = 2
- openid.dh_consumer_public:
- 値 base64(btwoc(g ^ xa mod p))
アソシエーションセッション応答
アソシエーションセッションの応答は、OPからRPに直接応答で返されます。応答はtext/plainで返され、パラメータはテキストの各行に"key:value\n"の形で示されます。 応答には、以後の通信で鍵として使われるassoc_handleとその鍵の使用期間であるexpire_inが返されます。 また、MAC(Message Authentication Code)も共有秘密鍵として渡されます。MACは要求時にno-encryptionが指定されていなければ暗号化されます。 httpsなどトランスポート層で暗号化がサポートされていない場合は、no-encryptionを使用してはいけません。
OpenID2.0の特徴(OpenID1.1からの変更点)
アップデートされたイニシエーションとディスカバリ
- OpenIDプロバイダ 識別子のサポート
- 識別子にXRIの使用をサポート
- URLが識別子に使用されるときにはRFC3986にしたがって標準化されること
- ディスカバリにYadisプロトコルを使うこと。これによりひとつの識別子に対して複数のOpenID プロバイダを使うことができます。
互換モードではopenid.nsは省かれます。OpenID1.1ではHMAC-SHA1 associationsだけがサポートされますが、 2.0ではHMAC-SHA256と DH-SHA256をサポートします。
OpenID用のライブラリを使う
http://openidenabled.com/ にある pythonのライブラリ内のサンプルコードを元に実際の
コーディングを見ていきます。
ここのライブラリは JanRain, Inc.によって
フリーウェア(Apache License Version 2.0)として提供されています。2007/12/5にOpenID2.0をサポートしたバージョンがリリースされています。
言語としては、PythonのほかPHPとRubyがサポートされています。
Javaでは Sxip Java や VeriSign Javaがあるようです.
ドキュメントはこちら。(12/25現在)
http://openidenabled.com/files/python-openid/docs/2.1.1/
サンプルサーバーの作成
python-openidのサンプル(examples)を動かしてみる。
サンプルにはBaseHTTPServer版とDjango版があります
BaseHTTPServer版
コンシューマのサーバを起動
$cd 展開したソース/examples
$python consumer.py --port=8001 --host=0.0.0.0
認証側のサーバーを起動
$python server.py --port=8000 --host=0.0.0.0
それぞれ次のURLでアクセスできます。
コンシューマ
http://ホスト名:8001
認証サーバ
http://ホスト名:8000
サンプルサイトの使い方:
最初に認証サーバー側でユーザー登録してください。名前を入れるだけです。
次に、コンシューマで、認証サーバ側でログインしたときに表示されるIDを入力すると動作するようです。
Django版
Django 0.95.1以上とpysqlite2が必要です。
サーバを起動
$cd 展開したソース/examples/djopenid/
$python manage.py syncdb
$python manage.py runserever 0.0.0.0:8000
consumer(RP)とserver(OP)を両方同時に動かすためには
別のコンソールで
$python manage.py runserever 0.0.0.0:8001
を実行してください。
サンプルサイトの使い方:
このサイトではユーザー登録はできません。サーバ側のIDはhttp://サーバ:ポート番号/server/user/だけです。
http://サーバ:ポート番号/を開き"Example Consumer (Relying Party)"のリンクを開き別ポートのIDを入力します。
今回はここまでとして、次回にサンプルサイトに使われているコードを追ってみたいと思います。