WebSphere＋OpenAMのSAML SSO環境構築（1/3）（IdP構築編）

# WebSphere（WAS）でのSAMLによるSSOの構成
SAMLによるSingle-Sign-On（SSO）をWebSphereでサポートしているということで、実際に試してみました。正直、SAMLという言葉を知ったのですらつい最近なので、認識相違等あればご指摘いただきたいです。

基本用語
===========================================================================================
いろいろな人がいろいろにまとめているので今更感はあるものの、基本用語を自分なりの解釈でまとめておきます。
<dl>
<dt>SAML</dt><dd>"Security Assertion Markup Language"の略。"security assertion"とは直訳すればセキュリティ表明。この人（subject）はこういう認証を受けたよ、という情報を書くためのX509署名つきXML文書[^1]。さらにSAML Attributeといわれる属性を持たせることができる（UID等の具体的なユーザ属性）。</dd>
[^1]: 正式なドキュメントは[OASIS](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html)文書を参照

<dt>IdP</dt><dd>"Identity Provider"の略。"identity"すなわち認証を司る。本人認証にはパスワード認証や生体認証、ICカードなどの所持による認証など様々だが、SAML的にはIdPがどんな認証スキームをとろうがどうでもいい。とにかく、本人認証してアサーションを投げる（ID Federationなどともいう）機能を持ったエンティティ（存在、サービス）。</dd>

<dt>SP</dt><dd>"Service Provider"の略。サービスを誰に提供するかといえばもちろんユーザ。IdP・SPはどちらかというとユーザ視点での見え方。IdPからのアサーションを受取り、認証（"authentication"）されたものとして扱い、認可（"authorization"）した上でサービスを提供するエンティティ（存在、サービス）[^2]。もちろん、アサーションを検証する（正しい、信頼関係のあるIdPからの情報であることを確認する）というステップが不可欠。</dd>
[^2]: 実際に、認証されたユーザを具体的にどのロールにマッピングするかはSPに委ねられます。WASでは「特殊サブジェクト（"special-subject"）」として、「全員（"EVERYONE"）」、「全ての認証ユーザ（"ALL_AUTHENTICATED_USERS"）」、および「信頼されたレルム内で認証されたユーザ（"ALL_AUTHENTICATED_USERS_IN_TRUSTED_REALMS"）」に対してロールをマッピングできます。

<dt>TAI</dt><dd>"Trust Association Interceptor"の略。WASが提供する機能で、SAMLアサーション（署名つきXML文書）を解析して検証し、問題がなければ"`javax.security.auth.Subject`"オブジェクトを生成してくれる。`Subject`にはWASのオブジェクトが格納され、その中にSAML属性情報も含まれる。</dd>

<dt>ACS</dt><dd>"Assertion Consumer Service"の略。WASでは、TAIが検証してOKとなったアサーションを消費して（要は実際に使って）ユーザにサービスを提供するエンティティ。WASにもACSアプリケーションが`"<WAS_ROOT>/installableApps/WebSphereSamlSP.ear"`として提供されている（詳しくはあとでみていく）。一般的には、TAIの機能もACSに含まれる（つまりアサーションを検証し、認可してユーザにロールを割り当てて実際に使用するサービス）。</dd>

<dt>LTPA</dt><dd>"Lightweight Third Party Authentication"の略。IBM社独自の規格である。WASインスタンスで認証したユーザに対し、特殊なクッキー（その名も"`LTPAToken2`"）を発行して別のWASインスタンスでも認証を通す（SSOする）仕組み。</dd>

<dt>IdP-initiated SAML</dt><dd>IdPをリクエストの起点としたSSO。つまり、IdP側でSPへのリンクを持ったポータルサイトなどを作っておいて、ユーザがそのリンクをクリックしたら未認証であれば認証して、SAMLアサーションをつけてSP（ACS）へHTTPリクエストするようなやり方。</dd>

<dt>SP-initiated SAML</dt><dd>IdP-initiatedとは逆に、SP側からSAMLアサーションをIdPに対して要求する。未認証のユーザがSP（ACS）へアクセスしてきたら、正式なSAMLの要求文書（SAMLリクエスト）をIdPに投げて、これをIdP側が処理（ユーザ認証）した上でSPへHTTPリクエスト（アサーション）を投げ返すやり方。</dd>

<dt>SP-redirected SAML</dt><dd>SP-initiatedで「正式なSAMLの要求文書」と書いたのとは対照的に、未認証のユーザがSP（ACS）へアクセスしてきたら、とりあえずIdPのログインページ（たいていform loginのページ）へリダイレクトしてしまえ、その後ここ（SP）に戻ってこられるようにHTTPリクエストのパラメタ（`RelayState`）に自分のURLを書き込んでおく、というやり方。</dd></dl>現状、WASでは"IdP-initiated SAML"と"SP-redirected"方式をサポートしており、"SP-initiated"方式は怪しそうなので、前二つの方式で認証するところまでやってみようと思います。

