セキュリティ

SAML-tracerの使い方:SSO失敗の原因を採取した通信から切り分ける

会員管理システムにおける主な機能一覧

SAML-tracerは、SSOの途中でブラウザを通るSAMLRequestとSAMLResponseを捕まえ、復号したXMLをその場で見せるブラウザ拡張です。この記事では、2026年11月時点の1.9.5系を前提に、入手と権限の確認、SSOを再現して通信を採取する手順、表示された値からSSO失敗の原因を切り分ける順番を扱います。さらに、書き出したJSONに何が残るかをソースで確かめ、診断と伏せ字処理をするPythonスクリプトを実行結果つきで載せました。

まとめ:SAML-tracerで採取して原因を絞る順番と共有前の伏せ字処理

使い方の要点は3つの段階に分かれます。まず拡張を開いてからSSOを最初からやり直し、IdPへ向かう要求とACSへ戻る応答の2本を採取してください。次にSAMLタブで応答のStatusCodeを読み、Success以外ならIdPが拒否、SuccessなのにログインできないならSPの検証で落ちていると判断します。後者はAudience、有効期間、署名の位置の3点を照合すれば大半が片付きます。

注意したいのは書き出しです。既定の「Mask values」はPOSTの値とCookieだけを伏せ、復号済みXMLを入れたsaml欄とURLのクエリは原文のまま残します。ベンダーや社外に渡す前に、利用者のNameIDや属性を自分で伏せてください。

本番の利用者端末には入れません。全URLの通信を読める権限を持つため、使うのは検証用のブラウザプロファイルに限ります。

SAML-tracerの入手と対応ブラウザ・権限を導入前に確かめる手順

SAML-tracerはSimpleSAMLphpプロジェクトが公開しているオープンソースの拡張で、ライセンスは2条項BSDです。名前の似た別物が複数あるため、入れる前に配布元を確かめます。

Firefox・Chrome・Edgeで入れる正規版の配布元と版番号1.9.5の確認

正規版の配布元はGitHubのsimplesamlphp/SAML-tracerのREADMEに載っている2か所です。FirefoxはFirefox Add-ons、ChromeとEdgeはChrome Web Storeから入れます。

2026年11月5日時点で、リポジトリの最新タグはsamltracer-1.9.5、manifest.jsonの版も1.9.5でした。Chrome Web Storeの掲載は同じ1.9.5で、最終更新は2026年8月20日、利用者数は40万と表示されています。提供元の欄が「SimpleSAMLphp」になっていることが、正規版を見分ける手がかりです。

Firefox版はmanifest.jsonのstrict_min_versionが140.0のため、古いESR版のFirefoxには入りません。社内の標準ブラウザが古い場合は、Chrome系で使うほうが早く済みます。

全URLへのwebRequest権限を持つ拡張を業務端末に入れる際の判断

manifest.jsonが要求する権限はwebRequestと、対象ホストとして<all_urls>です。つまり、拡張を開いている間はすべてのサイトのURL、ヘッダ、フォームの送信内容を読めます。SAMLだけを選んで読む作りではありません。

この権限の重さを踏まえ、業務端末ではSSOの検証専用のブラウザプロファイルを作り、そこにだけ入れる運用を勧めます。普段の業務で使うプロファイルに入れたままにすると、メールや経理システムの通信まで記録の対象に入るためです。拡張の管理をグループポリシーで制限している組織なら、情シスに許可リストへの追加を依頼してから入れてください。

SAML-tracerでSSOを再現して要求と応答を採取する操作手順

SAML-tracerは開いた後の通信しか記録しません。すでにSPにログイン済みのセッションがあると、要求も応答も流れずに画面だけが進みます。

起動から再現までの4工程とSP起点・IdP起点での採取の違い

