KeycloakとmidPointを連携させてみよう(connector-keycloak編)

# はじめに

[NRI OpenStandia Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nri-openstandia)の4日目は、OSSのIAM (Identity and Access Management) プラットフォームである[Keycloak](https://github.com/keycloak/keycloak)とIDM (Identity Management) / IGA (Identity Governance and Administration) プラットフォームである[midPoint](https://github.com/Evolveum/midpoint)を連携させる方法について紹介します。

midPointについてより理解を深めるには、[1日目で紹介](https://qiita.com/daian183/items/1129980c9456f86ad1b9)されていますがmidPointの開発者Radovan氏が書き下ろした[Practical Identity Management with MidPoint](https://docs.evolveum.com/book/)を読むと良いでしょう。バージョンがちょっと古いですが、NRI OpenStandiaで翻訳された[日本語版](https://openstandia.jp/oss_info/midpoint/index.html)もあります。また、昨年のAdvent Calendarである[midPoint by OpenStandia Advent Calendar 2019](https://qiita.com/advent-calendar/2019/midpoint-by-openstandia)も参考にしてください。

連携といっても以下のように色々な箇所での連携があります。

* 認証連携(midPointのログインをKeycloak経由にする)
* KeycloakのユーザーをmidPointで管理する(midPointからKeycloakに対してIDプロビジョニングする)
* Keycloakのパスワード変更をmidPoint側に連携する

今回は上記の2つ目の、midPointからKeycloakに対してIDプロビジョニングする部分についてのお話です。これを行うことで、midPointでユーザーを登録することでKeycloakにログインできるようになります。Keycloakにログインできるようになるということは、Keycloakでセキュリティ保護している各アプリケーションにSSOできるということです。

また、IDプロビジョニングするにしても、LDAPを間に挟むという方式[^1]もありますが、今回はLDAPなしでダイレクトにmidPointからKeycloakに対して[KeycloakのAdmin REST API](https://www.keycloak.org/docs-api/11.0/rest-api/index.html)を利用してプロビジョニングします。そのためのmidPoint用コネクターとして、[Keycloak Connector](https://github.com/openstandia/connector-keycloak)を使います。

[^1]: Keycloakは標準でLDAPをユーザーストレージとして使うこともでき、midPointも標準でLDAPに対してプロビジョニングするコネクターを備えていますので、LDAPを中心に置いてKeycloakとmidPointを連携することができます。

# 前提

本記事ではDockerを使用して環境を構築するため、Dockerが使える環境を用意してください。また、Keycloak、midPoint、Keycloak Connectorはそれぞれ以下のバージョンを使用しています。

* Keycloak: 11.0.3
* midPoint: 4.2
* Keycloak Connector: 1.0.1

# Dockerネットワークの作成

最初に midPoint => Keycloakへの通信を行うため、Dockerネットワークを作成しておきます。

```
docker network create midpoint_keycloak
```

# Keycloakの起動

さくっとDockerでKeycloakを起動します。環境変数 `KEYCLOAK_USER`、`KEYCLOAK_PASSWORD` を設定して管理者アカウントも作成します。

```
docker run --rm --name keycloak -it -p 8080:8080 --net midpoint_keycloak -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak:11.0.3
```

http://localhost:8080/auth/admin にアクセスし、`admin/admin` で管理コンソールにログインできればOKです。

# レルムの追加

動作確認用にレルムを1つ追加しておきます。ここでは `test` という名前で作成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/e9612892-a9f9-4d44-b9ec-7a5ccb373c4c.png)

# midPointの起動

コネクターのインストール用ディレクトリを作成して、Keycloak ConnectorのJARファイルをMavenリポジトリよりダウンロードします。

```
mkdir -p midpoint-kc/var/icf-connectors
cd midpoint-kc/var/icf-connectors
curl -LO "https://search.maven.org/remotecontent?filepath=jp/openstandia/connector/connector-keycloak/1.0.1/connector-keycloak-1.0.1.jar"
cd ../..
```

あとは作成したディレクトリをマウントしてmidPointコンテナを起動します。Keycloakと同一マシンで実行する場合はポートが8080で被らないように、今回は8081にマッピングしています。

```
docker run --rm --name midpoint -it -p 8081:8080 -v $(pwd)/var:/opt/midpoint/var --net midpoint_keycloak evolveum/midpoint:4.2
```

http://localhost:8081/ にアクセスし、 `Administrator/5ecr3t`[^2] でログインできればOKです。

[^2]: お決まりのmidPointの初期システムアカウントのIDとパスワードです。

また、管理コンソールの「リポジトリ・オブジェクト」にアクセスし、画面上のプルダウンメニューで「コネクター」を選択してインストールされているコネクター一覧を確認します。ここでKeycloakConnectorが存在していれば無事に追加したコネクターも認識されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/953eae7e-11c7-bc00-73f2-8c357211369e.png)


# リソース定義の作成

コネクターがインストールできましたので、リソース定義を作成します。リソース定義は外部のリソース(源泉システムや連携先システムのこと)に対する接続定義で、接続情報や属性をどのようにマッピングするか、どのように同期制御を行うかといった設定を行うためのものです。

リソース定義の作成方法には、予め用意したXMLをインポートする方法とGUIで設定する方法の2つがあります。リソース定義のGUI設定は操作がかなり複雑なので、ここではXMLをインポートして設定する方法で解説します。

画面左メニューの 「オブジェクトのインポート」をクリックしてXMLインポートの画面を開きます。「オブジェクトインポート元」を「組み込みエディター」に変更し、以下のXMLを貼り付けて画面最下部の「オブジェクトのインポート」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/d23d00c7-be78-95b6-73a6-c7a44d0afce4.png)


