AzureAD SAML SSOを使うときの SAMLRequestを生成する

# 前提
- SAML初心者です
- 全体的によくわかってないけれどやってみた結果をまとめてみました
- ちょっと冗長です

# 経緯
- AzureADをIDプロバイダ（IdP）、AWSをサービスプロバイダ（SP）として使う設定をします
- [Microsoftのチュートリアル](https://docs.microsoft.com/ja-jp/azure/active-directory/saas-apps/amazon-web-service-tutorial) に沿って設定しました
- https://myapps.microsoft.com へアクセスし、Azureログインした上で、AWSアイコンをクリックすると、AWSへ設定したロールでアクセスできました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/21fe49bf-cbd2-f9f9-3cf1-9e0dda81d2ca.png)

- IdP側に各SPへのメニューがあるので、これで使えるのでまあ問題ないと思うのですが、サインオンしていない状態でSPにアクセスした場合はどうするのでしょう

# 調べる
- サインオンしていない場合はIdPへリダイレクトするのが標準のようです
    - 実際そういうサービスはよく見かけます
    - 「SP-Initiated SSO」と言うようです
- SP側から「AuthnRequest」を持ってIdPへリダイレクトするらしいです
- 「AuthnRequest」ってなんでしょう...ありました [シングル サインオンの SAML プロトコル](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/single-sign-on-saml-protocol)
- 必要な情報をXMLに詰め込んだモノみたいです。これをどうやって連携しつつIdPへリダイレクトするのでしょう...
- IdPのsaml2受付URLへGETパラメータ `SAMLRequest` で指定するようです
    - `https://idp.example.com/saml2?SAMLRequest=連携情報`
- GETパラメータにXMLそのまま載せられないですよね。どうするんだろう...
- みつけました 「DeflateしてBase64エンコードしてURLエンコードする」難易度高いな...
- Deflateは圧縮ですね.とりあえずpythonで試すので [zlib](https://docs.python.org/ja/3/library/zlib.html) でできそうです
- 圧縮結果はバイナリなのでbase64でエンコードしてGETパラメータに乗せるのでURLエンコードするってことみたいです
- 途中で気づいたのですが、Deflateは「ヘッダとチェックサムを含めない」という指定がありました
    - `zlib.compress()` だとヘッダ、チェックサムが付加されるので `zlib.compressobj(wbits=)` を使えば良さそうです

# SP-Initiated SSOをやってみる

## SAMLRequestに指定する文字列を生成する

- AuthnRequest XML → Deflate → base64 → urlエンコードして、printしています

```get_saml_request_param.py
import datetime
import zlib
from base64 import b64encode
from urllib import parse


def build_saml_request(request_id, issuer):
    issue_instant=datetime.datetime.utcnow().isoformat(timespec='milliseconds') # 現在時刻でいいかな
    return f'''
        <samlp:AuthnRequest xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
            ID="{request_id}" Version="2.0" IsPassive="false" ForceAuthn="false"
            IssueInstant="{issue_instant}Z"
            xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
            <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">{issuer}</Issuer>
        </samlp:AuthnRequest>'''

def deflate(text):
    compressobj = zlib.compressobj(wbits=-zlib.MAX_WBITS)
    return compressobj.compress(text.encode()) + compressobj.flush()

def get_saml_request_param(text):
    return parse.quote_plus(b64encode(deflate(text)))

saml_request_text = build_saml_request(
    request_id='id356a192b7913b04c54574d18c28d46e6395428ab', # 実際はハッシュとかにするのかな
    issuer='https://signin.aws.amazon.com/saml')             # Identifier (Entity ID)

print(get_saml_request_param(saml_request_text))
```

- `request_id` は適当（ただし英字から始める必要あり）
- `issuer` はAzure側のSAML SSO設定の値 `Identifier (Entity ID)` を使います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/3c202aba-9395-4bc5-e802-31977ad00d8f.png)

- 実行すると下記のようになります
- 出力された長い文字列を使います

```text
$ python get_saml_request_param.py
jZFNS8NAEIbv%2FRVh7002X226JIWgCIUK0loP3qab0S5kd%2BPOpoq%2F3hotGgR1jgPvxzwzCT6nJNBtJ%2BreH8wGn3okH7zo1lDFemeEBVIkDGgk4aXY1tdrkYRcaPTQgAc2Cb7N6rJiqknzGcSLZD9fxOmeZzLP8nnWxIVMiiab4Sxd5FlSwJ4Fd%2BhIWVOxkyULVnQDROqIFXuAlpAFV9ZJHKqdV%2BM4oh5XhjwYf%2FLgCZ%2Fy%2BZRnt7wQeSziIuR5ej%2FW1ETo%2FCn0whrqNbotuqOSuNusK3bwviMRRaQejTIhPFMIGl6tCaXV0TupsdkASgwEf8fVOeuttC1bjvTlcIH7D3A492bLv1uW0YfxV1oZ%2Ffzz8g0%3D
```

## ブラウザからテストしてみる

- GETすればいいので、URLを組み立てればブラウザで確認できますね
- Azure側のSAML SSO設定ページの `Login URL` を使います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53115/52100bd1-6f89-cb05-77f6-4a7eea895daa.png)

URLはこんな感じです（もっと長い）

```text
https://login.microsoftonline.com/cd91xxxxxxxxxxxx/saml2
```

上で生成したSAMLRequest文字列をパラメータとしてくっつけます
（キチントためすなら、さらに `&RelayState=` ＋ SAML SSO設定ページの `Relay State` を指定します）

```text
https://login.microsoftonline.com/cd91xxxxxxxxxxxx/saml2?SAMLRequest=jZFNS8NAEIbv%2F...snip...zz8g0%3D
```

できたURLをブラウザで開くと、MicrosoftアカウントのSigninページが表示されて、正常にサインインできれば、AWSのコンソール画面へ遷移します :hand_splayed: 
（Microsoftアカウントへログイン済みであれば、Signinページは表示されず、AWSのコンソール画面へ遷移します）

# エラー


- AADSTS750054: SAMLRequest or SAMLResponse must be present as query string parameters in HTTP request for SAML Redirect binding.
    - GETパラメータに `SAMLRequest=<request>` が指定されていません
- AADSTS750056: SAML message was not properly base64-encoded.
    - SAMLRequestパラメータの内容がbase64デコードできない内容になっています
    - base64エンコードがうまくできていないか、URLエンコードが悪いかもしれません
- AADSTS7500529: The value '1......' is not a valid SAML ID. The ID must not begin with a number.
    - AuthnRequestXML内のID値は英字から始まる必要があります。数字から始めてはいけません
- AADSTS700016: Application with identifier 'https://signin.aws.amazon.com/saml' was not found in the directory 'cd91xxxxxxxxxxxx'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.
    - AuthnRequestで指定したIssuerと一致する設定がありません。Azure SAML SSO設定の `Identifier (Entity ID)` を確認します

# リンク
下記ページを参照させていただき、助けられました。ありがとうございます

- [SAML2.0でのシングルサインオン実装と戦うあなたに(.NET編)](https://qiita.com/ea54595/items/e932644477a45070690b) 
- [SAMLの仕様を読む。](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)
- [シングル サインオンの SAML プロトコル](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/single-sign-on-saml-protocol)
- [チュートリアル:Azure Active Directory シングル サインオン (SSO) とアマゾン ウェブ サービス (AWS) の統合](https://docs.microsoft.com/ja-jp/azure/active-directory/saas-apps/amazon-web-service-tutorial)