起動はツールバーのアイコンか、manifest.jsonに登録されたショートカットのAlt+Shift+Sです。別ウィンドウでトレース画面が開きます。

  1. トレース画面の「Clear」で過去の記録を消し、SPとIdPのCookieが残っていないプライベートウィンドウを用意する
  2. SP起点ならSPの保護ページを開き、IdP起点ならIdPのポータルでアプリのアイコンを押す
  3. IdPでサインインし、エラー画面かSPの画面が出るまで操作する
  4. 「Show protocol requests only」を押してSAMLのある行だけに絞り、SAMLロゴの付いた行を選ぶ

SP起点なら、IdPへ向かう要求の行と、ACSへ戻るPOSTの行の2本が並びます。IdP起点では要求の行が無く、届くのは応答だけです。Okta DeveloperのSAML-tracerの手順書も、SP起点とIdP起点を分けて再現する流れで説明しています。起点による応答の違いはSAML IdP-initiatedとは:SP-initiatedとの違いとACS実装の判断で扱いました。

HTTP・Parameters・SAML・Summaryの4タブで読む値と見え方の違い

行を選ぶと、SAMLを含む行には4つのタブが出ます。READMEに書かれているのは3つですが、ソースのSAMLTrace.jsではSummaryタブも追加されています。

タブ 表示される内容 障害調査で見る値
HTTP 要求と応答のヘッダ 応答のステータスと遷移先
Parameters GETとPOSTの値 RelayStateと署名の有無
SAML 復号済みのXML全文 StatusCodeと有効期間
Summary 主要な属性の一覧表 NameIDと受け取った属性

SAMLタブの中身は、送られ方によって復号の手順が違います。Redirectで届いた値はbase64を戻した後にDEFLATEを展開し、POSTで届いた値はbase64を戻すだけで表示しています。符号化の違いそのものはSAML HTTP POSTバインディングとは:Redirect・Artifactとの違いを参照してください。

Summaryは便利ですが、Response側で載る項目はDestination、ID、IssueInstant、Issuerと、アサーションのNameIDと属性に限られます。AudienceとNotOnOrAfter、StatusCodeは表に出ないので、SAMLタブのXMLで確かめてください。埋め込まれた署名証明書は、Summaryからsaml0.cerの名前で保存できます。

SAML-tracerに表示された値でSSO失敗の原因を工程別に切り分ける

採取した応答の読み方は、StatusCodeの値で2つに分かれます。最初にACSへのPOSTの行を選び、SAMLタブでsamlp:StatusCodeのValueを読みます。

StatusCodeがSuccess以外なら直前のAuthnRequestで見る3か所

OASIS SAML 2.0 Core仕様のSection 3.2.2.2は、最上位のStatusCodeをSuccess、Requester、Responder、VersionMismatchの4つに限っています。Requesterなら要求を送ったSP側の不備、ResponderならIdP側の処理の失敗です。

Requesterが出たら、1本前の要求の行に戻ります。Microsoft LearnのSAML SSOのデバッグ手順が、Entra IDで照合するよう挙げているのは次の3か所です。

  • Destinationが、IdPのSSO URLと一致しているか
  • Issuerが、IdPに登録したアプリの識別子と1文字違わず一致しているか
  • AssertionConsumerServiceURLが、SPが応答を受け取るURLになっているか

Requesterの下に第2階層のStatusCodeが付くこともあり、InvalidNameIDPolicyならNameIDPolicyのFormatを疑います。要求の要素ごとの読み方とEntra IDのAADSTSコードの対応はSAMLリクエストとは:AuthnRequestの要素・デコード・署名とエラー対処にまとめました。

Successなのにログイン画面へ戻るときのAudience・時刻・署名の照合

StatusCodeがSuccessなら、IdPは認証を済ませて応答を返しています。それでもログインできないのは、SPが受け取った応答の検証で落としているためです。SAMLタブのXMLで、次の4か所をSPの設定値と比べます。

見る要素 SPの設定と比べる値 よくある不一致
Audience SPのentityID 末尾のスラッシュの有無
NotBefore・NotOnOrAfter SPサーバーの現在時刻 SPの時計の遅れや進み
Destination ACSのURL httpとhttpsの食い違い
ds:Signatureの位置 SPが署名を求める要素 応答全体だけに署名

