WebSphere Application ServerでSAML SSO（SAML属性取得編）

[前回](https://qiita.com/ch7821/items/143ee26678532ec86f74)はSP-RedirectedのSSOシナリオまで試しましたので、今回はようやくSAMLの属性を取ってきます。TAIが`Subject`を生成してくれることはわかっていますので、これをどうやって取ってくるかというと、

```java
Subject subject = WSSubject.getCallerSubject();
```

となります。標準APIの`Subject.getSubject()`にはJAAS1.0の設計上のミスがあるらしく、`WSSubject.getCallerSubject()`を使用しなさいということのようです[^1]。
[^1]: 詳細は[IBM Knowledge Centerの記事](https://www.ibm.com/support/knowledgecenter/ja/SSAW57_9.0.0/com.ibm.websphere.nd.multiplatform.doc/ae/rsec_logmod.html)を参照してください。
で、このAPIライブラリがどこにあるかというと、
`<WAS_HOME>/plugins/com.ibm.wsfp.main.jar`です。このjarファイルをビルドパスに追加しておきます。

# SAML属性の取得
先に主題であるSAML属性の取得からやってしまいます。細かいところは後から見ていきます。
まずは`Subject`から`com.ibm.websphere.wssecurity.wssapi.token.SAMLToken`を取得します。さらに`SAMLToken`から`List<com.ibm.wsspi.wssecurity.saml.data.SAMLAttribute>`を取ってきます。SAML属性は`SAMLAttribute.getName()`、値（`String[]`）は`SAMLAttribute.getStringAttributeValue()`で取得できます。

```java
LinkedHashMap<String, String> samlmap = new LinkedHashMap<String,String>();
Set<SAMLToken> samlTokenSet = callerSubject.getPrivateCredentials(SAMLToken.class);
for (SAMLToken samlToken : samlTokenSet) {
	List<SAMLAttribute> allAttributes = samlToken.getSAMLAttributes(); //SAMLTokenの属性情報を取得
	if (allAttributes.isEmpty()) {
		out.println("<p class=\"text-danger\">no SAML attribute</p>");
		return;
	}
	for (SAMLAttribute anAttribute : allAttributes) {
		String attributeName = anAttribute.getName(); //属性名を取得
		String[] attributeValues = anAttribute.getStringAttributeValue(); //属性値配列を取得
		String value = null;
		if(attributeValues.length == 1) {
			value = attributeValues[0];
		} else if(attributeValues.length == 0) {
			value = "no value";
		} else {
			StringBuilder sb = new StringBuilder("[");
			for(int i = 0; i < attributeValues.length; i++) {
				sb.append(attributeValues[i]);
				sb.append(", ");
			}
			sb.append("]");
			value = sb.toString();
		}
		samlmap.put(attributeName, value);
	}
}
toTable(samlmap, out); //LinkedHashMapをHTMLのテーブルに出力
```

```text:結果
MailAddress	alto@mognet.net
Description	[hi! i'm alto the border collie,lovely dog,]
Locality	Tokyo
EmployeeNumber	100
GivenName	Alto
UserPassword	{SSHA512}4IQR42cG2N4Wqu/SCFM1eRLwENTFYWWcJoN0DcGjiQz/aZR0eU7csdshMugJqTVbE7KNJ6AcSswHdqkjU1ap2BnwNcd1AY3H
DepartmentNumber	0
TelephoneNumber	0120123456
BusinessCategory	sheep dog
UserId	alto
CarLicense	持ってません
DisplayName	alto the sweet
SirName	Benjiamin
MognetId	alto1
CommonName	Jiang Benjiamin Alto of Mountfield
```

無事にLDAPのユーザ情報（OpenAMでSAML属性としてマッピングした名前とその値）が取得できました。

# HttpServletRequestからの情報
ここから細かい話になります。まずはどんな`Subject`が返ってきてどのような情報が入っているのか見てみたいと思います。とその前に、`HttpServletRequest`にも認証タイプ、リモートユーザ、プリンシパルといったオブジェクトを取得するメソッドがありますので、まずはここからみてみます。

```java
private void printRequestAuthinfo(HttpServletRequest request, HttpServletResponse response, PrintWriter out) {
	LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
	out.println("<h1>Request Authentication Info</h1>");
	map.put("AuthType", request.getAuthType());
	map.put("Remote User", request.getRemoteUser());
	map.put("Principal", request.getUserPrincipal().toString());
	toTable(map, out); //LinkedHashMapをHTMLテーブルにしてoutに出力するメソッド
}
```

```text:結果
Request Authentication Info
AuthType	BASIC
Remote User	/www.alto.net/openam/p0BwwjTucmBs0UiYUGpu0f4Z31JM
Principal	p0BwwjTucmBs0UiYUGpu0f4Z31JM
```

Basic認証とみなされるようです。Principalにはリモートユーザ名の末尾文字列しか持っていないようです。そしてリモートユーザがなんだか見慣れない文字列になっています。これはIdPが生成した一時的なユーザ名です。

## SAMLリクエストパラメータ
今回のSSOシナリオでは、`NameIDFormat`として`urn:oasis:names:tc:SAML:2.0:nameid-format:transient `を指定しています。どこで指定しているかといえば、IdPにアクセスする際のURLです[^2]。具体的には、リクエストのURLクエリで`NameIDFormat=urn:oasis:names:tc:SAML:2.0:nameid-format:transient`と指定しています。ここで改めてIdP-initiatedにおけるHTTPパラメタの意味を整理してみます。ちなみに、全文はこうです。

```text
https://www.alto.net/openam/idpssoinit?metaAlias=/idp&spENtityID=https://www.docker.mognet.net/samlsps/acs&binding=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&NameIDFormat=urn:oasis:names:tc:SAML:2.0:nameid-format:transient&RelayState=https://www.docker.mognet.net/snoop`
```

|パラメタ名 | 値 |内容|
|:--|:--|:--|
|metaAlias|/idp|[OASISの文書](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html)にも記載されておらずわかりづらいですが、SP-initiatedシナリオで、IdP認証後のSPへのリダイレクト時には`/sp`となります[^3]。|
| spENtityID |[ACS_URL]|SPからインポートした`entityID`を指定します。|
| binding   |`~省略~:HTTP-POST `|SAMLのバインディング（HTTPのPOSTなのかSOAP（artifact）なのか、つまりどんなリクエスト形式を使用するか）を指定します。|
|NameIDFormat|`~省略~transient`|IdPが認証した時に発行するユーザ名の属性です。`persistent`を指定すると永続的な名前（つまり同じユーザでログインしたら同じユーザ名になる）、`transient`を指定すると一時的な名前になります。他に`email`等の属性値を指定することもできます。|
|RelayState|リダイレクト先URL|そのままですがACSへのアクセス後にリダイレクトする先のURLを指定します。|

というわけで、このユーザー名はIdPが適当に振った文字列ということでした。

# Subjectの情報
それではSubjectを取得します。とりあえず文字列にして出力（`toString()`）してみます。

```java
Subject callerSubject = WSSubject.getCallerSubject(); //Subjectを取得
out.println("<h2>Caller Subject toString()</h2>");
out.println("<p class=\"text-info\">" + callerSubject.toString() + "</p>");
```

```text:結果（適宜改行）
Subject: 
Principal: https://www.alto.net/openam/p0BwwjTucmBs0UiYUGpu0f4Z31JM
Public Credential: com.ibm.ws.security.auth.WSCredentialImpl@3857789b
Private Credential: {
 com.ibm.wsspi.security.cred.cacheKey=saml_acs_tai:sso_saml20_1868203562n2095008900,
 com.ibm.wsspi.security.cred.uniqueId=https://www.alto.net/openam/p0BwwjTucmBs0UiYUGpu0f4Z31JM,
 com.ibm.wsspi.security.cred.realm=https://www.alto.net/openam,
 Saml20TaiSsoPartners=saml_acs_tai:sso_saml20_,
 com.ibm.wsspi.security.cred.securityName=p0BwwjTucmBs0UiYUGpu0f4Z31JM,
 com.ibm.wsspi.security.cred.groups=[]
}
Private Credential: com.ibm.ws.wssecurity.platform.websphere.wssapi.token.impl.WasSAML20TokenImpl:s2b428665157184b8683f56e597fafa926c4a8d2c4
Private Credential: com.ibm.ws.security.token.SingleSignonTokenImpl@f4a3dc8a
Private Credential: com.ibm.ws.security.token.AuthenticationTokenImpl@95ad9ac0
Private Credential: com.ibm.ws.security.token.AuthorizationTokenImpl@dfa455b1 
```

いろいろ入ってますね。一個めのPrivate Credentialには固定値が入っていますが、他はオブジェクトへのポインタになっています。Private Credentialセット内に５つのオブジェクトが確認でき、うち一つ目はなんだか`itaratable`なもののようです（serializeされて表示されています）。これは`getClass().getName()`してみたところ、`java.util.Hashtable`でした。その中身も含め、`Subject`は全てWASが生成したオブジェクトの集合ですね。

ちなみに、このうち以下のオブジェクトは`null`でした。

+ Public Credential: com.ibm.ws.security.auth.WSCredentialImple
+ Private Credential: com.ibm.ws.wssecurity.platform.websphere.wssapi.token.impl.WasSAML20TokenImpl:s2b428665157184b8683f56e597fafa926c4a8d2c4
+ Private Credential: com.ibm.ws.security.token.SingleSignonTokenImpl@f4a3dc8a
+ Private Credential: com.ibm.ws.security.token.AuthenticationTokenImpl@95ad9ac0

"Private Credential: com.ibm.ws.security.token.AuthorizationTokenImpl"だけが`not null`だったわけですが、これをさらに`Introspection`してみると、以下のような内容でした。

|name|type|getter|setter|value|
|:--|:--|:--|:--|:--|
|attributeNames|java.util.Enumeration|getAttributeNames|n/a|java.util.Collections$3@97ee02c7|
|basicAuth|boolean|isBasicAuth|n/a|false|
|bytes|[B|getBytes|n/a|[B@2328114e|
|class|java.lang.Class|getClass|n/a|class com.ibm.ws.security.token.AuthorizationTokenImpl|
|expiration|long|getExpiration|n/a|1517654532343|
|forwardable|boolean|isForwardable|n/a|true|
|name|java.lang.String|getName|n/a|com.ibm.ws.security.token.AuthorizationTokenImpl|
|principal|java.lang.String|getPrincipal|n/a|user:https://www.alto.net/openam/gzzUy3PD1zVMI8BnrgkHL6uXgCHZ|
|token|com.ibm.wsspi.security.ltpa.Token|getToken|setToken|1517654532343|com.ibm.wsspi.security.cred.cacheKey:saml_acs_tai\:sso_saml20_1868203562n54927714$com.ibm.wsspi.security.cred.expiration:1517654580000$com.ibm.wsspi.security.cred.forwardable:true$com.ibm.wsspi.security.cred.longSecurityName:/www.alto.net/openam/gzzUy3PD1zVMI8BnrgkHL6uXgCHZ$com.ibm.wsspi.security.cred.oid:oid\:1.3.18.0.2.30.2$com.ibm.wsspi.security.cred.primaryGroupId:$com.ibm.wsspi.security.cred.realm:https\://www.alto.net/openam$com.ibm.wsspi.security.cred.securityName:gzzUy3PD1zVMI8BnrgkHL6uXgCHZ$com.ibm.wsspi.security.cred.uniqueId:user\:https\://www.alto.net/openam/gzzUy3PD1zVMI8BnrgkHL6uXgCHZ$hashed_uid:57422832185085709272126784492566922242993614868$u:user\:https\://www.alto.net/openam/gzzUy3PD1zVMI8BnrgkHL6uXgCHZ|
|uniqueID|java.lang.String|getUniqueID|n/a|saml_acs_tai:sso_saml20_1868203562n54927714|
|valid|boolean|isValid|n/a|true|
|version|short|getVersion|n/a|1|```

何度かやり直しながらなので、一時的な値は前に載せているものと異なりますが、確かに"attributeNames"といった情報が入っていそうなフィールドがみつかりました。`SAMLToken`自体はインタフェースなので、どこかに実装クラスがあるはずですが、これ以上はWASが提供する`jar`をデコンパイルしていかないと追いかけられませんね。というわけで今回はSAML属性の値をサーブレットから取得しました、そしてそれは`Subject`内の`AuthorizationTokenImpl`に入っていそうだというお話でした。

[^3]: [OTNの記事](http://otn.oracle.co.jp/technology/global/jp/sdn/javasystem/techtopics/identity/200804.html)に登場はしますが説明は特にありません。
[^2]: 拙記事[SSO実践](https://qiita.com/ch7821/items/c01d1fdb23bb5d8caec8)の回で少しだけ触れています。