概要
SSO (Single Sign-On)を実現するための仕組みの1つであるSAMLでは、SP (Service Provider)を起点とする (SP-initiated)場合、SPからIdP (Identity Provider) にSAML Requestを送る必要があります。
この際、SAML RequestはXMLで記述されますが、SPからIdPに渡される際は(特にGET
のパラメータとして渡される場合)通常はDeflateで圧縮+Base64エンコードされ、人間の目では解読できなくなってしまいます。
先日、急いで自前のデコーダを用意する必要があり、Pythonで書いたところサクッと書けて便利だったので、小ネタとして投稿します。
背景
障害対応の一環で、SPがIdPに渡しているSAML Requestが正常に生成されているかや、中のデータは正しく設定されているか、などを調査する必要が生じました。
しかしこのSAML Request はDeflateで圧縮され、Base64でエンコードされていたため、そのままでは読むことができず、デコーダを用意する必要がありました。
そこで「SAML Request decode」などでググると、JavaScriptなどを使ってサイト上でデコードしてくれるサービスは沢山ある事は分かりました。
ただ、調査したいSAML Requestは(主にコンプライアンス的な理由で)社外に出すとマズいものでした。
そのため、そういったネット上のツールに頼ることなく、自力でデコーダを用意することにしました。
必要最低限のコード
import zlib
import base64
saml_request = ''
print(zlib.decompress(base64.b64decode(saml_request), -zlib.MAX_WBITS))
障害対応という状況の性質上、必要最低限動けば良いコードをなるべく短時間で実装する必要がありました。
その時に実装したコードが上記のものです。
デコードしたいSAML Request1を
saml_request = 'fVBNS8NAEP0ry97ziT10SAKhRQioiBUPXmRJpnRhP+LOrNZ/7yZFqJde533Me68hZc0MfeSTe8HPiMTibI0jWIFWxuDAK9IETlkk4BEO/eMD1HkJc/DsR2/kleS2QhFhYO2dFMO+lXpCx/qoMXxUUrxhoAS1MjETThRxcMTKcTqV5V1W1Vm5eS23UFew2b5L0f/Z7byjaDEcMHzpMckmPLeykl2zZILVK3Qn5pmgKGjO8azsbDAfvS2WdHVTXDObyyxPqcCwf/ZGjz+iN8Z/7wIqxlZyiCjFvQ9W8e3Ky0VP2XGlAgflSKfWsuguL/+P3/0C'
のように記述し、実行すると
$ python3 saml_request_decoder.py
b'<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="identifier_1" Version="2.0" IssueInstant="2004-12-05T09:21:59Z" AssertionConsumerServiceIndex="1"><saml:Issuer>https://sp.example.com/SAML2</saml:Issuer><samlp:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/></samlp:AuthnRequest>'
と、人間にも読める形で出力されました。
「ライブラリがないよ」とか怒られる場合
pip3 install zlib
等のコマンドで足りないライブラリをインストールしてください。
たった数行(実際にデコードしているのは最後の1行)で実装できてしまうとは驚きました。
PythonのようなLL (Lightweight Language)はこういう場面でサクッと問題を解決できるので、改めて便利さを思い知りました。
もうちょっと便利にしたコード
緊急時に「とりあえず使える」レベルのコードとしては上記で充分ですが、せっかくなので普通に使えるレベルに改良したいですね。
最低限
- いちいちPythonコードを編集するのが面倒。コマンドライン引数で渡せた方が便利
-
GET
で渡される場合、パラメータはURLエンコードされている。まずそれをデコードする必要がある2 - (今回は該当しなかったが)
POST
で渡される場合、圧縮されずにそのままBase64エンコードされたものが渡されることがある
ぐらいには対応したいです。
という訳で、それらに対応するよう改良しました。
import zlib
import base64
import sys
import urllib.parse
saml_request: str = sys.argv[1]
decoded_bytes: bytes = ''
try:
decoded_bytes = base64.b64decode(saml_request)
except:
decoded_bytes = base64.b64decode(urllib.parse.unquote(saml_request))
try:
decoded_bytes = zlib.decompress(decoded_bytes, -zlib.MAX_WBITS)
except:
pass
print(decoded_bytes.decode())
これで
$ python3 saml_request_decoder.py fVBNS8NAEP0ry97ziT10SAKhRQioiBUPXmRJpnRhP%2BLOrNZ%2F7yZFqJde533Me68hZc0MfeSTe8HPiMTibI0jWIFWxuDAK9IETlkk4BEO%2FeMD1HkJc%2FDsR2%2FkleS2QhFhYO2dFMO%2BlXpCx%2FqoMXxUUrxhoAS1MjETThRxcMTKcTqV5V1W1Vm5eS23UFew2b5L0f%2FZ7byjaDEcMHzpMckmPLeykl2zZILVK3Qn5pmgKGjO8azsbDAfvS2WdHVTXDObyyxPqcCwf%2FZGjz%2BiN8Z%2F7wIqxlZyiCjFvQ9W8e3Ky0VP2XGlAgflSKfWsuguL%2F%2BP3%2F0C
のようにコマンドライン引数としてデコードしたいSAML Requestを渡せば3
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="identifier_1" Version="2.0" IssueInstant="2004-12-05T09:21:59Z" AssertionConsumerServiceIndex="1"><saml:Issuer>https://sp.example.com/SAML2</saml:Issuer><samlp:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/></samlp:AuthnRequest>
のように出力してくれるようになりました。
もう少しコードを改良できそうな気もします4が、今回はこれぐらい書いておけば充分でしょう。
何かのご参考になれば幸いです。
参考文献
- SAMLを使ったSSOの挙動とSAML Response、SAML Requestの概要 - Qiita
- SAMLの仕様を読む。 - マイクロソフト系技術情報 Wiki
- zlib --- gzip 互換の圧縮 — Python 3.10.0b2 ドキュメント
- base64 --- Base16, Base32, Base64, Base85 データの符号化 — Python 3.10.0b2 ドキュメント
- urllib.parse --- URL を解析して構成要素にする — Python 3.10.0b2 ドキュメント