時刻は、SAMLタブだけでは判断できません。SP側の時計で比べる必要があるため、同じ行のHTTPタブで、ACSが返した応答のDateヘッダを読みます。これがSPサーバーの時刻です。NotOnOrAfterより後なら、SPの時計か処理の遅れが原因です。

署名は、Response要素の直下とAssertion要素の直下のどちらにds:Signatureがあるかを見ます。SPがアサーションの署名を必須にしているのに応答全体にしか付いていなければ、IdP側の署名設定を変えてください。アサーションの検証の実装はSAMLアサーションとは:XML構造の読み方と署名・Audience・有効期限の検証実装、NameIDの形式が噛み合わない場合はSAML NameIDFormatとメタデータの不一致でSSOが失敗する原因の切り分けで扱っています。

SAMLタブにアサーションが出ないときのArtifactと暗号化の見分け方

SAMLロゴは付くのに、アサーションの中身が読めない場合が2つあります。

1つはArtifactバインディングです。ブラウザを通るのはSAMLartという参照値だけで、SAML-tracerはその中のEndpoint IndexとSource IDを表示します。応答本体は、OASIS SAML 2.0 Bindings仕様のSection 3.6が定めるとおりSPとIdPのサーバー間で引き取られます。ブラウザには流れません。この場合はSP側のログで応答を確認します。

もう1つは暗号化されたアサーションです。XMLにsaml:EncryptedAssertionがあれば、中身はSPの秘密鍵が無いと読めません。SAML-tracerのソースに復号の処理は無いため、検証環境でIdPの暗号化を一時的に外すか、SP側で復号した後の値をログに出して確かめます。

書き出しJSONの中身とMask valuesでは伏せられない欄の扱い

ベンダーのサポートに調査を依頼すると、SAML-tracerの記録を送るよう求められることがあります。トレース画面の「Export」で書き出すと、SAML-tracer-export-に書き出し時刻を付けた名前のJSONファイルになります。

None・Mask values・Remove valuesの3設定別の書き出し内容

書き出しの画面では、伏せ方を3つから選びます。既定で選ばれているのはMask valuesです。処理の中身はSAMLTraceIO.jsとfilters.jsで確かめました。

設定 POSTの値 CookieとSet-Cookie
None 原文のまま 原文のまま
Mask values SHA-256のハッシュ値 SHA-256のハッシュ値
Remove values 連番の置き換え文字列 連番の置き換え文字列

Mask valuesでは、値が{hash:…}の形に置き換わります。Remove valuesでは{obf:alternative_0}のような連番になり、同じ値に振られる番号は同一です。どちらの設定でも、POSTの生データの欄は{overwritten}で上書きされます。

saml欄の復号済みXMLとURLのクエリが伏せ字の対象外になる理由

3つの設定が触るのは、POSTの値、POSTの生データ、要求のCookieヘッダ、応答のSet-Cookieヘッダの4か所だけです。書き出しは記録の各行をそのままJSONに変換するので、ほかの欄は原文で残ります。

問題になるのはsaml欄です。ここには、SAML-tracerが表示用に復号したXMLの全文が入っています。POSTのSAMLResponseをハッシュにしても、同じ内容が復号済みの形で残るわけです。NameIDのメールアドレス、氏名や所属などの属性、署名証明書まで読めます。

Redirectで届いた要求も同様です。URLとGETパラメータの欄は伏せ字の対象外なので、SAMLRequestとRelayStateが原文で残ります。RefererやAuthorizationなど、Cookie以外のヘッダも書き換えられません。

社外に送るJSONは、受け取る側が必要な行だけに絞り、saml欄の個人情報を伏せてから渡します。トレース画面の「Hide resources」と「Show protocol requests only」で絞った状態で書き出すと、表示中の行だけがファイルに入ります。

書き出しJSONをPythonで診断し伏せ字版を作るスクリプト

ここまでの照合と伏せ字の処理を、書き出したJSONに対して機械的に行うスクリプトです。外部パッケージは使わず、Pythonの標準ライブラリだけで動きます。

