Skip to content
Published on

SAML 2.0 ディープダイブ — Assertion・Binding・Metadata 完全攻略

Authors

はじめに

OIDC が新規構築のデフォルトとなった 2026 年でも、エンタープライズ B2B SSO の現場では依然として SAML 2.0 が共通語です。顧客企業の Entra ID、Okta、あるいは自社構築の IdP と連携せよという要件を受け取ると、半分以上の確率で SAML メタデータの XML ファイルが送られてきます。2005 年に標準化されたプロトコルが 20 年以上も現役である理由は単純です。すでに敷設された信頼関係の慣性と、ブラウザさえあれば動くというシンプルな前提です。

問題は、SAML が「設定すれば動くブラックボックス」として扱われ、障害が起きると誰も中身を知らないという点です。本記事では SAML 2.0 の核心である Assertion、Protocol(AuthnRequest/Response)、Binding、Metadata を実際の XML レベルで解剖し、XML Signature Wrapping のような攻撃と防御、さらにクロックスキューのような運用課題まで扱います。

SAML 2.0 の 4 層構造

SAML スペックは 4 つのレイヤーで構成されます。この区分を知っていると、スペック文書がはるかに読みやすくなります。

+-----------------------------------------------------------+
| Profiles   : レイヤーを組み合わせた利用シナリオ             |
|              (Web Browser SSO Profile、Single Logout など) |
+-----------------------------------------------------------+
| Bindings   : メッセージを運ぶ転送方法                       |
|              (HTTP-Redirect, HTTP-POST, Artifact, SOAP)    |
+-----------------------------------------------------------+
| Protocols  : 要求/応答メッセージの形式                      |
|              (AuthnRequest, Response, LogoutRequest など)  |
+-----------------------------------------------------------+
| Assertions : アイデンティティ情報の本体                     |
|              (AuthnStatement, AttributeStatement など)     |
+-----------------------------------------------------------+
  • Assertion: 「このユーザーは誰で、いつどのように認証され、どんな属性を持つか」という陳述の XML 文書。
  • Protocol: Assertion を要求し応答するメッセージの規格。
  • Binding: そのメッセージを HTTP の上にどう載せるかのルール。
  • Profile: 上の 3 つを束ねて「Web ブラウザ SSO」という完結したシナリオにしたもの。

私たちが普段「SAML 連携」と呼ぶものは、ほぼ常に Web Browser SSO Profile です。

Assertion の解剖 — アイデンティティ陳述の XML

以下は実際の IdP が発行する Assertion の骨格です(署名は省略)。

<saml:Assertion
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_a1b2c3d4e5f6"
    Version="2.0"
    IssueInstant="2026-06-12T09:30:00Z">

  <saml:Issuer>https://idp.corp.com/saml</saml:Issuer>

  <saml:Subject>
    <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">
      f9a8b7c6-1234-5678-90ab-cdef12345678
    </saml:NameID>
    <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
      <saml:SubjectConfirmationData
          NotOnOrAfter="2026-06-12T09:35:00Z"
          Recipient="https://app.example.com/saml/acs"
          InResponseTo="_req-98765"/>
    </saml:SubjectConfirmation>
  </saml:Subject>

  <saml:Conditions
      NotBefore="2026-06-12T09:29:00Z"
      NotOnOrAfter="2026-06-12T09:35:00Z">
    <saml:AudienceRestriction>
      <saml:Audience>https://app.example.com/saml/metadata</saml:Audience>
    </saml:AudienceRestriction>
  </saml:Conditions>

  <saml:AuthnStatement
      AuthnInstant="2026-06-12T09:30:00Z"
      SessionIndex="_sess-112233">
    <saml:AuthnContext>
      <saml:AuthnContextClassRef>
        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
      </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>

  <saml:AttributeStatement>
    <saml:Attribute Name="email">
      <saml:AttributeValue>yj.kim@corp.com</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="department">
      <saml:AttributeValue>Platform Engineering</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="groups">
      <saml:AttributeValue>sso-admins</saml:AttributeValue>
      <saml:AttributeValue>developers</saml:AttributeValue>
    </saml:Attribute>
  </saml:AttributeStatement>
