SAML
WebSphere
SSO

WebSphere Application ServerでSAML SSO(SAML属性取得編)

前回はSP-RedirectedのSSOシナリオまで試しましたので、今回はようやくSAMLの属性を取ってきます。TAIがSubjectを生成してくれることはわかっていますので、これをどうやって取ってくるかというと、

Subject subject = WSSubject.getCallerSubject();

となります。標準APIのSubject.getSubject()にはJAAS1.0の設計上のミスがあるらしく、WSSubject.getCallerSubject()を使用しなさいということのようです1
で、この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()で取得できます。

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のテーブルに出力
結果
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にも認証タイプ、リモートユーザ、プリンシパルといったオブジェクトを取得するメソッドがありますので、まずはここからみてみます。

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に出力するメソッド
}
結果
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パラメタの意味を整理してみます。ちなみに、全文はこうです。

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の文書にも記載されておらずわかりづらいですが、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())してみます。

Subject callerSubject = WSSubject.getCallerSubject(); //Subjectを取得
out.println("<h2>Caller Subject toString()</h2>");
out.println("<p class=\"text-info\">" + callerSubject.toString() + "</p>");
結果(適宜改行)
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セット内に5つのオブジェクトが確認でき、うち一つ目はなんだか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
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に入っていそうだというお話でした。


  1. 詳細はIBM Knowledge Centerの記事を参照してください。 

  2. 拙記事SSO実践の回で少しだけ触れています。 

  3. OTNの記事に登場はしますが説明は特にありません。