SSO
openid_connect
SingleSignOn
Keycloak

KeycloakでOpenID Connectを使ってシングルサインオンをしてみる(認可コードフロー(Authorization Code Flow)編)

今日やること

Keycloakアドベンド 17日目は、OpenID Connectの認可コードフローをやってみたいと思います。Relying Partyを作って、認可コードフローでシングルサインオンをしてみましょう。

というか、認可コードフローってなんだっけ?

こんなの。
無題01.png
無題02.png
無題03.png

事前準備

Keycloakは2日目の記事などを参考にしてインストールしておいてください。テストユーザーはこの記事の中で作ります。

Relying Party (RP) を作る

OpenID Connectをやるなら、当然シングルサインオンするRelying Partyが必要なので、これから作っていきます。Relying Partyは何でもいいのですが、WebサーバーはおなじみのApache HTTPDにして、これにmod_auth_openidcを入れてみます。

mod_auth_openidcは、名前の通りApacheに入れるmod_xxx系で、httpd.confに設定するだけでRelying Party化することができ、Apache上のWebアプリケーションには、IDトークンとかアクセストークンとかがリクエストヘッダで渡ります。

:information_source: 認証するのに必要なトークンは理屈上IDトークンだけですが、実際の使用場面では、アプリケーション(RP)のユーザーと紐付けるID(ユーザー名とかメールアドレスとか)を取得するために、ユーザー情報(Userinfo)エンドポイントを使うためにアクセストークンも受け取ることが多いです。

環境

  • Relying Party (RP) の環境
項目
OS CentOS 7.4
URL https://172.26.22.25
クライアントID (client_id) apache24
リダイレクトURI https://172.26.22.25/private/callback

7系のOSなら、rpm が yum レポジトリにあるのでインストールが簡単だと思います。また、雑な構築なのでURLはIPアドレスにしていますが、ちゃんとFQDNにしましょう。httpsになっているのは中の人の趣味の問題で、httpでもできます(OpenID Connectの仕様はhttps必須ですが)。

  • OpenID Provider (OP) の環境
項目
URL https://172.26.22.5
エンドポイント https://172.26.22.5/auth/realms/master/.well-known/openid-configuration

Keycloakの環境です。こちらも雑な構築なのでIPアドレスですが、ちゃんとFQDNに。。httpsも以下同様。

Keycloakのエンドポイントは、URLにレルム名が入っている通り、レルム別になっています。つまり鍵とかアルゴリズムとかそういったモノは レルム単位に別々に設定できることになります。レルム(≒マルチテナント)という用途を考えると、まぁ当然かな、と。

必要なモノを手に入れる

とにかく動く環境があればいいや、ならyumで入ります。

$ yum install mod_auth_openidc

でも、最新版が使いたい! というわがままな人の場合は GitHub からソースか RPM を取得する必要があります。

本体(ダウンロード先)

この記事執筆時点(2017年11月)では、最新安定版は 2.3.2 みたいなので、これを使ってソースからビルドしてインストールすることにします。

無題1.png

依存ライブラリ

yumで入りますが、cjose だけ yum レポジトリにないので個別にダウンロードします。mod_auth_openidcダウンロードページと同じ場所にあります。この記事執筆時点(2017年12月)では v.2.3.0 のページからダウンロードしろ、だそうです。
https://github.com/pingidentity/mod_auth_openidc/releases/v2.3.0

ソースビルドしてインストールする

  • 依存ライブラリを yum とか rpm で突っ込む
$ yum install automake
$ yum install curl curl-devel
$ yum install jansson jansson-devel
$ rpm -ivh cjose-0.5.1-1.el7.centos.x86_64.rpm
  • configureする