</saml:Assertion>

要素ごとの意味と検証ポイント

要素意味SP が検証すべきこと
IssuerAssertion 発行者(IdP)の entityID信頼する IdP の entityID と正確に一致するか
Subject/NameIDユーザー識別子Format が合意済みのものか(persistent、emailAddress など)
SubjectConfirmation「この Assertion を提示する者」の条件Recipient が自分の ACS URL か、NotOnOrAfter が有効か、InResponseTo が自分が送った要求 ID か
Conditions有効期間と受信対象NotBefore/NotOnOrAfter の時間枠、Audience が自分の entityID か
AuthnStatementいつ/どのように認証されたかAuthnContextClassRef がポリシー(MFA 要求など)を満たすか
AttributeStatementユーザー属性マッピングルールどおりに解析し、認可の入力として使用

NameID Format は運用で頻繁に問題になる部分です。persistent はサービスごとに固定された不透明な識別子、transient はセッションごとに変わる使い捨て、emailAddress は人間が読めるメールアドレスです。SP が emailAddress を期待しているのに IdP が persistent を送ると、「ログインはできるのにアカウントのマッチングができない」障害が発生します。連携の初期段階で必ず合意してください。

AuthnRequest / Response のフロー

SP-initiated SSO(標準経路)

ユーザーが SP に先にアクセスした場合です。SP が AuthnRequest を作り、ブラウザを IdP へ送ります。

<samlp:AuthnRequest
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_req-98765"
    Version="2.0"
    IssueInstant="2026-06-12T09:29:50Z"
    Destination="https://idp.corp.com/saml/sso"
    AssertionConsumerServiceURL="https://app.example.com/saml/acs"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
  <saml:Issuer>https://app.example.com/saml/metadata</saml:Issuer>
  <samlp:NameIDPolicy
      Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
      AllowCreate="true"/>
  <samlp:RequestedAuthnContext Comparison="minimum">
    <saml:AuthnContextClassRef>
      urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
    </saml:AuthnContextClassRef>
  </samlp:RequestedAuthnContext>
</samlp:AuthnRequest>

IdP は認証を終えると Response を返します。Response の中に Assertion が入っています。

<samlp:Response
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="_resp-55555"
    Version="2.0"
    IssueInstant="2026-06-12T09:30:00Z"
    Destination="https://app.example.com/saml/acs"
    InResponseTo="_req-98765">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
    https://idp.corp.com/saml
  </saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <!-- 署名された saml:Assertion がここに入る -->
</samlp:Response>

重要な対応関係: AuthnRequest の ID は、Response の InResponseTo、および Assertion 内 SubjectConfirmationData の InResponseTo と一致しなければなりません。この検証を省略すると、別セッション用の Response を注入する攻撃が可能になります。

SP-initiated vs IdP-initiated

SP-initiated(推奨)                     IdP-initiated
--------------------                     --------------------
ユーザー -> SP にアクセス                ユーザー -> IdP ポータルにアクセス
SP が AuthnRequest を生成(ID を記録)   アプリのタイルをクリックすると
IdP 認証後に Response                    AuthnRequest なしでいきなり
  (InResponseTo=要求 ID)                 Unsolicited Response を発行
SP: InResponseTo の検証が可能            SP: InResponseTo の検証が不可能
                                         (そもそも要求が存在しない)
CSRF/注入の防御が容易                    Response 注入に相対的に脆弱

IdP-initiated は「会社ポータルでアプリのアイコンをクリックして入る」UX のためエンタープライズでよく要求されますが、InResponseTo の検証ができないためセキュリティ上は劣位です。可能であれば IdP ポータルのアプリタイルが SP のログイン開始 URL を指すようにし、実質的に SP-initiated へ迂回実装するのがベストプラクティスです。

Binding — メッセージを運ぶ 3 つの方法

HTTP-Redirect Binding

AuthnRequest のような小さいメッセージに使います。メッセージを DEFLATE 圧縮 → base64 → URL エンコードしてクエリ文字列に載せます。