応答の照合と伏せ字の処理を1本にまとめたPythonスクリプトの全文

次のsamltrace_check.pyは、書き出しJSONの各行からSAMLのXMLを取り出し、要求のIssuerと、応答のStatusCode、InResponseTo、Destination、Audience、有効期間、署名の位置を確かめます。--redact-toを付けると、Mask valuesが残す欄を伏せたJSONを別名で保存します。

import argparse
import base64
import json
import re
import xml.etree.ElementTree as ET
import zlib
from datetime import datetime, timedelta
from email.utils import parsedate_to_datetime
from urllib.parse import urlsplit

NS = {
    "samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
    "saml": "urn:oasis:names:tc:SAML:2.0:assertion",
    "ds": "http://www.w3.org/2000/09/xmldsig#",
}


def parse_time(s):
    # 2026-11-05T01:00:00Z / 2026-11-05T01:00:00.123Z の両方を読む
    s = re.sub(r"\.\d+", "", s.replace("Z", "+00:00"))
    return datetime.fromisoformat(s)


def saml_xml(req):
    # 書き出しJSONの saml 欄は復号済みXML。無ければ POST の値を自分で復号する
    if req.get("saml"):
        return req["saml"]
    for k, v in req.get("post") or []:
        if k in ("SAMLRequest", "SAMLResponse") and not v.startswith("{"):
            return base64.b64decode(v).decode("utf-8")
    for k, v in req.get("get") or []:
        if k in ("SAMLRequest", "SAMLResponse"):
            return zlib.decompress(base64.b64decode(v), -15).decode("utf-8")
    return None


def check(trace, sp_entity_id, acs_url, skew):
    request_ids = {}
    for n, req in enumerate(trace["requests"], 1):
        xml = saml_xml(req)
        if not xml:
            continue
        # 学習用に標準ライブラリで読む。本番の受信処理では defusedxml 等を使う
        root = ET.fromstring(xml)
        kind = root.tag.split("}")[1]
        print(f"[{n}] {req['method']} {urlsplit(req['url']).netloc} {kind}")
        if kind == "AuthnRequest":
            request_ids[root.get("ID")] = n
            issuer = root.findtext("saml:Issuer", namespaces=NS)
            if issuer != sp_entity_id:
                print(f"  NG Issuer {issuer!r} が SP の entityID と一致しない")
            continue
        if kind != "Response":
            continue
        status = root.find("samlp:Status/samlp:StatusCode", NS).get("Value").rsplit(":", 1)[1]
        sub = root.find("samlp:Status/samlp:StatusCode/samlp:StatusCode", NS)
        print(f"  StatusCode: {status}" + (f" / {sub.get('Value').rsplit(':', 1)[1]}" if sub is not None else ""))
        if status != "Success":
            print("  -> IdP側で拒否。直前の AuthnRequest の Issuer・ACS・NameIDPolicy を見る")
            continue
        irt = root.get("InResponseTo")
        if irt is None:
            print("  INFO InResponseTo なし(IdP-initiated)")
        elif irt not in request_ids:
            print(f"  NG InResponseTo {irt} に対応する AuthnRequest がトレース内に無い")
        if root.get("Destination") != acs_url:
            print(f"  NG Destination {root.get('Destination')} が ACS URL と一致しない")
        a = root.find("saml:Assertion", NS)
        if a is None:
            print("  INFO Assertion が暗号化されている(EncryptedAssertion)")
            continue
        signed = [p for p, el in (("Response", root), ("Assertion", a)) if el.find("ds:Signature", NS) is not None]
        print(f"  署名の位置: {', '.join(signed) or 'なし'}")
        aud = a.findtext("saml:Conditions/saml:AudienceRestriction/saml:Audience", namespaces=NS)
        if aud != sp_entity_id:
            print(f"  NG Audience {aud!r} が SP の entityID と一致しない")
        cond = a.find("saml:Conditions", NS)
        nb, noa = parse_time(cond.get("NotBefore")), parse_time(cond.get("NotOnOrAfter"))
        # ACS が返した応答の Date ヘッダ=SP サーバーの時計で有効期間を判定する
        date = next((h["value"] for h in req.get("responseHeaders") or [] if h["name"].lower() == "date"), None)
        if date:
            sp_now = parsedate_to_datetime(date)
            if sp_now < nb - skew or sp_now >= noa + skew:
                print(f"  NG SP時刻 {sp_now:%H:%M:%S} が有効期間 {nb:%H:%M:%S}〜{noa:%H:%M:%S} の外")
        nid = a.find("saml:Subject/saml:NameID", NS)
        print(f"  NameID Format: {nid.get('Format', '').rsplit(':', 1)[-1]}")


