SAML RequestのデコーダをPythonでサクッと書く

## 概要

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」などで[ググる](https://www.google.com/search?q=SAML+Request+decode)と、JavaScriptなどを使ってサイト上でデコードしてくれるサービスは沢山ある事は分かりました。

ただ、調査したいSAML Requestは（主にコンプライアンス的な理由で）社外に出すとマズいものでした。
そのため、そういったネット上のツールに頼ることなく、自力でデコーダを用意することにしました。

## 必要最低限のコード

```python:saml_request_decoder.py
import zlib
import base64

saml_request = ''
print(zlib.decompress(base64.b64decode(saml_request), -zlib.MAX_WBITS))
```

障害対応という状況の性質上、必要最低限動けば良いコードをなるべく短時間で実装する必要がありました。
その時に実装したコードが上記のものです。

デコードしたいSAML Request[^sample]を

[^sample]: 今回は[こちら](https://techinfoofmicrosofttech.osscons.jp/index.php?SAML%E3%81%AE%E4%BB%95%E6%A7%98%E3%82%92%E8%AA%AD%E3%82%80%E3%80%82#r4f7a760)のサンプルを使用させていただき、それを圧縮＋エンコードしたものを使用しています

```python
saml_request = 'fVBNS8NAEP0ry97ziT10SAKhRQioiBUPXmRJpnRhP+LOrNZ/7yZFqJde533Me68hZc0MfeSTe8HPiMTibI0jWIFWxuDAK9IETlkk4BEO/eMD1HkJc/DsR2/kleS2QhFhYO2dFMO+lXpCx/qoMXxUUrxhoAS1MjETThRxcMTKcTqV5V1W1Vm5eS23UFew2b5L0f/Z7byjaDEcMHzpMckmPLeykl2zZILVK3Qn5pmgKGjO8azsbDAfvS2WdHVTXDObyyxPqcCwf/ZGjz+iN8Z/7wIqxlZyiCjFvQ9W8e3Ky0VP2XGlAgflSKfWsuguL/+P3/0C'
```

のように記述し、実行すると

```bash
$ 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>'
```

と、人間にも読める形で出力されました。

:::note

「ライブラリがないよ」とか怒られる場合

```bash
pip3 install zlib
```

等のコマンドで足りないライブラリをインストールしてください。
:::

たった数行（実際にデコードしているのは最後の1行）で実装できてしまうとは驚きました。
PythonのようなLL (Lightweight Language)はこういう場面でサクッと問題を解決できるので、改めて便利さを思い知りました。

## もうちょっと便利にしたコード

緊急時に「とりあえず使える」レベルのコードとしては上記で充分ですが、せっかくなので普通に使えるレベルに改良したいですね。
最低限

- いちいちPythonコードを編集するのが面倒。コマンドライン引数で渡せた方が便利
- `GET`で渡される場合、パラメータはURLエンコードされている。まずそれをデコードする必要がある[^urlencode]
- （今回は該当しなかったが）`POST`で渡される場合、圧縮されずにそのままBase64エンコードされたものが渡されることがある

[^urlencode]: 今回はブラウザの開発者ツールがURLエンコードをデコードしたものを用意してくれたので、対処不要でした

ぐらいには対応したいです。

という訳で、それらに対応するよう改良しました。

```python:saml_request_decoder.py
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())
```

これで

```bash
$ python3 saml_request_decoder.py fVBNS8NAEP0ry97ziT10SAKhRQioiBUPXmRJpnRhP%2BLOrNZ%2F7yZFqJde533Me68hZc0MfeSTe8HPiMTibI0jWIFWxuDAK9IETlkk4BEO%2FeMD1HkJc%2FDsR2%2FkleS2QhFhYO2dFMO%2BlXpCx%2FqoMXxUUrxhoAS1MjETThRxcMTKcTqV5V1W1Vm5eS23UFew2b5L0f%2FZ7byjaDEcMHzpMckmPLeykl2zZILVK3Qn5pmgKGjO8azsbDAfvS2WdHVTXDObyyxPqcCwf%2FZGjz%2BiN8Z%2F7wIqxlZyiCjFvQ9W8e3Ky0VP2XGlAgflSKfWsuguL%2F%2BP3%2F0C
```

のようにコマンドライン引数としてデコードしたいSAML Requestを渡せば[^urlencode2]

[^urlencode2]: 上記の例は、よく見るとURLエンコードされたものを渡しています

```bash
<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>
```

のように出力してくれるようになりました。

もう少しコードを改良できそうな気もします[^improve]が、今回はこれぐらい書いておけば充分でしょう。

[^improve]: 例えばもう少し例外処理を丁寧にするとか、XMLを見やすいようにフォーマットして出力するとか

何かのご参考になれば幸いです。

## 参考文献

- [SAMLを使ったSSOの挙動とSAML Response、SAML Requestの概要 - Qiita](https://qiita.com/sibakenY/items/113ef1439aaaa1e86184)
- [SAMLの仕様を読む。 - マイクロソフト系技術情報 Wiki](https://techinfoofmicrosofttech.osscons.jp/index.php?SAML%E3%81%AE%E4%BB%95%E6%A7%98%E3%82%92%E8%AA%AD%E3%82%80%E3%80%82)
- [zlib --- gzip 互換の圧縮 — Python 3.10.0b2 ドキュメント](https://docs.python.org/ja/3/library/zlib.html)
- [base64 --- Base16, Base32, Base64, Base85 データの符号化 — Python 3.10.0b2 ドキュメント](https://docs.python.org/ja/3/library/base64.html)
- [urllib.parse --- URL を解析して構成要素にする — Python 3.10.0b2 ドキュメント](https://docs.python.org/ja/3/library/urllib.parse.html)