GET /saml/sso?SAMLRequest=fZJNb9swDIb%2FisG7...&RelayState=abc123
    &SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256
    &Signature=KJh8... HTTP/1.1
Host: idp.corp.com
  • URL 長の制限があるため、大きなメッセージ(署名を含む Response)には不向きです。
  • 署名は XML 内部ではなく**クエリパラメータ(SigAlg、Signature)**として別途渡されます(detached signature)。

HTTP-POST Binding

Response のような大きいメッセージに使います。IdP が自動送信される HTML フォームを返し、ブラウザが SP の ACS へ POST します。

<form method="post" action="https://app.example.com/saml/acs">
  <input type="hidden" name="SAMLResponse" value="PHNhbWxwOlJlc3BvbnNlIC4uLg=="/>
  <input type="hidden" name="RelayState" value="abc123"/>
</form>
<script>document.forms[0].submit();</script>
  • base64 のみ適用(圧縮なし)、署名は XML 内部に含まれます。
  • 事実上すべての Web SSO 連携における Response の受け渡しはこのバインディングです。

HTTP-Artifact Binding

機微な内容をブラウザに露出させたくない場合に使います。ブラウザには短い参照値(artifact)だけを渡し、SP が back-channel(SOAP)で IdP から実際のメッセージを取得します。

[ブラウザ]            [SP]                       [IdP]
    |<-- artifact 受領 -|                           |
    |--- artifact ----->|                           |
    |                   |--- ArtifactResolve(SOAP)->|
    |                   |<-- ArtifactResponse ------|
    |                   |    (実際の SAMLResponse)|
  • セキュリティは高いものの、SP-IdP 間の直接のネットワーク接続が必要で実装の複雑さも高く、実務では稀です。

バインディングの比較

項目HTTP-RedirectHTTP-POSTHTTP-Artifact
用途AuthnRequest, LogoutRequestResponse の受け渡し高セキュリティ環境
エンコーディングDEFLATE + base64 + URLbase64artifact 参照値
署名の位置クエリパラメータ(detached)XML 内部(enveloped)XML 内部
サイズ制限URL 長の制限あり事実上なし該当なし
back-channel の要否不要不要必要(SOAP)
実務での頻度高い(要求)非常に高い(応答)低い

Metadata — 信頼関係の設定ファイル

SAML 連携の出発点はメタデータの交換です。SP メタデータの例は次のとおりです。

<md:EntityDescriptor
    xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
    entityID="https://app.example.com/saml/metadata">
  <md:SPSSODescriptor
      AuthnRequestsSigned="true"
      WantAssertionsSigned="true"
      protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

    <md:KeyDescriptor use="signing">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>MIIDdzCCAl+gAwIBAgIE...</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>

    <md:SingleLogoutService
        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
        Location="https://app.example.com/saml/slo"/>

    <md:AssertionConsumerService
        index="0" isDefault="true"
        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
        Location="https://app.example.com/saml/acs"/>
  </md:SPSSODescriptor>
</md:EntityDescriptor>

IdP メタデータには SingleSignOnService エンドポイントと IdP の署名証明書が入っています。運用上のポイントは次のとおりです。

  • **entityID は識別子であり URL ではありません。**慣例として URL の形を使いますが、文字列が正確に一致するかがすべてです。末尾のスラッシュ 1 つの違いで連携が壊れます。
  • **証明書の期限切れが SAML 障害の原因第 1 位です。**メタデータ内の証明書は TLS 証明書とは別に期限切れになります。期限の 90/30/7 日前のアラートを自動化してください。
  • 鍵のロールオーバー: KeyDescriptor は複数置けます。新しい証明書をメタデータに追加 → 相手側の更新を確認 → 署名鍵を切り替え → 旧証明書を削除、という順序で無停止ローテーションが可能です。
  • メタデータ自体に署名するか、信頼できるチャネル(管理コンソール、署名付き URL)でのみ交換してください。

XML Signature と暗号化

署名の構造