def redact(trace):
    # SAML-tracer の Mask values が触らない欄を伏せる(saml・URLのクエリ・GETの値・全ヘッダ値)
    for req in trace["requests"]:
        req["saml"] = "{removed}" if req.get("saml") else req.get("saml")
        req["url"] = req["url"].split("?", 1)[0] + ("?{removed}" if "?" in req["url"] else "")
        req["get"] = [[k, "{removed}"] for k, _ in req.get("get") or []]
        for h in (req.get("requestHeaders") or []) + (req.get("responseHeaders") or []):
            if h["name"].lower() in ("cookie", "set-cookie", "authorization", "location", "referer"):
                h["value"] = "{removed}"
    return trace


if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("export_json")
    p.add_argument("--sp-entity-id", required=True)
    p.add_argument("--acs-url", required=True)
    p.add_argument("--skew-sec", type=int, default=180)
    p.add_argument("--redact-to")
    args = p.parse_args()
    with open(args.export_json, encoding="utf-8") as f:
        trace = json.load(f)
    check(trace, args.sp_entity_id, args.acs_url, timedelta(seconds=args.skew_sec))
    if args.redact_to:
        with open(args.redact_to, "w", encoding="utf-8") as f:
            json.dump(redact(trace), f, ensure_ascii=False, indent=2)
        print("伏せ字版を書き出し:", args.redact_to)

有効期間は、書き出し時刻ではなく、ACSが返した応答のDateヘッダで判定しています。書き出しは調査が終わった後に行うことが多く、その時刻で比べると正常な応答まで期限切れに見えるためです。

実行コマンドと結果で見るAudienceの末尾スラッシュと期限切れの検出

SP起点のログインを1回と、別の失敗を1回含む書き出しJSONで実行しました。SPのentityIDとACSのURLは、SPのメタデータに書かれた値を渡します。

python samltrace_check.py SAML-tracer-export-sample.json \
  --sp-entity-id https://sp.example.jp/metadata \
  --acs-url https://sp.example.jp/saml/acs \
  --redact-to shared.json

Python 3.14.10での出力は次のとおりです。

[1] GET idp.example.jp AuthnRequest
[2] POST sp.example.jp Response
  StatusCode: Success
  署名の位置: Assertion
  NG Audience 'https://sp.example.jp/metadata/' が SP の entityID と一致しない
  NG SP時刻 01:09:12 が有効期間 00:55:05〜01:05:05 の外
  NameID Format: emailAddress
[3] POST sp.example.jp Response
  StatusCode: Requester / InvalidNameIDPolicy
  -> IdP側で拒否。直前の AuthnRequest の Issuer・ACS・NameIDPolicy を見る
伏せ字版を書き出し: shared.json

2行目の応答は、Successなのに2つの理由で落ちる例です。Audienceの末尾にスラッシュが1つ多く、ACSが受け取った時点のSPの時刻はNotOnOrAfterを4分過ぎていました。3行目はIdPが要求を拒否した例で、NameIDPolicyのFormatを直す対象だと分かります。

元のJSONにはNameIDのメールアドレスが1回、URLのSAMLRequest=が1回含まれていました。伏せ字版のshared.jsonでは、どちらも0回です。

SAML-tracerを使う場面と開発者ツールやベンダー拡張へ切り替える条件