Apacheのmod系インストールのお約束ですが、aprとapr-utilはApache HTTPDをインストールしたファイルを参照する必要があります。いわゆるpkgconfigです。Apache HTTPDをyumでインストールした場合は不要ですが、ソースビルドで入れた場合は、pkgconfigのパスを環境変数PKG_CONFIG_PATHで設定する必要があります。pkgconfigは (Apacheインストールディレクトリ)/lib/pkgconfig にあります。また、configureのときにも apxs のパスを--with-apxs2で指定する必要があります。(パスは(Apacheインストールディレクトリ)/bin/apxs) 。

$ ./autogen.sh
$ export PKG_CONFIG_PATH=/opt/apache-2.4/lib/pkgconfig
$ ./configure --with-apxs2=/opt/apache-2.4/bin/apxs
$ make && make install

うまくいくと、(Apacheインストールディレクトリ)/modules/mod_auth_openidc.so が生成されます。

設定する

Keycloakに設定する

Keycloak管理コンソールに管理者でログインして、設定します。

メニューの「クライアント」から「作成」を押します。
2017-10-27_134025.png

「クライアントID」は Relying Party の設定で書いた apache24にして、「クライアントプロトコル」はopenid-connectを選択して「保存」を押します。
2017-10-27_134157.png

デフォルトの設定がちょっと残念なので、少し変更しておきます。「アクセスタイプ」をpublicconfidential に変えておきます。また、「有効なリダイレクトURI」には Relying Party の設定で書いたURLを設定します。
2017-10-27_134428.png

「保存」した後、「クレデンシャル」を選択して、「クライアント認証」を「Client ID & Secret」を選択し、「シークレット」の値をコピーして控えておきます。この値がクライアントシークレットになります。
2017-10-27_135025.png

mod_auth_openidc(Apache)を設定する

httpd.confに Relying Party の設定します。実際はhttpd.confとはファイルを分けて、Includeするようにしますが。

LoadModule auth_openidc_module modules/mod_auth_openidc.so

OIDCProviderMetadataURL       https://172.26.22.5/auth/realms/master/.well-known/openid-configuration
OIDCClientID                  apache24
OIDCClientSecret              f6757521-f4e9-4f5e-9ed1-0bf2a6d06cfe
OIDCResponseType              code
OIDCScope                     "openid"
OIDCSSLValidateServer         Off
OIDCProviderTokenEndpointAuth client_secret_basic

OIDCRedirectURI               https://172.26.22.25/private/callback
OIDCCryptoPassphrase          passphrase
OIDCPreservePost              On

<Location /private>
   AuthType         openid-connect
   Require          valid-user
</Location>
<Location /public>
   OIDCUnAuthAction pass
   AuthType         openid-connect
   Require          valid-user
</Location>

パラメータを説明しますと..

パラメータ名 説明
OIDCProviderMetadataURL OpenID Provider(OP)の.well-knownのURL
OIDCClientID クライアントID
OIDCClientSecret クライアントシークレット。さっきメモっておいたやつ。
OIDCResponseType response_typeの値。認可コードフローなので code を指定する
OIDCScope スコープ。OpenID Connectを使いたいのでopenidを指定する。他のスコープはとりあえず指定しない
OIDCSSLValidateServer OpenID Provider(OP)にアクセスするとき、SSL証明書を検証するか? 今回はテスト環境なので、自己署名証明書(いわゆるオレオレ証明書)なので、検証しないにしておく。本番環境ではちゃんとOnにして検証するようにしましょう。
OIDCProviderTokenEndpointAuth クライアント認証のやり方。クライアントID&シークレットを指定。
OIDCRedirectURI リダイレクトURI
OIDCCryptoPassphrase なんだこれは? 多分IDトークンの暗号化かな
OIDCPreservePost POSTのときリクエストパラメータを保存しておくか? とりあえず無視しておいていい。

他に重要な設定は、どのURLをOpenID Connectによる認証を有効にするか、です。今回は、/private配下を、要OpenID Connect認証にします。

<Location /private>
   AuthType         openid-connect
   Require          valid-user
</Location>

