契約を結んだ最初の大口顧客は、自社の ID プロバイダーとのシングルサインオンを求めてきます。そしてその多くは SAML 2.0 です。この標準は 2005 年のもので、ブラウザーのリダイレクトとフォーム送信で XML を運び、エラーメッセージは曖昧なものから無言のものまでさまざまです。深刻なセキュリティホールを含め、SAML のバグの多くは、どのメッセージが正確にどこへ行き、サービスプロバイダーが何を確認すべきかを知らないことから生まれます。
この記事では、SP 起点のフローをシーケンス図でメッセージごとに追い、続いて IdP 起点のバリエーションとそのリスク、SAML レスポンスの構造、アプリのための検証チェックリスト、メタデータと証明書のローテーション、そして SAML と OpenID Connect の比較を説明します。
3 つの当事者
| 当事者 | SAML の用語 | 例 |
|---|---|---|
| ユーザーのブラウザー | ユーザーエージェント | 任意のブラウザー |
| 自社のアプリケーション | サービスプロバイダー(SP) | 自社の SaaS アプリ |
| 顧客のログインシステム | ID プロバイダー(IdP) | Microsoft Entra ID、Okta、Google Workspace、AD FS |
一般的な Web Browser SSO プロファイルでは、SP と IdP が直接通信することはありません。すべてのメッセージは、リダイレクトか自動送信されるフォームとしてブラウザーを経由します。両者が共有するのは、メタデータとして一度だけ交換する設定です。双方の entity ID、IdP のサインイン URL、SP の Assertion Consumer Service(ACS)URL、IdP の署名用証明書です。
SP 起点のフローを順に追う
ユーザーはアプリから始めます。
- ユーザーがセッションなしで
https://app.example.com/dashboardを開きます。 - アプリは、一意の ID、自身の entity ID、ACS URL を持つ
AuthnRequestを作成し、その ID を保留中のログイン状態に保存して、ブラウザーを IdP にリダイレクトします。HTTP-Redirect バインディングでは、リクエストはクエリ文字列のSAMLRequest(deflate 圧縮して base64 エンコードしたもの)として送られ、ユーザーが行きたかった場所を覚えておくRelayStateの値が添えられます。 - IdP がユーザーを認証します。パスワードと MFA、または既存の IdP セッションです。後者の場合、ユーザーには何も表示されません。
- IdP は、署名付きの
SAMLResponseと同じRelayStateを ACS URL に POST するフォームを含む HTML ページを返します。これが HTTP-POST バインディングで、小さなスクリプトがフォームを自動送信します。 - アプリはレスポンスを検証し、自身のセッション Cookie を作成して、
RelayStateに保存されたページにユーザーをリダイレクトします。
それ以降、ユーザーが誰かを決めるのはアプリ自身のセッションです。SAML が再び関わるのは、そのセッションが切れたときだけです。
バインディング仕様では RelayState は 80 バイトまでに制限されています。実際の戻り先 URL はサーバー側に保存し、短いキーを送りましょう。
SAML レスポンスの中身
アプリが読む部分だけに絞ると、レスポンスは次のようになります。
<samlp:Response ID="_r81c" InResponseTo="_req42" Destination="https://app.example.com/saml/acs">
<saml:Assertion ID="_a77e">
<saml:Issuer>https://idp.customer.com/entity</saml:Issuer>
<ds:Signature>…</ds:Signature>
<saml:Subject>
<saml:NameID>ana@customer.com</saml:NameID>
<saml:SubjectConfirmationData InResponseTo="_req42"
Recipient="https://app.example.com/saml/acs" NotOnOrAfter="2026-11-02T09:05:00Z"/>
</saml:Subject>
<saml:Conditions NotBefore="2026-11-02T08:59:30Z" NotOnOrAfter="2026-11-02T09:05:00Z">
<saml:AudienceRestriction><saml:Audience>https://app.example.com</saml:Audience></saml:AudienceRestriction>
</saml:Conditions>
<saml:AttributeStatement>…email, name, groups…</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
アサーションは署名付きの表明です。ユーザーが誰か(NameID)、誰がそう言っているか(Issuer)、誰に向けたものか(Audience)、有効期間、そしてどのリクエストへの応答か(InResponseTo)を示します。
検証チェックリスト
SAML のセキュリティが成功するか失敗するかは、ACS エンドポイントで決まります。ACS エンドポイントは次のことを行わなければなりません。
- 署名は IdP のメタデータにある証明書で検証します。 メッセージ自体に埋め込まれた証明書を使ってはいけません。
- 署名の対象になっている部分だけを読みます。 XML 署名ラッピング攻撃は、署名のない 2 つ目のアサーションを差し込み、コードがある要素を検証して別の要素を読むことを狙います。研究者たちは 2012 年に、主要な SAML フレームワーク 14 個のうち 11 個でこの欠陥を発見しました。そしてこの問題は繰り返し現れています。ruby-saml は 2024 年に CVSS 10.0 の署名回避(CVE-2024-45409)を、2025 年にはパーサー間の差異による 2 つの回避(CVE-2025-25291 と CVE-2025-25292)を修正しました。
Issuerを、この顧客の IdP の entity ID と照合します。Audienceを、自分の entity ID と照合します。DestinationとRecipientを、自分の ACS URL と照合します。NotBeforeとNotOnOrAfterを、時計のずれに備えて 1〜2 分の許容幅を持たせて確認します。InResponseToを、このブラウザー向けに発行し、まだ使われていないAuthnRequestの ID と照合します。- リプレイを拒否します。 アサーション ID を有効期限が切れるまで記録しておきます。
RelayStateは信頼できない入力として扱います。 リダイレクト先は相対パスか許可リストに限定してください。そうしないとオープンリダイレクトになります。
10 年にわたる SAML の脆弱性から得られる教訓はこうです。メンテナンスされているライブラリを使い、バージョンを固定し、すぐにパッチを当て、XML 署名の検証を自作しないことです。
IdP 起点の SSO と、それがより危険な理由
IdP 起点のフローでは、ユーザーは IdP から始めます。たいていは Okta や Microsoft My Apps ポータルのようなダッシュボードで、アプリのタイルをクリックします。IdP は、要求されていないレスポンスを ACS URL に POST します。AuthnRequest がなかったので、照合すべき InResponseTo もありません。
| SP 起点 | IdP 起点 | |
|---|---|---|
| 開始地点 | 自社のアプリ | IdP のポータル |
AuthnRequest |
あり | なし |
InResponseTo の確認 |
あり | 不可能 |
| ログイン CSRF への防御 | 組み込み | 追加の注意が必要 |
| ディープリンク | RelayState で可能 |
通常は既定のページに着地する |
InResponseTo がないと、有効なレスポンスを入手した攻撃者や、被害者のブラウザーにレスポンスを POST させた攻撃者は、被害者を攻撃者のアカウントにログインさせたり、盗んだレスポンスを有効期間内にリプレイしたりできます。顧客がダッシュボードのタイルを必要とする場合、より安全なのは、アプリのログイン URL を開くタイルにして、そこから通常の SP 起点のフローを始めるパターンです。要求されていないレスポンスを受け付けざるを得ない場合は、有効期間を短くし、アサーション ID を 1 回限りに強制し、全体ではなく顧客ごとに許可してください。
メタデータ、証明書、マルチテナント構成
顧客ごとの IdP は、それぞれ独立した信頼関係です。テナントごとに、IdP の entity ID、サインイン URL、署名用証明書、属性マッピングを保存します。このテナントごとの設定が通常どこに置かれるかは、マルチテナント SaaS アーキテクチャ図で示しています。
署名用証明書には有効期限があり、期限切れや差し替えられた証明書は、顧客全体が突然サインインできなくなる最も一般的な原因です。切り替え期間中は IdP ごとに 2 つの証明書を受け入れ、IdP がメタデータ URL を公開しているなら定期的に読み込み、期限切れの数週間前にアラートを出しましょう。
SAML は認証を行いますが、ユーザー一覧の同期は行いません。初回ログイン時にアカウントを作るのは簡単ですが、退職した人のアクセスを取り消すにはプロビジョニングが必要で、通常は SCIM 2.0(RFC 7644)を使います。これがないと、無効化された従業員も、アプリがすでに発行したセッションを使い続けられます。
SAML か OpenID Connect か
| SAML 2.0 | OpenID Connect | |
|---|---|---|
| 形式 | XML 署名付きの XML アサーション | JSON と JWT の ID トークン |
| 伝送 | ブラウザーのリダイレクトとフォーム送信 | リダイレクトとバックチャネルでのトークンリクエスト |
| モバイルアプリと SPA | 扱いにくい | それらのために設計されている |
| 典型的な用途 | SaaS アプリへのエンタープライズ SSO | コンシューマー向けログイン、API、最新のエンタープライズ SSO |
新しいアプリは OpenID Connect から始めてもかまいませんが、企業の顧客は今後も SAML を求めてくるので、ほとんどの B2B 製品は両方に対応しています。OpenID Connect の土台となるプロトコルは、OAuth 2.0 フロー図で扱っています。
SAML SSO の図
ダイアグラムの内容
- User
- Browser
- Service provider (your app)
- Identity provider
- no IdP session yet
- 1. Open the app
- 2. GET /dashboard (no session)
- 3. 302 to IdP: SAMLRequest + RelayState
- 4. GET /sso?SAMLRequest=…&RelayState=… (HTTP-Redirect)
- 5. Login page
- 6. Password + MFA
- 7. HTML form: signed SAMLResponse + RelayState
- 8. POST /saml/acs (HTTP-POST, auto-submit)
- 9. 302 /dashboard + session cookie
- 10. GET /dashboard (cookie)
- 11. 200 OK
図の読み方は次のとおりです。
- 4 つの参加者。 ユーザー、ブラウザー、サービスプロバイダーとしてのアプリ、ID プロバイダーです。時間は下に向かって流れます。
- リダイレクトは、
AuthnRequestとRelayStateを、ブラウザーを経由してアプリから IdP に運びます。 - 任意のフラグメントはログインのステップです。既存の IdP セッションがあればスキップされ、ユーザーには一瞬のリダイレクトしか見えません。
- フォーム送信は、署名付きの
SAMLResponseを ACS URL に届けます。アプリはそれを検証し、自身のセッション Cookie を設定します。 - Markdown ブロックには、ACS エンドポイントが行うべき確認をまとめています。
デバッグに役立つ SSO フローを描くには
- ブラウザーを独立した参加者として描きます。 SAML はフロントチャネルのプロトコルです。ブラウザーを隠すと、メッセージが傍受やリプレイされうる場所も隠れてしまいます。
- 各矢印にバインディングを書きます。 リダイレクトか POST か、そしてパラメーター名です。設定ミスの大半は、URL かバインディングの誤りです。
- メッセージに ID を書きます。
InResponseToがリクエストの ID を返していることを示せば、なぜそれを確認するのかが伝わります。 - IdP 起点のフローは別の図に描き、メインの図の分岐にはせず、リスクを書き添えます。
参加者、フラグメント、ラベルについてはシーケンス図の例で詳しく扱っています。より広いアクセスモデルの中で ID プロバイダーがどこに位置するかは、ゼロトラストアーキテクチャ図を参照してください。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- ユーザー用にシーケンスパックの アクター を追加し、続いて ライフライン ノードを 3 つ追加します。Browser、Service provider、Identity provider です。
- メッセージを上から下へ、ライフライン間のエッジとして描きます。最初のリクエスト、
SAMLRequestとRelayStateを付けたリダイレクト、IdP へのリクエスト、SAMLResponseを含むフォーム、ACS URL への POST、そしてセッション Cookie を伴う最後のリダイレクトです。 - ログインと MFA のステップを囲むように、演算子を
optにしたシーケンスパックの 複合フラグメント を追加します。 - 検証チェックリストを書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 6 個で、Free プランの範囲に収まります。
よくある質問
SP 起点と IdP 起点の SSO の違いは何ですか? SP 起点のログインはアプリから始まり、アプリが IdP に AuthnRequest を送ります。IdP 起点のログインは IdP のポータルから始まり、IdP が要求されていないレスポンスをアプリに POST します。後者には照合すべきリクエストがないため、リプレイやログイン CSRF への防御が弱くなります。
ACS URL とは何ですか? Assertion Consumer Service URL は、SAML レスポンスを HTTP POST で受け取るアプリ側のエンドポイントです。IdP で設定し、各レスポンスの Destination と Recipient の値と一致していなければなりません。
RelayState は何に使われますか? ユーザーが行きたかった場所を運び、ログイン後にアプリがそこへ案内できるようにします。80 バイトまでに制限されており、信頼できない入力なので、サーバー側の状態を指すキーとして使うか、許可リストで検証してください。
SAML は時代遅れですか? OpenID Connect より古く、安全に実装するのも難しいですが、多くの企業向け ID プロバイダーや購買担当者が期待する標準であり続けています。ほとんどの B2B 製品は両方に対応しています。
SAML は退職したユーザーを削除してくれますか? いいえ。SAML が認証するのはログイン時だけです。プロビジョニングの解除には、SCIM などのプロビジョニング連携と、アプリ側での妥当なセッション有効期間が必要です。