SAML-tracerは、IdPとSPのどちらにも依存せずに通信を見られる点で、SAML連携の開発と検証の標準の道具になります。一方で、使わないほうがよい場面もはっきりしています。

本番の利用者端末でSAML-tracerを使わせない条件と代わりの採取方法

本番環境でログインできない利用者がいても、その人の端末にSAML-tracerを入れてもらう方法は取りません。全URLの通信を読める拡張を、検証の目的で一般の利用者に入れさせることになるからです。採取の後に外し忘れると、全URLを読める拡張がその端末に残ったままになります。

代わりに、同じ属性を持つ検証用アカウントを用意し、開発者の端末で再現します。どうしても利用者の環境で採る必要があるなら、ブラウザ標準の開発者ツールで「ログを保持」を有効にし、ACSへのPOSTの本文だけを送ってもらう方法です。拡張を入れずに済み、採取の範囲も1本に絞れます。

Entra IDのMy Apps拡張やOktaの手順書を先に使うべき構成

IdPがMicrosoft Entra IDなら、まず管理センターの「Test single sign-on」を使ってください。Microsoft Learnの手順では、Microsoft純正のMy Apps Secure Sign-in Extensionを入れておくと、エラー時にSAMLの要求を取得して、設定の直し方まで表示されます。Entra ID側の設定ミスなら「Fix it」で直せることもあります。

SAML-tracerに切り替えるのは、IdP側のツールで原因が出ず、SPの検証で落ちていると分かった段階です。複数の取引先のIdPを受け入れるSaaSや、IdPを自社で運用しない業務システムでは、最初からSAML-tracerで採るほうが早く片付きます。

テナントごとのIdP設定、署名鍵の更新、障害時に採取する手順の整備まで含めたSAML SPの組み込みは、一創の認証基盤・ID管理システム開発で設計から実装まで担当しています。

よくある質問

SAML-tracerの導入と使い方で、よく出る質問をまとめました。

SAML-tracerはChromeでも使えますか?

使えます。READMEはFirefox版のほかに、ChromeとEdge向けのChrome Web Store版を正規の配布先として載せています。2026年11月5日時点で、Chrome Web Store版の版番号は1.9.5、提供元はSimpleSAMLphpです。名前の似た別の拡張も並ぶため、提供元の表示を確かめてから入れてください。

SAML-tracerにSAMLの行が表示されないのはなぜですか?

多いのは、SPかIdPにログイン済みのセッションが残っていて、SAMLの通信そのものが流れていない場合です。プライベートウィンドウでSSOを最初からやり直してください。Artifactバインディングでは、ブラウザに流れるのはSAMLartだけで、応答本体はサーバー間で引き取られるため表示されません。

SAML-tracerで書き出したJSONはそのまま社外に送ってよいですか?

送らないでください。既定のMask valuesで伏せられるのは、POSTの値とCookieだけです。復号済みXMLが入ったsaml欄、URL、GETパラメータは原文で残るため、NameIDのメールアドレスや属性がそのまま読めます。必要な行に絞り、個人情報を伏せてから渡します。

SAML-tracerでSAMLResponseの署名は検証できますか?

検証はできません。SAML-tracerが行うのは復号と表示までで、署名の正しさは確かめません。Summaryタブから埋め込まれた証明書を保存し、IdPのメタデータの証明書と同じかを比べることはできます。署名そのものの検証は、SPのライブラリかxmlsec1などのツールで行います。

SAML-tracerとブラウザの開発者ツールはどう使い分けますか?

開発やIdPとの結合試験ではSAML-tracerが向いています。復号とXMLの整形を自動で行い、要求と応答を並べて見られるためです。拡張を入れられない利用者の端末で採るときは、開発者ツールのネットワークタブでログを保持し、ACSへのPOSTの本文だけを採ります。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  2. 2026.11.05 テックブログ Linuxカーネルの脆弱性CVE-2026-98432:修正版の見分け方とnftablesの緩和策
  3. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  4. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  5. 2026.11.04 テックブログ CVE-2026-119977:WordPress更新中のRCEと安全な更新手順

RELATED POSTS 関連記事

目次