# 必要環境
WAS/IHSは構築済みとして、実験にあたって以下の環境が必要になります。

+ ユーザリポジトリミドルウェア：具体的にはLDAP、今回は`OpenDJ`を使用
+ IdPミドルウェア：OSS（？）の`OpenAM`を使用
+ 認証局（CA）：証明書発行（特にIdP用とSSL用）、今回は`OpenSSL`を使用

いずれもMacOS上で動作可能なので、IdP機能は全てMacOS上に構築します。もう少し砕いたソフトウェアスタックは以下のようになります。
![名称未設定.png](https://qiita-image-store.s3.amazonaws.com/0/232587/85c606b4-af0f-c52f-6b1b-3c2cd796feba.png)
IdPとSPとでホスト名が一緒だと紛らわしいので、このような構成にしています。IdPは`alto.mognet.net`でMacのループバックアドレス`127.0.0.1`の別名として`hosts`ファイルに定義します。SP側は構築済みの`docker`コンテナで、これらも実体はループバックアドレスへのポートフォワードでアクセスしますが、ループバックのエイリアスとして`10.1.1.1-254`を切っており、`docker`も同じホストアドレスのサブネットを切って構築しています。

|ホスト情報   | ホスト名 | IPアドレス |
|:--        |:--|:--|
| IdP   |alto.mognet.net|127.0.0.1|
| SP |www.docker.mognet.net|10.1.1.10|

ポイントがあるとすれば、SPはWEB（IHS）とAP（WAS）の2サーバで構成しますが、IdPから見たSPのホストがWebサーバになる、ということでしょうか。もし負荷分散装置（ロードバランサ）を導入していれば、そのIPアドレスに対応するホスト名がIdPから見たSPのホスト名になります。あとでWASからSPのメタデータ（IdPに渡すSPの情報）を出力するときに割と効いてくるので念のため。

認証局構築
===========================================================================================

まずはオレオレ認証局を構成します。格好良くいえば「プライベート認証局」ですね。昔、職場で`VerySign`という架空の認証局を、中間CA構成含めたあらゆる属性を真似て作ったことがありますが、このご時世なんとなく怖いので簡単にRoot認証局がEE証明書を発行する形でいきます。

MacOSにも`openssl`は導入されていますが、うまく動かないので`brew install`で持ってきます。

```bash
mac:$ brew update
mac:$ brew install openssl
mac:$ echo 'export PATH=/usr/local/opt/openssl/bin:${PATH}' >> ~/.bash_profile  # PATH設定
mac:$ . ~/.bash_profile
mac:$ which openssl
/usr/local/opt/openssl/bin/openssl
mac:$ openssl version
OpenSSL 1.0.2n  7 Dec 2017
mac:$ mkdir ~/mognetCA         # CAのルートディレクトリを作成
mac:$ cd ~/mognetCA
mac:$ cp /usr/local/etc/openssl/misc/CA.sh ./   # 構築スクリプト雛形をコピー
mac:$ cp /usr/local/etc/openssl/openssl.cnf ./  # 設定ファイル雛形をコピー
```

CA構築スクリプトを編集します。あまりこだわるところではないので、コマンドパスを明示、証明書有効期限を約３０年に延長し、CAディレクトリを変更している程度です。

```diff
61,62c61
< if [ -z "$OPENSSL" ]; then OPENSSL=openssl; fi
< 
---
> OPENSSL=openssl
64c63
< CADAYS="-days 1095"	# 3 years
---
> CADAYS="-days 10950"	# 30 years
70,71c69
< 
< if [ -z "$CATOP" ] ; then CATOP=./demoCA ; fi
---
> CATOP=~/mognetCA
```

スクリプト実行。

```bash
mac$ ./CA.sh
```

設定ファイルを編集します。発行ポリシーを`policy_anything`（誰でもWelcome）、サブジェクトのデフォルト値を使いやすいように変更、`KeyUsage`と`nsCertType`をなんでもにしています。特に、サーバ証明書（SSL用）として使用する場合、`KeyUsage`に`KeyEnciperment`（SSLハンドシェイクの鍵交換時に公開鍵暗号方式でマスターキーを暗号化して交換するため）、`nsCertType`に`server`がないとダメだったと思います。

```diff
8c8
< HOME			= .
---
> HOME			= ~/mognetCA
42c42
< dir		= ./demoCA		# Where everything is kept
---
> dir		= ~/mognetCA		# Where everything is kept
81c81
< policy		= policy_match
---
> policy		= policy_anything
129c129
< countryName_default		= AU
---
> countryName_default		= JP
134c134
< stateOrProvinceName_default	= Some-State
---
> stateOrProvinceName_default	= Tokyo
136a137
> localitiName_default		= Koutou-Ku
139c140
< 0.organizationName_default	= Internet Widgits Pty Ltd
---
> 0.organizationName_default	= mognet.net
179c180
< # nsCertType = objsign
---
>  nsCertType = objsign
185c186
< # nsCertType = client, email, objsign
---
>  nsCertType = server, client, email, objsign
188c189
< # keyUsage = nonRepudiation, digitalSignature, keyEncipherment
---
>  keyUsage = nonRepudiation, digitalSignature, keyEncipherment
191c192
< nsComment			= "OpenSSL Generated Certificate"
---
> #nsComment			= "OpenSSL Generated Certificate"
290c291
< # nsCertType = client, email, objsign
---
>  nsCertType = client, email, objsign
296c297
< nsComment			= "OpenSSL Generated Certificate"
---
> #nsComment			= "OpenSSL Generated Certificate"
```

`openssl`コマンドを発行するたびに`-config ~/mognetCA/openssl.cnf`を書くのは面倒なので、環境変数にしておきます。コマンドエイリアスまで切ってしまうと、サブコマンドを最初の引数に取る`openssl`コマンドの仕様上厳しいので（wrapperスクリプトを書けばいいんでしょうけど）、引数のみ。

```bash
mac:$ echo 'export CA="-config ~/mognetCA/openssl.cnf"' >> ~/.bash_profile
mac:$ . ~/.bash_profile
```

これで証明書発行時は、`openssl ca ${CA} ...`と実行すればOKです。では、一気に今回使用する鍵と証明書を生成していきます。

```bash
mac:$ mkdir -p ~/openAM/work; cd ~/openAM/work; pwd                       # ワークディレクトリ作成して移動
mac:$ openssl genrsa 2048 > opendj.key                                    # LDAPs用鍵生成
mac:$ openssl req ${CA} -new -key opendj.key -out opendj.req              # 同証明書発行要求生成（CN=alto.mognet.net）
mac:$ openssl ca ${CA} -in opendj.req -days 3650 -out opendj.crt          # 証明書発行（有効期限１０年）
mac:$ cat opendj.key opendj.crt | openssl pkcs12 -export -out opendj.p12  # 鍵と証明書をPKCS12形式にエクスポート
```

これで１セットです。あとは`for`で回します。

```bash
mac$: for subject in idp apache ihs; do
  openssl genrsa 2040 > ${subject}.key
  openssl req ${CA} -new -key ${subject}.key -out ${subject}.req
  openssl ca ${CA} -in ${subject}.req -days 3650 -out ${subject}.crt
  cat ${subject}.key ${subject}.crt | openssl pkcs12 -export -out ${subject}.p12
done
```

対話型のところやパスワードは適宜設定します。注意するのは`CN(Common Name)`を自分のホスト名にすることくらいです。あと、`apache`用の鍵にはパスワードをかけられないことでしょうか（最近`apache httpd`を触っていないんですが、さすがにもう鍵パスワードかけられるのかな？）。

Apache httpdインストール 
===========================================================================================

これもMacOSに付属していますが、古そうなのでソースからコンパイル、、、などという~~面倒な~~野暮なことはせずに`brew`で持ってきます。

```bash
mac:$ brew update
mac:$ brew install httpd
```

`httpd.conf`を修正します。

```diff:http.conf
52c52
< Listen 8080
---
> Listen 127.0.0.1:80
85,88c85,88
< #LoadModule auth_digest_module lib/httpd/modules/mod_auth_digest.so
< #LoadModule allowmethods_module lib/httpd/modules/mod_allowmethods.so
< #LoadModule file_cache_module lib/httpd/modules/mod_file_cache.so
< #LoadModule cache_module lib/httpd/modules/mod_cache.so
---
> LoadModule auth_digest_module lib/httpd/modules/mod_auth_digest.so
> LoadModule allowmethods_module lib/httpd/modules/mod_allowmethods.so
> LoadModule file_cache_module lib/httpd/modules/mod_file_cache.so
> LoadModule cache_module lib/httpd/modules/mod_cache.so
128c128
< #LoadModule proxy_module lib/httpd/modules/mod_proxy.so
---
> LoadModule proxy_module lib/httpd/modules/mod_proxy.so
136c136
< #LoadModule proxy_ajp_module lib/httpd/modules/mod_proxy_ajp.so
---
> LoadModule proxy_ajp_module lib/httpd/modules/mod_proxy_ajp.so
146c146
< #LoadModule ssl_module lib/httpd/modules/mod_ssl.so
---
> LoadModule ssl_module lib/httpd/modules/mod_ssl.so
188,189c188,189
< User _www
< Group _www
---
> User piro
> Group staff
210c210
< ServerAdmin you@example.com
---
> ServerAdmin dummy@mognet.net
219c219
< #ServerName www.example.com:8080
---
> ServerName www.alto.net
519c519
< #Include /usr/local/etc/httpd/extra/httpd-ssl.conf
---
> Include /usr/local/etc/httpd/extra/httpd-ssl.conf
529a530,531
> 
> ProxyPass /openam ajp://www.alto.net:8009/openam
```

先を見越して`OpenAM on Tomcat`との連携設定もいれています（`proxy_module, proxy_ajp_module, ProxyPass`）。続いて`httpd-ssl.conf`を修正します。

```diff:httpd-ssl.conf
27c27
< #SSLRandomSeed startup file:/dev/urandom 512
---
> SSLRandomSeed startup file:/dev/urandom 512
29c29
< #SSLRandomSeed connect file:/dev/urandom 512
---
> SSLRandomSeed connect file:/dev/urandom 512
36c36
< Listen 8443
---
> Listen 127.0.0.1:443
92,93c92,93
< SSLSessionCache        "shmcb:/usr/local/var/run/httpd/ssl_scache(512000)"
< SSLSessionCacheTimeout  300
---
> #SSLSessionCache        "shmcb:/usr/local/var/run/httpd/ssl_scache(512000)"
> #SSLSessionCacheTimeout  300
121c121
< <VirtualHost _default_:8443>
---
> <VirtualHost _default_:443>
124,128c124
< DocumentRoot "/usr/local/var/www"
< ServerName www.example.com:8443
< ServerAdmin you@example.com
< ErrorLog "/usr/local/var/log/httpd/error_log"
< TransferLog "/usr/local/var/log/httpd/access_log"
---
> ServerName www.alto.net:443
144,146c140,142
< SSLCertificateFile "/usr/local/etc/httpd/server.crt"
< #SSLCertificateFile "/usr/local/etc/httpd/server-dsa.crt"
< #SSLCertificateFile "/usr/local/etc/httpd/server-ecc.crt"
---
> 	SSLCertificateFile /Users/piro/openAM/work/apache.crt
> 	SSLCertificateKeyFile /Users/piro/openAM/work/apache.key
> 	SSLCertificateChainFile /Users/piro/mognetCA/cacert.cer
154c150
< SSLCertificateKeyFile "/usr/local/etc/httpd/server.key"
---
> #SSLCertificateKeyFile "/usr/local/etc/httpd/server.key"
```

特別な設定はいりません。さっき作った鍵と証明書のパスを指定してやるくらいです（ポートは443を使います）。

Apache Tomcatインストール 
===========================================================================================

おとなしく[公式サイト](https://tomcat.apache.org/download-80.cgi)からダウンロードします。解凍したディレクトリごと、`~/openAM/tomcat`として移動します。特にすることはありませんが、１点だけ`Java`のバージョンが`1.8`系であることだけ確認しておきます。

```bash
mac:$ java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
```

OpenDJインストール 
===========================================================================================

[ForgeRock社の公式サイトからOpenDJをダウンロード](https://www.forgerock.com/platform/directory-services/)します。リンク先から`TRY NOW`を辿ってダウンロードします（要ユーザ登録＠無料）。ZIPを解凍したら、ディレクトリごと`~/openAM/opendj`として移動します。`setup`コマンドを実行して対話型セットアップを実行します。セットアップスクリプトが存在するディレクトリをベースにして、データディレクトリ等が作成されるので、ご自分の環境に合ったディレクトリに配置してください。

```bash
mac:$ cd ~/openAM/opendj; pwd
mac:$ ./setup
〜省略〜
Do you accept the License Agreement? (yes / no) [no]: yes
Select the type of server to set up:

    1)  Directory Server
    2)  Proxy Server
    3)  Replication Server

Enter choice [1]: 1
〜省略〜
Provide the directory root user DN [cn=Directory Manager]: cn=admin
〜省略〜
Provide the server's fully qualified host name [alto.local]: www.alto.net
〜省略〜
Options to secure server connections:

    1)  自己署名付き証明書を生成します (テスト目的のみでの使用を推奨)
    2)  PKCS#12 鍵ストアに存在する既存の証明書を使用します
    3)  JCEKS 鍵ストアに存在する既存の証明書を使用します
    4)  Java 鍵ストア (JKS) に存在する既存の証明書を使用します
    5)  PKCS#11 トークン上の既存の証明書を使用します

Enter choice [1]: 2
PKCS#12 鍵ストアのパス: /Users/piro/openAM/work/opendj.p12
鍵ストアの PIN: 
〜省略〜
Enable LDAP? (yes / no) [yes]: 
LDAP port [1389]: 38900
Enable StartTLS on LDAP port? (yes / no) [yes]: 

Enable LDAPS? (yes / no) [yes]: 38901

Invalid response. Please enter "yes" or "no"

Enable LDAPS? (yes / no) [yes]: 
LDAPS port [1636]: 38901
〜省略〜
ディレクトリデータのベース DN を指定してください: [dc=example,dc=com]: dc=mognet,dc=net
〜省略〜
All Server Parameters
---------------------------------------------------------------------
Server instance path                   /Users/piro/openAM/opendj
ルートユーザー DN:                            cn=admin
Root user password                     ******
Hardened configuration for production  無効
Server's fully qualified host name     www.alto.net
管理コネクタポート:                             6444
Start server after setup               有効
Server security                        Use existing PKCS#12 keystore
Keystore file                          /Users/piro/openAM/work/opendj.p12
Keystore password                      ******
Certificate nickname(s) to use         opendj
LDAP (cleartext)                       Listening on port 38900
Start TLS (secure) for LDAP            有効
LDAPS (secure)                         Listening on port 38901
HTTP (cleartext)                       Listening on port 18080
HTTPS (secure)                         Listening on port 18081
Data storage option                    Generate 6000 sample entries
Base DN to create                      dc=mognet,dc=net
Data storage (backend) type            JE Backend
〜省略〜
```

サンプルデータを6000エントリ生成するオプションをつけましたがどちらでもよいです。ちなみにサンプルエントリは、オブジェクトクラス`inetOrgPerson`として生成されます。
ただし、実際のユースケースでは、組織コードや社員番号等、LDAP標準スキーマをそのまま使って運用することは少ないと思います（というかそうしている例を知らない）。そこで、今回は一つのオブジェクトクラスに一つの属性を持ったスキーマを追加で作成します。
セットアップが正常に完了すれば、ディレクトリサーバが起動していますので、まずは以下のようにスキーマ定義ファイルを作成します。

```text:addschema.ldif
1: # "mognetid"という名前で、文字列（UTF-8）を値に持つ属性（attributeTypes）を追加する
2: dn: cn=schema
3: changetype: modify
4: add: attributeTypes
5: attributeTypes: (
6:   1.1.2.1.1-myOwnSchema
7:   NAME 'mognetid'
8:   DESC 'mognet.netextended attribute'
9:   EQUALITY caseIgnoreOrderingMatch
a:   ORDERING caseIgnoreMatch
b:   SUBSTR caseIgnoreSubstringsMatch
c:   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE userApplications )
d: 
e: # "mognetobject"という名前で、inetOrgPersonオブジェクトクラスを拡張した、
f: # 属性"mognetid"を必須値に持つオブジェクトクラスを追加する
g: dn: cn=schema
h: changetype: modify
i: add: objectclasses
j: objectclasses: (
k:   1.1.2.2.1-myOwnSchema
l:   NAME 'mognetobject'
m:   DESC 'mognet extended objectclass'
n:   SUP inetOrgPersono
o:   STRUCTURAL
p:   MUST mognetid )
q: 
```

説明の都合で行番号（1~q:）を記載しています（ていうかこれ、マークダウンインタープリタで自動でやってくれないかなぁ。。。）。一応解説しておくと、まず2〜c行目までで一つの属性（属性名：`mognetid`）を追加しています（RDBでいうところのカラムに近いです）。6行目は当該属性のOID（Object IDentifier）[^2]を記載します。本来であればIETFあたりが管理していますが（だったかな？）、検証用の独自属性なので適当なIDをいれておきます。9〜b行目は照合規則等の指定です。c行目でシンタックス、つまりこの属性の構文（データ型）を指定しています（RDBでいうところの、カラムの型に近いです）。この場合は、`SYNTAX`の`OID`が`1.3.6.1.4.1.1466.115.121.1.15 `なので、`Directory String syntax`になります（UTF-8の文字列だと思えばよいです）。

g行目からがオブジェクトクラスの追加です。既存の標準オブジェクトクラスに、追加した独自属性を持たせることも物理的には可能ですが、移植性等の問題からあまりというかほとんど禁じ手に近いです。普通は、このように別のオブジェクトクラスを作成します。今回はn行目にある通り、`inetOrgPerson`オブジェクトクラスを拡張し（Javaのクラスの拡張（`extend`）同様、子のオブジェクトクラスは暗黙的に親オブジェクトクラスの属性を継承します。`is A`の関係です）、p行目でさきほど追加した`mognetid`属性を`MUST`で持つ、l行目`mognetobject`というオブジェクトクラスを追加します。

なお、LDIF形式はディレクトリサーバとの通信で使用するディレクトリ操作言語（SQLのような規格）ですが、スペースや空行の扱いが結構厳格です。基本形は`aaa: bbb`ですが、ここで`aaa`が属性、`bbb`が値になります。値を複数行で書く場合は、2行目以降の先頭にスペース文字が一つ必要です。さらに、`attributeTypes`属性の構文として、たとえば`(`と`OID`の間には一つのスペースがなくていはいけない、などのルールもあります。なので、6〜c行目とk〜p行目ではあえて行頭に2個の半角スペースをいれています。

ひと昔まえによく`ldif`を触っていましたが、そのときは各エントリ間は`\n-\n`で区切らなければいけなかった気がします。なぜかそれをいれたら弾かれたのでいれていません。もしかしたら他の構文エラーがあって弾かれたのを勘違いしただけかもしれません。上の表記でうまくいかない場合は、d行目の後ろに`-`だけの行ともう一つ空行をいれてください。さらにいうと、LDAPサーバには基本的に1操作単位でしかトランザクションの概念がありません（一つの変更に対してはほとんどの実装でATOMIC性があります）。このLDIFの場合、一つのエントリ（`dn: cn=schema`エントリ）を２回`modify`しますので、2トランザクションになります。一件ずつコミットされますので、同じLDIFを複数回実行することはできません（同じ`attributeTypes`を`add`することはできません）。
[^2]: NW屋さんでMIBなんかを触る人はよく目にするものかと思います。[OID検索サイト](http://www.oid-info.com/index.htm)なども参考にしてください。

```bash
mac:$ ./bin/ldapmodify -p 38900 -D cn=admin -w password -f ./addschema.ldif
```

無事に正常終了すればスキーマ拡張は完了です。このオブジェクトクラスを持つエントリー（ユーザ）を以下の`ldif`で定義して同じように追加します。

```text:alto.ldif
dn: uid=alto,ou=People,dc=mognet,dc=net
objectClass: mognetobject
mail: alto@mognet.net
departmentNumber: 0
carLicense: n/a
employeeNumber: 100
telephoneNumber: 0120123456
givenName: Alto
mognetid: alto1
displayName: alto the sweet
sn: Benjiamin
cn: Jiang Benjiamin Alto of Mountfield
businessCategory: sheep dog
description: lovely dog
description: hi! i'm alto the border collie
userPassword: password
l: Tokyo
```

ディレクトリに登録します。

```bash
mac:$ ./bin/ldapmodify -p 38900 -D cn=admin -w password -f ./alto.ldif
mac:$ ./bin/ldapsearch -p 38900 -D cn=admin -w password -b dc=mognet,dc=net objectclass=mognetobject
```

これでエントリの検索に一件ヒットすればOKです。今作ったユーザが、実際にシングルサインオンシナリオで使用するユーザなので、特に`uid`と`userPassword`の値は手元に控えておきます。ついでに、検索した結果の`userPassword`属性の値がハッシュ値になっていることも確認できます。多くのディレクトリサーバの実装では、設定値`passwordStorageScheme`に則って、平文ではなくハッシュ値を格納します（逆にいえばこのスキームを`text-plain`などとすれば、平文で格納することができる実装もあります）。また、登録時には`mognetobject`オブジェクトクラスしか指定していなかったのに、検索すると親オブジェクトクラスである`inetOrgPerson`なども追加されているのがわかります。

OpenAMインストール
===========================================================================================
`OpenDJ`と同様に、[ForgeRock社の公式サイトからOpenAMをダウンロード](https://backstage.forgerock.com/downloads/OpenAM)します。
<img width="1344" alt="スクリーンショット 2018-01-24 18.57.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/2b115e44-4db5-9d78-85a3-2170ffd143ef.png">
"Item..."の後ろに`S`のマークがついているものはサブスクリプション（有償）版です。今回は`13.0.0`を選択します。<img width="1231" alt="スクリーンショット 2018-01-24 18.58.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/a24c8a28-fb39-323a-f64a-1310d4dc174d.png">Tomcatにデプロイするので、`war`形式のファイルをダウンロードします。ダウンロードしたら、名前を`openam.war`に変更して、先にインストールしたTomcatのドロップインディレクトリ（`~/openAM/tomcat/webapps`）に移動またはコピーします。

IdP環境構築
===========================================================================================
さて、ここからはIdPに必要なサービスを起動して、初期設定をやっていきます。`openDJ`、`httpd`と`tomcat`を一つずつ落としあげするのは面倒なので、以下のようなコントロールスクリプトを作っておきます（エラー処理等省略）。

```bash:/usr/local/bin/openamctl
#!/bin/sh

OPENAM_HOME=/Users/piro/openAM
export CATALINA_HOME=${OPENAM_HOME}/tomcat

case "${1}" in
	start) 
	${OPENAM_HOME}/opendj/bin/start-ds
	${CATALINA_HOME}/bin/startup.sh 
	sudo /usr/local/bin/apachectl start
	;;
	stop)
	sudo /usr/local/bin/apachectl stop
	${CATALINA_HOME}/bin/shutdown.sh
	${OPENAM_HOME}/opendj/bin/stop-ds
	;;
	*)
	echo "Usage: ${0} start|stop"
	;;
esac
```

パスワードなしで`sudo`できるよう`sudo visudo`で`piro ALL=NOPASSWD: /sbin/ifconfig,/usr/bin/vi,/usr/local/bin/apachectl
`のように`/usr/local/bin/apachectl`をパスワードなしで`sudo`実行できるようにしておきます。それでは、IdPを起動します。

```sh
mac:$ openamctl start
```

起動できたらブラウザを立ち上げ、`https://www.alto.net/openam`にアクセスします。初期設定ウィザード画面になりますので、カスタム設定（スクショ撮り忘れているので項目名違うかも）で新規作成していきます。

| 設定項目 | 設定値           | 備考                 |
|:--- |:---                 |:---------------------|
|adminユーザ名  |amadmin|   |
|adminパスワード|p@ssw0rd|   |
|サーバURL|https://www.alto.net/|   |
|Cookieドメイン|www.alto.net|Tomcat 8以降のバージョンでは、`.`（ドット）から始まるCookieドメイン名は使用不可   |
|プラットフォームロケール|ja_JP|   |
|設定ディレクトリ|/Users/piro/openAM/config|事前に作成不要。存在する場合は空ディレクトリでないとNG|
|設定データストア|OpenAM|OpenAM自体の情報を格納するディレクトリサーバ|
|ポート|52389|デフォルト|
|管理者ポート|7444|デフォルト   |
|JMXポート|3689|デフォルト|
|暗号化キー|[ランダム文字列]|   |
|ルートサフィックス|dc=mognet,dc=net|ユーザリポジトリLDAPサーバのルートDN|
|デフォルトポリシーエージェント（UrlAccessAgent）|password|adminパスワードと同じ文字列は不可|

初期設定ができたらログイン画面が表示されます。ここから具体的な設定に入ります。`amadmin`ユーザでログインしてください。

LDAP属性設定
--------------------------------
まずはOpenAMに今回使用するユーザリポジトリのスキーマ情報を定義します（初期設定では`inetOrgPerson`の一部属性しか認識しません）。"Top Level Realm"に入ります（Realmとは抽象的な概念ですが、認証が通る境界内の名前空間といったニュアンスです。認証ドメインといってみてもいいかもしれません）。
<img width="1283" alt="スクリーンショット 2018-01-24 21.31.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/cebed894-abc7-60c9-43af-a677762e3b3d.png">
「Data Sources」に入ります。
<img width="1283" alt="スクリーンショット 2018-01-24 21.31.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/28bd3b86-3a58-44ba-c61a-2fce8b1c2f87.png">
「OpenDJ」に入ります。
<img width="1283" alt="スクリーンショット 2018-01-24 21.31.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/25dab69a-6864-b6b8-1988-a66a9d11fd1d.png">
「ユーザー設定」のセクションに、「LDAP ユーザーオブジェクトクラス」に`mognetobject`オブジェクトクラスを追加します。また、同様に「LDAP ユーザー属性」に、以下の属性が登録されている状態にします。

```text
userPassword
telephoneNumber
mail
displayName
givenName
mognetid
description
dn
cn
l
carLicense
employeeNumber
uid
departmentNumber
businessCategory
sn
```

<img width="1283" alt="スクリーンショット 2018-01-24 21.32.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/b3de2109-d66c-171a-fba8-25f2008ddd76.png">
設定したらページ上部にある「保存」ボタンを押します。

IdP鍵と証明書の設定
--------------------------------
割と前に作った鍵と証明書をOpenAMに登録します。`PKCS12`キーストアフォーマットには対応していないようなので、`jre`の`keytool`コマンドを使って、まずは`JKS`タイプのキーストアを作成します（`keytool`のインポートコマンドで`PKCS12`→`JKS`に変換）。

```bash
mac:$ cd ~/openAM/config/openam; pwd
mac:$ mkdir private
mac:$ cd private; pwd
mac:$ keytool -importkeystore -srckeystore ~/openAM/work/idp.p12 \
>     -srcstoretype PKCS12 -destkeystore keystore.jks -deststoretype JKS \
>     -storepass changeit  # パスワードは適宜で設定
mac:$ keytool -changealias -alias 1 -keystore ./keystore.jsk -storepass changeit # キーストア内のエイリアス名を"idp"に変更
mac:$ keytool -list -v -keystore ./keystore.jks -storepass changeit              # 一覧出力
```

キーストアと秘密鍵にはパスワードがかかっているので、このままでは`OpenAM`から鍵を使えません。そこで、同じディレクトリに暗号化したパスワード情報を格納しておきます。パスワードの暗号化は以下のURLで行います（`amadmin`ユーザでログイン済みでないとページを開けません）。
`https://www.alto.net/openam/encode.jsp`
フォームにパスワード平文を入力して送信すれば、暗号化されたパスワードが画面に出力されます。
<img width="1283" alt="スクリーンショット 2018-01-24 22.13.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/c244c433-75a9-7cdc-ae71-c3a99fc86563.png">
ちなみに、暗号化キーはセットアップ時にランダムで設定されます。

ファイルに書き出します。末尾に改行が入らないよう、`echo`ではなく`printf`をおすすめします。ファイル名は任意ですが、以降は記載のファイル名で作成した前提で進めます（今更ですが）。

```bash
mac:$ printf [出力されたキーストアパスワード暗号化文字列] > .storepass
mac:$ printf [出力された秘密鍵パスワード暗号化文字列]    > .keypass
```

Webコンソールに戻ります（セッションが切れている場合は再ログインします）。画面上部のナビメニューから「CONFIGURATION」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 22.17.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/56d68aa6-f634-363c-95e5-a291a8b367f4.png">「サーバーおよびサイト」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 22.19.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/9b46c3b3-e6d2-d3fa-95c1-ee3ce003c6e7.png">「デフォルトのサーバー設定値」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 22.20.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/21117cd3-238e-6bbb-b2f1-a55d9b44b85a.png">「セキュリティー」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 22.24.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/8d4a0748-a0b7-3235-6db4-d2bf1137c476.png">「キーストア」セクションにて、前の手順で作成したキーストアの情報を設定します。

|設定項目|設定値|
|:---|:---|
|キーストアファイル|%BASE_DIR%/%SERVER_URI%/private/keystore.jks|
|キーストアパスワードファイル|%BASE_DIR%/%SERVER_URI%/private/.storepass|
|非公開鍵パスワードファイル|%BASE_DIR%/%SERVER_URI%/private/.keypass|
|証明書エイリアス|idp|

ページトップの「保存」を押します。

IdPの作成
---------------------------------
Webコンソールから、「Top Level Realm」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 21.31.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/cebed894-abc7-60c9-43af-a677762e3b3d.png">「Create SAMLv2 Providers」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 22.43.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/7f311f6a-c3be-b9e4-ee53-f0e83a98d35e.png">「Create Hosted Identity Provider」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 22.44.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/2b151ef2-2fb9-08f8-5405-d68046862bbd.png">「このプロバイダのメタデータがありますか？」が「いいえ」になっていることを確認しつつ値を入れます。変更が要るのは「署名鍵：`keytool`で指定したエイリアス名」と「新しいトラストサークル」くらいです。トラストサークル[^3]は任意の名前でOKです。ここでは`mognettrustcircle`としています。<img width="1283" alt="スクリーンショット 2018-01-24 22.46.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/595eb39f-7783-acc8-cd5f-4dfe5be227ed.png">
入力できたら「設定」を押します。
[^3]: トラストサークルは、OpenAM用語です。レルムのなかに存在する、信頼関係にあるIdPやSPといったエンティティの集まり（サークル）という解釈でよいと思います。

SAMLの設定
---------------------------------
IdPがSPに渡すSAML文書に載せるユーザ属性（SAML属性、"SAML Attribute"）を設定します。<img width="1283" alt="スクリーンショット 2018-01-24 23.00.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/205764df-45cd-e348-390b-51563fc2cb78.png">コンソールトップから「FEDERATION」に入ります。エンティティプロバイダのIdPのURLを押します。<img width="1283" alt="スクリーンショット 2018-01-24 23.01.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/b9e64c26-dfb3-f0c8-a09a-145803c3d0bc.png">「表明処理」に入ります。<img width="1283" alt="スクリーンショット 2018-01-24 23.03.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/232587/c352b7d2-fc97-6aaa-1a21-a935b1af4518.png">「属性マッパー」セクションの「属性マップ」を以下のように設定してください。なお、画面内に注記されている通り、`SAML属性名=LDAP属性名`という形式でマッピングしていきます。ここを設定しないとSAMLのアサーションに属性が載りません。

```
CommonName=cn
MognetId=mognetid
SirName=sn
BusinessCategory=businessCategory
CarLicense=carLicense
DepartmentNumber=departmentNumber
Description=description
DisplayName=displayName
EmployeeNumber=employeeNumber
GivenName=givenName
Locality=l
MailAddress=mail
TelephoneNumber=telephoneNumber
UserId=uid
UserPassword=userPassword
```

入力できたら「保存」します。ここで、敢えて`userPassword`属性もSAML属性「UserPassword」として送ることにしていますが、実際はこんなことはしないでしょう。そもそもLDAP上はハッシュ値しか持っていませんし。今回は実験なので、このようにしていますが、本来SSOシナリオにおいてユーザの認証に必要なデータ（パスワードの情報）をSPに送るのはアンチパターンですね。

これでIdPとしての設定は完了です。[次回](https://qiita.com/ch7821/items/b7e55ea2c194a5380f2d)はSP環境を構築していきます。

参考文献
===========================================================================================
* [IBM Knowledge Center](https://www.ibm.com/support/knowledgecenter/ja/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/cwbs_samlssosummary.html)
* [WebSphere SAML構成ガイド](https://www.ibm.com/developerworks/jp/websphere/library/was/saml_sso_guide/index.html)
* [Understanding the SAML trust association interceptor for the WebSphere Application Server](https://www.ibm.com/developerworks/library/mw-1612-lansche-trs/index.html)