もう一つ重要なことは、リダイレクトURIはOpenID Connectの認証が有効になっているパス配下でないとダメだ、です。今回は /private 配下に対してOpenID Connect認証を有効にしているので、https://172.26.22.5/private/xxxxxでないとダメ、ということです。xxxxxの部分は、今回は callback というパスにしていますが、このURLには何かコンテンツを置いておく必要はありません。逆に言うと、mod_auth_openidcがリクエストをフックしちゃうので、このURLにコンテンツは置けません。それと、スクリーンショットには載せませんが、パスワードの設定をお忘れなく。

これで設定は全部完了です。Apacheを再起動しておきましょう。

テストユーザーを作っておく

じゃあ早速シングルサインオンを… ユーザーを作っていませんでした。もう一回Keycloakに管理者でログインして「ユーザー」の「ユーザーの追加」からユーザーを作成します。設定する項目はなんでもいいのですが、必要最小限の項目+「姓」「名」を設定しておきます。
2017-10-27_141434.png

2017-10-27_141839.png

やってみる

これで設定は全部完了です(2度目)。ではOpenID Connectでシングルサインオンしましょう。まずは Relying Party (https://172.26.22.25/private/headers.pl)にアクセスすれば、後は勝手に進みます。
2017-10-27_142421.png

2017-10-27_142539.png

やったぜ。ちなみに headers.pl はリクエストヘッダを表示するCGIです。phpinfo()でもいいです。mod_auth_openidcの場合、IDトークンは、というかIDトークンのClaimOIDC_CLAIM_xxxという名前のヘッダに入ります。アクセストークンは OIDC_access_tokenです。

何かおかしいぞ

よく見ると、何か変ですね。

  • scope=openidしか指定していないのに、姓名(name)と姓(family_name)と名(last_name)が入っている
  • というか、同意画面が出なかったような

今一度、Keycloakのクライアントの設定を見てみると..
2017-10-27_143454.png

同意画面を出してみる

「同意が必要」を有効にした後、気を取り直して、もう一度やってみます。
2017-10-27_143816.png

同意画面はでました、が... 明らかに要求していないスコープを返そうとしています(名、氏名、Eメール、姓)。また role_uma_authorizationって何だ?

スコープを設定する

クライアント(mod_auth_openidc)が要求するスコープに応じて、Keycloakが返す値が変わるようにしてみます。今はopenidだけなので、「ユーザー名」だけになるようにします。Keycloakのスコープのデフォルト設定は、何か変な感じです。ちゃんとスコープを設定してみましょう。「クライアント」から「スコープ」を見てみます。
2017-10-27_144424.png

フルスコープをオフにしました。
2017-10-27_144613.png

やってみます。
2017-10-27_144717.png

ロールは出なくなりましたが、スコープの調整ができてないです。今度は「クライアント」から「マッパー」を見てみましょう。「full name」(姓名)だけ設定を見てみます。
2017-10-27_144904.png

「同意が必要」を無効にすればいいのでしょうか。
2017-10-27_145053.png

やってみます。
2017-10-27_145318.png

同意画面からは「姓名」が消えました。同意します。
2017-10-27_145438.png

うーん、ダメみたいですね... どうもKeycloakはユーザー属性を含めるかどうかは、同意の有無・IDトークン・アクセストークン・UserInfoですべて別々になっているようです。

IDトークンの内容(Claim)を変えてみる

今度はIDトークンの内容(Claim)を調整してみましょう。「クライアント」>「マッパー」の画面に「IDトークンに追加」という項目があるので、これで調整できそうです。usernameだけ有効にして、他を無効にしてみてやってみましょう。(下の図はgiven nameだけですが、username以外をすべて無効にする)
2017-10-30_102733.png

結果
2017-10-30_103513.png

普通にIDトークンに「姓」「名」が入っているように見えます。本当はどうなっているのか、HTTP通信を覗いて見ましょう。

IDトークンの内容を見てみる

というか、この地点で気づいたのですが、エンドポイントの送信先が https ではなく http になっていますね… 見なかったことにして(無理)、先に進みます。Relying Party(RP) → Keycloak(OP) への通信を tcpdump します。

tcpdump -i any port 80 -w 1.pac -s 0
  • リクエスト
POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1
Authorization: Basic YXBhY2hlMjQ6ZjY3NTc1MjEtZjRlOS00ZjVlLTllZDEtMGJmMmE2ZDA2Y2Zl
User-Agent: mod_auth_openidc
Host: 172.26.22.5
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: 251

grant_type=authorization_code&code=uss.BVNJp6jf36eljWCjnWVjDIw3CAt04GBPPVJ2H_2uFW0.79e07c02-fe25-4829-9f37-c40a0a24f838.9115397f-1a68-444c-bdc4-1f5ac372a1f6&redirect_uri=https%3A%2F%2F172.26.22.25%2Fprivate%2Fcallback&state=Uwkp9CRo0vtNSrSVIQOSLP0PIpU
  • レスポンス(※JSONは見やすいように整形していますが、実際は1行です)
HTTP/1.1 200 OK
Date: Mon, 30 Oct 2017 02:05:09 GMT
Server: WildFly/11
X-Powered-By: Undertow/1
Content-Type: application/json
Content-Length: 3249

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWa1hGdXFRYldjUnJzbEduZGwzYWtnMkxsdjk0dzlZWHl0aW1qM24tX25JIn0.eyJqdGkiOiJlNjBjOGNlMS05MjcyLTQ3YTQtYjFlMi02NjIyZjU0YjdjMTEiLCJleHAiOjE1MDkzMjkxNjksIm5iZiI6MCwiaWF0IjoxNTA5MzI5MTA5LCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYXBhY2hlMjQiLCJzdWIiOiI5YjUzNTIwMC1lZDRjLTQ0NjEtYjM4My1jMTI5ZmY1NzUxMDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcGFjaGUyNCIsIm5vbmNlIjoiZkhNNXNndWtiZlI0TW1TN3g1R2E2LUdsSU1UZ0hIeVQzTUxvVGMzNWh4WSIsImF1dGhfdGltZSI6MTUwOTMyOTEwOSwic2Vzc2lvbl9zdGF0ZSI6Ijc5ZTA3YzAyLWZlMjUtNDgyOS05ZjM3LWM0MGEwYTI0ZjgzOCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOltdLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJuYW1lIjoidTExMSB0ZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidTExMSIsImdpdmVuX25hbWUiOiJ1MTExIiwiZmFtaWx5X25hbWUiOiJ0ZXN0In0.M5kBlL7_kVXAjW5nOFyRlfP7EqPq2XbM1MGdw6-cqfOTbgU2rwFHIx99YZ6HSAlyf2ezBuVgKfsJPC74SNZ8EkDHTAw6fYnMAyo2wwKX4HOuqPZ6tjjUKL9hPn4rkzN8TdOFD1teCTTvkG_RbtOvJYs4LaLjTpLWKsS6RzxVmqD3woKzedKM9q-lx8mMJgNnmQl7867Pr7ACPMCxBb_6XILO2w2v6kXP0LCiNwHePPYpmIawnwf7uizhXF1KmLgwZKbZ5gsGni-Bzm7Kh3N8vF3bBOQF-NxNhkgZ73GKqZWY04i3kgJmPsy7s8wFBAEZ2pC2BLOwPx_rLCCctBMKSA",
    "expires_in": 60,
    "refresh_expires_in": 1800,
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWa1hGdXFRYldjUnJzbEduZGwzYWtnMkxsdjk0dzlZWHl0aW1qM24tX25JIn0.eyJqdGkiOiIwMmU0OGZkZi1hM2JlLTQ1ZTQtOTdlOC1hNjU0MjY5MDFmYzUiLCJleHAiOjE1MDkzMzA5MDksIm5iZiI6MCwiaWF0IjoxNTA5MzI5MTA5LCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYXBhY2hlMjQiLCJzdWIiOiI5YjUzNTIwMC1lZDRjLTQ0NjEtYjM4My1jMTI5ZmY1NzUxMDAiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiYXBhY2hlMjQiLCJub25jZSI6ImZITTVzZ3VrYmZSNE1tUzd4NUdhNi1HbElNVGdISHlUM01Mb1RjMzVoeFkiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI3OWUwN2MwMi1mZTI1LTQ4MjktOWYzNy1jNDBhMGEyNGY4MzgiLCJyZXNvdXJjZV9hY2Nlc3MiOnt9fQ.SyzqS-2iQczTJCfxzfwXop1yfcDRFU-btCSm0kKheRx4rydIYpCPT1nB_NBmvjh6YZrZTJBykDMkio_VfxX5KoRLHGc_lmPfy0mKnEVDhC4xgXDQrVnu5pQa3_JztpzWZ49mYbjX9aMX4XD6e-qlneg6yf-9MzRrTXFZHFFpSPAEBSMwFW1K5zS6QuYf3iYNeaoeF46xsvhSXS535zWBymx9na0AZu4k_crlDKSHOWQ1fMYpRyKb9LXeciuAOzkeun7aHqtRyj3wSk1N3UGSoZk7CXc1tSZKL5AslJSsVjDI-P4UhjDJ4F7r-NY19MYcKwQCsOObmxZXtRj56t8Mww",
    "token_type": "bearer",
    "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWa1hGdXFRYldjUnJzbEduZGwzYWtnMkxsdjk0dzlZWHl0aW1qM24tX25JIn0.eyJqdGkiOiJjNzUwMGU0ZC01OWYwLTQ5NzYtYmU0MS1jMjdiZjI5MTA1ZjYiLCJleHAiOjE1MDkzMjkxNjksIm5iZiI6MCwiaWF0IjoxNTA5MzI5MTA5LCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYXBhY2hlMjQiLCJzdWIiOiI5YjUzNTIwMC1lZDRjLTQ0NjEtYjM4My1jMTI5ZmY1NzUxMDAiLCJ0eXAiOiJJRCIsImF6cCI6ImFwYWNoZTI0Iiwibm9uY2UiOiJmSE01c2d1a2JmUjRNbVM3eDVHYTYtR2xJTVRnSEh5VDNNTG9UYzM1aHhZIiwiYXV0aF90aW1lIjoxNTA5MzI5MTA5LCJzZXNzaW9uX3N0YXRlIjoiNzllMDdjMDItZmUyNS00ODI5LTlmMzctYzQwYTBhMjRmODM4IiwiYWNyIjoiMSIsInByZWZlcnJlZF91c2VybmFtZSI6InUxMTEifQ.W0gBnz2m0HbuSlYPD4VV24UEfC8BMtDoRl3Rikh6KcYw8QXf9O51KSXhx8xdS9kuAepRfCVAhP3ckk5TdPLCk8fESTWy6s1UaC4i4AcpFpF7-ZtZCkRtpk5Ehd7x-SLg204vBsC5C84Kct68AZU1yqsblKWSlhzoxXNnR2k1D6gqLHYWiaqZO2BG14EYy_BSNhEbhWQEF5Gw8MQwl6BQ7WsZ1lF-ox6Xg6kDYuHmKkSxXo-IxjIpiE-r5z8NuwpUla_I1pp0dvUGXTCn6uJwp08tJ6V5gCjcWdoJKQiXJT9Cafe63GJwPb0T7IQWfdqKJuBLACnwB1NU0_TMbXZQXQ",
    "not-before-policy": 0,
    "session_state": "79e07c02-fe25-4829-9f37-c40a0a24f838"
}
  • IDトークン(Claim)のデコード(※見やすいように整形しています)
{
  "jti": "c7500e4d-59f0-4976-be41-c27bf29105f6",
  "exp": 1509329169,
  "nbf": 0,
  "iat": 1509329109,
  "iss": "http://172.26.22.5/auth/realms/master",
  "aud": "apache24",
  "sub": "9b535200-ed4c-4461-b383-c129ff575100",
  "typ": "ID",
  "azp": "apache24",
  "nonce": "fHM5sgukbfR4MmS7x5Ga6-GlIMTgHHyT3MLoTc35hxY",
  "auth_time": 1509329109,
  "session_state": "79e07c02-fe25-4829-9f37-c40a0a24f838",
  "acr": "1",
  "preferred_username": "u111"
}

