KeycloakでLDAP認証を試してみる

# 今日やること

Keycloakアドベンドカレンダー13日目の今回はKeycloakの機能として提供されている「ユーザーフェデレーション機能」を利用した、LDAPでの認証および情報の連携を試してみようと思います。

KeycloakでLDAP認証を有効にすると、LDAP上に登録されているユーザー情報を用いてSSOを実現することができます。これによって、元々はログイン時にLDAPサーバを参照していたというアプリケーションをKeycloakのSSO連携先として追加することが容易になります。

認証だけでなく、LDAPに登録されているユーザー情報をKeycloakへ一括登録したり、逆にKeycloakからLDAPにユーザー情報を登録したりといったこともついでに試してみました。

やることをまとめると以下の3つです。

* LDAP上に登録されているユーザーでログインする
* LDAPからKeycloakにユーザーを追加する
* KeycloakからLDAPにユーザーを追加する

# 今回利用した環境

## サーバ構成

|  | FQDN | OS | プロダクト |
|:-:|:-:|:-:|:-:|
| SSOサーバ | keycloak.example.com | CentOS 7.4 (kernel 3.10.0-693) | Keycloak 3.4.0.Final |
| LDAPサーバ | ldap.example.com | CentOS 7.4 (kernel 3.10.0-693) | OpenLDAP 2.4.44 |


## LDAPのディレクトリの階層構成

今回はベースディレクトリを`dc=example,dc=com`として、`ou=People,dc=example,dc=com`配下にユーザーを作成していきます。

`test0001`、`test0002`、`test0003`というユーザーを用意しています。これらのユーザーのobjectClassには`top`、`account`、`posixAccount`、`shadowAccount`を指定しています。
`posixAccount`を指定していることにより、`cn`、`uid`、`uidNumber`、`gidNumber`、`homeDirectory`が必須となっています。

Managerのパスワードは今回`password`に設定しています。

`ldapsearch  -x -b 'dc=example,dc=com'`の結果は以下の通りです。

~~~
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# example.com
dn: dc=example,dc=com
objectClass: dcObject
objectClass: organization
dc: example
o: Example Inc.

# People, example.com
dn: ou=People,dc=example,dc=com
objectClass: organizationalUnit
ou: People

# Group, example.com
dn: ou=Group,dc=example,dc=com
objectClass: organizationalUnit
ou: Group

# Manager, example.com
dn: cn=Manager,dc=example,dc=com
objectClass: organizationalRole
cn: Manager

# test0001, People, example.com
dn: uid=test0001,ou=People,dc=example,dc=com
uid: test0001
cn: test0001
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
shadowLastChange: 0
shadowMin: 0
shadowMax: 99999
loginShell: /bin/bash
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/test0001

# test0002, People, example.com
dn: uid=test0002,ou=People,dc=example,dc=com
uid: test0002
cn: test0002
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
shadowLastChange: 0
shadowMin: 0
shadowMax: 99999
loginShell: /bin/bash
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/test0002

# test0003, People, example.com
dn: uid=test0003,ou=People,dc=example,dc=com
uid: test0003
cn: test0003
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
shadowLastChange: 0
shadowMin: 0
shadowMax: 99999
loginShell: /bin/bash
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/test0003

# search result
search: 2
result: 0 Success

# numResponses: 8
# numEntries: 7
~~~


# LDAP上に登録されているユーザーでログインする

まずはすでにLDAPに保存されているユーザーを認証に利用する手順を見ていきます。

## Keycloakの設定手順

ここではKeycloakのユーザーフェデレーションの設定を実施していきます。なお、今回使用するレルムは`demo`レルムとします。

1. Keycloakの管理コンソールにログインします。
2. 左メニューバーから、`demo`レルムを選択します。
3. 左メニューバーで`ユーザーフェデレーション`をクリックします。
4. 右上の`プロバイダーを追加...`から`LDAP`を選択します。
5. 以下のように設定値を入力して、`保存`をクリックします。
 * Import Users:`オフ`
 * 編集モード:`READ_ONLY`
 * ベンダー:`Other`
 * UUID LDAP 属性:`uid`
 * ユーザーオブジェクトクラス:`top,account,posixAccount,shadowAccount`
 * 接続 URL:`ldap://ldap.example.com`
 * ユーザー DN:`ou=People,dc=example,dc=com`
 * Bind DN:`cn=Manager,dc=example,dc=com`
 * Bind のクレデンシャル:`password`
 * 上記以外はデフォルトのまま
 * デフォルトのままだとコンソール表示名は`ldap`となります

