SAML認証ができるまで

こんにちは、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で行うことを想定しています。

saml_settings_ja_01

  1. ユーザーがcybozu.comにアクセスする
  2. ユーザーが未ログイン状態な場合、cybozu.comが認証要求メッセージを生成する
  3. ユーザーがcybozu.comから認証要求メッセージを受け取り、それをIdPに送る
  4. IdPが認証要求メッセージを受け取り、ユーザーを認証する
  5. IdPが認証応答メッセージを作成する
  6. ユーザーがIdPから認証応答メッセージを受け取り、それをcybozu.comに送る
  7. cybozu.comが認証応答メッセージを受け取り、検証する
  8. メッセージの内容に問題がない場合は、ユーザーが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の情報を登録します。 具体的には以下の情報を登録します。

saml_settings

  • 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側に必要な機能は以下の四つです。

  1. 認証要求メッセージの作成
  2. 認証要求メッセージをIdPに送る
  3. IdPが発行した認証応答メッセージを受け取る
  4. 認証応答メッセージを検証する

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>を以下の順序でエンコードします。

  1. Deflateエンコード
  2. Base64エンコード
  3. 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>を以下の順序でエンコードします。

  1. Base64エンコード
  2. 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認証を実装する方の参考になれば幸いです。


  1. SAML認証を使用したシングルサインオンを設定する - cybozu.com
  2. SAML Specifications
  3. OASIS
  4. SAML Core(pdf)
  5. SAML Bindings(pdf)
  6. SAML Profiles(pdf)
  7. SAML Metadata(pdf)
  8. XML Schema ID Type
  9. SAML正誤表(pdf)
  10. OpenAM
  11. OpenSAML