ユーザーID(sub)とユーザー名(preferred_usename)だけで、ちゃんと姓名は入っていませんでした。

ユーザー情報エンドポイントの通信内容を見てみる

実はmod_auth_openidcは、「⑥ユーザー情報エンドポイント」にも(あれば)アクセスして追加のユーザー情報を取得しに行きます。同じようにHTTP通信の中を見てみましょう。

  • リクエスト

リクエストヘッダ:Authorization: Bearerにアクセストークンを付与して送信しています。

GET /auth/realms/master/protocol/openid-connect/userinfo HTTP/1.1
User-Agent: mod_auth_openidc
Host: 172.26.22.5
Accept: */*
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJWa1hGdXFRYldjUnJzbEduZGwzYWtnMkxsdjk0dzlZWHl0aW1qM24tX25JIn0.eyJqdGkiOiJlNjBjOGNlMS05MjcyLTQ3YTQtYjFlMi02NjIyZjU0YjdjMTEiLCJleHAiOjE1MDkzMjkxNjksIm5iZiI6MCwiaWF0IjoxNTA5MzI5MTA5LCJpc3MiOiJodHRwOi8vMTcyLjI2LjIyLjUvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjoiYXBhY2hlMjQiLCJzdWIiOiI5YjUzNTIwMC1lZDRjLTQ0NjEtYjM4My1jMTI5ZmY1NzUxMDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhcGFjaGUyNCIsIm5vbmNlIjoiZkhNNXNndWtiZlI0TW1TN3g1R2E2LUdsSU1UZ0hIeVQzTUxvVGMzNWh4WSIsImF1dGhfdGltZSI6MTUwOTMyOTEwOSwic2Vzc2lvbl9zdGF0ZSI6Ijc5ZTA3YzAyLWZlMjUtNDgyOS05ZjM3LWM0MGEwYTI0ZjgzOCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOltdLCJyZXNvdXJjZV9hY2Nlc3MiOnt9LCJuYW1lIjoidTExMSB0ZXN0IiwicHJlZmVycmVkX3VzZXJuYW1lIjoidTExMSIsImdpdmVuX25hbWUiOiJ1MTExIiwiZmFtaWx5X25hbWUiOiJ0ZXN0In0.M5kBlL7_kVXAjW5nOFyRlfP7EqPq2XbM1MGdw6-cqfOTbgU2rwFHIx99YZ6HSAlyf2ezBuVgKfsJPC74SNZ8EkDHTAw6fYnMAyo2wwKX4HOuqPZ6tjjUKL9hPn4rkzN8TdOFD1teCTTvkG_RbtOvJYs4LaLjTpLWKsS6RzxVmqD3woKzedKM9q-lx8mMJgNnmQl7867Pr7ACPMCxBb_6XILO2w2v6kXP0LCiNwHePPYpmIawnwf7uizhXF1KmLgwZKbZ5gsGni-Bzm7Kh3N8vF3bBOQF-NxNhkgZ73GKqZWY04i3kgJmPsy7s8wFBAEZ2pC2BLOwPx_rLCCctBMKSA
  • レスポンス(※JSON部分は、見やすいように整形しています)
HTTP/1.1 200 OK
Date: Mon, 30 Oct 2017 02:05:09 GMT
Server: WildFly/11
Cache-Control: no-cache
X-Powered-By: Undertow/1
Content-Type: application/json
Content-Length: 115

{
    "sub": "9b535200-ed4c-4461-b383-c129ff575100",
    "preferred_username": "u111",
    "given_name": "u111",
    "family_name": "test"
}

こっちのアクセスで追加のユーザー情報を取得していました。

ユーザー属性をカスタムしてみる

Keycloak 定義済みだけではなく、勝手に定義したユーザー属性をIDトークンやユーザー情報エンドポイントの公開情報に入れてみましょう。

「クライアント」>「マッパー」から「作成」を押します。
2017-10-30_113625.png

「名前」(属性名)はなんでもいいですが、今回はhogeで。同意の有無はどちらでもいいです。「マッパータイプ」は色々あってよく分かりませんが、なんとなく「User Attribute」を選択。「トークンクレーム名」にはhoge、「ユーザー属性」はfugaにしておきます。あとは「IDトークンに追加」「UserInfo②追加」を有効にしておきます。
2017-10-30_115855.png

ユーザーにも取得元属性であるfugaを追加しましょう。「ユーザー」からu111を選択して「属性」タブを選択します。
2017-10-30_114325.png

この画面で keyfuga、「値」に「ふが」を入力して、「追加」を押して保存します。
2017-10-30_120022.png

さて、どうなるでしょうか、やってみましょう。
2017-10-30_120335.png

ちゃんと属性 hoge にユーザー属性 fuga の値「ふが」が入っていました。IDトークンの中身も見てみましょう。(見やすいように整形しています)

{
  "jti": "d86d78d7-7c99-440a-95d0-bcbd9d53aa69",
  "exp": 1509332783,
  "nbf": 0,
  "iat": 1509332723,
  "iss": "http://172.26.22.5/auth/realms/master",
  "aud": "apache24",
  "sub": "9b535200-ed4c-4461-b383-c129ff575100",
  "typ": "ID",
  "azp": "apache24",
  "nonce": "V1tIvqOpONeSSRfwsKIhJj7UFoaSxQNaELM6WDd5euI",
  "auth_time": 1509332723,
  "session_state": "fb659484-cb1d-4565-9239-dae3337bb4f5",
  "acr": "1",
  "hoge": "ふが",
  "preferred_username": "u111"
}

ちゃんと入っていました。

まとめ

Keycloakはクライアントからのスコープの要求とは関係なく、何を返すかはKeycloakの設定のみで決定してしまうようです。このことはIDトークンとユーザー情報エンドポイントの公開情報の両方とも同じようです。また、クライアントやスコープ単位に「同意が必要」の設定があるように、ユーザー同意自体を求めなかったり、スコープによっては同意を求めないで、Keycloakは属性をRelying Partyに渡してしまうようです。クライアントの要求していないスコープを返そうとするのは悪いわけではないのですが、少なくともスコープ単位で同意を求めない、という設定をするのはあまりよくないように思います。同意とは、ユーザーが自分自身の情報をRelying Partyに渡すか渡さないかを、ユーザー自身に決定させるためのものですから。ただ、少なくとも、Keycloakはかなり細かく設定ができることが分かりました。

一方、IDトークンやユーザー情報エンドポイントの公開情報は、各属性単位に設定できるようです。1つ問題になるのが、Relying Partyがユーザー情報を取得するとき、IDトークンに入れてもらうのか、ユーザー情報エンドポイントから取得するのか、どちらにすべきかということです。OpenID Connectの仕様(RFC)ではどちらでもOKなので、やりやすい方を選択、ということになるでしょう。OpenID Connectが世間に出てきたはじめのうちは、IDトークンに必要な属性を入れてもらうほうが主流でした。おそらく、Relying Partyの実装の負担を減らしたい、OpenID Provider(OP)がまだユーザー情報エンドポイントを未実装、という事情があったのだと思います。ただ、現在はユーザー情報エンドポイントから取得するのが主流になっているように思います。これは、リフレッシュトークンがあるからだと思います。

話題としては、クライアント認証の方法やアクセストークンの内容、リフレッシュトークン、認証認可リクエストでの細かいパラメータなどがあるのですが、さすがにこの記事のページが長すぎるので、今日はこの辺にしておきます。

参考資料