SAML 2.0 HTTP-Redirectバインディング:その「泥沼」を読み解く技術
ネットワークエンジニアの諸君、認証基盤の設計で一度は必ず頭を抱えるのが「SAML」だ。特に HTTP-Redirect バインディングは、ブラウザを介してIDP(Identity Provider)とSP(Service Provider)の間をパケットが飛び交う、いわば「黒魔術」のような挙動を見せる。
「なぜ署名検証でエラーが出るのか?」「なぜリクエストが弾かれるのか?」――今日は、RFC 7522やSAML 2.0の仕様書を読み漁っても解決しなかった、現場の泥臭い挙動とデバッグの極意を伝授しよう。
—
1. なぜ「URL」に認証情報を詰め込むのか?
まず仕様の背景を理解しよう。HTTP-Redirect バインディングは、その名の通り 302 Found を利用してブラウザを転送させる。SAMLリクエスト(AuthnRequest)をパラメータとしてURLに載せるため、通信経路には以下の変換が加わる。
1. Deflate圧縮: XML文字列を軽量化する。
2. Base64エンコード: URLで安全に運べる形式にする。
3. URLエンコード: 特殊文字をエスケープする。
この「URLを構築する」というステップが、実はトラブルの温床だ。特に、SP側で生成した SAMLRequest と、IDP側で検証する際のURLエンコードの解釈が微妙にズレるだけで、署名検証は無慈悲に失敗する。
—
2. 通信フローと署名のリアル
HTTP-Redirect では、署名は SAMLRequest そのものには含まれず、クエリパラメータの末尾に &SigAlg=...&Signature=... として付与される。
ここで重要なのは、「署名の対象は、エンコード後の文字列である」という点だ。
シーケンスの要点
1. SP: XMLを生成 → Deflate → Base64 → URLエンコード。
2. SP: SigAlg(署名アルゴリズム)と Signature(署名値)を計算。
3. ブラウザ: IDPへ転送。IDPは、受け取った SAMLRequest と SigAlg、Signature を元に検証を行う。
現場のTips: もし開発中に署名エラーが出たら、まず SAMLRequest をデコードして中身を確認する前に、ブラウザのアドレスバーに並んでいるクエリパラメータの「並び順」を疑え。署名検証はバイナリレベルの一致を求めるため、パラメータの順序が入れ替わるだけで失敗する。
—
3. 実践:Pythonで紐解く署名検証のロジック
理屈だけでは動かない。実際にどのような形式でIDPに投げているのか、Pythonで最小構成のロジックを組んでみよう。
import base64
import zlib
import urllib.parse
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
# 1. AuthnRequestのXML生成(簡略化)
xml_data = "<samlp:AuthnRequest ...>...</samlp:AuthnRequest>"
# 2. Deflate -> Base64 -> URLエンコード
# 注意:zlibのヘッダー(RFC 1950)を削除し、Raw Deflateにする必要がある
deflated = zlib.compress(xml_data.encode('utf-8'))[2:-4]
b64_encoded = base64.b64encode(deflated).decode('utf-8')
query_params = urllib.parse.quote_plus(b64_encoded)
# 3. 署名計算(Query String全体に対して行う)
# 例: "SAMLRequest=...&RelayState=...&SigAlg=..."
query_string = f"SAMLRequest={query_params}&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256"
# 秘密鍵で署名を作成
key = RSA.import_key(open("sp_private_key.pem").read())
h = SHA256.new(query_string.encode('utf-8'))
signature = pkcs1_15.new(key).sign(h)
# 最終的なBase64署名値
final_sig = base64.b64encode(signature).decode('utf-8')
print(f"IDPへ送る署名値: {final_sig}")
このコードの肝は [2:-4] のスライスだ。標準的な zlib を使うとヘッダーが付くが、SAMLの仕様ではそれを嫌うケースが多い。ここを外すだけで、「なぜか署名検証が通らない」という怪奇現象が即座に解決することはよくある。
—
4. トラブルシューティングの鉄則
もし君が現場でSAMLの認証障害に直面したら、以下の手順で切り分けてほしい。
1. SAML Tracerの活用: ブラウザ拡張機能の SAML Tracer を使い、生の HTTP リクエストをキャプチャしろ。ここが見えないと始まらない。
2. デコードして内容確認: SAMLRequest をデコードし、Issuer や Destination が期待通りか確認する。
3. 署名アルゴリズムの不一致: IDPが SHA-256 を期待しているのに SHA-1 で送っていないか?またはその逆か? SigAlg パラメータをしっかり確認せよ。
4. 証明書の不一致: IDPに登録したSPの公開鍵が正しいか。時々、古い証明書が残っていて更新が反映されていないケースがある。
—
最後に:ネットワークエンジニアとしての矜持
SAML 2.0は決して「楽なプロトコル」ではない。しかし、一度この挙動を体得してしまえば、IDP(Okta, Azure AD, Auth0など)の仕様がいかに複雑であっても、パケットの行方を見通せるようになる。
「コードが動かない」と嘆く前に、まずは curl -v でリクエストを叩き、レスポンスの Location ヘッダーを追いかけろ。ネットワークの裏側にある「仕様という名のルール」が見えてきたとき、君はもう一段階上のエンジニアに成長しているはずだ。
また次回、認証基盤の深淵で会おう。健闘を祈る。
コメント