こんにちは、Slashチームの渡辺です。
Slashチームでは、ユーザー管理や認証周りなどの、cybozu.comの各サービスに共通する機能を開発しています。今回は、3月にリリースされた、SAML認証を用いたシングルサインオン機能1についてお話させて頂きます。cybozu.comでのSAML認証の概要にくわえて、それらの機能をどのように設計・実装していったか、という誰も興味ないニッチな話題を扱います。
SAML2 って?
「SAMLなんて聞いたこと無いけどなんとなく興味があるぞ!!」という物好きな方のために、SAMLの概要とcybozu.comでの利用について、簡単に説明します。そんなものは既に知っているというSAML猛者な方は読み飛ばして頂いて構いません。
SAMLはSecurity Assertion Markup Languageの略で、OASIS3によって策定された、異なるセキュリティドメイン間で、認証情報を連携するためのXMLベースの標準仕様です。
これだけだと何のことだかよく分かりませんね。
具体的に何ができるかというと、例えば、社内ネットワークに存在するActive Directory Federation Services(ADFS)などの認証サーバーの認証情報を使って、クラウドにあるcybozu.comに安全にシングルサインオン(SSO)できるようになります。ユーザーは認証サーバーに一回ログインするだけです。便利ですね。
尚、SAMLの世界では、ADFSのように認証情報を提供する側をIdentity Provider(IdP) 、cybozu.comのように認証情報を利用する側をService Provider(SP)と呼びます。
cybozu.comでは、下図のようなシーケンスでSSOが実現されています。全ての通信はHTTPSで行うことを想定しています。
- ユーザーがcybozu.comにアクセスする
- ユーザーが未ログイン状態な場合、cybozu.comが認証要求メッセージを生成する
- ユーザーがcybozu.comから認証要求メッセージを受け取り、それをIdPに送る
- IdPが認証要求メッセージを受け取り、ユーザーを認証する
- IdPが認証応答メッセージを作成する
- ユーザーがIdPから認証応答メッセージを受け取り、それをcybozu.comに送る
- cybozu.comが認証応答メッセージを受け取り、検証する
- メッセージの内容に問題がない場合は、ユーザーがcybozu.comにログインできる
このように、認証のシーケンスがSP(cybozu.com)へのアクセスから始まるSSOを、SP Initiated SSOと呼びます。また、SAMLでSSOを実現するためには、事前にIdPとSPの間で信頼関係を構築しておく必要があります。これは、メタデータの読み込みや、公開鍵の登録などで実現します。
SAMLの仕様
SAMLの仕様(バージョン2.0)はいくつかのカテゴリに分かれています。その内、今後の説明で重要になるものを列挙します。
- SAML Core4
認証情報を表すXMLのスキーマ(SAML Assertions)と、メッセージ交換のプロトコル(SAML Protocols)を定義しています。 - SAML Bindings5
SAMLのメッセージを実際の通信プロトコル(HTTPなど)にマッピングする方法を定義しています。 - SAML Profiles6
特定のユースケースを実現するための、SAML Assertions、SAML Protocols、SAML Bindingsの組み合わせ方を定義しています。 - SAML Metadata7
IdPやSPに関する情報(メッセージを受け取るエンドポイントURLや利用するBindingなど)を表現するためのXMLのスキーマを定義しています。IdPとSPの間に信頼関係を構築する際に利用することができます。
以上でSAMLの概要説明は終わりです。そろそろ本題に入って、cybozu.comでのSAML認証の設計について解説していきます。
要件
cybozu.comでSAMLを利用して実現したかったことは 「cybozu.comをSPとして、IdPの認証情報を用いてSSOを行うこと」 です。また、連携先のIdPは社内ネットワークに存在する可能性があり、IdPとSPは直接通信できないことを想定しています。
このユースケースは、SAML ProfilesにおけるWeb Browser SSO Profileに該当します。したがって、今回の要件を実現するためには、以下の二つの機能を追加する必要があります。
- IdPとの信頼関係の構築
- Web Browser SSO Profileに従ったメッセージ処理
IdPとの信頼関係の構築機能の設計
前述の通り、SAMLでSSOを実現するには、事前にIdPとSPの間で信頼関係を構築しておく必要があります。つまり、SPが信頼するIdPの登録と、IdPが信頼するSPの登録をそれぞれ行う必要があります。
SPが信頼するIdPの登録
cybozu.comでは、共通管理画面の「ログインのセキュリティ設定」において、信頼するIdPの情報を登録します。 具体的には以下の情報を登録します。
- IdPが認証要求メッセージを受け取るURL
- cybozu.comからログアウトした後に遷移するURL
- IdPが認証応答メッセージの署名に用いる秘密鍵に対応する公開鍵
ログアウトURLには基本的にはIdPからログアウトするためのURLを設定します。このURLを用いて行うのは、SAMLのSingle Logoutではなく、単なるIdPのログアウト用URLへのリダイレクトです。cybozu.comからログアウトした後に、IdPにログインしたままだと、再びcybozu.comにアクセスした場合にSSOが実行されてログアウトが意味をなさないため、このような処理を行なっています。
IdPが信頼するSPの登録
IdPの管理画面で手動で設定するか、あるいはSPが提供するメタデータを読み込むことで、IdPにSPを登録します。具体的な設定方法はIdPによって異なるため割愛しますが、ここではcybozu.comが提供しているSPメタデータについて解説します。
SPメタデータはSAML Metadataに定義されている<EntityDescriptor>
要素と<SPSSODescriptor>
要素で表現されます。省略可能な要素・属性は省略するという方針を立てた結果、実際にcybozu.comの管理画面で取得できるSPメタデータのXMLは以下のようになりました。<NameIDFormat>
要素と<AssertionConsumerService>
要素を含んでいます。XML内の(sub_domain)は環境によって異なります。
<md:EntityDescriptor entityID="https://(sub_domain).cybozu.com"> <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> <md:NameIDFormat> urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified </md:NameIDFormat> <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://(sub_domain).cybozu.com/saml/acs" index="0"/> </md:SPSSODescriptor> </md:EntityDescriptor>
Web Browser SSO Profileに従ったメッセージ処理機能の設計
前述のように、cybozu.comではSP Initiated SSOを採用しています。Web Browser SSO Profileに従ってSP Iinitiated SSOを行う際に、SP側に必要な機能は以下の四つです。
- 認証要求メッセージの作成
- 認証要求メッセージをIdPに送る
- IdPが発行した認証応答メッセージを受け取る
- 認証応答メッセージを検証する
1. 認証要求メッセージの作成
認証要求メッセージはSAML Coreに定義されている<AuthnRequest>
要素で表現されます。省略可能な要素・属性は省略する、<AuthnRequest>
に署名しない、という方針を立てた結果、<AuthnRequest>
では以下の要素・属性のみを使用することとしました。< >で囲まれているのは要素、そうでないのは属性です。
要素・属性名 | 内容 |
---|---|
ID | 認証要求メッセージ毎にユニークなxs:ID型8のランダム文字列 |
Version | SAMLのバージョン |
IssueInstant | 認証要求メッセージの発行日時 |
AssertionConsumerServiceURL | SPが認証応答メッセージを受け取るエンドポイントのURL |
ProtocolBinding | 認証応答メッセージを受け取る際に利用するSAML Binding |
< Issuer > | SPのユニークなID |
< NameIDPolicy > | 認証応答メッセージ内のユーザーの識別子に関するポリシー |
これらの項目の値を検討した結果、cybozu.comで実際に出力するXMLは以下のようになりました。XML内の(sub_domain)は環境によって異なります。
<samlp:AuthnRequest AssertionConsumerServiceURL="https://(sub_domain).cybozu.com/saml/acs" ID="szqd0c3d0u3vpz5jwna4p24iso42opc4" IssueInstant="2013-04-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://(sub_domain).cybozu.com <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" /> </samlp:AuthnRequest>
2. 認証要求メッセージをIdPに送る
<AuthnRequet>
をIdPに届けるために、まずはIdPとの通信に利用するSAML Bindingを決めなくてはなりません。cybozu.comでは、IdPとSPが直接通信できないことを考慮し、 HTTP Redirect Binding を利用することとしました。
HTTP Redirect Bindingでは、<AuthnRequet>
を以下の順序でエンコードします。
- Deflateエンコード
- Base64エンコード
- URLエンコード
エンコード結果の文字列をクエリパラメータとして、IdPのSSOエンドポイントURLに付加し、そのURLにユーザーをリダイレクトさせることで、IdPに認証要求メッセージを届けます。その際のパラメータ名には SAMLRequst を用います。 IdPのSSOエンドポイントは、前述の「SPが信頼するIdPの登録」で登録したURLを利用します。
仮にそのURLをhttps://idp_host/path/to/sso
とした場合、何らかの方法(30X系のレスポンスなど)でユーザーを以下のURLにリダイレクトさせることで、IdPにメッセージを届けることができます。
https://idp_host/path/to/sso?SAMLRequest=<Encoded AuthnRequest>
また、<AuthnRequest>
をキャッシュされては困るので、以下のHTTPヘッダをレスポンスに含めます。
Pragma: no-cache
Cache-Control: no-cache, no-store
3. IdPが発行した認証応答メッセージを受け取る
IdPがユーザーの認証に成功すると、認証応答メッセージをSPに送り返します。認証応答メッセージは<Response>
要素で表現されます。認証要求メッセージの場合と同様に、メッセージの送信に利用するSAML Bindingを決める必要があります。
cybozu.comでは、IdPとSPが直接通信できないこと、<Response>
の内容は<AuthnRequest>
に比べて大きくURLに含めるのは難しいことから、 HTTP POST Binding を利用することとしました。この情報は、cybozu.comのSPメタデータや<AuthnRequest>
のProtocolBinding属性から確認できます。
HTTP POST Bindingでは<Response>
を以下の順序でエンコードします。
- Base64エンコード
- URLエンコード
最終的に、IdPはcybozu.comのAssertionConsumerServiceURLに以下の内容をPOSTします。
SAMLResponse=<Encoded Response>
4. 認証応答メッセージを検証する
最後に、SP内のAssertionConsumerServiceが受け取った<Response>
の内容を検証し、ログインの成否を判定します。 実際にIdPが出力する<Response>
の例を以下に示します。XML内の(sub_domain)や(idp_host)は環境によって異なります。
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="s2b39863179da0358f10bb499d6ac0e64062e89e1d" InResponseTo="szqd0c3d0u3vpz5jwna4p24iso42opc4" Version="2.0" IssueInstant="2013-04-01T00:30:00Z" Destination="https://(sub_domain).cybozu.com/saml/acs"> <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://(idp_host) <samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> <samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Value="urn:oasis:names:tc:SAML:2.0:status:Success"> </samlp:StatusCode> </samlp:Status> <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="s2822cac48e7b7ec82ff36710996423e7baec43a00" IssueInstant="2013-04-01T00:30:00Z" Version="2.0"> <saml:Issuer>https://(idp_host) <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> ...アサーションの署名の内容... </ds:Signature> <saml:Subject> <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameQualifier="https://(idp_host)" SPNameQualifier="https://(sub_domain).cybozu.com">watanabe</saml:NameID> <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml:SubjectConfirmationData InResponseTo="szqd0c3d0u3vpz5jwna4p24iso42opc4" NotOnOrAfter="2013-04-01T00:40:00Z" Recipient="https://(sub_domain).cybozu.com/saml/acs" /> </saml:SubjectConfirmation> </saml:Subject> <saml:Conditions NotBefore="2013-04-01T00:20:00Z" NotOnOrAfter="2013-04-01T00:40:00Z"> <saml:AudienceRestriction> <saml:Audience>https://(sub_domain).cybozu.com </saml:AudienceRestriction> </saml:Conditions> <saml:AuthnStatement AuthnInstant="2013-04-01T00:29:30Z" SessionIndex="s2901e6c0e0cc0c8f1aa1075215125b2676774dd01"> <saml:AuthnContext> <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef> </saml:AuthnContext> </saml:AuthnStatement> </saml:Assertion> </samlp:Response>
長くなるので全ては無理ですが、cybozu.comでの検証項目の一部を紹介します。
- Version属性の評価
- < Status >要素の評価
- < SubjectConfirmation >要素のmethod属性の評価
- < SubjectConfirmationData >要素の内容の評価
- < Conditions >要素の評価
- < AudienceRestriction >要素の評価
- < Assertion >要素の署名の検証
例えば、Version属性の評価では、<Response>
要素のVersion属性の値が2.0であることを検証しています。また、署名の検証には、事前に登録しておいたIdPの公開鍵を用います。
これらの検証項目は、基本的には、Web Browser SSO Profileの4.1.4.3 < Response > Message Processing Rulesに従っています。くわえて、関連するSAML CoreやSAML Bindingsの仕様も考慮して検証項目を検討しています。
検証した結果<Response>
の内容に問題がなければ、<NameID>
要素の値(例のXMLの場合はwatanabe)をログイン名としてcybozu.comにログインします。
実装にむけて
以上で設計は完了です。あとは設計通りに機能を実装していけば、晴れてSAML SPの完成です!!
また、実装の前に以下のような準備をしておくと開発が捗ります。これらも色々嵌りどころがあるのですが、今回は割愛させて頂きます。
- 仕様書の熟読
関連する仕様の詳細理解と正誤表9による仕様修正の確認を行います。 - 動作確認環境の構築
OpenAM10などを用いて、SAML認証が動作する環境を構築します。 - ライブラリの調査
OpenSAML11などの、XMLの生成・解析用のライブラリを選定します。
まとめ
簡単にですが、cybozu.comのSAML認証について、その概要と設計を解説しました。SP Initiated SSOのためのSPを実現するには、以下の機能が必要です。
- 信頼するIdPの登録
- IdPに登録するSPの情報の提供(メタデータの生成など)
- < AuthnRequest >の生成
- < AuthnRequest >の送信
- < Response >の受信
- < Response >の検証
SAMLはなかなか手強い相手ですが、本稿がこれからSAML認証を実装する方の参考になれば幸いです。