<details><summary>XML設定例</summary><div>

```xml
<resource
  xmlns="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
  xmlns:c="http://midpoint.evolveum.com/xml/ns/public/common/common-3"
  xmlns:icfs="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/resource-schema-3"
  xmlns:org="http://midpoint.evolveum.com/xml/ns/public/common/org-3"
  xmlns:q="http://prism.evolveum.com/xml/ns/public/query-3"
  xmlns:ri="http://midpoint.evolveum.com/xml/ns/public/resource/instance-3"
  xmlns:t="http://prism.evolveum.com/xml/ns/public/types-3">
    <name>keycloak</name>
    <connectorRef relation="org:default" type="c:ConnectorType">
        <filter>
            <q:equal>
                <q:path>c:connectorType</q:path>
                <q:value>jp.openstandia.connector.keycloak.KeycloakConnector</q:value>
            </q:equal>
        </filter>
    </connectorRef>
    <connectorConfiguration xmlns:icfc="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/connector-schema-3">
        <icfc:resultsHandlerConfiguration>
            <icfc:enableNormalizingResultsHandler>false</icfc:enableNormalizingResultsHandler>
            <icfc:enableFilteredResultsHandler>false</icfc:enableFilteredResultsHandler>
            <icfc:enableAttributesToGetSearchResultsHandler>false</icfc:enableAttributesToGetSearchResultsHandler>
        </icfc:resultsHandlerConfiguration>
        <icfc:configurationProperties xmlns:gen255="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/bundle/jp.openstandia.connector.connector-keycloak/jp.openstandia.connector.keycloak.KeycloakConnector">
            <gen255:serverUrl>http://keycloak:8080/auth</gen255:serverUrl>
            <gen255:username>admin</gen255:username>
            <gen255:password>admin</gen255:password>
            <gen255:clientId>admin-cli</gen255:clientId>
            <gen255:realmName>master</gen255:realmName>
            <gen255:targetRealmName>test</gen255:targetRealmName>
        </icfc:configurationProperties>
    </connectorConfiguration>
    <schemaHandling>
        <objectType>
            <kind>account</kind>
            <intent>default</intent>
            <displayName>User</displayName>
            <default>true</default>
            <objectClass>ri:user</objectClass>
            <attribute>
                <c:ref>ri:username</c:ref>
                <displayName>Username</displayName>
                <outbound>
                    <strength>strong</strength>
                    <source>
                        <c:path>$focus/name</c:path>
                    </source>
                </outbound>
            </attribute>
            <attribute>
                <c:ref>ri:email</c:ref>
                <displayName>Email</displayName>
                <matchingRule xmlns:mr="http://prism.evolveum.com/xml/ns/public/matching-rule-3">mr:stringIgnoreCase</matchingRule>
                <tolerant>false</tolerant>
                <outbound>
                    <strength>strong</strength>
                    <source>
                        <c:path>$focus/emailAddress</c:path>
                    </source>
                </outbound>
            </attribute>
            <attribute>
                <c:ref>ri:lastName</c:ref>
                <tolerant>false</tolerant>
                <outbound>
                    <strength>strong</strength>
                    <source>
                        <c:path>$focus/familyName</c:path>
                    </source>
                </outbound>
            </attribute>
            <attribute>
                <c:ref>ri:firstName</c:ref>
                <tolerant>false</tolerant>
                <outbound>
                    <strength>strong</strength>
                    <source>
                        <c:path>$focus/givenName</c:path>
                    </source>
                </outbound>
            </attribute>
            <activation>
                <administrativeStatus>
                    <outbound>
                        <strength>strong</strength>
                        <expression>
                            <asIs/>
                        </expression>
                    </outbound>
                </administrativeStatus>
            </activation>
            <credentials>
                <password>
                    <outbound>
                        <strength>strong</strength>
                        <expression>
                           <asIs/>
                        </expression>
                    </outbound>
                </password>
            </credentials>
        </objectType>
    </schemaHandling>
</resource>
```
</div></details>