SAML は XML Signature(XMLDSig)の enveloped signature を使います。署名対象のダイジェストを計算し、そのダイジェスト情報を含む SignedInfo を秘密鍵で署名します。

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
  <ds:SignedInfo>
    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
    <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
    <ds:Reference URI="#_a1b2c3d4e5f6">
      <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
      </ds:Transforms>
      <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
      <ds:DigestValue>uNk8...</ds:DigestValue>
    </ds:Reference>
  </ds:SignedInfo>
  <ds:SignatureValue>KJh8a9...</ds:SignatureValue>
</ds:Signature>

注目すべきは、Reference の URI が ID 属性の参照である点です。「ID が _a1b2c3d4e5f6 の要素」が署名対象であることを意味し、まさにこの間接参照が Signature Wrapping 攻撃の糸口になります。

署名の範囲は Response 全体と各 Assertion のそれぞれに適用できます。両方に署名するのが推奨であり、最低でも Assertion への署名は必須です。

暗号化(EncryptedAssertion)

Assertion はブラウザを経由(front-channel)するため、属性に機微情報が含まれる場合は XML Encryption で Assertion を SP の公開鍵で暗号化できます。署名が「偽造防止」なら、暗号化は「内容の秘匿」です。役割が異なるため、暗号化は署名の代わりにはなりません。

XML Signature Wrapping(XSW)攻撃と防御

SAML 史上もっとも有名な攻撃の系譜です。核心となるアイデアは、署名検証ロジックが見る「署名が有効な要素」と、アプリケーションが実際に読む要素が異なるように XML 構造を操作することです。

正常な Response                       XSW 攻撃の Response
--------------                        ------------------
Response                              Response
 └─ Assertion (ID=A, 署名済み)         ├─ [偽造 Assertion] (ID=B, 署名なし)
     └─ Subject: alice                 │    └─ Subject: admin   <-- アプリが読むもの
                                       └─ [元の Assertion] (ID=A, 署名は有効)
                                            └─ Subject: alice   <-- 検証器が見るもの

署名検証器: 「ID=A の要素の署名? 有効」 --> 通過
アプリケーション: 最初の Assertion(偽造)を解析 --> admin としてログイン

2012 年の研究では、当時の主要な SAML ライブラリ 14 個のうち多数がこの系統の攻撃に破られ、その後も亜種が周期的に発見されています。

防御チェックリスト

  1. 自作しないこと — 検証済みライブラリ(OpenSAML など)の最新版を使い、セキュリティパッチを追跡します。
  2. 「署名されたまさにそのノード」だけを使う — 署名検証に成功した要素の DOM ノードからのみデータを読みます。「文書から最初の Assertion を探す」式の解析は厳禁です。
  3. スキーマ検証を先に — SAML スキーマに反する構造(重複 Assertion、おかしな位置の要素)を署名検証の前に拒否します。
  4. Assertion の個数を強制 — Web SSO では Assertion は 1 つです。2 つ以上なら拒否します。
  5. Response と Assertion の両方に署名を要求 — WantAssertionsSigned と Response 署名の要求を両方有効にします。
  6. XML パーサーのハードニング — DTD と外部エンティティ(XXE)を無効化します。

RelayState — 忘れられがちな脇役

RelayState は「ログイン後にどこへ戻るか」を運ぶ不透明なパラメータです。SP-initiated では SP が送った値が Response とともにそのまま戻り、IdP-initiated では IdP が目的地の URL を入れることもあります。

運用/セキュリティのポイント:

  • スペック上 80 バイトの制限があるため、URL 全体ではなくサーバー側状態へのキーを入れるのが安全です。
  • 戻ってきた RelayState を検証なしにリダイレクトに使うと open redirect の脆弱性になります。ホワイトリストまたは署名/サーバー側照会で検証してください。
  • RelayState は署名の対象外なので、改ざんされうる前提で扱う必要があります。

クロックスキュー — 間欠的障害の常連犯

Assertion の NotBefore/NotOnOrAfter は通常、発行時刻を基準に ±数分の短い窓です。IdP と SP の時計がずれるとこうなります。

IdP の時計: 09:30:00  -->  NotBefore=09:29:00 で発行
SP の時計:  09:28:30  -->  「NotBefore が未来」 --> 拒否

症状: 同じユーザーがリトライすると成功することもある(間欠的)
      時計がずれた特定の SP ノードにルーティングされた時だけ失敗

