WebSphere Application ServerでSAML SSO（SP-redirected編）

今回はTAI（Trust Association Interceptor）のカスタムプロパティを使って、SP-redirectedのSAML SSOシナリオを実践していきます。

<!-- AdminTask.configureInterceptor('[-interceptor com.ibm.ws.security.web.saml.ACSTrustAssociationInterceptor -customProperties ["sso_1.sp.acsUrl=https://www.docker.mognet.net/samlsps/acs","sso_1.sp.idMap=idAssertion","sso_1.idp_1.EntityID=https://www.alto.net/openam","sso_1.idp_1.SingleSignOnUrl=https://www.alto.net:443/openam/SSOPOST/metaAlias/idp","sso_1.sp.login.error.page=https://www.alto.net:443/openam/SSOPOST/metaAlias/idp"] ]') -->

# TAIの設定追加
まずは、認証なしでサーブレットにアクセスしてきたときに、IdP-initiated SSOが発動するようにTAIのプロパティを追加します。

WASの管理コンソールから、「セキュリティー」ー「グローバルセキュリティー」ー「トラスト・アソシエーション」に入ります。![スクリーンショット 2018-01-27 7.04.04.png](https://qiita-image-store.s3.amazonaws.com/0/232587/b26e7fa7-f926-b466-625e-b90b6f8e7975.png)
「インターセプター」に入ります。![スクリーンショット 2018-01-27 7.04.14.png](https://qiita-image-store.s3.amazonaws.com/0/232587/2cee41b1-30d6-fe0e-e1bb-954c17aed943.png)
TAIのカスタムプロパティに入ります。![スクリーンショット 2018-01-27 7.04.31.png](https://qiita-image-store.s3.amazonaws.com/0/232587/054d559c-b975-7447-a821-dd020cea9043.png)
認証エラー時にリダイレクトさせるURLの設定は、`"sso_1.sp.login.error.page"`です。値にIdP-initiatedのときに使用したURL（`"https://www.alto.net/openam/idpssoinit?metaAlias=%2Fidp&spEntityID=https%3A%2F%2Fwww.docker.mognet.net%2Fsamlsps%2Facs&binding=urn%3Aoasis%3Anames%3Atc%3ASAML%3A2.0%3Abindings%3AHTTP-POST&NameIDFormat=urn:oasis:names:tc:SAML:2.0:nameid-format:transient&RelayState=https%3A%2F%2Fwww.docker.mognet.net%2Fsnoop"`）を入れて「OK」します。
![スクリーンショット 2018-01-27 7.35.42.png](https://qiita-image-store.s3.amazonaws.com/0/232587/93a01637-cd80-1233-c88e-647473bcd651.png)
サーバを再起動します。


# SP-redirected SSO確認
Firefoxを立ち上げ、`"https://www.docker.mognet.net/snoop"`に直接アクセスします。"`SAML tracer"`でキャプチャしておきます。
<img width="912" alt="スクリーンショット 2018-01-27 7.53.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/316808d9-89cb-24c4-e706-be2d3c447ab3.png"><img width="912" alt="スクリーンショット 2018-01-27 7.53.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/63ccbdc2-dde6-98e1-b34b-b05468a5e00b.png"><img width="912" alt="スクリーンショット 2018-01-27 7.53.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/e90bdbca-934b-7cdd-c9c4-b2e31ff3ccb6.png">

はい、無事にIdP-initiatedと同じフローになりました。`SAML tracer`でキャプチャした内容を見てみます。

```http:snoop（抜粋）
GET https://www.docker.mognet.net/snoop HTTP/1.1
Host: www.docker.mognet.net

HTTP/1.1 302 Found
Location: https://www.alto.net/openam/idpssoinit?metaAlias=%2Fidp&spEntityID=https%3A%2F%2Fwww.docker.mognet.net%2Fsamlsps%2Facs&binding=urn%3Aoasis%3Anames%3Atc%3ASAML%3A2.0%3Abindings%3AHTTP-POST&NameIDFormat=urn:oasis:names:tc:SAML:2.0:nameid-format:transient&RelayState=https%3A%2F%2Fwww.docker.mognet.net%2Fsnoop
```

レスポンスコード`302`とプロパティに設定したURLが`Location`として返されていることがわかります。


# 実験（ACSに直アクセスしてみる）
今度はビジネスアプリケーション（`snoop`）（笑）ではなく、ACSに直接アクセスしてみます。認証されていないという点では同じなので、同様にIdP-initiated SSOが発動されるはずです。

Firefoxを立ち上げ、`"https://www.docker.mognet.net/samlsps/acs"`に直接アクセスします。
結果、画面遷移は同じでしたが、トレース内容を見ると興味深い差異がありました。
<img width="1384" alt="スクリーンショット 2018-01-27 8.03.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/639d49fb-efce-978e-11cb-4f088f84689e.png">
ACSに対して２回SAMLアサーションをPOSTしています。１回目のレスポンスは未認証時のレスポンスと同じ`"302 Found"`です。SAMLアサーションをTAIが拒否したようです。`OpenAM`は、ログイン済みユーザがログインページにアクセスしてきたらそのまま次の画面へ自動遷移させるので、この辺の動きはユーザには見えません。

## ログ追跡
トラブルシュートを有効にしてトレース情報を追ってみます[^1]。
[^1]: [MustGather: Web Single Sign-on problems with WebSphere Application Server](http://www-01.ibm.com/support/docview.wss?uid=swg21971762)

トレースで役立ちそうなキーワードは`"ACSTrustAssoc"`と`"TAIWrapper"`あたりになりそうです。

```text:trace.log（抜粋加工）
>  createTAIErrorResult(req[com.ibm.ws.webcontainer.srt.SRTServletRequest], res[com.ibm.ws.webcontainer.srt.SRTServletResponse], msg[CWSML7033E: The Security Assertion Markup Language (SAML) Web single sign-on TAI is unable to perform a redirect to the requested target URL [https://www.docker.mognet.net/samlsps/acs].], before[true]) Entry
〜省略〜
3   Resolved error page [https://www.alto.net/openam/idpssoinit?metaAlias=%2Fidp&spEntityID=https%3A%2F%2Fwww.docker.mognet.net%2Fsamlsps%2Facs&binding=urn%3Aoasis%3Anames%3Atc%3ASAML%3A2.0%3Abindings%3AHTTP-POST&NameIDFormat=urn:oasis:names:tc:SAML:2.0:nameid-format:transient&RelayState=https%3A%2F%2Fwww.docker.mognet.net%2Fsnoop]
3   Sending redirect
<  createTAIErrorResult Exit
3   SAMLResponse could not be verified. Auto Re-login. 
<  invokeTAIbeforeSSO:null Exit
<  negotiateValidateandEstablishTrust returns [not null] Exit
<  negotiateAndValidateEstablishedTrust(): status code = 403 Exit
```

あ、ちゃんとエラーコード出てますね。。。３０分くらいトレース追いかけてたアホは私です。

```text:SystemOut.log
[1/27/18 8:44:35:154 JST] 000000bf ACSTrustAssoc E   CWSML7033E: The Security Assertion Markup Language (SAML) Web single sign-on TAI is unable to perform a redirect to the requested target URL [https://www.docker.mognet.net/samlsps/acs].
[1/27/18 8:44:35:156 JST] 000000bf ACSTrustAssoc E   CWSML7030E: The redirect target URL, [https://www.docker.mognet.net/samlsps/acs], matches the value for the assertion consumer service (ACS) URL configured for this service provider.  You cannot redirect to the ACS URL.  The ACS URL is configured on the [sso_<id>.sp.acsUrl] TAI custom property.  CWSML7034I: The redirect target URL was retrieved from the WasSamlSpReqUrl cookie on the request.  
```

`SAML tracer`によるトレースから以下のことがわかります。

+ 未認証のユーザがACSにアクセスしてきたら、TAIのカスタムプロパティ`"sso_1.sp.login.error.page"`に指定したURLをレスポンスヘッダ`Location`にセットして、`HTTP 302`レスポンスを返す
+ さらにこのときに特殊な`Cookie`を返す（`WasSamlSpReqURL`）
+ この`WasSamlSpReqURL`の値はオリジナルのリクエストURL（このケースでは`https://www.docker.mognet.net/samlsps/acs`）
+ IdPで認証してACS URLへアクセスした際にクライアントは`WasSamlSpReqURL`クッキーをリクエストヘッダに付与する

また、`SystemOut.log`から以下のことがわかります。

+ リダイレクトターゲットのURLは`WasSamlSpReqUrl`クッキーの値で決まる
+ リダイレクトターゲットのURLはACSのURLと同じではダメ

１回目のSAMLアサーションのPOSTでエラー（`302`）になったのはこういうわけだったようです。この時のレスポンスでは当該のクッキーを空にして再度リダイレクトレスポンスを返すので、２回目のPOST時には`WasSamlSpReqUrl`クッキーがリクエストヘッダに付与されず、通常のIdP-initiatedのリクエストと同様に処理されたと考えられます。


## 事象の再整理
`SAML tracer`のトレースからこの現象を時系列に再整理します。

### 1. ACSへ直接クライアントから"GET"リクエスト

<img width="912" alt="スクリーンショット 2018-01-27 11.08.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/3e2b72b5-fc10-10f1-7db9-7b12c1d45d5f.png">

* レスポンスコード`302`
* `Location`に`sso_1.sp.login.error.page`のURLをセット
* `WasSamlSpReqURL`クッキーに"GET"リクエストのリクエストURLをセット

### 2. ユーザ認証したクライアントがACSへ"POST"リクエスト（___with SAMLアサーション＆`WasSamlSpReqURL`クッキー___）

<img width="912" alt="スクリーンショット 2018-01-27 11.17.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/182b8285-8864-e670-1495-27c537043e9a.png">

* TAILはリダイレクト先URL（`WasSamlSpReqURL`の値）が自分自身（ACS）なのでNGと判断しレスポンスコード`302`
* 再度`Location`に`sso_1.sp.login.error.page`のURLをセット
* `WasSamlSpReqURL`クッキーをクリア

### 3. ２度目のACSへの"POST"リクエスト（___with SAMLアサーション、NO___ `WasSamlSpReqURL`Cookie）

<img width="912" alt="スクリーンショット 2018-01-27 11.22.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/4d706e66-6290-75d4-9715-e20d4d544ab7.png">
</li></ol>

* まともなクリエストなので認証OKで`302`レスポンス
* `Location`に`RelayState`のURLをセット
* `LtpaToken2`クッキーを発行

比較のため、`snoop`へ未認証アクセスした際のトレースも同様に整理しておきます。

### 1. `snoop`へ直接クライアントから"GET"リクエスト

<img width="884" alt="スクリーンショット 2018-01-27 11.36.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/57fb1d73-9e80-5700-3a4d-2a60639f7740.png">

* レスポンスコード`302`
* `Location`に`sso_1.sp.login.error.page`のURLをセット
* `WasSamlSpReeqURL`クッキーに"GET"リクエストのリクエストURLをセット

### 2. ユーザ認証したクライアントがACSへ"POST"リクエスト（___with SAMLアサーション＆`WasSamlSpReqURL`クッキー___）

<img width="912" alt="スクリーンショット 2018-01-27 11.39.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/83846e2b-31d2-a1c4-0cd8-d7f0ae00baad.png">

* まともなクリエストなので認証OKで`302`レスポンス
* `Location`に`RelayState`のURLをセット
* `LtpaToken2`クッキーを発行

おもいがけず深い沼にはまったので今回はこれくらいにしておきます。次回こそはJavaの話題に入りたいところです。

# 参考文献
+ [IBM Knowledge Center - SAML Web シングル・サインオン (SSO) トラスト・アソシエーション・インターセプター (TAI) のカスタム・プロパティー](https://www.ibm.com/support/knowledgecenter/ja/SSEQTP_9.0.0/com.ibm.websphere.base.doc/ae/rwbs_samltaiproperties.html)