正常にインポートできた場合は、緑色の成功メッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/9bf32a77-dc15-b0ab-a175-992157da8b04.png)


続いて、画面左メニューの 「リソース」>「すべてのリソース」をクリックしてリソース一覧のページを開きます。インポートした「keycloak」リソースがあるはずなので、クリックします。
keycloakリソースの画面で「接続テスト」をクリックし、接続確認を行います。問題なければオールグリーンの結果が返ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/ceb73c89-88ef-d019-ebea-85ac055199d0.png)


これで追加したリソースを使える状態になりました。

## XML内容の補足

インポートしたXMLで設定している内容について少し補足しておきます。

### 接続先設定

以下のように接続先であるKeycloakのサーバURL、レルム名、認証情報などを設定しています。もし、異なる値を使っている場合は適宜変更してください。

**本記事では、簡略化のため `admin-cli` クライアントと管理者アカウント・クレデンシャルを使ってKeycloakのAdmin REST APIを呼び出す設定にしています。実際の運用では、レルム管理の権限を付与した専用のクライアント (サービスアカウント) を作成して接続する方が望ましいです。**

```xml
        <icfc:configurationProperties xmlns:gen255="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/bundle/jp.openstandia.connector.connector-keycloak/jp.openstandia.connector.keycloak.KeycloakConnector">
            <gen255:serverUrl>http://keycloak:8080/auth</gen255:serverUrl>
            <gen255:username>admin</gen255:username>
            <gen255:password>admin</gen255:password>
            <gen255:clientId>admin-cli</gen255:clientId>
            <gen255:realmName>master</gen255:realmName>
            <gen255:targetRealmName>test</gen255:targetRealmName>
        </icfc:configurationProperties>
```

### ResultsHandlerの無効化

以下のように3つのResultsHandlerを無効化します。これをしておかないと正常に動作しません。

なぜこれをわざわざ無効化する必要があるかは、midPointのドキュメント「[What are ConnId result handlers?](
https://docs.evolveum.com/faq/connid-result-handlers/) 」に書かれていますが、midPointが内部で使用しているコネクターフレームワーク(midPointと独立しており、他のIDMプロダクトでも利用している)の歴史的な経緯により、デフォルトで有効化となっています。コネクター種類によっては明示的に無効化する必要があり、Keycloak Connectorもその1つです。


```xml
    <connectorConfiguration xmlns:icfc="http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/connector-schema-3">
        <icfc:resultsHandlerConfiguration>
            <icfc:enableNormalizingResultsHandler>false</icfc:enableNormalizingResultsHandler>
            <icfc:enableFilteredResultsHandler>false</icfc:enableFilteredResultsHandler>
            <icfc:enableAttributesToGetSearchResultsHandler>false</icfc:enableAttributesToGetSearchResultsHandler>
        </icfc:resultsHandlerConfiguration>
```

### マッピング設定

今回のXMLでは以下の `outbound` のマッピングを設定しています。

| midPoint | | Keycloak |
| ---- | ----  | ---- |
| name | => | username |
| emailAddress | => | email |
| givenName | => | firstName |
| familyName | => | lastName |

また、アカウント有効/無効の連携のために `<activation>...</activation>` を、パスワードの連携のため `<credentials>...</credentials>` を設定しています。

# ユーザーの追加

では実際にユーザーをプロビジョニングさせてみましょう。画面左メニューの 「ユーザー」>「新規ユーザー」をクリックして新規ユーザー作成画面を開きます。ここでは、以下の項目を入力します。

* 名前
* 名
* 姓
* Eメール
* パスワード > 値

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/114ee2f4-ced7-f7f0-3ff6-4ead07af0379.png)


続いて「アサイン」タブをクリックしてアサイン画面を開きます。アサインアイコンをクリックすると、アサイン先を選ぶポップアップウィンドウが開きます。ここで「リソース」タブをクリックすると、先程作成した「keycloak」リソースが見えます。これを選択して、「パラメータ種類」を「アカウント」、「用途」を「default」に設定して「追加」をクリックします。

