SAMLのHTTP POSTバインディングは、IdPやSPが作ったSAMLメッセージをbase64でフォームに埋め、ブラウザに自動送信させて相手へ届ける方式です。この記事では、POSTバインディングの仕組みをOASIS SAML 2.0の仕様書で確かめ、HTTP-Redirect、HTTP-Artifact、SOAPとの違いをエンコード、署名、通信経路で比べます。標準ライブラリだけで符号化を再現するPythonコード、python3-samlの設定、SameSite属性でACSが失敗する落とし穴、自社のSPで有効にするバインディングの選び方まで扱います。
まとめ:SAMLバインディングの選び方とHTTP POSTで外さない実装の要点
バインディングは「SAMLメッセージをどの通信手段に載せるか」の取り決めで、SAML 2.0はSOAP、PAOS、HTTP-Redirect、HTTP-POST、HTTP-Artifact、URIの6種類を定めています。ブラウザSSOで実際に使うのは、AuthnRequestをRedirectかPOSTで送り、ResponseをPOSTで受け取る組合せがほとんどです。ResponseにRedirectは使えません。
POSTは圧縮せずbase64だけを掛け、署名はXMLの中。Redirectはraw DEFLATEで縮め、署名をXMLから外してクエリに付け直します。この差の取り違えが、復号や署名検証の失敗の主因です。
新規のSPは、送信をRedirect、受信をPOSTの2本に絞ってください。Artifactは要件が明文化されたときだけ足すもの。ACS用のCookieにはSameSite=NoneとSecureを付けるか、状態をサーバー側に持たせます。
SAMLバインディングの役割とPOST・Artifact・SOAPなど6種類
SAMLの仕様は、中身を決めるCore、運び方を決めるBindings、両者を業務の流れにするProfilesに分かれます。SAMLの定義やIdPとSPの関係はSAMLとは?認証フロー・IdP/SPの仕組みとOAuth・OIDCとの使い分けを実装視点で解説に譲り、ここでは運び方の層に絞ります。
バインディングとプロファイルの関係と6種類の識別URIを仕様書で確認
OASIS SAML 2.0 Bindings仕様(2005年3月15日のOASIS標準)は、Section 3.2から3.7で6種類のバインディングを定義しています。メタデータのBinding属性や、python3-samlなどの設定ファイルに書く値はこの識別URIです。
| バインディング | 仕様の節 | 運び方 | 主な用途 |
|---|---|---|---|
| SOAP | 3.2 | サーバー間のSOAP通信 | Artifact解決・属性照会 |
| PAOS(Reverse SOAP) | 3.3 | HTTP応答でSOAPを返す | ECPプロファイル |
| HTTP-Redirect | 3.4 | GETのクエリ文字列 | AuthnRequest・ログアウト |
| HTTP-POST | 3.5 | 自動送信フォーム | Responseの受け渡し |
| HTTP-Artifact | 3.6 | 参照値だけを渡す | 実体をブラウザに通さない |
| URI | 3.7 | URIで参照して取得 | アサーションの取得 |
識別URIはurn:oasis:names:tc:SAML:2.0:bindings:に続けて、HTTP-POST、HTTP-Redirect、HTTP-Artifact、SOAP、PAOS、URIを付けた形です。設定値の大文字小文字を1文字でも違えると、IdPはエンドポイントを見つけられません。メタデータでBindingとLocationの組を読む手順はSAMLメタデータとは:EntityDescriptorの読み方と署名証明書の更新手順にまとめています。
ブラウザSSOでAuthnRequestとResponseに使える組合せの制約
ブラウザSSOで使える組合せは、OASIS SAML 2.0 Profiles仕様のSection 4.1が決めています。SPからIdPへのAuthnRequestは、Redirect、POST、Artifactのいずれでも送信可能。逆にResponseはPOSTかArtifactに限られ、Section 4.1.2は「HTTP Redirect binding MUST NOT be used」と明記しています。認証アサーションを含むResponseは、多くのブラウザが受け付けるURL長を超えるためです。
外側の署名は外せても、アサーション内の署名は外せません。属性を数十個載せたResponseはbase64にすると数KBから十数KBに達し、Bindings仕様のSection 3.4.4.1も、署名を内側に含むメッセージはRedirectに向かないと書いています。
実在するIdPもこの形です。Microsoft LearnのSingle sign-on SAML protocolによると、Microsoft Entra IDはAuthnRequestをHTTP Redirectで受け取り、ResponseをHTTP POSTでSPへ送ります。
HTTP POSTバインディングの仕組みとSAMLResponseを運ぶフォームの中身
POSTバインディングの正体は、hiddenの入力欄を1つ持つHTMLフォームです。送り手はフォームを含むページをブラウザに返し、ブラウザがそれを相手のエンドポイントへPOSTします。
base64だけでDEFLATEを掛けないフォームの符号化と入力欄の名前
Bindings仕様のSection 3.5.4によると、XMLにbase64を掛けた値をhiddenのフォーム要素に入れて運びます。入力欄の名前は、要求ならSAMLRequest、応答ならSAMLResponseで、RelayStateは同じフォームのRelayState要素に入れる決まりです。
圧縮は掛けません。DEFLATEはURL長を縮めるための工程で、本文に載るPOSTには不要だからです。base64の値は改行してもよいと仕様が認めているので、復号前に改行を除いておくとIdPごとの差を吸収できます。
自動送信フォームの構造とCache-ControlとDestinationの規定
フォームのaction属性は受け手のエンドポイント、method属性はPOSTです。仕様の例はonloadで送信するJavaScriptを使いますが、受け手は送信のきっかけに依存してはならないため、JavaScript無効の環境向けに送信ボタンも置くのが定石です。
Section 3.5.5.1は、SAMLメッセージを返す応答にCache-Control: no-cache, no-storeとPragma: no-cacheを付けるよう求めています。戻るボタンでフォームが再送されると、使用済みのアサーションが再びACSに届くからです。
署名付きのメッセージでは、受け手がルート要素のDestination属性と実際の受信URLの一致を確かめる決まりです(Section 3.5.5.2)。リバースプロキシの裏でSPが自分のURLをhttpや内部ホスト名で認識していると、ここで不一致になります。
POSTで届くResponseのアサーション署名必須とリプレイ防止の規定
ブラウザが仲介する以上、通信路の暗号化だけでは送り手を確かめられません。Profiles仕様のSection 4.1.4.5は、POSTでResponseを届ける場合に中のアサーションへの署名を必須とし、Artifactでは任意としています。
同じ節は、使用済みのアサーションIDをSubjectConfirmationDataのNotOnOrAfterまで保持し、リプレイを拒否することも求めています。保持期間は固定値にせず、受け取ったアサーションの期限から決めてください。要素ごとの検証順序はSAMLアサーションとは:XML構造の読み方と署名・Audience・有効期限の検証実装で扱っています。
HTTP-Redirectバインディングとの違いをエンコードと署名と長さで比較
どちらもブラウザを仲介役にしますが、メッセージの載せ場所の違いが、エンコードと署名の方法まで変えています。
| 比較観点 | HTTP-Redirect | HTTP-POST |
|---|---|---|
| HTTPメソッド | GET | POST |
| 載せ場所 | URLのクエリ文字列 | フォームのhidden要素 |
| 符号化 | raw DEFLATE→base64→URL | base64のみ |
| 署名の置き場所 | SigAlgとSignatureの引数 | XML内のds:Signature |
| 長さの制約 | URL長に縛られる | 実質なし |
| ブラウザSSOのResponse | 使用不可 | 標準の受け渡し方式 |
混乱が起きるのは符号化と署名の2行です。「署名が無効」と出ても、原因は鍵より符号化の取り違えであることが多くあります。
raw DEFLATEとSigAlg・Signatureの引数で分かれる署名の置き場所
Section 3.4.4.1の手順では、まずXML内のds:Signature要素を取り除き、残りにRFC 1951のDEFLATEを掛け、base64とURLエンコードを施します。zlibヘッダを付けないraw DEFLATEである点が、PythonやJavaの既定値と異なるので注意が要ります。
署名は「SAMLRequest=値&RelayState=値&SigAlg=値」の順に連結した文字列に対して計算し、Signature引数に入れます。順序を変えたり、URLデコード後の値で署名したりすると検証に通りません。Redirectのクエリを手で復元する手順はSAML NameIDFormatとメタデータの不一致でSSOが失敗する原因の切り分けで扱っています。
RelayStateの80バイト上限と差し替え攻撃に備える値の持ち方
RelayStateの上限は、Redirect、POST、Artifactのいずれも80バイトです(Section 3.4.3、3.5.3、3.6.3.1)。POSTの節には、正当な応答2つの間でRelayStateだけを入れ替える攻撃への注意もあります。メッセージとRelayStateの組合せを守る仕組みは仕様に無いため、RelayStateには短いランダム値だけを入れ、戻り先URLはサーバー側に保存してください。
Pythonでバインディングごとの符号化と復号を試す手順と実行結果
違いは標準ライブラリだけで手元で確かめられます。
標準ライブラリでRedirectとPOSTの符号化を再現するスクリプト
次のスクリプトは、同じAuthnRequestをRedirect用とPOST用に符号化し、Artifactの値も組み立てます。zlibのwbitsに-15を渡すと、ヘッダもチェックサムも付かないraw DEFLATEになります。外部パッケージは不要です。
import base64
import hashlib
import html
import os
import urllib.parse
import zlib
AUTHN_REQUEST = (
'<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" '
'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" '
'ID="_req-4f1c9a" Version="2.0" IssueInstant="2026-11-05T01:00:00Z" '
'Destination="https://idp.example.jp/sso" '
'AssertionConsumerServiceURL="https://sp.example.jp/saml/acs" '
'ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">'
'<saml:Issuer>https://sp.example.jp/metadata</saml:Issuer>'
'</samlp:AuthnRequest>'
)
def redirect_encode(xml):
# HTTP-Redirect:raw DEFLATE(zlibヘッダなし)→ base64 → URLエンコード
c = zlib.compressobj(9, zlib.DEFLATED, -15)
raw = c.compress(xml.encode("utf-8")) + c.flush()
return urllib.parse.quote(base64.b64encode(raw).decode("ascii"), safe="")
def redirect_decode(value):
raw = base64.b64decode(urllib.parse.unquote(value))
return zlib.decompress(raw, -15).decode("utf-8")
def post_encode(xml):
# HTTP-POST:圧縮せず base64 だけを掛ける
return base64.b64encode(xml.encode("utf-8")).decode("ascii")
def post_form(action, field, value, relay_state=None):
# 受信側が自動送信の方法に依存しないよう、noscript のボタンも置く
rs = ""
if relay_state:
rs = '<input type="hidden" name="RelayState" value="%s"/>' % html.escape(relay_state)
return (
'<html><body onload="document.forms[0].submit()">'
'<form method="post" action="%s">'
'<input type="hidden" name="%s" value="%s"/>%s'
'<noscript><button type="submit">続行</button></noscript>'
"</form></body></html>" % (html.escape(action), field, value, rs)
)
def build_artifact(entity_id, endpoint_index=0):
# TypeCode 0x0004 + EndpointIndex 2バイト + SourceID(SHA-1 20バイト) + MessageHandle 20バイト
source_id = hashlib.sha1(entity_id.encode("utf-8")).digest()
handle = os.urandom(20)
raw = b"\x00\x04" + endpoint_index.to_bytes(2, "big") + source_id + handle
return base64.b64encode(raw).decode("ascii"), len(raw)
if __name__ == "__main__":
print("XML:", len(AUTHN_REQUEST), "bytes")
r = redirect_encode(AUTHN_REQUEST)
print("Redirect SAMLRequest:", len(r), "chars")
assert redirect_decode(r) == AUTHN_REQUEST
p = post_encode(AUTHN_REQUEST)
print("POST SAMLRequest:", len(p), "chars")
try:
zlib.decompress(base64.b64decode(p), -15)
print("POSTの値をinflate: 成功")
except zlib.error as e:
print("POSTの値をinflate: 失敗 ->", e)
art, n = build_artifact("https://idp.example.jp/metadata")
print("SAMLart:", n, "bytes ->", len(art), "chars")
Python 3.14系で実行すると、次の結果になります(SAMLartの値は毎回変わります)。
XML: 434 bytes
Redirect SAMLRequest: 358 chars
POST SAMLRequest: 580 chars
POSTの値をinflate: 失敗 -> Error -3 while decompressing data: invalid code lengths set
SAMLart: 44 bytes -> 60 chars
434バイトのXMLが、Redirectでは358文字、POSTでは580文字になりました。届いたSAMLResponseの復号でこのzlibエラーが出たら、Redirect用のコードをPOSTの受信に流用していないか確かめてください。
python3-samlの設定でSSOとACSのバインディングを指定する
実際のSPでは符号化をライブラリに任せます。python3-samlのREADMEは、ACSはHTTP-POSTのみ、シングルログアウトはHTTP-Redirectのみの対応と明記しており、Artifactには対応していません。PyPIの最新版は2026年11月時点で1.16.0(2023年10月公開)です。
{
"strict": true,
"sp": {
"entityId": "https://sp.example.jp/metadata",
"assertionConsumerService": {
"url": "https://sp.example.jp/saml/acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"x509cert": "(SPの署名用証明書)",
"privateKey": "(SPの秘密鍵。リポジトリに置かない)"
},
"idp": {
"entityId": "https://idp.example.jp/metadata",
"singleSignOnService": {
"url": "https://idp.example.jp/sso",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509cert": "(IdPメタデータの署名用証明書)"
},
"security": {
"authnRequestsSigned": true,
"wantAssertionsSigned": true
}
}
authnRequestsSignedをtrueにすると、Redirectの要求にSigAlgとSignatureが付きます。wantAssertionsSignedは、Profiles仕様のSection 4.1.4.5に合わせてPOSTで受けるアサーションに署名を求める設定です。strictは本番で必ずtrueにします。
HTTP-ArtifactとSOAPバインディングの通信経路とバックチャネル
Artifactは、ブラウザ経由の参照値とサーバー間の通信を組み合わせます。その裏側を担うのがSOAPバインディングです。
SAMLartの44バイト構造とArtifactResolveで実体を取りに行く流れ
Section 3.6.4によると、アーティファクトは型コード0x0004、エンドポイント番号2バイト、SourceID 20バイト、MessageHandle 20バイトの計44バイトをbase64にした値です。SourceIDは発行者のentityIDのSHA-1、MessageHandleは16バイト以上の乱数とされています。
受け手はSAMLartのSourceIDで発行者を特定し、そのArtifact Resolution ServiceへArtifactResolveをSOAPで送ります。アーティファクトは発行者側で1回限りとする決まりで、受け手側でも1回限りにすることが推奨されています(Section 3.6.5.2)。
負担はインフラ側に出るのが難点です。発行者は解決まで状態を保持する必要があり、仕様自身がロードバランス環境への影響を指摘しています。SPからIdPへの直接経路も要るため、社内にIdPがある構成ではファイアウォールの穴あけが発生します。
SOAPとPAOSが担うバックチャネル通信とECPプロファイルの用途
SOAPバインディングは、ブラウザを介さないサーバー間通信に使います。ArtifactResolveのほか、属性を問い合わせるAttributeQueryやバックチャネルのログアウトが代表例です。Profiles仕様のSection 4.1.4.4は、Artifactの解決に相互認証、完全性保護、機密性を求めており、クライアント証明書を使うTLSかメッセージ署名で満たします。
PAOSはHTTPの応答としてSOAPを返す逆向きのバインディングで、ブラウザ以外のクライアントが認証を仲介するECPプロファイル用です。業務Webシステムでは検討対象から外してかまいません。
ACSへのクロスサイトPOSTでセッションが消えるSameSite属性の落とし穴
仕様書にもIdPの設定画面にも出てこないのに、POSTバインディングでSPを止めやすいのがCookieです。IdPからSPのACSへのPOSTは、ブラウザから見るとクロスサイトのPOSTになります。
SameSite=LaxでリクエストIDを持つCookieが届かない仕組み
MDNのSet-Cookieの解説によると、SameSite=Laxのクッキーがクロスサイトで送られるのは、トップレベルのナビゲーションかつ安全なメソッドの場合だけで、POSTは対象外。SameSite未指定をLaxとして扱うブラウザもあります。
AuthnRequestのIDをセッションCookieに結び付けていると、ACSへのPOSTにCookieが付かず、InResponseToを照合できません。ライブラリはInResponseTo不一致やセッション無しのエラーを返し、IdPのログは成功のままです。
MDNには、既定としてLaxが適用される場合、設定から2分以内のPOSTにはCookieを送る緩和があるとも書かれています。素早く通過するテストでは成功し、MFAに時間がかかった利用者だけが失敗するという、再現しにくい障害になります。
SameSite=NoneとSecureの付与かサーバー保持で解く2つの対処
1つは、IDを持つCookieにSameSite=NoneとSecureを付ける方法で、NoneにはSecureが必須なのでHTTPSが前提です。もう1つは、IDをサーバー側に保存し、RelayStateの短いキーで引き当てる方法です。
次のFlaskの例は前者で、get_last_request_id()で得たIDを5分だけ保持します。pathを/saml/に限定し、アプリ本体のセッションCookieはLaxのまま残します。
from flask import Flask, request, redirect, make_response, abort
from onelogin.saml2.auth import OneLogin_Saml2_Auth
app = Flask(__name__)
def saml_req():
return {
"https": "on",
"http_host": request.host,
"script_name": request.path,
"get_data": request.args.copy(),
"post_data": request.form.copy(),
}
@app.route("/saml/login")
def login():
auth = OneLogin_Saml2_Auth(saml_req(), custom_base_path="saml_conf")
url = auth.login() # SAMLRequest・SigAlg・Signature 付きの HTTP-Redirect 用URL
resp = make_response(redirect(url))
# IdP から ACS へのクロスサイト POST でも届くよう SameSite=None と Secure を付ける
resp.set_cookie("saml_req", auth.get_last_request_id(), max_age=300,
secure=True, httponly=True, samesite="None", path="/saml/")
return resp
@app.route("/saml/acs", methods=["POST"])
def acs():
auth = OneLogin_Saml2_Auth(saml_req(), custom_base_path="saml_conf")
# InResponseTo を保存済みの AuthnRequest ID と照合させる
auth.process_response(request_id=request.cookies.get("saml_req"))
if auth.get_errors() or not auth.is_authenticated():
abort(403)
resp = make_response(redirect("/"))
resp.delete_cookie("saml_req", path="/saml/")
return resp
process_responseのrequest_idに渡したIDは、ResponseのInResponseToと照合されます。照合を省くと、攻撃者のアサーションを被害者のブラウザへ送り込むログインCSRFの余地が残ります。
自社システムで有効にするバインディングの判断基準と見送る条件
受託開発でSPを作るときの判断です。IdPがEntra ID、Okta、Keycloakのどれでも、結論は変わりません。
新規のSP実装は送信Redirectと受信POSTの2本に絞るべき条件
ブラウザから業務システムへのSSOなら、AuthnRequestはRedirect、ResponseはPOSTの2本だけを有効にします。主要なIdPとpython3-samlなどのライブラリがこの組合せを前提にしており、ACSをPOSTの1本にすればIdP側の登録ミスも減ります。
AuthnRequestをPOSTに変えるのは、IdPがRedirectを受け付けない場合と、要求の署名をXML内に持たせたい監査要件がある場合だけ。複数企業のIdPを受け入れるSaaSでは、テナントごとにIdPメタデータのSingleSignOnServiceから対応バインディングを読んで選ぶ実装にします。
Artifactバインディングを採用する場面と既定で無効にしておく理由
Artifactを足すのは、個人情報や権限情報を多く載せたアサーションをブラウザに通したくない要件が明文化されている場合です。暗号化アサーションで目的を果たせるなら、そちらを先に検討してください。
SPからIdPへの直接通信を通せない、複数台構成で状態を共有できない、ライブラリが対応していない。このどれかに当たればArtifactは見送ります。使わないバインディングをメタデータに載せたままにすると、検証されていない受信口が残ります。
テナント別のIdP設定やSameSiteを考慮したセッション設計を含むSAML SPの組み込みは、一創の認証基盤・ID管理システム開発で設計から実装まで担当しています。
よくある質問
SAMLのバインディングについて、実装や設定でよく出る質問をまとめました。
SAMLのバインディングとは何ですか?
SAMLメッセージを、HTTPやSOAPといった通信手段にどう載せるかの取り決めです。OASIS SAML 2.0のBindings仕様が、SOAP、PAOS、HTTP-Redirect、HTTP-POST、HTTP-Artifact、URIの6種類を定義。中身はCore仕様、業務の流れはProfiles仕様が担い、バインディングは運び方だけを受け持ちます。
HTTP POSTバインディングとHTTP Redirectバインディングの違いは何ですか?
POSTはフォームのhidden要素にbase64の値を入れ、署名をXMLの中に持ちます。Redirectはクエリ文字列に載せるため、raw DEFLATEで圧縮してからbase64とURLエンコードを掛け、署名はSigAlgとSignatureの引数に付け直します。URL長の制約から、ブラウザSSOのResponseにRedirectは使えません。
SAML Artifact Bindingはどんなときに使いますか?
アサーションの実体をブラウザに通したくない場合です。ブラウザには44バイトの参照値をbase64にしたSAMLartだけを渡し、SPはIdPへSOAPでArtifactResolveを送って実体を受け取ります。直接通信と状態保持が必要になるため、一般的な業務システムではPOSTを使います。
SAMLのSOAPバインディングはブラウザSSOで使いますか?
ブラウザとのやり取りには使いません。SOAPはサーバー同士の直接通信用で、Artifactの解決、属性の問い合わせ、バックチャネルのログアウトで使われます。POSTでResponseを受け取る一般的な構成なら、SOAPのエンドポイントは不要です。
ACSでInResponseToが一致しないエラーが出るのはなぜですか?
多い原因は、AuthnRequestのIDを結び付けたCookieが、IdPからACSへのクロスサイトPOSTで送られていないことです。SameSite=LaxのクッキーはクロスサイトのPOST要求に付きません。CookieにSameSite=NoneとSecureを付けるか、IDをサーバー側に保存してRelayStateのキーで引き当ててください。
関連記事
- SAMLとは?認証フロー・IdP/SPの仕組みとOAuth・OIDCとの使い分けを実装視点で解説:SAML全体の仕組みと認証フロー
- SAML NameIDFormatとメタデータの不一致でSSOが失敗する原因の切り分け:Redirectの復号とRelayStateの扱い
- SAMLアサーションとは:XML構造の読み方と署名・Audience・有効期限の検証実装:POSTで届くアサーションの検証順序
- SAMLメタデータとは:EntityDescriptorの読み方と署名証明書の更新手順:BindingとLocationの登録方法
- フェデレーション認証とは?IdPとSPの信頼設計と属性連携を実装視点で解説:IdPとSPの信頼関係の設計