対応:

  1. すべてのノードに NTP/chrony を強制し、ドリフトを監視します。
  2. SAML ライブラリの clock skew 許容値を 60〜120 秒に設定します(多くはデフォルト 0 か非常に小さい値)。
  3. 障害分析のために「どのノードで失敗したか」と当該ノードの時刻を併せて記録するログを整備します。

検証失敗ログには最低限、Issuer、InResponseTo、拒否理由(時刻条件/Audience/署名)、そしてサーバー時刻を残してください。「Invalid SAML response」一行だけのログは運用者への拷問です。

SP 実装時の検証チェックリスト

SAML Response 受信時(ACS エンドポイント):
[ ] 1. ハードニング済み XML パーサーで解析(DTD/XXE 遮断)
[ ] 2. スキーマ検証
[ ] 3. Response の署名検証(要求している場合)
[ ] 4. Status が Success であることを確認
[ ] 5. Assertion の署名検証 — 信頼された IdP 証明書で
[ ] 6. 以降のデータは署名されたノードからのみ読む
[ ] 7. Issuer が期待した IdP の entityID か
[ ] 8. Conditions: NotBefore/NotOnOrAfter(skew 許容値込み)
[ ] 9. AudienceRestriction が自分の entityID か
[ ] 10. SubjectConfirmationData: Recipient が自分の ACS URL か、
        NotOnOrAfter が有効か、InResponseTo が自分が発行した要求 ID か
[ ] 11. Assertion ID の再利用チェック(リプレイ防止キャッシュ)
[ ] 12. NameID Format が合意済みの形式か
[ ] 13. すべての検証を通過した後にのみアプリセッションを確立

SAML が 2026 年も生きている理由

  1. B2B 信頼関係の慣性 — 数万社の企業 IdP と SaaS がすでに SAML で接続されています。動いている信頼関係を撤去するビジネス上の動機は弱いのです。
  2. 調達要件 — エンタープライズ SaaS の購買チェックリストに「SAML SSO 対応」が今も明記されます。SSO をプレミアムプランに閉じ込める慣行(いわゆる SSO tax)への批判があること自体、SAML が標準要件である証左です。
  3. レガシー IdP エコシステム — Broadcom SiteMinder(現行 12.9)のようなレガシー WAM 製品群が今も大企業で稼働しており、これらの標準的な連携経路が SAML です。ヘッダーベース認証から標準プロトコルへの移行の過程でも、SAML が最初の停車駅になることが多いのです。
  4. プロトコル自体の完結性 — Web SSO という用途に限れば、SAML はすでに完成したプロトコルです。変化がないことは安定性でもあります。

ただし方向性は明確です。新機能(passkeys 連携、トークン交換、FAPI など)はすべて OIDC 陣営で起きており、Keycloak のようなモダン IdP は SAML と OIDC の両方をサポートするため、IdP をハブに置き、レガシー SP は SAML、新規アプリは OIDC で接続するハイブリッドが 2026 年の標準アーキテクチャです。

            [Keycloak 26.6 / Okta / Entra ID]
                  |                |
        SAML 2.0  |                |  OIDC
                  v                v
        [レガシー/B2B SaaS]   [新規 Web/モバイル/API]

おわりに

SAML 2.0 を一文で要約すると「XML Assertion をブラウザのリダイレクトとフォーム POST で運び、XML Signature で信頼を保証する Web SSO プロトコル」です。実務で覚えておくべきことは 3 つです。

  • Assertion の条件(時刻、Audience、Recipient、InResponseTo)を 1 つも漏らさず検証しなければなりません。署名検証だけでは不十分です。
  • Signature Wrapping の教訓: 署名が有効なノードとデータを読むノードは同一でなければなりません。自作せず、検証済みライブラリを使ってください。
  • 運用障害の三大要因は証明書の期限切れ、クロックスキュー、entityID/URL の不一致です。いずれも監視と自動化で予防できます。

次回の記事では OIDC の内部 — Authorization Code Flow、Discovery、JWKS、トークン検証 — を同じ深さで扱います。

参考資料