今回はユーザーに対して直接リソースをアサインして動作確認していますが、実運用ではロールやアーキタイプを使ったロールベース・プロビジョニングを使う方が望ましいです。詳しくは[こちらの記事](https://qiita.com/zakiyamac09/items/f95a013a06152248a2b6#%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%99%E3%83%BC%E3%82%B9%E3%83%97%E3%83%AD%E3%83%93%E3%82%B8%E3%83%A7%E3%83%8B%E3%83%B3%E3%82%B0)などを参考にしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/bb51ad66-1447-ecae-4177-56c007b61563.png)

これで保存する準備ができました。最後に保存の前に、「変更のプレビュー」をクリックするとどのような変更、プロビジョニングが行われるか確認することができます。二次変更というところに、Keycloakへのプロビジョニング内容が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/c1f6d21c-dab5-4e3d-5317-ab9b191ef6e9.png)


プレビューでエラーも発生せず、プロビジョニングされるデータも想定どおりであれば「保存」をクリックしてユーザー登録を完了します。

# Keycloak側の確認

Keycloak側を確認してみます。管理者コンソールのユーザー一覧を参照すると、登録されていることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/1e1b5db8-34b6-c086-0235-258d73cb5c28.png)

# ログイン確認

実際にプロビジョニングされたアカウントでログイン確認してみます。今回、シングルサインオンのアプリケーションは用意していないですが、Keycloakが提供するエンドユーザー向けのアカウント管理画面を使ってログイン確認することができます。

http://localhost:8080/auth/realms/test/account にアクセスします。プロビジョニングしたアカウント(`user1/password`)でログインしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/871267be-9b04-b49c-5380-021cca2a5db4.png)


無事に認証が成功すれば、アカウント管理画面が表示されるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/9dd3ba46-c2cd-2f7b-16c0-181acbb8dd68.png)


# 変更の確認

midPointからユーザー属性を変更した際に、Keycloak側にも反映されるかどうか確認してみます。
画面左メニューの 「ユーザー」>「すべてのユーザー」をクリックしてユーザー一覧を表示します。先程作成したユーザーをクリックして、詳細画面を開きます。ここで、姓を「Bar」=>「Hoge」に変更して「変更のプレビュー」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/6ae4b092-8bff-be43-e2d9-2eec628888ad.png)

二次変更に変更内容が表示されていれば大丈夫そうですので、「保存」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/f1563fa1-cdfd-b1d4-783b-e8a6da75745b.png)


先程ログインしたアカウント管理画面をリロードさせてみて、姓 (Last name) が変更されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/610a2819-0bfc-2b0f-574b-3620008af08f.png)


# アカウント無効の確認

次はアカウントを無効化してみます。midPointのユーザー詳細画面から、「アクティベーション」>「管理ステータス」を「無効」に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/46a31b4f-6266-35cb-af52-cb511c837dbf.png)


「変更のプレビュー」をクリックしてまた変更内容を確認してみます。二次変更に管理ステータスが `ENABLED` => `DISABLED` と出ていれば大丈夫そうですので、「保存」をクリックします。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/33ad20e3-9f75-0feb-1a8d-86a2fc6ab9b9.png)


これでKeycloakのユーザー詳細画面を開くと、「User Enabled」が「OFF」になっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/cef3cc46-89ff-ee77-9d85-adf6ba7c58eb.png)


実際にログインしようとしてみると、アカウント無効というエラーメッセージが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/0db4076f-de11-6b24-8c59-88581e17129c.png)

# 削除の確認

最後に、アカウントの削除を確認してみます。midPointのユーザー一覧画面から、該当のアカウントを削除します。右側にあるアクションメニューから「削除」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/e2946d1a-ed33-35f4-d474-c0827ff4d4d5.png)


確認のポップアップが表示されるので、「はい」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/d5b626c8-3c65-7d60-cce6-0db5cb6def69.png)


Keycloak側のユーザー一覧で「View all users」をクリックしても、表示されないことを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35233/10b41c74-26a2-ea17-9990-12f067c41565.png)

# おわりに

KeycloakとmidPointの間で、Keycloak Connectorを使って直接アカウントを連携する方法を紹介しました。今回は、基本的なプロビジョニング動作としてユーザー情報の連携のみを紹介していますが、Keycloak側のカスタム・ユーザー属性への連携や、midPointの組織やロールをKeycloakのグループとして連携し、ユーザーに対してグループ所属情報を連携するといったもうちょっと高度な使い方もできます。