![2017-12-11_154501.png](https://qiita-image-store.s3.amazonaws.com/0/214621/4adedfae-3c47-aed2-d8ce-67a49f635d60.png)


> :information_source: この段階ではまだユーザーのインポートは実施しません。

## 動作確認

http://keycloak.example.com:8080/auth/realms/Demo/account にアクセスします。
![2017-12-11_163934.png](https://qiita-image-store.s3.amazonaws.com/0/214621/882971b3-fe32-b232-b73c-f54b346b197e.png)

ログイン画面が表示されるので、LDAPに登録されているユーザーでログインします。
![2017-12-11_164043.png](https://qiita-image-store.s3.amazonaws.com/0/214621/4718f0a7-34c2-ccbb-1d84-6571ae65aa3a.png)

ログインできました。この時のLDAPサーバ側のログには次のような出力がされています。

~~~
slapd[30280]: conn=1273 fd=24 ACCEPT from IP=XXX.XXX.XXX.XXX:XXXXX (IP=0.0.0.0:389)
slapd[30280]: conn=1273 op=0 BIND dn="cn=Manager,dc=example,dc=com" method=128
slapd[30280]: conn=1273 op=0 BIND dn="cn=Manager,dc=example,dc=com" mech=SIMPLE ssf=0
slapd[30280]: conn=1273 op=0 RESULT tag=97 err=0 text=
slapd[30280]: conn=1273 op=1 SRCH base="ou=People,dc=example,dc=com" scope=1 deref=3 filter="(&(objectClass=*)(uid=ユーザー名))"
slapd[30280]: conn=1273 op=1 SRCH attr=uid mail sn cn objectclass createTimestamp modifyTimestamp
slapd[30280]: <= bdb_equality_candidates: (uid) not indexed
slapd[30280]: conn=1273 op=1 SEARCH RESULT tag=101 err=0 nentries=1 text=
slapd[30280]: conn=1274 fd=25 ACCEPT from IP=XXX.XXX.XXX.XXX:XXXXX (IP=0.0.0.0:389)
slapd[30280]: conn=1274 op=0 BIND dn="uid=ユーザー名,ou=People,dc=example,dc=com" method=128
slapd[30280]: conn=1274 op=0 BIND dn="uid=ユーザー名,ou=People,dc=example,dc=com" mech=SIMPLE ssf=0
slapd[30280]: conn=1274 op=0 RESULT tag=97 err=0 text=
slapd[30280]: conn=1274 op=1 UNBIND
slapd[30280]: conn=1274 fd=25 closed
~~~
LDAPサーバでログインしようとしているユーザーの存在確認をManagerとして実行し、存在していれば認証処理を実行していることが分かります。

パスワードの変更を試みます。すると以下の画像のように変更ができません。これは編集モードを`READ_ONLY`としているためです。
![2017-12-11_164118.png](https://qiita-image-store.s3.amazonaws.com/0/214621/0a8ca8c6-411f-cf5c-274a-d24dcec042d5.png)


> :information_source:  編集モードを`WRITABLE`に変更すればKeycloakからLDAPへパスワードなどのユーザー情報を変更することが可能になります。また、編集モードを`UNSYNCED`に変更してもユーザー情報の変更は可能ですが、後述のKeycloakDBには変更が適用される一方、LDAPには書き戻しがされません。

> :warning: 編集モードを`UNSYNCED`にした場合の挙動は[こちら](https://qiita.com/izey0306/items/13075668536c62879f4e#%E8%A3%9C%E4%B8%8D%E6%80%9D%E8%AD%B0%E3%81%AA%E7%8F%BE%E8%B1%A1)もご参照ください。

# LDAPからKeycloakにユーザーを追加する

Keycloakの操作のみでLDAPに登録されているユーザーをKeycloakが持っているDB（KeycloakDB）に一括で追加することができます。

## Keycloakの設定手順

1. Keycloakの管理コンソールにログインします。
2. 左メニューバーから、`demo`レルムを選択します。
3. 左メニューバーで`ユーザーフェデレーション`をクリックします。
4. `ldap`をクリックします。
5. import Usersを`オン`にします。
6. `保存`をクリックします。
7. `すべてのユーザーを同期`をクリックします。

![2017-12-11_164306.png](https://qiita-image-store.s3.amazonaws.com/0/214621/029d92bc-bada-675a-0696-ac0c66a8ceee.png)


## 動作確認

左メニューバーで`ユーザー`をクリックしたあと、ページ上部にある`全てのユーザーを参照`をクリックするとLDAPに登録されているユーザーが表示されます。この操作によって、LDAPに登録されているユーザーがKeycloakDBに反映されていることが確認できます。
![2017-12-11_164418.png](https://qiita-image-store.s3.amazonaws.com/0/214621/5fa20155-0045-74c7-e616-f035db39bb0a.png)


なお、管理コンソールからユーザー情報の変更を試みても、編集モードが`READ_ONLY`となっているままでは変更に失敗します。先述の通り、編集モードを`WRITABLE`や`UNSYNCED`に変更すれば、Keycloakの画面でユーザー情報を変更することが可能です。

しかし、編集モードが`READ_ONLY`のままユーザーの削除を試みると削除することが可能です。削除を実行した直後、Keycloakのログに下記のような出力がされます。

~~~
WARN  [org.keycloak.storage.ldap.LDAPStorageProvider] (default task-5) User 'ユーザー名' can't be deleted in LDAP as editMode is 'READ_ONLY'. Deleting user just from Keycloak DB, but he will be re-imported from LDAP again once searched in Keycloak
~~~
これはKeycloakDBからは削除されるものの、LDAPでは削除されないという旨を示しています。もう一度Keycloakからこの削除したユーザーを検索した場合（例えばこの削除したユーザーがログインをした場合など）は再度LDAPからKeycloakDBにインポートされます。

> :information_source: 編集モードが`UNSYNCED`の場合も同様の挙動を示しますが、`WRITABLE`の場合はKeycloakDBからもLDAPからも削除されます。

# KeycloakからLDAPにユーザーを追加する

こちらもKeycloakの操作のみでKeycloakからLDAPへユーザー登録が可能になります。

ここまではマッパーの設定をしなくても使用することはできましたが、KeycloakからLDAPにユーザーを登録する場合はobjectClassの必須項目についてマッパーの設定を適切に行う必要があります。このマッパーの設定を行うことにより、Keycloak側とLDAP側のそれぞれの属性の対応関係を定義することができます。

## Keycloakの設定手順

1. Keycloakの管理コンソールにログインします。
2. 左メニューバーから、`demo`レルムを選択します。
3. 左メニューバーで`ユーザーフェデレーション`をクリックします。
4. `ldap`をクリックします。
5. 以下のように設定値を入力して、`保存`をクリックします。
 * 編集モード:`WRITABLE`
 * 登録の同期:`オン`
![2017-12-11_173643.png](https://qiita-image-store.s3.amazonaws.com/0/214621/f111db2e-af4b-520d-8af0-871d3c708a25.png)

6. 画面上部の`マッパー`のタブをクリックします。
7. 不要な項目の削除します。
     * `first name`をクリックして画面上部のゴミ箱のアイコンをクリック
     * `last name`をクリックして画面上部のゴミ箱のアイコンをクリック
     * `email`をクリックして画面上部のゴミ箱のアイコンをクリック
     * この時点のマッパーの一覧は次の通りになっています。
![2017-12-12_192324.png](https://qiita-image-store.s3.amazonaws.com/0/214621/90afd583-5273-d1cb-79bf-e77715be74ef.png)

8. `作成`ボタンをクリックして以下のように設定項目を変更します。
 * cnのマッピング追加
     * `作成`ボタンをクリック
     * 以下項目を入力
         * 名前:`cn`（任意）
         * マッパータイプ:`user-attribute-ldap-mapper`
         * User Model Attribute:`username`
         * LDAP Attribute:`cn`
         * 上記以外は上記以外はデフォルトのまま
     * `save`をクリック
 * uidNumberのマッピング追加
     * `作成`ボタンをクリック
     * 以下項目を入力
         * 名前:`uidNumber`（任意）
         * マッパータイプ:`hardcoded-ldap-attribute-mapper`
         * LDAP Attribute Name:`uidNumber`
         * LDAP Attribute Value:`1001`（インクリメント機能などがあれば良いのですが、実装されていないようです。:cry:）
         * 上記以外は上記以外はデフォルトのまま
     * `save`をクリック
 * gidNumberのマッピング追加
     * `作成`ボタンをクリック
     * 以下項目を入力
         * 名前:`gidNumber`（任意）
         * マッパータイプ:`hardcoded-ldap-attribute-mapper`
         * LDAP Attribute Name:`gidNumber`
         * LDAP Attribute Value:`1001`
         * 上記以外は上記以外はデフォルトのまま
     * `save`をクリック
 * homeDirectoryのマッピング追加
     * `作成`ボタンをクリック
     * 以下項目を入力
         * 名前:`homeDirectory`（任意）
         * マッパータイプ:`hardcoded-ldap-attribute-mapper`
         * LDAP Attribute Name:`homeDirectory`
         * LDAP Attribute Value:`/home/${RANDOM}`（いまのところ固定値以外を振りたい場合はランダムとする方法しかないようです。:cry:）
         * 上記以外は上記以外はデフォルトのまま
     * `save`をクリック
 * 例えばcnのマッピング情報は以下のようになっているはずです
![2017-12-12_192045.png](https://qiita-image-store.s3.amazonaws.com/0/214621/7fc6fe32-31ca-6584-cd61-c516c15c80b5.png)
 * 最終的にマッパーの一覧は次の通りになります。
![2017-12-11_164846.png](https://qiita-image-store.s3.amazonaws.com/0/214621/f420e7cd-28cc-e192-daa3-8b170fca3023.png)


## 動作確認

左メニューバーで`ユーザー`をクリックしたあと画面右上の`ユーザーの追加`をクリックします。ユーザー名を入力し、`保存`をクリックするとユーザーの登録が完了します。

今回は`kctest`というユーザーを作成しました。

まずKeycloakDB側ですが、左メニューバーで`ユーザー`をクリックすると新たに追加されたユーザーを確認することができます。
![2017-12-11_164943.png](https://qiita-image-store.s3.amazonaws.com/0/214621/315c106c-4141-dae5-4e8a-2980ecb132df.png)

次にLDAP側ですが、`ldapsearch  -x -b 'uid=kctest,ou=People,dc=example,dc=com'`を実行すると対象のユーザーが確認することができます。

~~~
# extended LDIF
#
# LDAPv3
# base <uid=kctest,ou=People,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# kctest, People, example.com
dn: uid=kctest,ou=People,dc=example,dc=com
gidNumber: 1001
uidNumber: 1001
uid: kctest
objectClass: posixAccount
objectClass: top
objectClass: account
objectClass: shadowAccount
homeDirectory: /home/hjRHlStH8aOI0PdxjbmjCxbnK3cRMp
cn: kctest

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1
~~~

このままではまだパスワードが設定されていないので、ログインする前にパスワードを設定するのを忘れないようご注意ください。パスワードを設定すればログインすることができます。

# まとめ

今回はKeycloakのユーザーフェデレーション機能によって、LDAPに登録されているユーザーでログインをする方法や、Keycloak・LDAPそれぞれにユーザーを追加する方法を確認しました。一つのLDAPで設定を試しましたが、同じ手順を踏むことで複数のLDAPやADにも対応することができます。

ちなみに今回はあまり表立って触れませんでしたが、マッパーの設定を行うことによってLDAPに登録されているさまざまな属性をKeycloakへ連携できますのでぜひお試しください。

LDAPに登録されている情報を利用しているシステムであれば、Keycloakで認証をしているかのようにLDAPを利用したSSOを簡単に実現することができますので、この記事の内容ご参照頂いて身近に利用している既存システムへもご活用いただければと思います！！

# 補)不思議な現象

今回、途中に編集モードを変えた場合の挙動も解説しましたが、Import Usersを`オフ`、編集モードを`UNSYNCED`に設定した状態でパスワードに変更を加えた場合に少し不思議だと思った現象が発生しましたので軽く紹介しようと思います。:alien:

Import Usersが`オフ`であることからKeycloakDBにはユーザー情報が全くないものと考えられます。また、編集モードが`UNSYNCED`なためLDAPへは変更内容が反映されず結果的に変更は一切加わらないと考えたのですが、その変更を実施した後では変更後のパスワードでないと認証が通りませんでした。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/214621/d43c403b-f4c0-7252-68ed-b5cc86061e4e.png)
![図2.png](https://qiita-image-store.s3.amazonaws.com/0/214621/0f697613-86fb-c3b8-a5f3-15b60ab4f813.png)


この時のLDAPのログを見るとユーザーの存在確認は行っているのですが、認証処理はLDAPで行っておらずKeycloak側で行っていました。

画面側には表示されないだけで、パスワードの変更を行った時点でKeycloakのDB側では何らかの形でユーザー情報が格納されているということなのでしょうか。この現象については引き続き調査したいと思っています。

このように編集モードを`UNSYNCED`にすると若干直感的でない挙動を示すことがありそうなので注意が必要そうです。


# 参考資料
* [Keycloak Documentation](http://www.keycloak.org/documentation.html)
    * [LDAP and Active Directory](http://www.keycloak.org/docs/latest/server_admin/index.html#_ldap)
* [NRI OpenStandia Keycloak日本語ドキュメント](http://keycloak-documentation.openstandia.jp/)
* [ZyTrax](http://www.zytrax.com/)
    * [Appendix E: LDAP - Object Classes and Attributes](http://www.zytrax.com/books/ldap/ape/)