---
title: "SAML NameIDFormatとメタデータの不一致でSSOが失敗する原因の切り分け"
url: "https://www.issoh.co.jp/tech/details/17589/"
published: 2026-09-11
updated: 2026-09-25
categories: ["セキュリティ"]
publisher: "株式会社一創"
---

# SAML NameIDFormatとメタデータの不一致でSSOが失敗する原因の切り分け

SAML連携の障害は、設定画面のどこが間違っているかを探すより、どの工程で止まったかを先に確定させるほうが速く片付きます。この記事では、IdPとSPの間で交換される実データを採取したうえで、原因をNameIDFormat、メタデータ、バインディングの3系統に分けて切り分ける手順を扱います。NameIDFormatのURI、RelayStateの80バイト上限、HTTP-Redirect時のDEFLATE処理は、OASIS SAML 2.0の各仕様書で原文を確認して記載しました。SAMLResponseを復号して条件と宛先を確認するコードも置いています。規格そのものの仕組みと正常系のフローは扱いません。

## まとめ：NameIDFormatとメタデータの照合から始めるSAML連携の失敗切り分け

SAML連携が繋がらないとき、原因の大半はNameIDFormatの不一致、メタデータのentityIDとACS URLの不一致、バインディングのエンコード差という3系統に収まります。切り分ける前に設定値を手当たり次第に変えると、IdP側とSP側の両方が中途半端な状態になり復旧が遠のきます。

最初にやることは1つだけ。SAMLRequestとSAMLResponseの実データを採取し、base64をデコードして中身を読みます。StatusCodeがSuccess以外ならIdP側で止まっており、Successなのに画面が進まないならSP側の検証で落ちている状態です。突き合わせるのは設定画面に入力した値ではなく、返ってきた値のほう。entityIDとAudienceは末尾のスラッシュ1文字の差でも落ちます。

判断としては、SAMLのSP実装を署名検証まで自前で書くのは避けてください。新規で認証だけを外に出すなら、SAMLよりOIDCのほうが実装量も運用負荷も小さく収まります。条件は後半の章で明示しました。

## SAML連携が止まる工程の特定とブラウザに残る通信の採取手順

切り分けの出発点は、設定ファイルではなく実際に流れた通信です。SAMLはブラウザを経由してメッセージが往復するため、サーバーのログだけではどちらがどの値を送ったのか分かりません。

### SAML-tracerで通信を採取しStatusCodeで原因を三系統に分ける手順

採取には、ブラウザ拡張のSAML-tracerか、開発者ツールのネットワークタブでフォームPOSTのペイロードを見る方法を使います。後者はリダイレクトで画面が遷移するとログが消えるため、保持オプションを有効にしてから再現してください。

採取したSAMLResponseをデコードしたら、まず`samlp:StatusCode`のValue属性を読み、次の3つに振り分けます。

- Success以外：IdPが拒否している。AuthnRequestのIssuer、ACS URL、NameIDPolicyを疑う
- Successだがログイン画面に戻る：SPの検証で落ちている。署名、Audience、NotOnOrAfter、NameIDの形式を疑う
- 届かない：バインディングかネットワーク経路の問題。POST先URLとHTTPメソッドを疑う

IdP起点ではSAMLResponseに`InResponseTo`属性が付きません。SP側がこれを必須にしていると、IdP起点のログインだけが落ちる点に注意してください。Microsoft Entra IDなら`StatusMessage`にAADSTSで始まるコードが入り、返却形式は[Microsoft Learnのシングルサインオン SAMLプロトコル](https://learn.microsoft.com/ja-jp/entra/identity-platform/single-sign-on-saml-protocol)に実例つきで載っています。規格そのものや正常系のフローは[SAMLとは？認証フロー・IdP/SPの仕組みとOAuth・OIDCとの使い分け](https://www.issoh.co.jp/tech/details/13299/)に整理してあります。

## NameIDFormatの仕様値とIdPが実際に返すNameIDの突き合わせ

NameIDはSPがユーザーを識別するための値です。ここが噛み合わないと、認証自体は通るのにユーザー特定ができずログインが完了しません。設定画面の選択肢名ではなくURIそのものを突き合わせます。

### 仕様が定めるNameIDFormatのURI一覧と1.1系と2.0系の別

指定できるURIは[OASIS SAML 2.0 Core仕様](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)のSection 8.3に列挙されています。実装で取り違えやすいのは、SAMLは2.0なのにFormatのURIには1.1系の名前空間が混ざる点です。

- 1.1系：unspecified（解釈は実装に委ねられる）、emailAddress（RFC 2822のaddr-spec形式）、X509SubjectName
- 2.0系：persistent（IdPとSPの組ごとの不透明な恒久ID）、transient（そのSSO操作限りの一時ID）、entity、kerberos

URIは`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`のように名前空間の版番号を挟んだ形です。unspecifiedには1.0系の表記も仕様書中に現れますが、Section 8.3.1が定義しているのは1.1系のほう。設定ファイルに1.0系を書いてIdPに弾かれる事故が起きるので、コピー元の版を確認してください。emailAddressの値は`local-part@domain`の形でなければならず、表示名を添えた形式は仕様違反になります。

### persistentとtransientの256文字制限とDB設計への影響

persistentとtransientは、どちらも値の長さが256文字を超えてはならないとCore仕様が定めています。persistentはIdPとSPの組み合わせごとに擬似乱数で生成され、ユーザー名など実際の識別子との対応が推測できる値であってはなりません。SP側のDBにはメールアドレスではなく不透明な文字列が入ります。

これがDB設計に効きます。外部ID列をvarchar(64)などで切っていると、長いpersistent IDが切り詰められて別ユーザーと衝突しかねません。上限に合わせてvarchar(256)以上を確保し、ユニーク制約を付けてください。transientは毎回値が変わるため、これを選んだまま初回ログイン時にユーザーを自動作成する実装を入れると、ログインのたびに新規ユーザーが増えます。

### NameIDPolicyを送っても指定外の値が返る場合の対処手順

SPはAuthnRequestの`NameIDPolicy`要素で希望するFormatを要求できますが、この要素はオプションでIdPが要求どおりに返す保証はありません。Microsoft Entra IDは指定できる値を4つに限定しており、それ以外を指定するとエラーになります。unspecifiedならEntra ID側が形式を選び、ペアワイズ識別子を発行する挙動です。

対処は3段階で進めます。

1. 採取したSAMLResponseから`saml:NameID`のFormat属性を読み、IdPが実際に何を返しているかを確定する
2. SP側の期待値を、返ってきたFormatに合わせる。IdP側を変えるのは他SPへの影響を確認してから
3. 属性文で代替できるなら、NameIDに依存せず`AttributeStatement`の特定属性でユーザーを特定する実装へ切り替える

Entra IDが発行するNameIDとクレームの形式は[Microsoft LearnのSAMLトークンのリファレンス](https://learn.microsoft.com/ja-jp/entra/identity-platform/reference-saml-tokens)に記載があります。IdPの製品選定そのものを見直す段階なら、[IDaaSとは？SSO・IdPとの違いとSAML・OIDC・SCIM連携](https://www.issoh.co.jp/tech/details/16401/)で各方式の守備範囲を整理しています。

## SAMLメタデータXMLの読み方とentityID・ACS・証明書の登録手順

メタデータはIdPとSPが互いの設定を交換するためのXMLです。設定画面しか触っていないと、どの値がどこに対応するのか分からなくなります。XMLを直接読むほうが速く確実です。

### entityIDとAudienceの一致条件と設定ミスの見つけ方

SPメタデータのルート要素`EntityDescriptor`が持つ`entityID`属性は、そのSPを一意に識別するURIです。IdPはこの値を、返すアサーションの`AudienceRestriction`内の`Audience`にそのまま入れ、SPは自分のentityIDと一致するかを検証します。

不一致は、設定画面上は同じに見えるのに落ちる形で現れます。多いのは末尾スラッシュの有無、httpとhttpsの違い、大文字小文字の差の3つ。entityIDはURIの形をしていますがSPの実在URLである必要はなく、文字列として完全一致するかだけが問われます。確認は、SAMLResponseのAudience要素の値とSP設定のentityIDをテキストエディタに並べて差分を取るのが確実。目視では末尾スラッシュを見落とします。

### ACSエンドポイントのindexとisDefaultの指定と複数登録

[OASIS SAML 2.0 Metadata仕様](https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf)では、SPは`AssertionConsumerService`要素を1つ以上持つと定められています。複数登録して`index`属性で区別し、既定のものに`isDefault`を付ける形です。ステージングと本番で同じentityIDを使い回すと、IdPは登録済みのACS URLにしかアサーションを返さないため片方が必ず落ちます。環境ごとにentityIDを分けてください。

### validUntilとcacheDurationで決まる証明書更新の運用設計

メタデータのルート要素には`validUntil`と`cacheDuration`という2つの時間属性が付きます。validUntilはそのメタデータの有効期限、cacheDurationは受け取り側がキャッシュしてよい最大の長さで、どちらもオプション属性です。

この2つを読み飛ばすと、IdPの署名証明書が更新された日にSSOが全面停止します。メタデータをファイルで取り込んで放置していると、証明書が切り替わっても古い公開鍵で検証し続けるためです。メタデータURLから定期取得して自動反映する仕組みを入れるのが本筋。手動運用なら、取り込んだ証明書の期限を次のコマンドで確認してカレンダーに登録してください。

```
openssl x509 -in idp-signing.crt -noout -subject -enddate -fingerprint -sha256
```

複数のIdPと繋いでいる場合は証明書ごとに期限が違うため、enddateを一覧化して管理します。

## HTTP-RedirectとHTTP-POSTのエンコード差とRelayStateの制限

バインディングはSAMLメッセージをHTTPにどう載せるかの規約です。同じbase64でも前処理が違うため、取り違えるとデコードの時点で失敗します。

### Redirectバインディングのraw deflateと署名の分離規則

[OASIS SAML 2.0 Bindings仕様](https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf)のSection 3.4.4.1は、HTTP-Redirectバインディングでの手順を3段階で定めています。メッセージ上の`ds:Signature`要素を取り除き、RFC 1951のDEFLATEで圧縮し、その結果をbase64でエンコードする順です。base64の結果から改行や空白は必ず取り除きます。

zlibヘッダ付きの圧縮を掛けると復元できません。Pythonなら`zlib.decompress(data, -15)`のようにwbitsを負値にしてraw deflateを指定します。

署名はメッセージから分離され、クエリ文字列の`SigAlg`と`Signature`パラメータに載ります。つまりRedirectバインディングでは、XMLの中に署名が無いのが正常な状態。XML内に署名が無いことを理由に検証エラーを返す実装は誤りです。

### POSTバインディングでDEFLATEを掛けると失敗する理由

HTTP-POSTバインディングの手順はSection 3.5.4にあり、XMLにbase64エンコードを適用してhiddenのフォームコントロールに入れる、とだけ定めています。DEFLATE圧縮は含まれません。

ここがRedirectとの決定的な差です。RedirectのデコーダをPOSTの受信側に流用すると、base64デコード直後にzlibが不正データとして例外を投げます。逆にPOST用のデコーダでRedirectのSAMLRequestを読むと、圧縮されたバイナリがXMLパーサに渡ってパースエラーになる仕組みです。フォームコントロール名も`SAMLRequest`と`SAMLResponse`に規定されています。SaaSを相手にした設定手順と詰まる箇所は[GitHub SSOの設定手順とSAML連携・PAT/SSH認可の落とし穴](https://www.issoh.co.jp/tech/details/5717/)にまとめました。

### RelayStateの80バイト上限と戻り先URLの保持方法

RelayStateは、ログイン前に見ていたページへ認証後に戻すための値です。Bindings仕様のSection 3.4.3は、この値が80バイトを超えてはならないと定めています。空間の制約から署名は現実的でないため、値を作る側が別途完全性を保護すべきだとも書かれています。

深い階層のURLやクエリ付きURLをそのまま入れると上限を超えます。IdPによっては切り詰めて返すため、戻り先が壊れた状態でリダイレクトされ、原因の分かりにくい不具合になります。戻り先URLはサーバー側のセッションかストアに保存し、RelayStateには参照キーだけを入れてください。UUIDなら36文字で収まります。RelayStateの値をそのままリダイレクト先に使う実装はオープンリダイレクトの脆弱性になるため、自ドメインへのパスだけを許可する検証も必須です。

## SAMLResponseを復号して条件と宛先を検証するコードの実行手順

設定画面の突き合わせで解決しない場合は、実データをコードで開きます。ライブラリの検証エラーメッセージだけでは、どの条件で落ちたか分からないことが多いためです。

### base64のSAMLResponseからNameIDと条件を取り出すコード

HTTP-POSTで受け取ったSAMLResponseは、base64をデコードするだけでXMLに戻ります。次のスクリプトは、採取したbase64文字列を引数に渡すと切り分けに必要な値を出力します。

```
import base64, sys
from xml.etree import ElementTree as ET

NS = {
    "samlp": "urn:oasis:names:tc:SAML:2.0:protocol",
    "saml": "urn:oasis:names:tc:SAML:2.0:assertion",
}

# HTTP-POST の SAMLResponse は base64 のみ。DEFLATE は掛かっていない
root = ET.fromstring(base64.b64decode(sys.argv[1]))

print("Destination :", root.get("Destination"))
print("InResponseTo:", root.get("InResponseTo"))

code = root.find("./samlp:Status/samlp:StatusCode", NS)
print("StatusCode  :", code.get("Value"))

nid = root.find(".//saml:Subject/saml:NameID", NS)
print("NameID Fmt  :", nid.get("Format"))
print("NameID Val  :", nid.text)

cond = root.find(".//saml:Conditions", NS)
print("NotBefore   :", cond.get("NotBefore"))
print("NotOnOrAfter:", cond.get("NotOnOrAfter"))

for aud in root.findall(".//saml:AudienceRestriction/saml:Audience", NS):
    print("Audience    :", aud.text)
```

DestinationがSPのACS URLと一致しない、AudienceがentityIDと一致しない、NameID Fmtが期待と違う、のいずれかが見つかれば、その時点で原因が確定します。ただしこれは検証を行わない解析専用のコードで、本番の受信処理にそのまま使ってはいけません。署名検証を省くと、攻撃者が任意のアサーションを投げ込める状態になります。

### DEFLATE圧縮されたSAMLRequestを復元して時刻を確認する手順

HTTP-RedirectのSAMLRequestは、URLデコード、base64デコード、raw deflate展開の順で処理します。wbitsに-15を渡す点が要点になります。

```
import base64, sys, zlib
from urllib.parse import unquote

# クエリの SAMLRequest をそのまま引数に渡す
q = unquote(sys.argv[1])
xml = zlib.decompress(base64.b64decode(q), -15).decode("utf-8")
print(xml)
```

復元したXMLで確認するのは`Issuer`、`AssertionConsumerServiceURL`、`IssueInstant`の3つ。IssueInstantがずれていればSPサーバーの時刻同期が崩れており、アサーションの`NotBefore`と`NotOnOrAfter`の検証で落ちる形で表面化します。Entra IDの場合、NotOnOrAfterはNotBeforeの70分後に設定され、IdP側はSPとの時間差を考慮したバッファを加えません。時刻合わせはSPの責任範囲です。

```
python -c "import datetime; print(datetime.datetime.now(datetime.timezone.utc).isoformat())"
chronyc tracking
```

SP実装で許容する時刻の揺らぎは数十秒から数分に留めます。広げすぎるとリプレイ攻撃の猶予を与えるため、NTP同期を直すほうが先です。検証ロジックを自分で書かずに済ませたいなら、[python3-saml](https://github.com/SAML-Toolkits/python3-saml)のようにOASIS仕様に沿った検証を実装済みのライブラリを使います。

## SAMLの自前実装とIdP機能への寄せ方を分ける判断基準と見送る条件

切り分けをやり切ると、次に出てくるのは「そもそもSAMLを自前で持つべきか」という問いです。条件を示して結論を書きます。

### SAMLのSP実装を自前で書かずライブラリへ寄せる判断の分岐点

SP側を署名検証まで含めてゼロから書く判断は避けてください。XML Signature Wrapping攻撃のように、XMLの構造を操作して検証を迂回する手法があり、検証順序を1つ誤るだけで認証バイパスが成立します。

自前で書いてよいのは、既存ライブラリが対応していない独自要件がある場合に限られます。社内IdPが仕様外の拡張要素を返す、複数IdPを動的に切り替える、といった条件です。それでも署名検証部分はライブラリに任せ、自前コードは属性のマッピングとセッション確立に限定してください。判断基準は単純で、署名検証コードを自分で書く理由を1文で説明できないなら書かないほうが安全です。

### SAMLを見送りOIDCを選ぶべき場面の条件付きの切り分け基準

新規開発でプロトコルを選べる立場なら、次の条件ではSAMLを見送ります。

- モバイルアプリやSPAが対象に含まれる：SAMLはブラウザのリダイレクトとフォームPOSTを前提とし、ネイティブアプリでは扱いにくい
- 連携先が自社サービスのみでIdPも自社で選べる：XMLの署名検証とメタデータ運用のコストを負う理由がない
- APIへのアクセス委譲が主目的：認可の委譲はOAuth 2.0の領分になる

逆にSAMLを選ぶのは、接続先の企業がSAMLしか受け付けない場合です。大企業のIdPやSaaSの企業向けプランでは、SSOの実装方式がSAML 2.0に固定されていることが珍しくありません。この場合は選択の余地がなく、前述の切り分け手順を運用に組み込む前提で設計します。両プロトコルの責務の差は[OIDC（OpenID Connect）とは？仕組み・OAuthとの違い](https://www.issoh.co.jp/tech/details/6532/)で整理しました。

### 既存の会員基盤へSAML連携を後付けするときの設計上の確認事項

稼働中の会員基盤にSAML SSOを足す場合、既存ユーザーとIdPが返すNameIDをどう結び付けるかという論点が出ます。persistentは不透明な文字列のため既存のメールアドレスとは自動で一致せず、初回だけ別の属性で本人を特定し以後はNameIDで引く二段構えにします。この初回紐付けの経路が、なりすましの入口になりやすい箇所です。

パスワードログインとSSOを併存させる期間も設計対象。SSO必須へ切り替える日を決めずに両方を残すと、退職者のアカウントがパスワード経路で生き続けます。既存の会員基盤への認証方式の追加や、IdP連携を含む改修の設計から実装までは[認証基盤・ID管理システム開発](https://www.issoh.co.jp/service/system/identity/)で対応しています。

## よくある質問

実装と切り分けで繰り返し出てくる質問を、仕様の記述に沿って整理しました。

### SAMLのNameIDFormatは何を指定すればよいですか？

接続先のIdPが対応している値の中から選びます。迷う場合はpersistentが基本。メールアドレスでユーザーを引きたい要件があるならemailAddressを指定しますが、変更時に紐付けが切れる点を設計で吸収してください。指定できるURIの一覧はOASIS SAML 2.0 Core仕様のSection 8.3にあります。

### SAMLのメタデータには何が入っていますか？

接続相手に渡すべき設定値がXMLでまとまっています。SP側なら、自分を識別するentityID、アサーションの受け取り先であるAssertionConsumerServiceのURLとバインディング、署名や暗号化に使う証明書。IdP側ならSingleSignOnServiceのURLと署名証明書が中心です。設定画面しかない製品でもXMLを出力できることが多いので、突き合わせにはXMLを使ってください。

### SAML TracerでSAMLResponseが見えないときはどうしますか？

まず拡張機能のフィルタがSAMLメッセージのみに絞られているか確認します。それでも見えない場合、IdPがエラー画面で止まってSAMLResponseを生成していない可能性が高く、IdP側の管理画面でサインインログを確認してください。なおIdP起点のログインではSPがAuthnRequestを送らないため、SAMLRequestが見えないのは正常な状態です。

### HTTP-POSTバインディングでDEFLATEは必要ですか？

必要ありません。Bindings仕様のSection 3.5.4は、HTTP-POSTではXMLにbase64エンコードを適用してフォームのhiddenコントロールに入れる、とだけ定めています。DEFLATE圧縮が規定されているのはSection 3.4.4.1のHTTP-Redirectのほうです。

### Artifact Bindingはどんなときに使いますか？

アサーション本体をブラウザ経由で渡したくない場合に使います。IdPは短い識別子であるアーティファクトだけをブラウザ経由でSPに渡し、SPはIdPのArtifact Resolution Serviceへバックチャネルで問い合わせて実体を受け取る方式。反面、SPからIdPへ直接通信できる経路が必要で、ファイアウォール越しの構成では実現できないことがあります。多くのSaaS連携ではHTTP-POSTで足ります。

## 関連記事

- [SAMLとは？認証フロー・IdP/SPの仕組みとOAuth・OIDCとの使い分けを実装視点で解説](https://www.issoh.co.jp/tech/details/13299/)：規格そのものの定義、SP-InitiatedとIdP-Initiatedの正常系フロー、SP実装の全体像を扱っています
- [IDaaSとは？SSO・IdPとの違いとSAML・OIDC・SCIM連携を実装視点で解説](https://www.issoh.co.jp/tech/details/16401/)：IdP製品を選ぶ段階の判断軸と、SCIMによるユーザープロビジョニングまで整理しています
- [GitHub SSOの設定手順｜SAML連携・PAT/SSH認可の落とし穴](https://www.issoh.co.jp/tech/details/5717/)：具体的なSaaSを相手にしたSAML連携の設定手順と、トークン認可で詰まる箇所の実例です
- [OIDC（OpenID Connect）とは？仕組み・OAuthとの違いをわかりやすく解説](https://www.issoh.co.jp/tech/details/6532/)：SAMLを見送ってOIDCを選ぶ場合の、プロトコルの構造と用語の対応を確認できます

---

出典: [SAML NameIDFormatとメタデータの不一致でSSOが失敗する原因の切り分け](<https://www.issoh.co.jp/tech/details/17589/>)（株式会社一創）
