1. はじめに

いくつかの例にあるように、1行で表示されることを意図した行がページ幅に収まらないことがあります。これらの行は改行されています。行末の '\' は、次の行がインデントされたページに収まるように改行が導入されたことを意味します。したがって、

Let's pretend to have an extremely \
long line that \
does not fit
This one is short

上記は、実際には下記のとおりになります。

Let's pretend to have an extremely long line that does not fit
This one is short

2. 管理REST API

Keycloakには、管理コンソールが提供するすべての機能を完全に備えた管理REST APIが付属しています。

APIを呼び出すには、適切な権限を持つアクセストークンを取得する必要があります。必要な権限については Server Administration Guide を参照してください。

トークンは、Keycloakを使用したアプリケーションへの認証を有効にすることで取得できます。 Securing Applications and Services Guide を参照してください。また、ダイレクト・アクセス・グラントを使用しても、アクセストークンを取得できます。

完全なドキュメントについては、 API Documentation を参照してください。

2.1. CURLを使用した例

ユーザー名 admin とパスワード password を使用して、以下のとおり、 master レルム内のユーザー用にアクセストークンを取得します。

curl \
  -d "client_id=admin-cli" \
  -d "username=admin" \
  -d "password=password" \
  -d "grant_type=password" \
  "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
デフォルトではこのトークンは1分で使用期限切れとなります。

結果はJSONドキュメントになります。APIを呼び出すには、 access_token プロパティーの値を抽出する必要があります。その後、APIへのリクエストの Authorization ヘッダーに値を含めることで、APIを呼び出すことができます。

次の例は、masterレルムの詳細を取得する方法を示しています。

curl \
  -H "Authorization: bearer eyJhbGciOiJSUz..." \
  "http://localhost:8080/auth/admin/realms/master"

2.2. Javaを使用したサンプル

Javaから簡単に使用できるようにする管理REST API用のJavaクライアント・ライブラリーがあります。アプリケーションからそれを使用するには、 keycloak-admin-client ライブラリーに依存関係を追加します。

次の例は、Javaクライアント・ライブラリーを使用して、masterレルムの詳細を取得する方法を示しています。

import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RealmRepresentation;
...

Keycloak keycloak = Keycloak.getInstance(
    "http://localhost:8080/auth",
    "master",
    "admin",
    "password",
    "admin-cli");
RealmRepresentation realm = keycloak.realm("master").toRepresentation();

管理クライアントの完全なJavaのドキュメントは、 API Documentation を参照してください。

3. テーマ

Keycloakでは、webページと電子メール用のテーマがサポートされます。これによって、エンドユーザーが見るページのルック・アンド・フィールをカスタマイズすることができ、そのページをアプリケーションに統合することができます。

login sunrise
日の出のサンプルのテーマとログインページ

3.1. テーマの種類

テーマには1つ以上の種類が用意されており、Keycloakのさまざまな部分をカスタマイズすることができます。用意されている種類は以下のとおりです。

  • Account - アカウント管理

  • Admin - 管理コンソール

  • Email - 電子メール

  • Login - ログイン画面

  • Welcome - ウェルカムページ

3.2. テーマの設定

ウェルカムページを除く、すべてのテーマの種類は Admin Console を通じて設定されます。レルム用に使用されるテーマを変更するには Admin Console を開き、画面左上端にあるドロップダウン・ボックスからレルムを選択します。 Realm Settings の下で Themes をクリックします。

master の管理コンソール用にテーマをセットするには、 master レルム用の管理コンソール・テーマを設定する必要があります。管理コンソールへの変更を確認するには、ページをリフレッシュしてください。

ウェルカムページのテーマを変更するには standalone.xmlstandalone-ha.xml 、または domain.xml のいずれかを編集する必要があります。 standalone.xmlstandalone-ha.xml 、または domain.xml のファイルがある場所について、詳しくは、Server Installation and Configuration Guideを参照してください。

welcomeTheme をテーマ要素に追加します。サンプルは以下のとおりになります。

<theme>
 ...
 <welcomeTheme>custom-theme</welcomeTheme>
 ...
</theme>

サーバーが実行されている場合、サーバーを再起動してウェルカムページのテーマ変更を有効にする必要があります。

3.3. デフォルトのテーマ

Keycloakには、サーバーのルート themes ディレクトリー内のデフォルト・テーマがバンドルされています。アップグレードを簡単にするために、バンドルされたテーマを直接編集しないでください。代わりに、バンドルされたテーマの1つを拡張する独自のテーマを作成してください。

3.4. テーマの作成

テーマは、以下の項目で構成されています。

  • HTML templates (Freemarker Templates)

  • 画像

  • メッセージ・バンドル

  • スタイルシート

  • スクリプト

  • テーマ用のpropertiesファイル

ページをすべて置き換えるわけではない場合は、いずれかのテーマを1つ拡張する方法をお勧めします。通常の場合Keycloakのテーマを拡張したいでしょうが、ページのルック・アンド・フィールを大幅に変更する場合は基本テーマを拡張するということも検討対象となります。基本テーマは主にHTMLのテンプレートとメッセージ・バンドルで構成され、 Keycloakテーマには主にイメージとスタイルシートが含まれます。

テーマを拡張する場合、個々のリソース(テンプレート、スタイルシートなど)を上書きできます。HTMLのテンプレートを上書きする場合、新規リリースへのアップグレードの際に、カズタマイズしたテンプレートをアップグレードする必要があることに留意してください。

テーマを作成する際は、キャッシュを無効にすることをお勧めします。なぜなら、無効にしたことにより、Keycloakを再起動せずに、 themes ディレクトリーからテーマ・リソースを直接編集することができるからです。これを実行するには standalone.xml を編集します。以下のとおり、 theme 用に staticMaxAge-1 に設定し、 cacheTemplatescacheThemes の両方は false に設定します。

<theme>
 <staticMaxAge>-1</staticMaxAge>
 <cacheThemes>false</cacheThemes>
 <cacheTemplates>false</cacheTemplates>
 ...
</theme>

パフォーマンスにかなり影響を与える可能性があるので、プロダクション環境でキャッシュを再度有効にすることを忘れず実行してください。

新しいテーマを作成するには、まず themes ディレクトリー内で新しいディレクトリーを作成します。ディレクトリー名がテーマの名前になります。たとえば、 mytheme という名前のテーマを作成するには、 themes/mytheme ディレクトリーを作成します。

テーマのディレクトリー内に、テーマで用意されている各種ディレクトリーをそれぞれ作成します。たとえば、ログインのタイプを mytheme テーマに追加するには、 themes/mytheme/login ディレクトリーを作成します。

タイプ毎に、テーマ用に設定することができる theme.properties ファイルを作成します。たとえば、基本テーマを拡張して共通のリソースをインポートするために作成した themes/mytheme/login テーマを設定するには、以下の内容で themes/mytheme/login/theme.properties ファイルを作成します。

parent=base
import=common/keycloak

これで、ログイン・タイプをサポートするテーマが作成されました。これが動作するか確認するには、管理コンソールを開きます。レルムを選択し、 Themes をクリックします。 Login Theme 用には、 mytheme を選択して Save をクリックします。それから、レルム用のログインページを開きます。

これは、アプリケーションを経由してログインしても、アカウント管理コンソール( /realms/{realm name}/account )を開いても、実行できます。

現在のテーマを変更した際にどのような影響があるか確認するには、 theme.properties 内で parent=keycloak を設定してログイン・ページをリフレッシュします。

3.4.1. テーマのプロパティー

テーマのプロパティーは、テーマ・ディレクトリー内にある <THEME TYPE>/theme.properties ファイルで設定されています。

  • parent - 拡張が可能な親テーマ

  • import - 他テーマからリソースをインポートすること

  • styles - スペースで区切られた、インクルードするスタイルのリスト

  • locales - カンマで区切られた、サポートされるロケールのリスト

特定の要素タイプのために使用される、cssクラスの変更用として使用可能なプロパティーのリストがあります。これらのプロパティーのリストについては、keycloakテーマ( themes/keycloak/<THEME TYPE>/theme.properties )に対応するタイプのtheme.propertiesファイルを参照してください。

カスタム・プロパティーを追加し、それらをカスタム・テンプレートから使用することができます。

3.4.2. スタイルシート

テーマには、1つ以上のスタイルシートを持たせることができます。スタイルシートを追加するには、テーマの <THEME TYPE>/resources/css ディレクトリー内でファイルを作成します。それから、 theme.properties 内の styles プロパティーへ、そのファイルを追加します。

たとえば、 mythemestyles.css を追加するには、以下の内容で themes/mytheme/login/resources/css/styles.css を作成します。

.login-pf body {
 background: DimGrey none;
}

次に、 themes/mytheme/login/theme.properties を編集し、以下を追加します。

styles=css/styles.css

変更されたか確認するには、レルム用のログインページを開きます。適用されたスタイルはカスタム・スタイルシートからのものだけだと分かります。親テーマからのスタイルも含ませるには、そのテーマからのスタイルも同様にロードする必要があります。これは、 themes/mytheme/login/theme.properties を編集し、以下のとおり styles を変更することで実行できます。

styles=node_modules/patternfly/dist/css/patternfly.css node_modules/patternfly/dist/css/patternfly-additions.css lib/zocial/zocial.css css/login.css css/styles.css
親のスタイルシートからのスタイルを上書きするには、スタイルシートがリストの最後にあることが重要です。

3.4.3. スクリプト

テーマには、1つ以上のスクリプトを持たせることができます。スクリプトを追加するには、テーマの <THEME TYPE>/resources/js ディレクトリー内でファイルを作成します。それから、 theme.properties 内の scripts プロパティーへ、そのファイルを追加します。

たとえば、 mythemescript.js を追加するには、以下の内容で themes/mytheme/login/resources/js/script.js を作成します。

alert('Hello');

次に、 themes/mytheme/login/theme.properties を編集し、以下を追加します。

scripts=js/script.js

3.4.4. 画像

テーマで画像を使用できるようにするには、テーマの <THEME TYPE>/resources/img ディレクトリーに画像を追加します。これらの画像は、スタイルシート内または直接HTMLテンプレート内で使用することができます。

たとえば、 mytheme へ画像を追加するには、 themes/mytheme/login/resources/img/image.jpg へ画像をコピーします。

これで、以下のようにカスタム・スタイルシート内からこのイメージを使うことができます。

body {
 background-image: url('../img/image.jpg');
 background-size: cover;
}

または、HTMLテンプレート内で直接使う場合は、以下をカスタムHTMLテンプレートへ追加します。

<img src="${url.resourcesPath}/img/image.jpg">

3.4.5. メッセージ

テンプレート内のテキストは、メッセージ・バンドルからロードされます。他のテーマを拡張するテーマは、親メッセージ・バンドルからすべてのメッセージを引き継ぎ、テーマに <THEME TYPE>/messages/messages_en.properties を追加することで個々のメッセージを上書きすることができます。

たとえば、 mytheme でログイン画面上の UsernameYour Username に置き換えるには、以下の内容で themes/mytheme/login/messages/messages_en.properties ファイルを作成します。

usernameOrEmail=Your Username

メッセージ内では、メッセージが使用される際、 {0} および {1} のような値が引数に置き換えられます。たとえば、 Log in to {0} 内の {0} がレルム名に置き換えられます。

3.4.6. 国際化

Keycloakでは、国際化がサポートされています。レルム用に国際化を有効にするには、 Server Administration Guide を参照してください。このセクションでは、言語を追加する方法について説明します。

新しく言語を追加するには、テーマのディレクトリー内で <THEME TYPE>/messages/messages_<LOCALE> ファイルを作成します。次に、そのファイルを <THEME TYPE>/theme.properties 内の locales プロパティーへ追加します。ユーザーが言語を使用できるようにするには、レルムの loginaccount および email のテーマによってその言語がサポートされなければなりません。したがって、それらのテーマ・タイプとしてその言語を追加する必要があります。

たとえば、 mytheme テーマへノルウェー翻訳を追加するには、以下の内容で themes/mytheme/login/messages/messages_no.properties ファイルを作成します。

usernameOrEmail=Brukernavn
password=Passord

翻訳していないメッセージにはすべて、デフォルトの英語翻訳が使用されます。

次に、 themes/mytheme/login/theme.properties を編集し、以下を追加します。

locales=en,no

accountemail のテーマ・タイプにも同じことを実行する必要があります。これを実行するには、 themes/mytheme/account/messages/messages_no.propertiesthemes/mytheme/email/messages/messages_no.properties を作成します。これらのファイルを空にしたままにすると、英語のメッセージが使用されます。次に、 themes/mytheme/login/theme.properties をコピーして themes/mytheme/account/theme.propertiesthemes/mytheme/email/theme.properties へペーストします。

最後に、言語セレクターに翻訳を追加する必要があります。これは英語翻訳にメッセージを追加することによって実行されます。これを実行するには、以下を themes/mytheme/account/messages/messages_en.propertiesthemes/mytheme/login/messages/messages_en.properties に追加します。

locale_no=Norsk

デフォルトでは、メッセージ・プロパティー・ファイルはISO-8859-1を使用してエンコードされる必要があります。また、これは特別なヘッダーを使用してエンコーディングを指定する方法でも可能です。たとえば、UTF-8エンコーディングを使用するには、以下のとおりとなります。

# encoding: UTF-8
usernameOrEmail=....

現在のロケールの選択方法の詳細については、ロケール・セレクターを参照してください。

3.4.7. HTMLのテンプレート

Keycloakでは、HTMLを生成するために Freemarkerテンプレート が使用されます。テーマ内で <THEME TYPE>/<TEMPLATE>.ftl を作成すると、個々のテンプレートを上書きすることができます。使用したテンプレートのリストについては themes/base/<THEME TYPE> を参照してください。

カスタム・テンプレートを作成する場合、基本テーマからのテンプレートをテーマへコピーし、必要な修正を適用する方法をお勧めします。Keycloakの新しいバージョンへアップグレードする際に、適用可能であれば、カスタム・テンプレートをアップグレードし、オリジナルのテンプレートへ変更を適用する必要があることを留意しておいてください。

たとえば、 mytheme テーマにカスタム・ログイン画面を作成するには、 themes/base/login/login.ftlthemes/mytheme/login へコピーし、それをエディター内で開きます。以下のように最初の行(<#import …​>)の次行に <h1>HELLO WORLD!</h1> を追加します。

<#import "template.ftl" as layout>
<h1>HELLO WORLD!</h1>
...

テンプレートを編集する方法について、詳しくは、 FreeMarkerのマニュアル を参照してください。

3.4.8. 電子メール

電子メールの題名と内容(たとえばパスワード・リカバリー電子メールなど)を編集するには、テーマの email タイプへメッセージ・バンドルを追加します。各電子メールには3つのメッセージがあります。題名、プレーン・テキストの本文、およびhtmlのbodyです。

使用できる電子メールをすべて確認するには、 themes/base/email/messages/messages_en.properties を参照してください。

たとえば、 mytheme テーマ用にパスワード・リカバリー電子メールを変更するには、以下の内容で themes/mytheme/email/messages/messages_en.properties を作成します。

passwordResetSubject=My password recovery
passwordResetBody=Reset password link: {0}
passwordResetBodyHtml=<a href="{0}">Reset password</a>

3.5. テーマのデプロイ

テーマは、テーマ・ディレクトリーを themes へコピーすることによって、Keycloakへデプロイすることができ、アーカイブとしてデプロイすることも可能です。デプロイ中は、テーマを themes ディレクトリーへコピーしますが、プロダクション環境では archive の使用が検討した方がいいかもしれません。 archive を使用すると、テーマのコピー版の作成が簡単になります。たとえばクラスター構成のKeycloakなど、インスタンスが複数ある場合は特に便利です。

テーマをアーカイブとしてデプロイするには、テーマのリソースを使用してJARアーカイブを作成する必要があります。また、アーカイブで使用可能なテーマと各テーマが提供するタイプをリストするアーカイブに、 META-INF/keycloak-themes.json ファイルを追加することも必要です。

たとえば、 mytheme テーマの場合、以下の内容で mytheme.zip を作成します。

  • META-INF/keycloak-themes.json

  • theme/mytheme/login/theme.properties

  • theme/mytheme/login/login.ftl

  • theme/mytheme/login/resources/css/styles.css

  • theme/mytheme/login/resources/img/image.png

  • theme/mytheme/login/messages/messages_en.properties

  • theme/mytheme/email/messages/messages_en.properties

このケースの META-INF/keycloak-themes.json の内容は、以下のとおりです。

{
 "themes": [{
 "name" : "mytheme",
 "types": [ "login", "email" ]
 }]
}

1つのアーカイブには複数のテーマを含めることができ、各テーマは1つ以上のタイプをサポートすることができます。

アーカイブをKeycloakにデプロイするには、Keycloakの standalone/deployments/ ディレクトリーにドロップするだけで、それにより自動的にロードされます。

3.6. テーマセレクター

デフォルトでは、クライアントがログインテーマを上書きできる点を除き、レルムに設定されたテーマが使用されます。この動作は、テーマセレクターSPIによって変更できます。

これは、ユーザー・エージェント・ヘッダーを見ることにより、たとえばデスクトップおよびモバイルデバイス用の異なるテーマを選択するために使用することができます。

カスタム・テーマ・セレクターを作成するには、 ThemeSelectorProviderFactoryThemeSelectorProvider を実装する必要があります。

カスタムプロバイダーを作成してデプロイする方法の詳細については、サービス・プロバイダー・インターフェイスの手順に従ってください。

3.7. テーマリソース

Keycloakにカスタム・プロバイダーを実装する場合、テンプレートとリソースを追加する必要があることがよくあります。

ユースケースの例は、追加のテンプレートとリソースを必要とするカスタム・オーセンティケーターです。

追加のテーマリソースをロードする最も簡単な方法は、 theme-resources/templates のテンプレートと theme-resources/resources ディレクトリーのリソースを持つJARを作成し、Keycloakの standalone/deployments/ ディレクトリーに格納することです。

テンプレートとリソースをより柔軟にロードする方法が必要な場合、ThemeResourceSPIを使用して実現できます。 ThemeResourceProviderFactoryThemeResourceProvider を実装することで、テンプレートとリソースを読み込む方法を直に決めることができます。

カスタムプロバイダーを作成してデプロイする方法の詳細については、サービス・プロバイダー・インターフェイスの手順に従ってください。

3.8. ロケール・セレクター

デフォルトでは、ロケールは LocaleSelectorProvider インターフェイスを実装する DefaultLocaleSelectorProvider を使用して選択されます。国際化が無効の場合は、英語がデフォルトの言語です。国際化が有効になっている場合は、ロケールは次の優先順位で解決されます。

  1. kc_locale クエリー・パラメーター

  2. KEYCLOAK_LOCALE Cookie値

  3. ユーザー・インスタンスが利用可能な場合はユーザーの優先ロケール

  4. ui_locales クエリー・パラメーター

  5. Accept-Language リクエスト・ヘッダー

  6. レルムのデフォルトの言語

この動作は LocaleSelectorProviderLocaleSelectorProviderFactory を実装することで LocaleSelectorSPI を通して変更することができます。

LocaleSelectorProvider インターフェースは resolveLocale という単一のメソッドを持ちます。これは RealmModel とnull許容の UserModel を与えられたロケールを返さなければなりません。実際のリクエストは KeycloakSession#getContext メソッドから利用できます。

カスタム実装ではデフォルトの振る舞いの一部を再利用するために DefaultLocaleSelectorProvider を継承することができます。例えば Accept-Language リクエストヘッダを無視するために、カスタム実装はデフォルト・プロバイダーを継承し、 getAcceptLanguageHeaderLocale をオーバーライドし、そしてnull値を返すことができます。結果として、ロケールの選択はレルムのデフォルト言語に戻ります。

カスタムプロバイダーを作成してデプロイする方法の詳細については、サービス・プロバイダー・インターフェイスの手順に従ってください。

4. カスタムユーザー属性

カスタムテーマを使用して、登録ページとアカウント管理コンソールにカスタムユーザー属性を追加できます。 この章では、カスタムテーマに属性を追加する方法について説明しますが、カスタムテーマの作成方法については、テーマの章を参照してください。

4.1. 登録ページ

登録ページにカスタム属性を入力できるようにするには、テンプレート themes/base/login/register.ftl をカスタムテーマのログインタイプにコピーします。その後、エディターでコピーを開きます。

モバイル番号を登録ページに追加する例として、次のスニペットをフォームに追加します。

<div class="form-group">
   <div class="${properties.kcLabelWrapperClass!}">
       <label for="user.attributes.mobile" class="${properties.kcLabelClass!}">Mobile number</label>
   </div>

   <div class="${properties.kcInputWrapperClass!}">
       <input type="text" class="${properties.kcInputClass!}"  id="user.attributes.mobile" name="user.attributes.mobile"/>
   </div>
</div>

入力html要素の名前が user.attributes で始まっていることを確認してください。上記の例では、属性はKeycloakによって mobile という名前で保存されます。

変更を参照するには、レルムがログインテーマにカスタムテーマを使用していることを確認し、登録ページを開きます。

4.2. アカウント管理コンソール

アカウント管理コンソールのユーザー・プロファイルページでカスタム属性を管理できるようにするには、テンプレート themes/base/account/account.ftl をカスタムテーマのアカウントタイプにコピーします。その後、エディターでコピーを開きます。

携帯電話番号をアカウントページに追加する例として、次のスニペットをフォームに追加します。

<div class="form-group">
   <div class="col-sm-2 col-md-2">
       <label for="user.attributes.mobile" class="control-label">Mobile number</label>
   </div>

   <div class="col-sm-10 col-md-10">
       <input type="text" class="form-control" id="user.attributes.mobile" name="user.attributes.mobile" value="${(account.attributes.mobile!'')}"/>
   </div>
</div>

入力html要素の名前が user.attributes で始まっていることを確認してください。

変更を参照するには、レルムがアカウントテーマのカスタムテーマを使用し、アカウント管理コンソールでユーザー・プロファイルページを開いていることを確認します。

5. アイデンティティー・ブローカリングAPI

Keycloakはログインのために親IDPに認証を委譲できます。この典型的な例は、ユーザーがFacebookやGoogleなどのソーシャル・プロバイダーを介してログインできるようにしたい場合です。Keycloakでは、既存のアカウントを仲介されたIDPにリンクすることもできます。このセクションでは、アイデンティティー・ブローカリングに関連してアプリケーションが使用できるいくつかのAPIについて説明します。

5.1. 外部IDPトークンの取得

Keycloakでは、外部IDPを使用して認証プロセスからのトークンとレスポンスを保存できます。そのために、IDPの設定ページで Store Token の設定オプションを使用できます。

アプリケーション・コードで、追加のユーザー情報を取得するためにこれらのトークンとレスポンスを検索したり、外部IDPにリクエストを送信することができます。たとえば、アプリケーションは、Googleトークンを使用して、他のGoogleサービスやREST APIを呼び出すことができます。特定のアイデンティティー・プロバイダーのトークンを取得するには、次のようにリクエストを送信する必要があります。

GET /auth/realms/{realm}/broker/{provider_alias}/token HTTP/1.1
Host: localhost:8080
Authorization: Bearer <KEYCLOAK ACCESS TOKEN>

アプリケーションはKeycloakで認証され、アクセストークンを受け取っている必要があります。このアクセストークンには、 broker クライアントレベルの read-token ロールが設定されている必要があります。つまり、ユーザーはこのロールのロールマッピングを持っていなければならず、クライアント・アプリケーションのスコープ内でそのロールが必要です。この場合(Keycloak内のセキュリティー保護されたサービスにアクセスしている場合)は、ユーザー認証時にKeycloakが発行したアクセストークンを送信する必要があります。ブローカーの設定ページでは、 Stored Tokens Readable のスイッチをオンにすることで、新しくインポートされたユーザーにこのロールを自動的に割り当てることができます。

これらの外部トークンは、プロバイダーを介して再度ログインするか、Client Initiated Account Linking APIを使用して再確立できます。

5.2. Client Initiated Account Linking

アプリケーションの中には、Facebookなどのソーシャル・プロバイダーと統合したいが、これらのソーシャル・プロバイダーを介してログインするオプションを提供したくないものもあります。Keycloakは、既存のユーザー・アカウントを特定の外部IDPにリンクするためにアプリケーションが使用できる、ブラウザー・ベースのAPIを提供しています。これは、Client Initiated Account Linkingと呼ばれます。

これを動作させるには、アプリケーションがユーザーのブラウザーをKeycloakサーバー上のURLに転送して、ユーザーのアカウントを特定の外部プロバイダー(Facebookなど)にリンクすることを要求します。サーバーは、外部プロバイダーとのログインを開始します。ブラウザーは外部プロバイダーにログインし、認証サーバーにリダイレクトされます。認証サーバーはリンクを確立し、確認のためにアプリケーションにリダイレクトします。

このプロトコルを開始する上で、クライアント・アプリケーションが満たさなければならない、いくつかの前提条件があります。

  • 管理コンソールで、必要なアイデンティティー・プロバイダーを設定し、ユーザーのレルムに対して有効にする必要がある。

  • アプリケーションは、OIDCプロトコルを介して既存のユーザーとしてログインしている必要がある。

  • ユーザーには account.manage-account または account.manage-account-links のロールマッピングがなければならない。

  • アプリケーションは、アクセストークン内にあるそれらのロールのスコープを許可されている必要がある。

  • アプリケーションは、リダイレクトURLを生成するために情報が必要なので、アクセストークンにアクセスする必要がある

ログインを開始するには、アプリケーションがURLを作成し、ユーザーのブラウザーをこのURLにリダイレクトする必要があります。URLは次のようになります。

/{auth-server-root}/auth/realms/{realm}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}

各パスとクエリー・パラメータの説明は次のとおりです。

provider

管理コンソールの アイデンティティ・プロバイダー のセクションで定義した外部IDPのプロバイダー・エイリアスです。

client_id

アプリケーションのOIDCクライアントIDです。管理コンソールでアプリケーションをクライアントとして登録したときに、このクライアントIDを指定する必要があります。

redirect_uri

アカウントのリンクが確立された後にリダイレクトするアプリケーションのコールバックURLです。有効なクライアント・リダイレクトURIパターンでなければなりません。つまり、管理コンソールでクライアントを登録したときに定義した有効なURLパターンの1つと一致する必要があります。

nonce

アプリケーションが生成しなければならないランダムな文字列です。

hash

Base64 URLでエンコードされたハッシュです。このハッシュは、 nonce + token.getSessionState() + token.getIssuedFor() + provider のSHA_256ハッシュでエンコードされたBase64 URLによって生成されます。トークン変数はOIDCのアクセストークンから取得されます。基本的には、ランダムなnonce、ユーザーセッションID、クライアントID、およびアクセスするアイデンティティー・プロバイダーのエイリアスをハッシュしています。

次に、アカウントリンクを確立するためのURLを生成するJavaサーブレット・コードの例を示します。

   KeycloakSecurityContext session = (KeycloakSecurityContext) httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
   AccessToken token = session.getToken();
   String clientId = token.getIssuedFor();
   String nonce = UUID.randomUUID().toString();
   MessageDigest md = null;
   try {
      md = MessageDigest.getInstance("SHA-256");
   } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
   }
   String input = nonce + token.getSessionState() + clientId + provider;
   byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
   String hash = Base64Url.encode(check);
   request.getSession().setAttribute("hash", hash);
   String redirectUri = ...;
   String accountLinkUrl = KeycloakUriBuilder.fromUri(authServerRootUrl)
                    .path("/auth/realms/{realm}/broker/{provider}/link")
                    .queryParam("nonce", nonce)
                    .queryParam("hash", hash)
                    .queryParam("client_id", clientId)
                    .queryParam("redirect_uri", redirectUri).build(realm, provider).toString();

このハッシュはなぜ含まれるのでしょうか?これにより、認証サーバーはクライアント・アプリケーションが要求を開始したことと、ユーザー・アカウントが特定のプロバイダーにリンクされることをランダムに要求する悪意のあるアプリケーションが無いことを保証します。認証サーバーはまず、ログイン時に設定されたSSO Cookieをチェックして、ユーザーがログインしているかどうかを確認します。次に、現在のログインに基づいてハッシュを再生成し、アプリケーションによって送信されたハッシュと一致するか確認します。

アカウントがリンクされると、認証サーバーは redirect_uri にリダイレクトします。リンクリクエストの処理に問題がある場合、認証サーバーが redirect_uri にリダイレクトされる保障はありません。ブラウザーはアプリケーションにリダイレクトされるのではなく、エラーページにリダイレクトされることがあります。何らかのエラー状態があり、認証サーバーがクライアント・アプリケーションにリダイレクトするのに十分安全であると判断した場合、 error クエリー・パラメーターが redirect_uri に追加されます。

このAPIはアプリケーションが要求を開始したことを保証しますが、この操作に対するCSRF攻撃を完全に防止するわけではありません。このアプリケーションは、依然としてCSRFの攻撃のターゲットに対する防御の責任があります。

5.2.1. 外部トークンのリフレッシュ

プロバイダーにログインして生成した外部トークン(FacebookやGitHubトークンなど)を使用している場合は、Account Linking APIを再起動することで、このトークンを更新できます。

6. サービス・プロバイダー・インターフェイス(SPI)

Keycloakは、必要なカスタム・コードが無くても、ほとんどのユースケースをカバーできるように作られていますが、カスタマイズもできるようにする必要があります。これを実現するために、Keycloakには独自のプロバイダーを実装できる多数のサービス・プロバイダー・インタフェース(SPI)があります。

6.1. SPIの実装

SPIを実装するには、SPIのProviderFactoryとプロバイダー・インターフェイスを実装する必要があります。また、サービス設定ファイルを作成する必要があります。

たとえば、Theme Selector SPIを実装するには、ThemeSelectorProviderFactoryとThemeSelectorProviderを実装して、 META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory のファイルを提供する必要があります。

ThemeSelectorProviderFactoryのサンプルを次に示します。

package org.acme.provider;

import ...

public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory {

      @Override
        public ThemeSelectorProvider create(KeycloakSession session) {
            return new MyThemeSelectorProvider(session);
        }

        @Override
        public void init(Config.Scope config) {
        }

        @Override
        public void postInit(KeycloakSessionFactory factory) {
        }

        @Override
        public void close() {
        }

        @Override
        public String getId() {
            return "myThemeSelector";
        }
}
Keycloakは、複数のリクエストの状態を格納することを可能にするプロバイダー・ファクトリーの単一のインスタンスを作成します。プロバイダー・インスタンスは、それぞれのリクエストに対してファクトリーでcreateを呼び出すことによって作成されるため、軽量オブジェクトである必要があります。

ThemeSelectorProviderのサンプルを次に示します。

package org.acme.provider;

import ...

public class MyThemeSelectorProvider implements ThemeSelectorProvider {

    public MyThemeSelectorProvider(KeycloakSession session) {
    }


    @Override
    public String getThemeName(Theme.Type type) {
        return "my-theme";
    }

        @Override
        public void close() {
        }
}

サービス設定ファイル( META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory )を次に示します。

org.acme.provider.MyThemeSelectorProviderFactory

standalone.xmlstandalone-ha.xml 、または domain.xml を介してプロバイダーを設定することができます。 standalone.xmlstandalone-ha.xml 、または domain.xml ファイルがある場所についての詳細は、Server Installation and Configuration Guideを参照してください。

たとえば、下記を standalone.xml に追加します。

<spi name="themeSelector">
    <provider name="myThemeSelector" enabled="true">
        <properties>
            <property name="theme" value="my-theme"/>
        </properties>
    </provider>
</spi>

そうすると、 ProviderFactory のinitメソッドで設定を取得することができます。

public void init(Config.Scope config) {
    String themeName = config.get("theme");
}

また、プロバイダーも必要に応じて他のプロバイダーを参照することができます。たとえば、

public class MyThemeSelectorProvider implements EventListenerProvider {

    private KeycloakSession session;

    public MyThemeSelectorProvider(KeycloakSession session) {
        this.session = session;
    }

    @Override
    public String getThemeName(Theme.Type type) {
        return session.getContext().getRealm().getLoginTheme();
    }
}

6.1.1. 管理コンソールでのSPI実装の情報表示

Keycloak管理者にプロバイダーに関する追加情報を表示すると、便利なことがあります。 プロバイダー・ビルド・タイム情報(たとえば、現在インストール済みのカスタム・プロバイダーのバージョン)、プロバイダーの現在の設定(たとえば、プロバイダーが通信するリモートシステムのURL)、または動作情報(たとえば、プロバイダーが通信するリモートシステムからの平均レスポンス・タイム)を表示することができます。Keycloak管理コンソールでは、サーバーの情報ページが提供され、この種の情報が表示されます。

プロバイダーからの情報を表示するには、 ProviderFactory 内で org.keycloak.provider.ServerInfoAwareProviderFactory インターフェイスを実装するだけです。

前のサンプルの MyThemeSelectorProviderFactory のサンプル実装を次に示します。

package org.acme.provider;

import ...

public class MyThemeSelectorProvider implements ThemeSelectorProvider, ServerInfoAwareProviderFactory {
    ...

    @Override
    public Map<String, String> getOperationalInfo() {
        Map<String, String> ret = new LinkedHashMap<>();
        ret.put("theme-name", "my-theme");
        return ret;
    }
}

6.2. プロバイダー実装の登録

プロバイダーの実装を登録するには2通りの方法があります。ほとんどの場合、最も簡単な方法は、Keycloak deployerのアプローチを使用することです。なぜなら、この方法だと自動的にたくさんの依存関係が処理されるからです。また、リデプロイだけでなくホットデプロイもサポートされます。

代替のアプローチとしては、モジュールとしてデプロイするという方法があります。

カスタムSPIを作成する場合は、モジュールとして展開する必要があります。それ以外の場合は、Keycloak deployerのアプローチを使用することをお勧めします。

6.2.1. Keycloak Deployer の使用

プロバイダーのJARをKeycloakの standalone/deployments/ ディレクトリーにコピーすると、プロバイダーが自動的にデプロイされます。ホットデプロイも機能します。さらにプロバイダーのJARは、WildFly環境でデプロイされた他のコンポーネントと同じように機能し、その環境下では jboss-deployment-structure.xml ファイルと同じようにファシリティーを使用することができます。このファイルによって、他のコンポーネントへの依存関係を設定してサードパーティーのJARとモジュールを読み込むことができます。

また、プロバイダーjarを、EARおよびWARと同じように、デプロイ可能な他のユニット内に含めておくこともできます。EARでデプロイすると、実際、第三者のjarを非常に簡単に使用できるようになります。なぜなら、これらのライブラリーをEARの lib/ ディレクトリーに置くだけで済むからです。

6.2.2. モジュールを使用したプロバイダーの登録

たとえばモジュールを使用するプロバイダーを登録すると、まずモジュールが作成されます。これを実行するには、jboss-cliスクリプトを使用するか、手動で KEYCLOAK_HOME/modules 内にフォルダーを作成して、jarと module.xml を追加します。たとえば、 jboss-cli スクリプトを使用してイベントリスナーのsysoutサンプル・プロバイダーを追加するには、以下を実行します。

KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.acme.provider --resources=target/provider.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi"

または、それを手動で作成するには、 KEYCLOAK_HOME/modules/org/acme/provider/main フォルダーを作成して起動します。次に provider.jar をこのフォルダーにコピーし、以下の内容で module.xml を作成します。

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="org.acme.provider">
    <resources>
        <resource-root path="provider.jar"/>
    </resources>
    <dependencies>
        <module name="org.keycloak.keycloak-core"/>
        <module name="org.keycloak.keycloak-server-spi"/>
    </dependencies>
</module>

モジュールを作成すると、Keycloakでこのモジュールを登録する必要があります。 これは standalone.xmlstandalone-ha.xml 、または domain.xml のkeycloak-serverサブシステムセクションを編集し、それをプロバイダーに追加することにより行われます。

<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
    <web-context>auth</web-context>
    <providers>
        <provider>module:org.keycloak.examples.event-sysout</provider>
    </providers>
    ...

6.2.3. プロバイダーの無効化

standalone.xmlstandalone-ha.xml 、または domain.xml 内で、プロバイダーの有効な属性をfalseに設定することによって、プロバイダーを無効にすることができます。たとえば、Infinispanユーザー・キャッシュ・プロバイダーを無効にするには、以下を追加します。

<spi name="userCache">
    <provider name="infinispan" enabled="false"/>
</spi>

6.3. Java EEの活用

サービス・プロバイダーは、プロバイダーを指し示すように META-INF/services ファイルを正しく設定している限りは、いかなるJava EEコンポーネント内でもこれをパッケージ化することができます。たとえば、プロバイダーがサードパーティーのライブラリーを使用する必要がある場合、プロバイダーをEAR内にパッケージ化し、これらのサードパーティーのライブラリーをEARの lib/ ディレクトリーに格納します。また、プロバイダーjarは、WildFly環境でEJB、WAR、およびEARが使用可能な jboss-deployment-structure.xml ファイルを使用することができる点に注意してください。このファイルについて、詳しくは、WildFlyのドキュメントを参照ください。これにより、他の細かい動作間で外部との依存関係を引き出すことができます。

ProviderFactory の実装はプレーンなJavaオブジェクトである必要があります。ただし、現在は、ステートフルEJBとしてプロバイダー・クラスとして実装することもサポートもしています。

@Stateful
@Local(EjbExampleUserStorageProvider.class)
public class EjbExampleUserStorageProvider implements UserStorageProvider,
        UserLookupProvider,
        UserRegistrationProvider,
        UserQueryProvider,
        CredentialInputUpdater,
        CredentialInputValidator,
        OnUserCache
{
    @PersistenceContext
    protected EntityManager em;

    protected ComponentModel model;
    protected KeycloakSession session;

    public void setModel(ComponentModel model) {
        this.model = model;
    }

    public void setSession(KeycloakSession session) {
        this.session = session;
    }


    @Remove
    @Override
    public void close() {
    }
...
}

ここで、 @Local アノテーションを定義し、プロバイダー・クラスを指定する必要があります。これを行わないと、EJBはプロバイダー・インスタンスを正しくプロキシーせず、プロバイダーは機能しません。

@Remove アノテーションをプロバイダーの close() メソッドに付与する必要があります。これを行わないと、ステートフルbeanは決してクリーンアップされず、最終的にエラーメッセージが表示されることになります。

ProviderFactory の実装はプレーンなJavaオブジェクトである必要があります。ファクトリー・クラスは、その create() メソッド内でステートフルEJBのJNDIルックアップを実行します。

public class EjbExampleUserStorageProviderFactory
        implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {

    @Override
    public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        try {
            InitialContext ctx = new InitialContext();
            EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup(
                     "java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName());
            provider.setModel(model);
            provider.setSession(session);
            return provider;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

6.4. 利用可能なSPI

利用可能なすべてのSPIのリストを実行時に確認する必要がある場合は、管理コンソールセクションでの説明通りに、管理コンソール内の Server Info ページを確認します。

7. サーバーの拡張

KeycloakSPIフレームワークによって、特定のビルトイン・プロバイダーを実装、またはオーバーライドすることが可能になります。ただし、Keycloakでも、その主機能とドメインを拡張することはできます。これにより、以下も可能になります。

  • カスタムRESTエンドポイントをKeycloakサーバーに追加

  • 独自のカスタムSPIを追加

  • カスタムJPAエンティティーをKeycloakデータモデルへ追加

7.1. カスタムRESTエンドポイントを追加

これは大変強力な拡張機能で、独自のRESTエンドポイントをKeycloakサーバーにデプロイすることができます。これによって、あらゆる種類の拡張が可能になります。たとえば、Keycloakサーバー上の機能を起動すことができます。これについては、ビルトインのKeycloakRESTエンドポイントのデフォルトセットでは利用できません。

カスタムRESTエンドポイントを追加するには、 RealmResourceProviderFactoryRealmResourceProvider のインターフェイスを実装する必要があります。 RealmResourceProvider には、以下のとおり、重要なメソッドが1つあります。

Object getResource();

これによって、オブジェクトを返却し、 JAX-RSリソース として機能することができます。詳しくは、Javadocとサンプルを参照してください。 providers/rest のサンプル配布物には非常に簡単なサンプルがあり、 providers/domain-extension にはさらに高度なサンプルがあります。この高度なサンプルによって、認証済みのRESTエンドポイントと、独自のSPIを追加する独自のJPAエンティティーでデータモデルを拡張するのような、その他の機能を追加する方法が示されます。

カスタム・プロバイダーをパッケージングしてデプロイする方法についての詳細は、サービス・プロバイダー・インターフェイスの章を参照してください。

7.2. 独自のカスタムSPIを追加

これは特にカスタムRESTエンドポイントを使用する際に便利です。独自のSPIを追加するには、 org.keycloak.provider.Spi インターフェイスを実装して、SPIのIDと ProviderFactory および Provider のクラスを定義する必要があります。以下のようになります。

package org.keycloak.examples.domainextension.spi;

import ...

public class ExampleSpi implements Spi {

@Override
 public boolean isInternal() {
 return false;
 }

@Override
 public String getName() {
 return "example";
 }

@Override
 public Class<? extends Provider> getProviderClass() {
 return ExampleService.class;
 }

@Override
 @SuppressWarnings("rawtypes")
 public Class<? extends ProviderFactory> getProviderFactoryClass() {
 return ExampleServiceProviderFactory.class;
 }

}

次に、 META-INF/services/org.keycloak.provider.Spi ファイルを作成し、そのファイルにSPIのクラスを追加する必要があります。たとえば、

org.keycloak.examples.domainextension.spi.ExampleSpi

次の手順では、ExampleServiceProviderFactory インターフィスを作成します。このインターフィスは、 Provider を継承する ProviderFactoryExampleService を継承します。 ExampleService には通常、ユースケースで必要なビジネスメソッドが含まれます。 ExampleServiceProviderFactory インスタンスは常にアプリケーション毎にスコープされます。ただし、 ExampleService はリクエスト毎にスコープされます(または、より厳密に言うと KeycloakSession ライフサイクル毎になります)。

最後に、サービス・プロバイダー・インタフェースの章で説明したのと同じ方法で、プロバイダーを実装する必要があります。

詳しくは、 providers/domain-extension 配布物のサンプルを参照してください。そこには、上記と同じようなSPIのサンプルが示されています。

7.3. カスタムJPAエンティティーをKeycloakデータモデルへ追加

Keycloakデータモデルが要求するソリューションとは厳密には違っていた場合、またはコアの機能をKeycloakに追加する場合、もしくは独自のRESTエンドポイントがある場合、Keycloakデータモデルの拡張が検討されるかもしれません。独自のJPAエンティティーをKeycloakJPA EntityManager へ追加することが可能になりました。

独自のJPAエンティティーを追加するには、 JpaEntityProviderFactoryJpaEntityProvider を実装する必要があります。 JpaEntityProvider によって、カスタムJPAエンティティーのリストを返し、Liquibaseの変更履歴の場所とidを提供することができます。実装サンプルは、以下のとおりになります。

これはサポートされていないAPIです。つまり、使用することはできますが、警告なしで削除または変更される可能性があります。
public class ExampleJpaEntityProvider implements JpaEntityProvider {

// List of your JPA entities.
 @Override
 public List<Class<?>> getEntities() {
 return Collections.<Class<?>>singletonList(Company.class);
 }

// This is used to return the location of the Liquibase changelog file.
 // You can return null if you don't want Liquibase to create and update the DB schema.
 @Override
 public String getChangelogLocation() {
         return "META-INF/example-changelog.xml";
 }

// Helper method, which will be used internally by Liquibase.
 @Override
 public String getFactoryId() {
 return "sample";
 }

    ...
}

上記のサンプルに、 Company クラスによって表現された単一のJPAエンティティーを追加しました。次に、RESTエンドポイントのコード内で、これと同じようなものを使用して EntityManager を取得し、その上でDBオペレーションを呼び出すことができます。

EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
Company myCompany = em.find(Company.class, "123");

getChangelogLocationgetFactoryId のメソッドは、Liquibaseによるエンティティーの自動更新をサポートするために重要です。 Liquibase はデータベース・スキーマを更新するためのフレームワークです。これはKeycloakで内部的に使用され、DBスキーマを作成してバージョン間でDBスキーマを更新します。これを同じように使用して、エンティティーの変更履歴を作成する必要があるかもしれません。独自のLiquibaseの変更履歴のバージョニングはKeycloakのバージョンとは異なる独立したものであることに注意してください。つまり、新しいKeycloakバージョンへ更新した際、同時にスキーマを更新する必要はありません。また、その逆の場合でも、Keycloakバージョンを更新しなくてもスキーマを更新することができます。Liquibaseの更新は常にサーバー起動時に実行されるので、新しい変更セットをLiquibaseの変更履歴ファイル(上記のサンプルでは、これは META-INF/example-changelog.xml ファイル(これはJPAエンティティーと ExampleJpaEntityProvider と同じJAR内に含まれていなければなりません)になります)を追加して再起動するだけで、スキーマのDB更新のトリガーとなります。起動時に、DBスキーマが自動的に更新されます。

詳しくは、 providers/domain-extension サンプル内のサンプル配布物を参照してください。そこでは、上記で説明された ExampleJpaEntityProviderexample-changelog.xml のサンプルが示されています。

Liquibase変更履歴に変更を加えたりDBの更新をトリガーにする前に、必ずデータベースをバックアップするようにしてください。

8. 認証SPI

Keycloakには、ケルベロス、パスワード、OTPなどのさまざまな認証機構が用意されています。これらの機構は、要件をすべて満たしているわけではなく、独自のカスタムプラグインを必要とする場合もあります。Keycloakは、新しいプラグインの作成に使用できる認証SPIを提供します。管理コンソールは、これらの新しい機構の適用、順序、設定をサポートしています。

Keycloakでは簡単な登録フォームもサポートされます。このフォームのさまざまな要素を有効、無効にすることができます。つまり、reCAPTCHAのサポートをオフにすることができます。同じ認証SPIを使用して、他のページを登録フローに追加したり、それを完全に再実装することができます。また、追加の細かいSPIを使用して、組み込みの登録フォームに特定のバリデーションやユーザー拡張機能を追加することもできます。

Keycloakでの必須アクションとは、認証後にユーザーが実行する必要のあるアクションのことです。アクションが実行された後、ユーザーはそのアクションを再実行する必要はありません。Keycloakには、"パスワードリセット"などの必須アクションがいくつか組み込まれています。たとえば、パスワードリセットは、ユーザーがログインした後にパスワードを変更するよう強制します。必須アクションを作成してプラグインすることができます。

8.1. 用語

まず最初に、認証SPIについて学ぶには、それを説明するためのいくつかの用語を確認していきます。

認証フロー

フローは、ログインまたは登録中に発生する必要のある、すべての認証のためのコンテナーです。管理コンソールの認証ページに移動すると、システム内で定義されたすべてのフローと、どのようなオーセンティケーターで構成されたかが表示されます。フローには、他のフローを含めることができます。また、ブラウザーのログイン、ダイレクト・グラント・アクセス、および登録用に、新しい異なるフローをバインドすることもできます。

オーセンティケーター

オーセンティケーターは、フロー内で認証またはアクションを実行するためのロジックを保持する、プラグイン可能なコンポーネントです。通常は、シングルトンです。

エグゼキューション

エグゼキューションは、オーセンティケーターをフローにバインドしたり、オーセンティケーターの設定に対する認証を行うオブジェクトです。フローには、エグゼキューション・エントリーが含まれます。

エグゼキューション要件

エグゼキューションごとに、オーセンティケーターがフロー内でどのように動作するかを定義します。要件には、オーセンティケーターがenabledか、disabledか、optionalか、requiredか、またはalternativeかを定義します。alternativeの要件では、フロー内で他のalternativeのオーセンティケーターが成功しない限り、そのオーセンティケーターはoptionalであることを意味します。たとえば、Cookie認証、ケルベロス、ログイン画面のセットは、すべてalternativeです。そのうちの1つが成功した場合、他は実行されません。

オーセンティケーター設定

このオブジェクトは、認証フロー内の特定のエグゼキューションに対してのオーセンティケーターの設定を定義します。エグゼキューションごとに異なる設定を持つことができます。

必須アクション

認証が完了した後、ログインが許可される前に、ユーザーは、1つ以上のワンタイム・アクションを実行しなければならない場合があります。ユーザーは、OTPトークン・ジェネレーターを設定するか、有効期限切れのパスワードをリセットするか、または利用規約に同意する必要があります。

8.2. アルゴリズムの概要

これがブラウザー・ログインでどのように機能するか説明します。以下のフロー、エグゼキューション、サブフローを想定してみましょう。

Cookie - ALTERNATIVE
Kerberos - ALTERNATIVE
Forms Subflow - ALTERNATIVE
           Username/Password Form - REQUIRED
           OTP Password Form - OPTIONAL

フォームのトップレベルには、すべてが選択的に必要な3つのエグゼキューションがあります。これらのいずれかが成功した場合、ほかは実行する必要がないことを意味します。SSO Cookieセットまたはケルベロスのログインが成功した場合、Username/Passwordフォームは実行されません。クライアントが最初にKeycloakにリダイレクトし、ユーザーを認証するまでの手順を説明します。

  1. OpenID ConnectまたはSAMLプロトコル・プロバイダーは、関連するデータを展開し、クライアントと署名を検証します。AuthenticationSessionModelを作成します。ブラウザーフローを検索し、フローの実行を開始します。

  2. このフローは、Cookieのエグゼキューションから、それがALTERNATIVEであると見なします。Cookieプロバイダーがロードされ、ユーザーがすでに認証セッションに関連付けられているかをチェックします。Cookieプロバイダーには、ユーザーは必要ありません。必要とされる場合は、そのフローが中断され、ユーザーにはエラー画面が表示されます。その後、SSO Cookieセットがあるかどうかを確認することを目的として、Cookieプロバイダーが実行されます。1つのセットがある場合、そのSSO CookieとUserSessionModelが検証され、AuthenticationSessionModelに関連付けられます。SSO Cookieが存在し、検証が済むと、Cookieプロバイダーはsuccess()ステータスを返します。Cookieプロバイダーは成功を返すと、このフローのレベルでのそれぞれのエグゼキューションはALTERNATIVEであるため、他のエグゼキューションは実行されず、ログインに成功します。SSO Cookieが存在しない場合は、Cookieプロバイダーはattempted()のステータスを返します。この場合、エラー状態ではありませんが、成功でもないことを示します。プロバイダーを試してきましたが、リクエストはこのオーセンティケーターを処理するようには設定されていません。

  3. 次に、ケルベロスのエグゼキューションのフローを見ていきます。これもまたALTERNATIVEです。ケルベロス・プロバイダーは、ユーザーがすでに設定されていて、AuthenticationSessionModelに関連付けられている必要はないので、このプロバイダーを実行できます。ケルベロスは、SPNEGOブラウザー・プロトコルを使用します。これには、サーバーとクライアントがネゴシエーション・ヘッダーを交換する一連のチャレンジまたはレスポンスが必要となります。ケルベロス・プロバイダーは、ネゴシエーション・ヘッダーを見ないため、サーバーとクライアント間の最初のやりとりであることを前提とします。したがって、クライアントへのHTTPチャレンジ・レスポンスを作成し、forceChallenge()ステータスを設定します。forceChallenge()は、フローによってHTTPレスポンスを無視できないことを意味し、クライアントに返さなければなりません。代わりにプロバイダーがchallenge()ステータスを返した場合、フローは他のすべてのALTERNATIVEが試行されるまでチャレンジ・レスポンスを保持します。したがって、この初期フェーズでは、フローが停止し、チャレンジ・レスポンスがブラウザーに返されます。ブラウザーが成功したネゴシエーション・ヘッダーで応答すると、プロバイダーはユーザーをAuthenticationSessionに関連付けし、このフローのレベルの残りのエグゼキューションはALTERNATIVEであるため、フローが終了します。それ以外の場合は、ケルベロス・プロバイダーは、attempted()を設定し、フローを続行します。

  4. 次のエグゼキューションは、Formsと呼ばれるサブフローです。このサブフローのエグゼキューションがロードされ、同じ処理ロジックが発生します。

  5. Formsサブフローでの最初のエグゼキューションはUsernamePasswordプロバイダーです。このプロバイダーは、ユーザーがすでにフローに関連付けられている必要はありません。このプロバイダーは、チャレンジHTTPレスポンスを作成し、そのステータスをchallenge()に設定します。このエグゼキューションは必須なので、フローはこのチャレンジを優先し、ブラウザーにHTTPレスポンスを返します。このレスポンスは、Username/Password HTMLページのレンダリングです。ユーザーは、ユーザー名とパスワードを入力し、送信をクリックします。このHTTPリクエストは、UsernamePasswordプロバイダーに送信されます。ユーザーが無効なユーザー名またはパスワードを入力した場合、新しいチャレンジ・レスポンスが作成され、このエグゼキューションにfailureChallenge()のステータスが設定されます。failureChallenge()は、チャレンジがあることを表しますが、エラーログにerrorとして記録する必要があります。このエラーログは、ログイン失敗回数の多いアカウント、またはIPアドレスをロックするのに使用することができます。ユーザー名とパスワードが有効な場合、プロバイダーはUserModelをAuthenticationSessionModelに関連付け、success()ステータスを返します。

  6. 次のエグゼキューションは、OTPフォームです。このプロバイダーでは、ユーザーがフローに関連付けられている必要があります。UsernamePasswordプロバイダーがすでにユーザーをフローに関連付けているため、この要件は満たされます。このプロバイダーは、ユーザーが必須であることから、ユーザーがこのプロバイダーを使用するように設定されているかを求められます。ユーザーが設定されておらず、このエグゼキューションを必要とする場合、フローは認証が完了した後にユーザーが実行する必要のある必須アクションを設定します。OTPの場合、これはOTP設定ページを表します。エグゼキューションがOPTIONALの場合はスキップされます。

  7. フローが完了すると、認証プロセッサーはUserSessionModelを作成し、それをAuthenticationSessionModelに関連付けます。その後、ユーザーはログイン前に必須アクションを完了する必要があるかどうかを確認します。

  8. まず、それぞれの必須アクションのevaluateTriggers()メソッドが呼び出されます。これにより、必須アクション・プロバイダーは、アクションが実行されるトリガーとなる可能性があるかを判断できます。たとえば、レルムにパスワード有効期限ポリシーがある場合、このメソッドによってトリガーされる可能性があります。

  9. それぞれの必須アクションが、requiredActionChallenge()メソッドが呼び出されたユーザーに関連付けられました。ここでプロバイダーは、必須アクションのページをレンダリングするHTTPレスポンスを設定します。これは、チャレンジ・ステータスを設定することで実行されます。

  10. 必須アクションが最終的に成功すると、ユーザーの必須アクションリストから必須アクションが削除されます。

  11. すべての必須アクションが解決した後、ユーザーはようやくログインします。

8.3. オーセンティケーターSPIのウォークスルー

このセクションでは、オーセンティケーター・インターフェイスについて説明します。これを説明するために、"あなたの母親の旧姓は何ですか?"のような秘密の質問にユーザーが回答を入力する必要のあるオーセンティケーターを実装していきます。この例は、完全に実装されており、Keycloakのデモ配布物のexamples/providers/authenticatorディレクトリーに含まれています。

実装する必要があるクラスは、org.keycloak.authentication.AuthenticatorFactoryとオーセンティケーター・インターフェイスです。オーセンティケーター・インターフェイスはロジックを定義します。AuthenticatorFactoryは、オーセンティケーターのインスタンスの作成を担います。それらは両方とも、ユーザー・フェデレーションのような他のKeycloakコンポーネントが行う、より汎用的なプロバイダーとProviderFactoryのインターフェイスのセットを拡張します。

8.3.1. Packaging Classes and Deployment

1つのjarファイル内にクラスをパッケージ化します。このjarには、 org.keycloak.authentication.AuthenticatorFactory という名前のファイルが、jarの META-INF/services/ ディレクトリーに含まれている必要があります。このファイルには、jarファイル内にある各AuthenticatorFactory実装の完全修飾クラス名が一覧化されている必要があります。たとえば、以下のとおりになります。

org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
org.keycloak.examples.authenticator.AnotherProviderFactory

このサービスまたはファイルは、システムにロードする必要があるプロバイダーをKeycloakがスキャンするために使用されます。

このjarをデプロイするには、これをprovidersディレクトリーにコピーするだけです。

8.3.2. オーセンティケーターの実装

オーセンティケーター・インターフェイスを実装する場合、最初に実装する必要があるメソッドはrequiresUser()メソッドです。この例では、このメソッドは、ユーザーに関連付けられた秘密の質問を検証する必要があるため、trueを返す必要があります。ケルベロスなどのプロバイダーは、ネゴシエーション・ヘッダーからユーザーを解決することができるので、このメソッドからfalseを返します。ただし、この例では、特定のユーザーの特定のクレデンシャルを検証しています。

次に実装するメソッドはconfiguredFor()メソッドです。このメソッドは、ユーザーがこの特定のオーセンティケーターとして設定されているのかを判定する役割を担います。この例では、秘密の質問に対する回答がユーザーによって設定されているのかどうかを確認する必要があります。このケースでは、ハッシュ化された情報を、(パスワードが格納されているのと同じように)UserModel内のUserCredentialValueModel内に格納しています。これを簡単に確認する方法は、以下のとおりです。

@Override
  public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
     return session.users().configuredForCredentialType("secret_question", realm, user);
    }

configuredForCredentialType()コールは、ユーザーがそのクレデンシャル・タイプをサポートしているかどうかを問い合わせます。

オーセンティケーターで実装する次のメソッドはsetRequiredActions()です。configuredFor()がfalseを返し、フロー内でサンプルのオーセンティケーターが必要な場合、このメソッドが呼び出されます。このメソッドは、ユーザーが実行する必要がある必須アクションの登録を担います。この例では、秘密の質問に対する回答をユーザーに設定させるために必須アクションを登録する必要があります。この章の後半で、この必須アクション・プロバイダーを実装していきます。setRequiredActions()メソッドの実装は次のとおりです。

    @Override
    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
        user.addRequiredAction("SECRET_QUESTION_CONFIG");
    }

今度はオーセンティケーター実装の中身に入っていきます。次に実装するメソッドはauthenticate()です。これは、エグゼキューションが最初に訪れたときにフローが呼び出す初期メソッドです。ユーザーがすでにブラウザーのマシン上で秘密の質問に回答した場合、ユーザーはその質問に再度回答する必要はなく、そのマシンを"信頼できる"ものにすることが望まれます。authenticate()メソッドは、秘密の質問フォームに対する処理を実装する必要はありません。その唯一の目的は、ページをレンダリングするか、フローを継続することです。

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        if (hasCookie(context)) {
           context.success();
           return;
        }
        Response challenge = loginForm(context).createForm("secret_question.ftl");
        context.challenge(challenge);
    }

hasCookie()は、秘密の質問に回答したことを示すCookieがすでにブラウザーに設定されているかを確認します。trueを返す場合は、authentication()メソッドから返されるAuthenticationFlowContext.success()メソッドを使用し、エグゼキューションのステータスをSUCCESSとします。

hasCookie()メソッドがfalseを返す場合、秘密の質問のHTMLフォームをレンダリングするレスポンスを返す必要があります。AuthenticationFlowContextには、フォームを構築するために必要となる、適切な基本情報を持つFreemarkerページビルダーを初期化するform()メソッドがあります。このページ・ビルダーは org.keycloak.login.LoginFormsProvider と呼ばれています。LoginFormsProvider.createForm()メソッドは、ログインテーマからFreemarkerテンプレート・ファイルを読み込みます。さらに、Freemarkerテンプレートに追加情報を渡す場合は、LoginFormsProvider.setAttribute()メソッドを呼び出すことができます。こちらについては後ほど説明します。

LoginFormsProvider.createForm()を呼び出すと、JAX-RS Responseオブジェクトが返されます。次に、このレスポンスで渡すAuthenticationFlowContext.challenge()を呼び出します。これにより、エグゼキューションのステータスがCHALLENGEに設定され、エグゼキューションがREQUIREDの場合、このJAX-RSレスポンスのオブジェクトがブラウザーに送信されます。

したがって、秘密の質問に対する回答を求めるHTMLページがユーザーに表示され、ユーザーが回答を入力して送信をクリックします。HTMLフォームのアクションURLは、HTTPリクエストをフローに送信します。フローは、オーセンティケーター実装のaction()メソッドを呼び出すことになります。

    @Override
    public void action(AuthenticationFlowContext context) {
        boolean validated = validateAnswer(context);
        if (!validated) {
           Response challenge = context.form()
                                 .setError("badSecret")
                                 .createForm("secret-question.ftl");
           context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
           return;
        }
        setCookie(context);
        context.success();
    }

回答が有効でない場合、HTMLフォームを追加のエラーメッセージで再構築します。次に、原因となる値とJAX-RSレスポンスを渡すAuthenticationFlowContext.failureChallenge()を呼び出します。failureChallenge()はchallenge()と同じ働きをしますが、失敗も記録するので、攻撃検出サービスによって解析できます。

検証に成功すると、秘密の質問に回答したことを記憶するCookieを設定し、AuthenticationFlowContext.success()を呼び出します。

最後にsetCookie()メソッドを説明していきます。このメソッドは、オーセンティケーターの設定を提供する見本になります。このケースでは、Cookieの最大有効期間を設定する必要があります。

    protected void setCookie(AuthenticationFlowContext context) {
        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
        int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
        if (config != null) {
            maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));

        }
        ... set the cookie ...
    }

AuthenticationFlowContext.getAuthenticatorConfig()メソッドからAuthenticatorConfigModelを取得します。設定が存在する場合は、その設定の最大有効期間を取り出します。何を設定するべきかを定義する方法については、AuthenticatorFactoryの実装について説明する際に確認していきます。AuthenticatorFactory実装で設定の定義をした場合、管理コンソール内で設定値を定義できます。

8.3.3. AuthenticatorFactoryの実装

このプロセスでの次のステップは、AuthenticatorFactoryを実装することです。このファクトリーはオーセンティケーターのインスタンス化を担当します。また、オーセンティケーターに関する配備と設定のメタデータも提供します。

getId()メソッドは、コンポーネントの一意の名前です。create()メソッドは、オーセンティケーターを割り当てて処理するために、ランタイムによって呼び出されます。

public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {

    public static final String PROVIDER_ID = "secret-question-authenticator";
    private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    public Authenticator create(KeycloakSession session) {
        return SINGLETON;
    }

ファクトリーが次に受け持つのは、許可されたスイッチの要件を指定することです。ALTERNATIVE、REQUIRED、OPTIONAL、DISABLEDの4種類の要件タイプがありますが、AuthenticatorFactoryの実装では、フローを定義する際に管理コンソールに表示されるオプションの要件を制限できます。この例では、オプションの要件をREQUIREDとDISABLEDに制限します。

    private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
            AuthenticationExecutionModel.Requirement.REQUIRED,
            AuthenticationExecutionModel.Requirement.DISABLED
    };
    @Override
    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return REQUIREMENT_CHOICES;
    }

AuthenticatorFactory.isUserSetupAllowed()は、Authenticator.setRequiredActions()メソッドが呼び出されるかどうかをフローマネージャーに通知するフラグです。オーセンティケーターがユーザー用に設定されていない場合、フローマネージャーはisUserSetupAllowed()をチェックします。falseの場合、エラーを返して異常終了します。trueを返すと、フローマネージャーはAuthenticator.setRequiredActions()を呼び出します。

    @Override
    public boolean isUserSetupAllowed() {
        return true;
    }

次のいくつかのメソッドは、オーセンティケーターの設定方法を定義します。isConfigurable()メソッドは、オーセンティケーターをフロー内で設定することができるかを管理コンソールに指定するフラグです。getConfigProperties()メソッドは、ProviderConfigPropertyオブジェクトのリストを返します。これらのオブジェクトは、特定の設定属性を定義します。

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configProperties;
    }

    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();

    static {
        ProviderConfigProperty property;
        property = new ProviderConfigProperty();
        property.setName("cookie.max.age");
        property.setLabel("Cookie Max Age");
        property.setType(ProviderConfigProperty.STRING_TYPE);
        property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
        configProperties.add(property);
    }

それぞれのProviderConfigPropertyは、設定プロパティーの名前を定義します。これが、AuthenticatorConfigModelに格納されている設定マップで使用されるキーとなります。ラベルは、管理コンソールでどのように設定オプションが表示されるかを定義します。タイプは、そのプロパティーがString、Booleanまたはその他のタイプであるかを定義します。管理コンソールには、タイプによって異なるUI入力が表示されます。ヘルプのテキストは、管理コンソールの設定属性のツールチップに表示されるものです。詳しくは、ProviderConfigPropertyのjavadocを参照してください。

残りのメソッドは管理コンソール用です。getHelpText()は、エグゼキューションにバインドさせるオーセンティケーターを選択するときに表示されるツールチップ・テキストです。getDisplayType()は、オーセンティケーターを一覧表示する際に管理コンソールに表示されるテキストです。getReferenceCategory()は、オーセンティケーターが属するカテゴリーです。

8.3.4. オーセンティケーター・フォームの追加

Keycloakには、Freemarkerテーマとエンジン・テンプレート が付属しています。オーセンティケーター・クラスのauthenticate()内で呼び出されたcreateForm()メソッドは、ログインテーマ内のsecret-question.ftlファイルからHTMLページを構築します。このファイルは、JAR内の theme-resources/templates に追加する必要があります。詳細については、テーマ・リソース・プロバイダを参照してください。

secret-question.ftlの詳細を見ていきましょう。以下は、短いコードスニペットになります。

        <form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
            <div class="${properties.kcFormGroupClass!}">
                <div class="${properties.kcLabelWrapperClass!}">
                    <label for="totp" class="${properties.kcLabelClass!}">${msg("loginSecretQuestion")}</label>
                </div>

                <div class="${properties.kcInputWrapperClass!}">
                    <input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
                </div>
            </div>

${} で囲まれたテキストは、属性またはテンプレートの関数に対応します。フォームのアクションが表示された場合、それは ${url.loginAction} を指しています。この値は、AuthenticationFlowContext.form()メソッドを呼び出すと自動的に生成されます。JavaコードでAuthenticationFlowContext.getActionURL()メソッドを呼び出すことで、この値を取得することもできます。

${properties.someValue} も同じく表示されます。これらは、テーマのtheme.propertiesファイルで定義されたプロパティに対応します。 ${msg("someValue")} は、ログインテーマのmessages/ディレクトリーに含まれている国際化されたメッセージ・バンドル(.properties files)に対応します。英語のみを使用している場合は、 loginSecretQuestion の値を追加することができます。これがユーザーに要求する質問になります。

AuthenticationFlowContext.form()を呼び出すと、LoginFormsProviderインスタンスが生成されます。 LoginFormsProvider.setAttribute("foo", "bar") を呼び出した場合、"foo"の値は ${foo} という形式で参照することができます。属性の値は、Java beanでも構いません。

8.3.5. フローにオーセンティケーターを追加

フローへのオーセンティケーター追加は、管理コンソールで行う必要があります。Authenticationメニュー項目に移動してFlowタブを選択すると、現在定義されているフローが表示されます。組み込みのフローを変更することはできません。そのため、作成したオーセンティケーターを追加するには、既存のフローをコピーするか独自のフローを作成する必要があります。このUIは非常に分かりやすく作られているので、フローを作成してオーセンティケーターを追加する方法を見つけることができると思います。

フローを作成した後、バインドさせるログイン・アクションに、そのフローをバインドする必要があります。Authenticationメニューに移動して、Bindingsタブを選択すると、フローをBrowser、Registration、またはDirect Grantフローにバインドするオプションが表示されます。

8.4. 必須アクションのウォークスルー

このセクションでは、必須アクションを定義する方法について説明します。オーセンティケーターのセクションでは、"どのようにしてシステムに入力された秘密の質問に対するユーザーの回答をもらえば良いのだろう"と疑問に思ったかもしれません。例で示したように、回答が設定されていない場合は、必須アクションがトリガーされます。このセクションでは、シークレット・クエスチョン・オーセンティケーターに必須アクションを実装する方法について説明します。

8.4.1. Packaging Classes and Deployment

1つのjarファイル内にクラスをパッケージ化します。このjarは、他のプロバイダーのクラスとは別にする必要はありませんが、 org.keycloak.authentication.RequiredActionFactory という名前のファイルが、jarの META-INF/services/ ディレクトリーに含まれている必要があります。このファイルには、jarファイル内にある各RequiredActionFactory実装の完全修飾クラス名が一覧化されている必要があります。たとえば、以下のとおりになります。

org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory

このサービスまたはファイルは、システムにロードする必要があるプロバイダーをKeycloakがスキャンするために使用されます。

このjarをデプロイするには、これを standalone/deployments ディレクトリーにコピーしてください。

8.4.2. RequiredActionProviderの実装

必須アクションは、最初にRequiredActionProviderインターフェイスを実装する必要があります。RequiredActionProvider.requiredActionChallenge()は、フローマネージャーの必須アクションによる初回の呼び出しになります。このメソッドは、必須アクションを実行するHTMLフォームのレンダリングを行います。

    @Override
    public void requiredActionChallenge(RequiredActionContext context) {
        Response challenge = context.form().createForm("secret_question_config.ftl");
        context.challenge(challenge);

    }

RequiredActionContextには、AuthenticationFlowContextと同じようなメソッドがあります。form()メソッドにより、Freemarkerテンプレートからページをレンダリングすることができます。アクションURLは、このform()メソッドの呼び出しにより事前に設定されるため、HTMLフォーム内で参照するだけで済みます。これについては、後ほど説明します。

challenge()メソッドは、必須アクションを実行する必要があることをフローマネージャーに通知します。

次のメソッドは、必須アクションのHTMLフォームからの入力を処理します。フォームのアクションURLは、RequiredActionProvider.processAction()メソッドにルーティングされます。

    @Override
    public void processAction(RequiredActionContext context) {
        String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer"));
        UserCredentialValueModel model = new UserCredentialValueModel();
        model.setValue(answer);
        model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
        context.getUser().updateCredentialDirectly(model);
        context.success();
    }

回答はフォームのpostから取り出されます。UserCredentialValueModelが作成され、クレデンシャルのタイプと値がセットされます。その後、UserModel.updateCredentialDirectly()が呼び出されます。最後に、RequiredActionContext.success()は、必須アクションが成功したことをコンテナーに通知します。

8.4.3. RequiredActionFactoryの実装

このクラスは非常にシンプルです。これは必須アクション・プロバイダー・インスタンスの作成を行うだけです。

public class SecretQuestionRequiredActionFactory implements RequiredActionFactory {

    private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction();

    @Override
    public RequiredActionProvider create(KeycloakSession session) {
        return SINGLETON;
    }


    @Override
    public String getId() {
        return SecretQuestionRequiredAction.PROVIDER_ID;
    }

    @Override
    public String getDisplayText() {
        return "Secret Question";
    }

getDisplayText()メソッドは、必須アクションに対して分かりやすい名前を表示させたいときに、管理コンソールで使用されます。

8.4.4. 必須アクションの有効化

最後に、管理コンソールに移動する必要があります。左メニューのAuthenticationをクリックします。Required Actionsタブをクリックします。Registerボタンをクリックし、新しい必須アクションを選択します。必須アクションのリストに新しい必須アクションが表示され、有効になります。

8.5. 登録フォームの変更または拡張

オーセンティケーターを使用して独自のフローを実装することは可能なため、Keycloakでの登録方法を完全に変更することができます。しかし、独自の登録ページにちょっとしたバリデーションを加えたいことも普段はあるかと思います。これを行うために追加のSPIが作成されました。このSPIは、基本的には、ページにフォーム要素のバリデーションを追加するだけでなく、ユーザー登録後にUserModel属性とデータを初期化することもできます。ユーザー・プロファイルの登録処理の実装とGoogle Recaptchaプラグインの登録の両方を説明していきます。

8.5.1. FormActionインターフェイスの実装

実装する必要のあるコアとなるインターフェイスは、FormActionインターフェイスです。FormActionは、ページの一部のレンダリングと処理を受け待ちます。レンダリングはbuildPage()メソッド、バリデーションはvalidate()メソッド、バリデーション後の動作はsuccess()で行います。まずは、RecaptchaプラグインのbuildPage()メソッドを見ていきます。

    @Override
    public void buildPage(FormContext context, LoginFormsProvider form) {
        AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
        if (captchaConfig == null || captchaConfig.getConfig() == null
                || captchaConfig.getConfig().get(SITE_KEY) == null
                || captchaConfig.getConfig().get(SITE_SECRET) == null
                ) {
            form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
            return;
        }
        String siteKey = captchaConfig.getConfig().get(SITE_KEY);
        form.setAttribute("recaptchaRequired", true);
        form.setAttribute("recaptchaSiteKey", siteKey);
        form.addScript("https://www.google.com/recaptcha/api.js");
    }

Recaptcha buildPage()メソッドは、ページのレンダリングを支援するフォームフローからのコールバックです。これは、LoginFormsProviderであるフォーム・パラメーターを受け取ります。フォーム・プロバイダーに追加属性を加えて、登録Freemarkerテンプレートによって生成されたHTMLページに表示させることができます。

上記のコードは、登録recaptchaプラグインからのものです。Recaptchaには、設定から取得する必要のある特定の設定が必要です。FormActionsは、オーセンティケーターとまったく同じように設定されています。この例では、Google Recaptchaの公開鍵を設定から取り出し、フォーム・プロバイダーに属性として追加します。これで、登録テンプレート・ファイルはこの属性を読み込むことができます。

Recaptchaには、Javascriptの読み込み要件もあります。これを行うには、URLを渡すLoginFormsProvider.addScript()を呼び出します。

ユーザー・プロファイルを処理する場合、フォームに加える必要がある追加情報はないため、buildPage()メソッドは空です。

このインターフェイスの次の部分は、validate()メソッドです。これは、フォームのpostを受け取ると、直後に呼び出されます。最初に、Recaptchaのプラグインを見ていきます。

    @Override
    public void validate(ValidationContext context) {
        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        List<FormMessage> errors = new ArrayList<>();
        boolean success = false;

        String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
        if (!Validation.isBlank(captcha)) {
            AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
            String secret = captchaConfig.getConfig().get(SITE_SECRET);

            success = validateRecaptcha(context, success, captcha, secret);
        }
        if (success) {
            context.success();
        } else {
            errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
            formData.remove(G_RECAPTCHA_RESPONSE);
            context.validationError(formData, errors);
            return;


        }
    }

ここでは、Recaptchaウィジェットがフォームに追加するフォームデータを取得します。設定からRecaptchaの秘密鍵を取得します。次に、Recaptchaを検証します。成功すると、ValidationContext.success()が呼び出されます。成功しなかった場合は、formDataで渡すValidationContext.validationError()を呼び出します(ユーザーがデータを再入力する必要はありません)。また、表示するエラーメッセージを指定します。エラーメッセージは、国際化されたメッセージ・バンドルのメッセージ・バンドル・プロパティーを指し示している必要があります。他の登録拡張の場合、validate()はフォーム要素のフォーマット、つまり別の電子メール属性を検証する可能性もあります。

また、登録時にメールアドレスや他のユーザー情報を検証するために使用されるユーザー・プロファイル・プラグインについても見ていきましょう。

    @Override
    public void validate(ValidationContext context) {
        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        List<FormMessage> errors = new ArrayList<>();

        String eventError = Errors.INVALID_REGISTRATION;

        if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_FIRST_NAME)))) {
            errors.add(new FormMessage(RegistrationPage.FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME));
        }

        if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_LAST_NAME)))) {
            errors.add(new FormMessage(RegistrationPage.FIELD_LAST_NAME, Messages.MISSING_LAST_NAME));
        }

        String email = formData.getFirst(Validation.FIELD_EMAIL);
        if (Validation.isBlank(email)) {
            errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
        } else if (!Validation.isEmailValid(email)) {
            formData.remove(Validation.FIELD_EMAIL);
            errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
        }

        if (context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
            formData.remove(Validation.FIELD_EMAIL);
            errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));
        }

        if (errors.size() > 0) {
            context.validationError(formData, errors);
            return;

        } else {
            context.success();
        }
    }

上記に示されているとおり、ユーザー・プロファイル処理のvalidate()メソッドは、電子メール、名および姓がフォームに入力されていることを確認します。また、電子メールが適切なフォーマットであることを確認します。これらのバリデーションのいずれかが失敗すると、レンダリング用にエラーメッセージがキューに入れられます。エラーのあるフィールドは、すべてフォームデータから削除されます。エラーメッセージは、FormMessageクラスによって表されます。このクラスのコンストラクターでの最初のパラメーターは、HTML要素IDが取得されます。フォームが再レンダリングされると、エラー入力がハイライトされます。2番目のパラメーターはメッセージ参照IDです。このIDは、テーマ内でローカライズされたメッセージ・バンドル・ファイルのいずれかのプロパティーに対応している必要があります。

すべてのバリデーションが処理された後、フォームフローはFormAction.success()メソッドを呼び出します。Recapthaに対して、これは何も行わないので説明はしません。ユーザー・プロファイル処理の場合、このメソッドは登録ユーザーの値を入力します。

    @Override
    public void success(FormContext context) {
        UserModel user = context.getUser();
        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
        user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME));
        user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME));
        user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL));
    }

とてもシンプルな実装です。新しく登録されたユーザーのUserModelは、FormContextから取得されます。適切なメソッドが呼び出され、UserModelデータが初期化されます。

最後に、FormActionFactoryクラスを定義する必要もあります。このクラスは、AuthenticatorFactoryと同様に実装されるため、ここでは説明はしません。

8.5.2. アクションのパッケージ化

1つのjarファイル内にクラスをパッケージ化します。このjarには、 org.keycloak.authentication.FormActionFactory という名前のファイルが、jarの META-INF/services/ ディレクトリーに含まれている必要があります。このファイルには、jarファイル内にある各FormActionFactory実装の完全修飾クラス名が一覧化されている必要があります。たとえば、以下のとおりになります。

org.keycloak.authentication.forms.RegistrationProfile
org.keycloak.authentication.forms.RegistrationRecaptcha

このサービスまたはファイルは、システムにロードする必要があるプロバイダーをKeycloakがスキャンするために使用されます。

このjarをデプロイするには、これを standalone/deployments ディレクトリーにコピーしてください。

8.5.3. RegistrationフローへのFormActionの追加

登録ページフローへのFormActionの追加は管理コンソールで行う必要があります。 Authenticationメニュー項目へ移動してFlowタブを選択すると、現在定義されているフローが表示されます。組み込みのフローは変更することはできません。そのため、作成したオーセンティケーターを追加するには、既存のフローをコピーするか独自のフローを作成する必要があります。このUIは非常に分かりやすく作られているので、フローを作成してFormActionを追加する方法を見つけることができると思っています。

基本的には、Registrationフローをコピーする必要があります。次に、Registration Formの右側にあるActionsメニューをクリックし、"Add execution"を選択して新しいエグゼキューションを追加します。選択リストからFormActionを選択します。FormActionが"Registration User Creation"の後に表示されていない場合は、ダウンエラーを使用してFormActionが"Registration User Creation"の後に来ることを確認してください。Regsitration User Creationのsuccess()メソッドは、新しいUserModelの作成を受け持つため、FormActionはユーザー作成の後に表示される必要があります。

フローを作成した後、そのフローをRegistrationにバインドする必要があります。Authenticationメニューに移動してBindingsタブを選択すると、フローをBrowser、Registration、またはDirect Grantフローにバインドするオプションが表示されます。

8.6. パスワード忘れおよびクレデンシャル・フローの変更

Keycloakには、パスワードを忘れた場合の特定の認証フロー、またはユーザーにより発行されたクレデンシャルのリセットもあります。管理コンソールのフローページに移動すると、"Reset Credentials"フローがあります。デフォルトでは、Keycloakはユーザーの電子メールまたはユーザー名を要求し、ユーザーに電子メールを送信します。ユーザーがリンクをクリックすると、パスワードとOTP(OTPが設定されている場合)の両方をリセットすることができます。フローの"Reset OTP"オーセンティケーターを無効にすると、自動のOTPリセットを無効にすることができます。

このフローにも機能を追加することができます。たとえば、多くのデプロイメントでは、リンク付きの電子メールを送信することに加え、ユーザーは1つ以上の秘密の質問に回答することができます。配布物に付属している秘密の質問のサンプルを拡張し、それをReset Credentialフローに組み込むことができます。

Reset Credentialsフローを拡張する場合、1つ注意点があります。最初の"オーセンティケーター"は、ユーザー名または電子メールを取得するための単なるページです。ユーザー名または電子メールが存在する場合、AuthenticationFlowContext.getUser()はそのユーザーを返します。それ以外の場合はnullになります。このフォームは、以前の電子メールまたはユーザー名が存在しない場合、電子メールまたはユーザー名を入力するようにユーザーに再要求 しません 。攻撃者が有効なユーザーを推測できないようにする必要があるため、AuthenticationFlowContext.getUser()がnullを返す場合、有効なユーザーが選択されたかのように見せかけるために、フローを続行する必要があります。このフローに秘密の質問を追加する場合は、電子メールの送信後にこれらの質問をすることをお勧めします。つまり、"Send Reset Email"オーセンティケーターの後に、カスタム・オーセンティケーターを追加してください。

8.7. First Broker Loginフローの変更

First Broker Loginフローは、アイデンティティー・プロバイダーに最初にログインした時に使用されます。 First Login という用語は、特定の認証されたアイデンティティー・プロバイダー・アカウントにリンクされているKeycloakアカウントがまだ存在していないことを意味します。このフローの詳細については、Server Administration Guideアイデンティティー・ブローカリング の章を参照してください。

8.8. クライアントの認証

Keycloakは実際には、 OpenID Connect クライアント・アプリケーションのためのプラグイン可能な認証をサポートしています。クライアント(アプリケーション)の認証は、Keycloakアダプターがバックチャネル・リクエストをKeycloakサーバーに送信中に内部で使用されます(認証が成功した後にアクセストークンに交換コードを要求したり、リフレッシュトークンを要求したりするなど)。しかし、 ダイレクト・アクセス・グラント (OAuth2により表される Resource Owner Password Credentials Flow )または サービス・アカウント 認証(OAuth2により表される Client Credentials Flow )でクライアント認証を直接使用することも可能です。

KeycloakアダプターとOAuth2フローの詳細については、Securing Applications and Services Guideを参照してください。

8.8.1. デフォルト実装

実際、Keycloakには、以下の2つのクライアント認証の実装が組み込まれています。

client_idとclient_secretによる従来の認証

これは、 OpenID Connect または OAuth2 の仕様で記載されているデフォルトのメカニズムであり、Keycloakは初期段階からサポートしています。パブリック・クライアントには、POSTリクエストにIDを持つ client_id パラメーターが含まれている必要があり(実際は認証されていません)、コンフィデンシャル・クライアントには、クライアントIDの Authorization: Basic ヘッダーとユーザー名とパスワードとして使用されるクライアント・シークレットが含まれている必要があります。

署名付きJWTによる認証

これは、 JWT Bearer Token Profiles for OAuth 2.0 仕様に基づいています。クライアントまたはアダプターは、 JWT を生成し、秘密鍵で署名します。Keycloakは、署名されたJWTをクライアントの公開鍵で検証し、それに基づいてクライアントを認証します。

署名されたJWTでクライアント認証を使用するアプリケーションのサンプルを表示するには、デモの例と examples/preconfigured-demo/product-app にあるサンプルを参照してください。

8.8.2. 独自のクライアント・オーセンティケーターの実装

独自のクライアント・オーセンティケーターをプラグインするには、クライアント(アダプター)とサーバー側の両方にいくつかのインターフェイスを実装する必要があります。

クライアント側

ここでは、 org.keycloak.adapters.authentication.ClientCredentialsProvider を実装し、以下のいずれかにその実装を入れる必要があります。

  • WEB-INF/classesのWARファイル。ただし、この場合、実装はこの単一のWARアプリケーションに対してのみ使用できます。

  • WARのWEB-INF/libに追加されるJARファイル。

  • JBossモジュールとして使用され、WARのjboss-deployment-structure.xmlで設定されるJARファイル。 いずれの場合も、WARまたはJARに META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider ファイルを作成する必要があります。

サーバー側

ここでは、 org.keycloak.authentication.ClientAuthenticatorFactoryorg.keycloak.authentication.ClientAuthenticator を実装する必要があります。また、 META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory ファイルに実装クラスの名前を追加する必要があります。詳細については、オーセンティケーター を参照してください。

9. アクション・トークンSPI

アクション・トークンとは、Json Web Token(JWT)の特別なインスタンスで、そのベアラーがいくつかの動作を実行できるようにするものです。たとえば、パスワードをリセットしたり、電子メールアドレスを検証したりすることができます。それらは通常、特定のレルムのアクション・トークンを処理するエンドポイントを指し示すリンクのフォームでユーザーに送信されます。

Keycloakは、4つの基本のトークンタイプを提供して、ベアラーが以下を実行できるようにします。

  • クレデンシャルのリセット

  • 電子メールアドレスの確認

  • 必須アクションの実行

  • 外部のアイデンティティー・プロバイダーのアカウントを使用した、アカウントのリンク付けの確認

これに加えて、アクショントークンSPIを使用して認証セッションを開始したり、変更したりする機能を実装することができます。これについて、詳しくは、以下で説明していきます。

9.1. アクション・トークンの解剖

アクション・トークンは、ペイロードに複数のフィールドが含まれている、アクティブなレルム鍵で署名された標準のJson Webトークンです。

  • typ - アクションの識別(例: verify-email

  • iatexp - トークンの有効期限

  • sub - ユーザーのID

  • azp - クライアント名

  • iss - 発行者 - 発行するレルムのURL

  • aud - Audience - 発行するレルムのURLを含むリスト

  • asid - 認証セッションのID (optional)

  • nonce - 操作が一度しか実行できない場合は、使用の一意性を保証するランダムなノンス (optional)

さらに、アクション・トークンには、JSON内にシリアライズできるカスタム・フィールドをいくつか含めることができます。

9.2. アクション・トークン処理

アクション・トークンがKeycloakエンドポイント KEYCLOAK_ROOT/auth/realms/master/login-actions/action-tokenkey パラメーターを介して渡されると、それが検証され、適切なアクション・トークン・ハンドラーが実行されます。 この処理は常に認証セッションのコンテキストに沿って行われ 、新しいものかアクション・トークン・サービスが既存の認証セッションに参加します(詳しくは、後で説明します)。アクション・トークン・ハンドラーは、トークン(これで認証セッションを変更することが多い)によって規定されたアクションを実行し、HTTPレスポンスを返すことができます(たとえば、認証を続けるか、情報またはエラーページを表示することができます)。これらの手順について、詳しくは以下のとおりです。

  1. Basic action token validation 。シグネチャーと時間の有効性が確認され、アクション・トークン・ハンドラーが typ フィールドに基づいて定義されます。

  2. 認証セッションの決定。 アクション・トークンURLが既存の認証セッションを使用するブラウザーで開かれ、トークンにブラウザーからの認証セッションと一致する認証セッションIDが含まれている場合、アクション・トークンの検証と処理により、この進行中の認証セッションが追加されます。 そうでない場合は、アクション・トークン・ハンドラーはその時点でブラウザーに存在する他の認証セッションを置き換える新しい認証セッションを作成します。

  3. トークンタイプ固有のトークンの検証。 アクション・トークン・エンドポイント・ロジックは、トークンにユーザー( sub フィールド)とクライアント( azp )が存在し、無効ではないことを検証します。次に、アクション・トークン・ハンドラー内で定義されたカスタム・バリデーションがすべて検証されます。さらに、トークン・ハンドラーは、トークンを使い捨てるように要求できます。これで、すでに使用済みのトークンは、アクション・トークン・エンドポイント・ロジックによって拒否されます。

  4. アクションを実行する。 これらの検証がすべて行われた後で、トークン内のパラメーターに応じて実際の動作を実行する、アクション・トークン・ハンドラー・コードが呼び出されます。

  5. 使い捨てトークンの無効化。 トークンを使い捨てるように設定されている場合、認証フローが完了次第、アクション・トークンは無効になります。

9.3. 独自のアクション・トークンとそのハンドラーの実装

9.3.1. アクション・トークンの作成方法

アクション・トークンは、必須フィールドがほとんどない署名付きJWTなので、Keycloakの JWSBuilder クラスを使用してシリアライズして署名することができます(アクショントークンの解剖 を参照してください)。この方法は、 org.keycloak.authentication.actiontoken.DefaultActionTokenserialize(session, realm, uriInfo) メソッドにすでに実装されており、実装者はプレーンな JsonWebToken の代わりにそのクラスを使うことで活用できます。

次の例は、単純なアクショントークンの実装を示しています。クラスには引数のないprivateコンストラクターが必要です。これは、JWTからトークンクラスをデシリアライズするために必要です。

import org.keycloak.authentication.actiontoken.DefaultActionToken;

public class DemoActionToken extends DefaultActionToken {

    public static final String TOKEN_TYPE = "my-demo-token";

    public DemoActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId) {
        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
    }

    private DemoActionToken() {
        // Required to deserialize from JWT
        super();
    }
}

実装しているアクション・トークンが、JSONフィールドにシリアライズされる必要があるカスタム・フィールドを含んでいる場合、 org.keycloak.models.ActionTokenKeyModel のインターフェイスを実装する org.keycloak.representations.JsonWebToken クラスの子孫を実装することを検討する必要があります。この場合、既存の org.keycloak.authentication.actiontoken.DefaultActionToken クラスは、すでに両方の条件を満たしており、直接使用するか、そのクラスの子を実装することができます。フィールドには適切なJacksonアノテーション(たとえば、JSONにそれらをシリアライズするには com.fasterxml.jackson.annotation.JsonProperty )を付けることができます。

次の例は、前の例の DemoActionToken をフィールド demo-id で拡張しています。

import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.authentication.actiontoken.DefaultActionToken;

public class DemoActionToken extends DefaultActionToken {

    public static final String TOKEN_TYPE = "my-demo-token";

    private static final String JSON_FIELD_DEMO_ID = "demo-id";

    @JsonProperty(value = JSON_FIELD_DEMO_ID)
    private String demoId;

    public DemoActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String demoId) {
        super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
        this.demoId =  demoId;
    }

    private DemoActionToken() {
        // you must have this private constructor for deserializer
    }

    public String getDemoId() {
        return demoId;
    }
}

9.3.2. Packaging Classes and Deployment

独自のアクション・トークンとそのハンドラーをプラグインするには、サーバー側のインターフェイスを少し実装する必要があります。

  • org.keycloak.authentication.actiontoken.ActionTokenHandler - 特定のアクションに対する(つまり、 typ トークン・フィールドに指定された値に対する)アクション・トークンの実際のハンドラー。

    そのインターフェイス内の中心的なメソッドは、アクション・トークンを受け取る際に実行される実際の操作を定義する handleToken(token, context) です。通常、それは認証セッション記録の変更ですが、一般的には任意です。このメソッドは、ベリファイア( getVerifiers(context) で定義されたものを含む)がすべて成功し、 tokengetTokenClass() メソッドによって返されるクラスであることが保証されている場合にのみ呼び出されます。

    上記の2項目の説明どおり、現在の認証セッションに対してアクション・トークンが発行されたのか否かを決定するには、認証セッションIDを抽出するメソッドが getAuthenticationSessionIdFromToken(token, context) メソッドで宣言されなければなりません。 DefaultActionToken の実装は、トークンから asid フィールドの値を返します。このメソッドをオーバーライドして、トークンに関係なく、現在の認証セッションIDを返すことができることに注意してください。つまり、任意の認証フローを開始する前に進行中の認証フローにステップインするトークンを作成できます。

    トークンからの認証セッションが現在のものと一致しない場合、アクション・トークン・ハンドラーは、 startFreshAuthenticationSession(token, context) を呼び出すことにより新しいセッションを開始するよう要求されます。これにより、禁止されることを通知するための VerificationException (または、より説明的な表現である ExplainedTokenVerificationException )をスローできます。

    また、トークン・ハンドラーにより、 canUseTokenRepeatedly(token, context) メソッドを介して、トークンが使用されて認証が完了した後に、トークンが無効になるかどうかが決定されます。複数のアクション・トークンを利用するフローがある場合、最後のトークンだけが無効になることに注意してください。この場合、アクション・トークン・ハンドラー内で org.keycloak.models.ActionTokenStoreProvider を使用し、使用されたトークンを手動で無効にする必要があります。

    ほとんどの ActionTokenHandler メソッドのデフォルト実装は、 keycloak-services モジュール内の org.keycloak.authentication.actiontoken.AbstractActionTokenHander 抽象クラスです。実装する必要のある唯一のメソッドは、実際の動作を実行する handleToken(token, context) です。

  • org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory - アクション・トークン・ハンドラーを開始するファクトリー。アクション・トークン内の typ フィールドの値と正確に一致する値を返すため、実装は getId() をオーバーライドする必要があります。

    カスタムの ActionTokenHandlerFactory 実装を、このガイドのサービス・プロバイダー・インターフェイスのセクションで説明するとおりに、登録しなければならないことに注意してください。

10. イベントリスナーSPI

イベントリスナー・プロバイダーの作成は、 EventListenerProvider インタフェースと EventListenerProviderFactory インタフェースを実装することから始まります。これを行う方法の詳細については、Javadocとサンプルを参照してください。

For details on how to package and deploy a custom provider refer to the Service Provider Interfaces chapter.

11. ユーザー・ストレージSPI

ユーザー・ストレージSPIを使用して、外部ユーザー・データベースとクレデンシャル・ストアに接続するように、Keycloakの拡張機能を実装できます。組み込みのLDAPとActiveDirectoryのサポートは、このSPIを実際に実装したものです。設定などの作業をせず、すぐにKeycloakはローカル・データベースを使用して、ユーザーの作成、更新、検索とクレデンシャルの検証をします。しかし、多くの場合、組織はKeycloakのデータモデルに移行できない既存の外部独自のユーザー・データベースを持っています。このような状況では、アプリケーション開発者は、ユーザー・ストレージSPIの実装を記述して、外部ユーザーストアと、Keycloakがユーザーのログインおよび管理に使用する内部ユーザー・オブジェクト・モデルをブリッジすることができます。

Keycloakランタイムは、ユーザーがログインしているときなど、ユーザーを検索する必要があるときに、ユーザーを特定するためにいくつかの手順を実行します。まず、ユーザーがユーザー・キャッシュに入っているかどうかを調べます。ユーザーが見つかった場合は、そのメモリー内表現を使用します。次に、Keycloakローカル・データベース内のユーザーを探します。ユーザーが見つからない場合は、ユーザー・ストレージSPIプロバイダの実装をループして、そのうちの1つがランタイムが探しているユーザーを返すまでユーザークエリーを実行します。プロバイダーは外部ユーザーストアにユーザーを照会し、ユーザーの外部データ表現をKeycloakのユーザー・メタモデルにマップします。

ユーザー・ストレージSPIプロバイダーの実装では、複雑な条件のクエリーを実行したり、ユーザーに対してCRUD操作を実行したり、クレデンシャルを検証および管理したり、一度に多くのユーザーの一括更新を実行することもできます。これは外部ストアの機能に依存します。

ユーザー・ストレージSPIプロバイダーの実装は、Java EEコンポーネントと同様にパッケージ化され、デプロイされます(しばしばJava EEコンポーネントです)。デフォルトでは有効になっていませんが、管理コンソールの User Federation タブでレルムごとに有効にして設定する必要があります。

11.1. プロバイダー・インターフェイス

ユーザー・ストレージSPIを実装する場合、プロバイダー・クラスとプロバイダー・ファクトリーを定義する必要があります。プロバイダー・クラス・インスタンスは、プロバイダー・ファクトリーによってトランザクション毎に作成されます。プロバイダー・クラスは、ユーザーのルックアップやその他の操作などの重い処理をすべて請け負います。プロバイダー・クラスは、 org.keycloak.storage.UserStorageProvider インターフェイスを実装する必要があります。

package org.keycloak.storage;

public interface UserStorageProvider extends Provider {


    /**
     * Callback when a realm is removed.  Implement this if, for example, you want to do some
     * cleanup in your user storage when a realm is removed
     *
     * @param realm
     */
    default
    void preRemove(RealmModel realm) {

    }

    /**
     * Callback when a group is removed.  Allows you to do things like remove a user
     * group mapping in your external store if appropriate
     *
     * @param realm
     * @param group
     */
    default
    void preRemove(RealmModel realm, GroupModel group) {

    }

    /**
     * Callback when a role is removed.  Allows you to do things like remove a user
     * role mapping in your external store if appropriate

     * @param realm
     * @param role
     */
    default
    void preRemove(RealmModel realm, RoleModel role) {

    }

}

UserStorageProvider インターフェイスは非常に稀薄だと思うかもしれません。この章の後半で、ユーザー統合の重要性をサポートするために、プロバイダー・クラスが実装するかもしれないミックスイン・インターフェイスがあることが分かります。

UserStorageProvider インスタンスはトランザクション毎に都度作成されます。トランザクションが完了すると、 UserStorageProvider.close() メソッドが呼び出され、次にインスタンスがガーベッジ・コレクション処理されます。インスタンスはプロバイダー・ファクトリーにより作成されます。プロバイダー・ファクトリーは org.keycloak.storage.UserStorageProviderFactory インターフェイスを実装します。

package org.keycloak.storage;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public interface UserStorageProviderFactory<T extends UserStorageProvider> extends ComponentFactory<T, UserStorageProvider> {

    /**
     * This is the name of the provider and will be showed in the admin console as an option.
     *
     * @return
     */
    @Override
    String getId();

    /**
     * called per Keycloak transaction.
     *
     * @param session
     * @param model
     * @return
     */
    T create(KeycloakSession session, ComponentModel model);
...
}

プロバイダー・ファクトリー・クラスが UserStorageProviderFactory を実装する場合、具体的なプロバイダー・クラスをテンプレート・パラメーターとして指定する必要があります。ランタイムがこのクラスをイントロスペクトしてその機能(それが実装する他のインターフェイス)をスキャンするので、これは必須です。たとえば、現在は、プロバイダー・クラスが FileProvider という名前の場合、ファクトリー・クラスは以下のように表示されます。

public class FileProviderFactory implements UserStorageProviderFactory<FileProvider> {

    public String getId() { return "file-provider"; }

    public FileProvider create(KeycloakSession session, ComponentModel model) {
       ...
    }

getId() メソッドはユーザー・ストレージ・プロバイダーの名前を返します。特定のレルムのプロバイダーを有効にしたい場合、このidは管理コンソールの UserFederation ページ内に表示されます。

create() メソッドは、プロバイダー・クラスのインスタンスの割り当てを担当します。これは org.keycloak.models.KeycloakSession パラメーターを受け取ります。このオブジェクトは、ランタイム内で他のさまざまなコンポーネントへアクセスできるようにするだけでなく、他の情報やメタデータを参照することにも使用できます。 ComponentModel パラメーターは、特定のレルム内でプロバイダーがどのように有効にされ設定されたかを表します。これには、有効なプロバイダーのインスタンスIDと、管理コンソールで有効にした際に指定した可能性がある設定が含まれます

UserStorageProviderFactory には他にも機能がありますが、それはでこの章で後ほど説明します。

11.2. プロバイダー・ケイパビリティー・インターフェイス

UserStorageProvider インターフェイスを注意深く見てみると、ユーザーを検索したり管理したりするメソッドが定義されていないことに気がつくでしょう。これらのメソッドは ケイパビリティー・インターフェイス として定義されており、外部ユーザーストアが持つ機能に依存しています。たとえば、あるユーザーストアがリードオンリーで単純なクエリーとクレデンシャル検証機能のみ持つ場合、必要な作業はそれらの機能のみを ケイパビリティー・インターフェイス として実装することだけです。

SPI 説明

org.keycloak.storage.user.UserLookupProvider

このインターフェイスは外部ストアからのユーザーをログインさせるのに必要です。ほとんど(全ての?)プロバイダーはこのインターフェイスを実装します。

org.keycloak.storage.user.UserQueryProvider

1つ以上のユーザーを検索する複雑なクエリーを定義します。管理コンソールでユーザーを閲覧・管理する場合はこのインターフェイスを実装する必要があります。

org.keycloak.storage.user.UserRegistrationProvider

プロバイダーがユーザーの追加・削除をサポートする場合はこのインターフェイスを実装します。

org.keycloak.storage.user.UserBulkUpdateProvider

プロバイダーがユーザーのバルク更新をサポートする場合はこのインターフェイスを実装します。

org.keycloak.credential.CredentialInputValidator

プロバイダーが1つまたは複数の異なるクレデンシャル・タイプを検証する(たとえば、プロバイダーがパスワード検証をする)場合はこのインターフェイスを実装します。

org.keycloak.credential.CredentialInputUpdater

プロバイダーが1つ以上の異なるクレデンシャル・タイプを更新する場合はこのインターフェイスを実装します。

11.3. モデル・インターフェイス

capability interfaces で定義されたメソッドのほとんどは、ユーザーの表現が返されるか、または渡されます。これらの表現は、 org.keycloak.models.UserModel インターフェイスによって定義されます。アプリ開発者はこのインターフェイスを実装する必要があります。これは、外部ユーザーストアとKeycloakが使用するユーザー・メタモデルとの間のマッピングを提供します。

package org.keycloak.models;

public interface UserModel extends RoleMapperModel {
    String getId();

    String getUsername();
    void setUsername(String username);

    String getFirstName();
    void setFirstName(String firstName);

    String getLastName();
    void setLastName(String lastName);

    String getEmail();
    void setEmail(String email);
...
}

UserModel の実装は、ユーザー名、名前、電子メール、ロール、グループ・マッピング、その他の任意の属性などのユーザーに関するメタデータの読み取りと更新のためのアクセスを提供します。

org.keycloak.models パッケージには、Keycloakメタモデルの他の部分を表す他のモデルクラス、 RealmModelRoleModelGroupModel 、および ClientModel があります。

11.3.1. ストレージID

UserModel の重要なメソッドの1つは getId() メソッドです。 UserModel を実装する場合、開発者はユーザーIDの形式を意識している必要があります。フォーマットは以下のとおりでなければなりません。

"f:" + component id + ":" + external id

Keycloakランタイムは、多くの場合、ユーザーIDでユーザーを検索する必要があります。ユーザーIDには十分な情報が含まれているため、ランタイムはユーザーを検索する際にシステム内のすべての UserStorageProvider にクエリーを発行する必要がありません。

コンポーネントIDは、 ComponentModel.getId() から返されたIDです。 ComponentModel は、プロバイダー・クラスを作成するときにパラメーターとして渡されるので、そこから取得できます。外部IDは、プロバイダー・クラスが外部ストアでユーザーを見つけるために必要な情報です。これは多くの場合、ユーザー名かUIDです。たとえば、次のようになります。

f:332a234e31234:wburke

ランタイムがIDによるルックアップを実行すると、コンポーネントIDを取得するためにIDが解析されます。コンポーネントIDは、もともとユーザーをロードするために使用された UserStorageProvider の場所を特定するために使用されます。そのプロバイダーにはIDが渡されます。プロバイダーは、外部IDを取得するためにIDを再度解析し、それを外部ユーザー・ストレージにユーザーを配置するために使用します。

11.4. パッケージ化とデプロイ

ユーザー・ストレージ・プロバイダーは、WildFlyアプリケーション・サーバーに何かをデプロイするのと同じ方法で、JARにパッケージ化され、Keycloakランタイムにデプロイまたはアンデプロイされます。JARをサーバーの deploy/ ディレクトリーに直接コピーするか、またはJBoss CLIを使用してデプロイを実行することができます。

Keycloakがプロバイダーを認識するためには、JARに META-INF/services/org.keycloak.storage.UserStorageProviderFactory のファイルを追加する必要があります。このファイルには、次のような UserStorageProviderFactory の実装の完全修飾クラス名の行区切りリストが含まれていなければなりません。

org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

Keycloakは、これらのプロバイダーのJARのホット・デプロイメントをサポートしています。また、この章の後半では、Java EEコンポーネント内およびJava EEコンポーネントとしてパッケージ化できることが分かります。

11.5. 簡単な読み取り専用の参照のサンプル

ユーザー・ストレージSPIの基本実装を示すために、簡単なサンプルで説明します。この章では、簡単なプロパティー・ファイル内でユーザーを検索する簡単な UserStorageProvider の実装を見ていきます。プロパティー・ファイルには、ユーザー名とパスワードの定義が含まれており、クラスパス上の特定のロケーションにハードコードされています。プロバイダーにより、IDとユーザー名でユーザーを検索し、パスワードを検証することもできるようになります。このプロバイダーに基づくユーザーは、読み取り専用になります。

11.5.1. プロバイダー・クラス

はじめに UserStorageProvider クラスについて説明します。

public class PropertyFileUserStorageProvider implements
        UserStorageProvider,
        UserLookupProvider,
        CredentialInputValidator,
        CredentialInputUpdater
{
...
}

プロバイダー・クラス PropertyFileUserStorageProvider は、多くのインターフェイスを実装しています。SPIの基本要件なので、 UserLookupProvider が実装されています。このプロバイダーにより保存されたユーザーでログインできるようにするため、 UserLookupProvider インターフェイスが実装されています。ログイン画面で入力したパスワードの検証を可能にする必要があるので、 CredentialInputValidator インターフェイスが実装されています。プロパティー・ファイルは読み取り専用です。ユーザーがパスワードを更新しようとする時にエラー状態を通知する必要があるので、 CredentialInputUpdater が実装されています。

    protected KeycloakSession session;
    protected Properties properties;
    protected ComponentModel model;
    // map of loaded users in this transaction
    protected Map<String, UserModel> loadedUsers = new HashMap<>();

    public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
        this.session = session;
        this.model = model;
        this.properties = properties;
    }

このプロバイダー・クラスのコンストラクターには、 KeycloakSessionComponentModel 、およびプロパティー・ファイルへの参照が格納されます。後で、これらをすべて使用します。また、ロードされたユーザーのマップがあることにも注意してください。ユーザーを見つけるたびに、このマップに保存して、同じトランザクション内でそれを再度作成しないで済むようにします。多くのプロバイダがこれを行う必要があるので、従うことは良い習慣です(つまり、JPAと統合するすべてのプロバイダー)。プロバイダー・クラス・インスタンスは、トランザクション毎に都度作成され、トランザクション完了後にクローズされることも注意してください。

UserLookupProviderの実装
    @Override
    public UserModel getUserByUsername(String username, RealmModel realm) {
        UserModel adapter = loadedUsers.get(username);
        if (adapter == null) {
            String password = properties.getProperty(username);
            if (password != null) {
                adapter = createAdapter(realm, username);
                loadedUsers.put(username, adapter);
            }
        }
        return adapter;
    }

    protected UserModel createAdapter(RealmModel realm, String username) {
        return new AbstractUserAdapter(session, realm, model) {
            @Override
            public String getUsername() {
                return username;
            }
        };
    }

    @Override
    public UserModel getUserById(String id, RealmModel realm) {
        StorageId storageId = new StorageId(id);
        String username = storageId.getExternalId();
        return getUserByUsername(username, realm);
    }

    @Override
    public UserModel getUserByEmail(String email, RealmModel realm) {
        return null;
    }

getUserByUsername() メソッドは、ユーザーがログインする際にKeycloakログインページにより呼び出されます。実装では、最初に loadedUsers マップを確認して、ユーザーがすでにトランザクション内にロードされているかどうかを確かめます。ロードされていなかった場合、プロパティー・ファイル内でユーザー名を検索します。それが存在した場合、 UserModel の実装を作成し、今後の参照のために loadedUsers 内にそれを格納し、このインスタンスを返します。

createAdapter() メソッドは、ヘルパー・クラス org.keycloak.storage.adapter.AbstractUserAdapter を使用します。これは、 UserModel の基本実装を提供します。これにより、外部IDとしてユーザーのユーザー名を使用する、必須ストレージIDフォーマットに基づいたユーザーIDが自動的に生成されます。

"f:" + component id + ":" + username

AbstractUserAdapter のすべてのgetメソッドは、nullか空のコレクションを返します。しかし、ロールとグループ・マッピングを返すメソッドは、全ユーザーのレルム用に設定されたデフォルトのロールとグループを返します。 AbstractUserAdapter のすべてのsetメソッドは、 org.keycloak.storage.ReadOnlyException をスローします。そのため、管理コンソール内でユーザーを更新しようとすると、エラーが発生します。

getUserById() メソッドは、 org.keycloak.storage.StorageId ヘルパークラスを使って id パラメーターを解析します。 StorageId.getExternalId() メソッドは、 id パラメーターに埋め込まれたユーザー名を取得するために呼び出されます。そして、このメソッドは getUserByUsername() に委譲します。

電子メールは格納されていないので、 getUserByEmail() メソッドはnullを返します。

CredentialInputValidatorの実装

次に CredentialInputValidator の実装メソッドを見ていきましょう。

    @Override
    public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
        String password = properties.getProperty(user.getUsername());
        return credentialType.equals(CredentialModel.PASSWORD) && password != null;
    }

    @Override
    public boolean supportsCredentialType(String credentialType) {
        return credentialType.equals(CredentialModel.PASSWORD);
    }

    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
        if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;

        UserCredentialModel cred = (UserCredentialModel)input;
        String password = properties.getProperty(user.getUsername());
        if (password == null) return false;
        return password.equals(cred.getValue());
    }

isConfiguredFor() メソッドは、ランタイムによって呼び出されて、特定のクレデンシャル・タイプがユーザーのために設定されているかどうかを判断します。このメソッドは、パスワードがユーザー用に設定されていることを確認します。

supportsCredentialType() メソッドは、特定のクレデンシャル・タイプに対する検証がサポートされているかどうかを返します。クレデンシャル・タイプが password であるかどうかを確認します。

isValid() メソッドはパスワードの検証を担当します。 CredentialInput パラメーターは、すべてのクレデンシャル・タイプのための単なる抽象的なインターフェイスです。クレデンシャル・タイプをサポートしていること、およびそれが UserCredentialModel のインスタンスであることも確認します。ユーザーがログイン・ページからログインすると、入力されたパスワードのプレーン・テキストが UserCredentialModel のインスタンスに格納されます。 isValid() メソッドは、プロパティー・ファイルに格納されているプレーンテキストのパスワードに対してその値を確認します。 true の戻り値は、パスワードが有効であるということを意味します。

CredentialInputUpdaterの実装

以前説明したとおり、このサンプル内の CredentialInputUpdater インターフェイスを実装するのは、ユーザー・パスワードの変更を禁止するためだけです。これをしなければならない理由は、そうしないと、ランタイムによりKeycloakのローカル・ストレージ内でパスワードがオーバーライドできるようになってしまうからです。これについて、詳しくはこの章で後ほど説明します。

    @Override
    public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
        if (input.getType().equals(CredentialModel.PASSWORD)) throw new ReadOnlyException("user is read only for this update");

return false;
 }

    @Override
    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {

    }

    @Override
    public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
        return Collections.EMPTY_SET;
    }

updateCredential() メソッドは、クレデンシャル・タイプがパスワードであるかどうかを確認するだけです。そうであった場合、 ReadOnlyException がスローされます。

11.5.2. プロバイダー・ファクトリーの実装

プロバイダー・クラスに関しては完了したので、それでは次にプロバイダー・ファクトリー・クラスを見ていきましょう。

public class PropertyFileUserStorageProviderFactory
                 implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

    public static final String PROVIDER_NAME = "readonly-property-file";

    @Override
    public String getId() {
        return PROVIDER_NAME;
    }

まず最初に注意すべき点は、 UserStorageProviderFactory クラスを実装する際、テンプレート・パラメーターとして具体的なプロバイダー・クラス・実装を渡す必要があるということです。ここでは、以前に定義したプロバイダー・クラス PropertyFileUserStorageProvider を指定します。

テンプレート・パラメーターを指定しないと、プロバイダーは機能しません。ランタイムは、クラスのイントロスペクションが実行し、プロバイダーが実装する capability interfaces を定義します。

getId() メソッドは、レルム用のユーザー・ストレージ・プロバイダーを有効にする必要がある場合、ランタイム内のファクトリーを識別し、管理コンソールで表示される文字列にもなります。

初期化
    private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
    protected Properties properties = new Properties();

    @Override
    public void init(Config.Scope config) {
        InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties");

        if (is == null) {
            logger.warn("Could not find users.properties in classpath");
        } else {
            try {
                properties.load(is);
            } catch (IOException ex) {
                logger.error("Failed to load users.properties file", ex);
            }
        }
    }

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new PropertyFileUserStorageProvider(session, model, properties);
    }

UserStorageProviderFactory インターフェイスにはオプションで実装可能な init() メソッドがあります。Keycloakが起動すると、各プロバイダー・ファクトリーのインスタンスが1つだけ作成されます。また、起動時に、 init() メソッドがこれらのファクトリー・インスタンスでそれぞれ呼び出されます。また、実装可能な postInit() メソッドも同様にあります。ファクトリーの init() メソッドがそれぞれ呼び出された後、 postInit() メソッドが呼び出されます。

init() メソッドの実装では、クラスパスからのユーザー宣言を含むプロパティー・ファイルが見つかります。次に、そこに格納されているユーザー名とパスワードの組み合わせを、 properties フィールドをロードします。

Config.Scope パラメーターは、 standalone.xmlstandalone-ha.xml 、または domain.xml 内で設定することができるファクトリー設定です。 standalone.xmlstandalone-ha.xml 、または domain.xml ファイルがある場所について、詳しくはServer Installation and Configuration Guideを参照してください。

たとえば、以下を standalone.xml に追加します。

<spi name="storage">
    <provider name="readonly-property-file" enabled="true">
        <properties>
            <property name="path" value="/other-users.properties"/>
        </properties>
    </provider>
</spi>

これをハードコーディングせずに、ユーザー・プロパティー・ファイルのクラスパスを指定することができます。次に、以下のとおり PropertyFileUserStorageProviderFactory.init() で設定を取り込むことができます。

public void init(Config.Scope config) {
    String path = config.get("path");
    InputStream is = getClass().getClassLoader().getResourceAsStream(path);

    ...
}
メソッドの作成

プロバイダー・ファクトリーの作成における最後の手順は create() メソッドになります。

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        return new PropertyFileUserStorageProvider(session, model, properties);
    }

PropertyFileUserStorageProvider クラスを単にアロケートするだけです。このcreateメソッドはトランザクション毎に都度呼び出されます。

11.5.3. パッケージ化とデプロイ

プロバイダー実装のためのクラス・ファイルはjar内に置かれる必要があります。また、 META-INF/services/org.keycloak.storage.UserStorageProviderFactory ファイル内でプロバイダー・ファクトリー・クラスを宣言する必要もあります。

org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

一度jarを作成すれば、通常のWildFlyの方法を使って、それをデプロイすることができるようになります。その方法とは、jarを deploy/ ディレクトリーにコピーするか、JBoss CLIを使用することです。

11.5.4. 管理コンソール内でのプロバイダーの有効化

管理コンソールの User Federation ページ内で、レルム毎にユーザー・ストレージ・プロバイダーを有効にします。

User Federation

empty user federation page

readonly-property-file というリストから作成したプロバイダーを選択します。これで、プロバイダーの設定ページに移動します。何も設定する必要はないので Save をクリックします。

設定プロバイダー

storage provider created

メインの User Federation ページに戻ると、リストアップされたプロバイダーが表示されます。

User Federation

user federation page

これで、 users.properties ファイル内で宣言されたユーザーを使用してログインすることができます。このユーザーは、ログイン後、アカウントページを見ることしかできません。

11.6. 設定のテクニック

PropertyFileUserStorageProvider のサンプルには少し工夫された点があります。これはプロバイダーのjarに組み込まれたプロパティー・ファイルにハードコードされています。これはあまり役に立ちません。プロバイダーのインスタンス毎にこのファイルのロケーションを設定できるようにする必要があります。つまり、複数の異なるレルムで複数回にわたりこのプロバイダーを利用して、完全に異なるユーザー・プロパティー・ファイルを指し示す必要があるということです。また、この設定は管理コンソールのUI内でも実行する必要があります。

UserStorageProviderFactory には、プロバイダーの設定を処理する、実装可能な追加のメソッドがあります。プロバイダー毎に設定する必要がある変数を記述し、この設定を取り込むための一般的な入力ページを管理コンソールで自動的にレンダリングします。実装されると、それが保存される前、プロバイダーが初めて作成された時、および更新時に、callbackメソッドも設定を検証します。 UserStorageProviderFactoryorg.keycloak.component.ComponentFactory インターフェイスからこれらのメソッドを継承します。

    List<ProviderConfigProperty> getConfigProperties();

    default
    void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
            throws ComponentValidationException
    {

    }

    default
    void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {

    }

    default
    void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) {

    }

ComponentFactory.getConfigProperties() メソッドは、 org.keycloak.provider.ProviderConfigProperty インスタンスのリストを返します。これらのインスタンスは、プロバイダーの設定変数をそれぞれレンダリングして格納するのに必要なメタデータを宣言します。

11.6.1. 設定のサンプル

ディスク上の特定のファイルへプロバイダー・インスタンスを参照するように、 PropertyFileUserStorageProviderFactory のサンプルを拡張してみましょう。

PropertyFileUserStorageProviderFactory
public class PropertyFileUserStorageProviderFactory
                  implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

    protected static final List<ProviderConfigProperty> configMetadata;

    static {
        configMetadata = ProviderConfigurationBuilder.create()
                .property().name("path")
                .type(ProviderConfigProperty.STRING_TYPE)
                .label("Path")
                .defaultValue("${jboss.server.config.dir}/example-users.properties")
                .helpText("File path to properties file")
                .add().build();
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return configMetadata;
    }

ProviderConfigurationBuilder クラスは、設定プロパティーのリストを作成するのに非常に役立ちます。ここでは文字列の種類である path という名前の変数を指定します。このプロバイダーのための管理コンソール設定ページでは、この設定変数が Path としてラベル付けされ、デフォルト値は ${jboss.server.config.dir}/example-users.properties になります。この設定オプションのツールチップにマウスポインターを置くと、ヘルプテキスト File path to properties file が表示されます。

次にすべきことは、このファイルがディスク上に存在することの検証です。有効なユーザー・プロパティー・ファイルを指し示していない限り、レルム内でこのプロバイダーのインスタンスを有効にしません。これを行うには、 validateConfiguration() メソッドを実装します。

    @Override
    public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
                   throws ComponentValidationException {
        String fp = config.getConfig().getFirst("path");
        if (fp == null) throw new ComponentValidationException("user property file does not exist");
        fp = EnvUtil.replace(fp);
        File file = new File(fp);
        if (!file.exists()) {
            throw new ComponentValidationException("user property file does not exist");
        }
    }

validateConfiguration() メソッド内では、 ComponentModel から設定変数を取得し、そのファイルがディスク上に存在するかどうかを確認します。 org.keycloak.common.util.EnvUtil.replace() メソッドを使用していることに注意してください。このメソッドを使用すると、 ${} が入っている文字列によりシステム・プロパティー値でそれは置き換えられます。 ${jboss.server.config.dir} 文字列は、サーバーの configuration/ ディレクトリーに対応しており、このサンプルでは非常に便利です。

次にすべきことは、古い init() メソッドの削除です。これを行う理由は、ユーザー・プロパティー・ファイルがプロバイダー・インスタンス毎に独自のものになるからです。このロジックを create() メソッドへ移します。

    @Override
    public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        String path = model.getConfig().getFirst("path");

        Properties props = new Properties();
        try {
            InputStream is = new FileInputStream(path);
            props.load(is);
            is.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return new PropertyFileUserStorageProvider(session, model, props);
    }

このロジックは、すべてのトランザクションがディスクからユーザー・プロパティー・ファイル全体を読み込むので、もちろん非効率ですが、これは設定変数にフックする方法を簡単な方法で示しているはずです。

11.6.2. 管理コンソールでのプロバイダー設定

これで、設定ができるようになりました。管理コンソールでプロバイダーを設定すると、 path 変数を設定することができます。

設定プロバイダー

storage provider with config

11.7. ユーザーおよびクエリー機能の追加/削除インターフェイス

これまでの例で行っていないことの1つは、ユーザーの追加と削除やパスワードの変更を許可することです。これまでの例で定義されたユーザーは、管理コンソールで照会することも表示することもできません。これらの拡張機能を追加するには、サンプル・プロバイダーで UserQueryProviderUserRegistrationProvider インタフェースを実装する必要があります。

11.7.1. UserRegistrationProviderの実装

特定のストアにユーザーを追加または削除するために、プロパティー・ファイルをディスクに保存する必要があります。

PropertyFileUserStorageProvider
    public void save() {
        String path = model.getConfig().getFirst("path");
        path = EnvUtil.replace(path);
        try {
            FileOutputStream fos = new FileOutputStream(path);
            properties.store(fos, "");
            fos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

このとき、 addUser() メソッドと removeUser() メソッドの実装は単純になります。

PropertyFileUserStorageProvider
    public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";

    @Override
    public UserModel addUser(RealmModel realm, String username) {
        synchronized (properties) {
            properties.setProperty(username, UNSET_PASSWORD);
            save();
        }
        return createAdapter(realm, username);
    }

    @Override
    public boolean removeUser(RealmModel realm, UserModel user) {
        synchronized (properties) {
            if (properties.remove(user.getUsername()) == null) return false;
            save();
            return true;
        }
    }

ユーザーを追加するとき、プロパティー・マップのパスワード値を UNSET_PASSWORD に設定することに注意してください。これは、プロパティー値に対してnull値を持つことができないためです。これを反映するために、 CredentialInputValidator メソッドを変更する必要もあります。

プロバイダーが UserRegistrationProvider インターフェイスを実装している場合、 addUser() が呼び出されます。プロバイダーがユーザーを追加するための設定スイッチを持っている場合、このメソッドから null を返すとプロバイダーをスキップして、次のメソッドを呼び出します。

PropertyFileUserStorageProvider
    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
        if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;

        UserCredentialModel cred = (UserCredentialModel)input;
        String password = properties.getProperty(user.getUsername());
        if (password == null || UNSET_PASSWORD.equals(password)) return false;
        return password.equals(cred.getValue());
    }

プロパティー・ファイルを保存できるようになったので、パスワードの更新を許可するのも理にかなっています。

PropertyFileUserStorageProvider
    @Override
    public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
        if (!(input instanceof UserCredentialModel)) return false;
        if (!input.getType().equals(CredentialModel.PASSWORD)) return false;
        UserCredentialModel cred = (UserCredentialModel)input;
        synchronized (properties) {
            properties.setProperty(user.getUsername(), cred.getValue());
            save();
        }
        return true;
    }

また、パスワードを無効にすることもできます。

PropertyFileUserStorageProvider
    @Override
    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
        if (!credentialType.equals(CredentialModel.PASSWORD)) return;
        synchronized (properties) {
            properties.setProperty(user.getUsername(), UNSET_PASSWORD);
            save();
        }

    }

    private static final Set<String> disableableTypes = new HashSet<>();

    static {
        disableableTypes.add(CredentialModel.PASSWORD);
    }

    @Override
    public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {

        return disableableTypes;
    }

これらのメソッドを実装すると、管理コンソールでユーザーのパスワードを変更および無効にできるようになります。

11.7.2. UserQueryProviderの実装

UserQueryProvider を実装しなければ、管理コンソールはサンプル・プロバイダーによってロードされたユーザーを表示および管理できません。このインターフェイスの実装を見てみましょう。

PropertyFileUserStorageProvider
    @Override
    public int getUsersCount(RealmModel realm) {
        return properties.size();
    }

    @Override
    public List<UserModel> getUsers(RealmModel realm) {
        return getUsers(realm, 0, Integer.MAX_VALUE);
    }

    @Override
    public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
        List<UserModel> users = new LinkedList<>();
        int i = 0;
        for (Object obj : properties.keySet()) {
            if (i++ < firstResult) continue;
            String username = (String)obj;
            UserModel user = getUserByUsername(username, realm);
            users.add(user);
            if (users.size() >= maxResults) break;
        }
        return users;
    }

getUser() メソッドはプロパティー・ファイルのキーセットを反復し、 getUserByUsername() に委譲してユーザーをロードします。 firstResultmaxResults パラメーターに基づいてこの呼び出しにインデックスを付けることに注目してください。外部ストアがページネーションをサポートしていない場合は、同様のロジックを実行する必要があります。

PropertyFileUserStorageProvider
    @Override
    public List<UserModel> searchForUser(String search, RealmModel realm) {
        return searchForUser(search, realm, 0, Integer.MAX_VALUE);
    }

    @Override
    public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
        List<UserModel> users = new LinkedList<>();
        int i = 0;
        for (Object obj : properties.keySet()) {
            String username = (String)obj;
            if (!username.contains(search)) continue;
            if (i++ < firstResult) continue;
            UserModel user = getUserByUsername(username, realm);
            users.add(user);
            if (users.size() >= maxResults) break;
        }
        return users;
    }

searchForUser() の最初の宣言は文字列パラメーターを受け取ります。これは、ユーザー名と電子メールの属性を検索してユーザーを見つけるために使用する文字列です。この文字列は部分文字列にすることができるので、検索を行うときに String.contains() メソッドを使用しています。

PropertyFileUserStorageProvider
    @Override
    public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
        return searchForUser(params, realm, 0, Integer.MAX_VALUE);
    }

    @Override
    public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
        // only support searching by username
        String usernameSearchString = params.get("username");
        if (usernameSearchString == null) return Collections.EMPTY_LIST;
        return searchForUser(usernameSearchString, realm, firstResult, maxResults);
    }

Map パラメーターを受け取る searchForUser() メソッドは、姓、名、電子メールに基づいてユーザーを検索できます。ユーザー名のみを保存するので、ユーザー名に基づいて検索します。このため、searchForUser() に処理を委譲しています。

PropertyFileUserStorageProvider
    @Override
    public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
        return Collections.EMPTY_LIST;
    }

    @Override
    public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
        return Collections.EMPTY_LIST;
    }

    @Override
    public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
        return Collections.EMPTY_LIST;
    }

グループや属性は保存しないので、他のメソッドは空のリストを返します。

11.8. 外部ストレージの拡張

PropertyProfileUserStorageProvider の例は実際には限定されています。プロパティー・ファイルに保存されているユーザーでログインすることはできますが、他のことはあまりできません。このプロバイダーによってロードされたユーザーが、特定のアプリケーションに完全にアクセスするための特殊なロールまたはグループ・マッピングを必要とする場合、これらのユーザーにロールマッピングを追加する方法はありません。また、電子メール、姓名などの重要な属性を変更または追加することもできません。

このような状況の場合、KeycloakはKeycloakのデータベースに余分な情報を保存することで、外部ストアを拡張することができます。これはフェデレーテッド・ユーザー・ストレージと呼ばれ、 org.keycloak.storage.federated.UserFederatedStorageProvider クラス内にカプセル化されています。

UserFederatedStorageProvider
package org.keycloak.storage.federated;

public interface UserFederatedStorageProvider extends Provider {

    Set<GroupModel> getGroups(RealmModel realm, String userId);
    void joinGroup(RealmModel realm, String userId, GroupModel group);
    void leaveGroup(RealmModel realm, String userId, GroupModel group);
    List<String> getMembership(RealmModel realm, GroupModel group, int firstResult, int max);

...

UserFederatedStorageProvider インスタンスは、 KeycloakSession.userFederatedStorage() メソッドで利用できます。これには、属性、グループとロールマッピング、異なるクレデンシャル・タイプ、および必要なアクションを保存するためのすべての種類のメソッドがあります。外部ストアのデータモデルが完全なKeycloak機能セットをサポートできない場合、このサービスはそのギャップを埋めることができます。

Keycloakには、ユーザー名の取得/設定を除く全ての UserModel メソッドをユーザー・フェデレーテッド・ストレージに委譲するヘルパークラス org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage が付属しています。外部ストレージ表現に委譲するためにオーバーライドする必要があるメソッドをオーバーライドします。このクラスのjavadocを読むことを強くお勧めします。これは、オーバーライドしてもよい、より小さなprotectedのメソッドを持っているからです。特に、グループ・メンバーシップとロールマッピングを含みます。

11.8.1. 拡張の例

PropertyFileUserStorageProvider の例では、 AbstractUserAdapterFederatedStorage を使うためにプロバイダーに簡単な変更が必要です。

PropertyFileUserStorageProvider
    protected UserModel createAdapter(RealmModel realm, String username) {
        return new AbstractUserAdapterFederatedStorage(session, realm, model) {
            @Override
            public String getUsername() {
                return username;
            }

            @Override
            public void setUsername(String username) {
                String pw = (String)properties.remove(username);
                if (pw != null) {
                    properties.put(username, pw);
                    save();
                }
            }
        };
    }

代わりに AbstractUserAdapterFederatedStorage の匿名クラスの実装を定義します。 setUsername() メソッドはプロパティー・ファイルを変更して保存します。

11.9. インポート実装ストラテジー

ユーザー・ストレージ・プロバイダーを実装する場合、選択可能な別のストラテジーがもう1つあります。ユーザー・フェデレーティッド・ストレージを使用せずに、Keycloakの組み込みユーザー・データベース内でユーザーをローカルで作成し、外部のストアからこのローカルコピーに属性をコピーすることができます。このアプローチにはたくさんの利点があります。

  • Keycloakは、基本的に外部ユーザーストア用の永続的なユーザー・キャッシュになります。ユーザーがインポートされると、外部ストアにヒットすることはなくなり、その負荷が取り除かれます。

  • 公式のユーザーストアとしてKeycloakに移行し、従来の外部ストアは廃止する場合、アプリケーションを徐々に移行してKeycloakを使用することができます。すべてのアプリケーションが移行されると、インポートされたユーザーのリンクを解除し、従来のレガシー外部ストアを廃止します。

インポート・ストラテジーの使用には明らかな欠点がいくつかあります。

  • 初回のユーザーの検索では、Keycloakのデータベースを複数回更新する必要があります。これは大きなパフォーマンス低下を招き、Keycloakのデータベースに多くの負担をかけることになります。ユーザー・フェデレーティッド・ストレージのアプローチでは、必要に応じて追加のデータのみが保管され、外部ストアの機能に応じて使用されることはありません。

  • インポートのアプローチでは、ローカルのkeycloakストレージと外部ストレージを同期させておく必要があります。ユーザー・ストレージSPIには、同期をサポートできるために実装できるケーパビリティー・インターフェイスがありますが、これはすぐにやっかいで面倒なものになります。

インポート・ストラテジーを実装するには、ユーザーがローカルでインポートされたかどうかを最初だけ確認します。そうだった場合は、ローカルユーザーを返します。そうでなかった場合は、ユーザーをローカルで作成して外部ストアからデータをインポートします。また、ローカルユーザーをプロキシーして、ほとんどの変更を自動的に同期させます。

これは多少工夫を加えることになりますが、 PropertyFileUserStorageProvider を拡張してこのアプローチをとることができます。まずは createAdapter() メソッドを変更することからはじめます。

PropertyFileUserStorageProvider
    protected UserModel createAdapter(RealmModel realm, String username) {
        UserModel local = session.userLocalStorage().getUserByUsername(username, realm);
        if (local == null) {
            local = session.userLocalStorage().addUser(realm, username);
            local.setFederationLink(model.getId());
        }
        return new UserModelDelegate(local) {
            @Override
            public void setUsername(String username) {
                String pw = (String)properties.remove(username);
                if (pw != null) {
                    properties.put(username, pw);
                    save();
                }
                super.setUsername(username);
            }
        };
    }

このメソッドでは KeycloakSession.userLocalStorage() メソッドを呼び出し、ローカルのKeycloakユーザー・ストレージの参照を取得します。ユーザーがローカルで保存されたかどうかを確認し、そうでなかった場合はローカルにそれを追加します。ローカルユーザーの id を設定せずに、Keycloakが id を自動生成するようにしてください。また、 UserModel.setFederationLink() を呼び出してプロバイダーの ComponentModel のIDに渡すことにも注意してください。これにより、プロバイダーとインポートされたユーザーの間でリンクが設定されます。

ユーザー・ストレージ・プロバイダーが削除されると、それによりインポートされたユーザーもすべて削除されます。これが UserModel.setFederationLink() を呼び出す目的の1つです。

また、ローカルユーザーがリンクされている場合、ストレージ・プロバイダーは CredentialInputValidator インタフェースと CredentialInputUpdater インタフェースから実装されたメソッドに対して委譲されるという点にも注意してください。検証または更新から false が返ると、Keycloakでは、ローカルストレージを使用して検証または更新できるかどうかを確認します。

また、 org.keycloak.models.utils.UserModelDelegate クラスを使用するローカルユーザーをプロキシーしている点にも注意してください。このクラスは UserModel の実装です。すべてのメソッドは、インスタンス化された UserModel に委譲します。この委譲クラスの setUsername() メソッドをオーバーライドし、自動的にプロパティー・ファイルと同期させます。プロバイダーの場合は、これを使用して、ローカルの UserModel 上の他のメソッドを intercept して、外部ストアと同期させることができます。たとえば、getメソッドは、ローカルストアが同期していることを確認することができます。setメソッドは、外部ストアをローカルストアと同期し続けることができます。注目すべきは、 getId() メソッドは、ユーザーをローカルで作成したときに自動的に生成されたIDを常に返すべきだということです。 他の非インポートの例に示すように、フェデレーションIDを返すべきではありません。

プロバイダーが UserRegistrationProvider インターフェイスを実装している場合、 removeUser() メソッドはローカル・ストレージからユーザーを削除する必要はありません。ランタイムはこの操作を自動的に実行します。また、 removeUser() は、ローカル・ストレージから削除される前に呼び出される点に注意してください。

11.9.1. ImportedUserValidationインターフェイス

この章の前半で、ユーザーのためのクエリーの機能について説明しました。ローカル・ストレージが最初に照会され、ユーザーがそこで見つかった場合、クエリーは終了します。これは、ユーザー名を同期させるためにローカルの UserModel をプロキシする必要があるので、上記の実装では問題になります。ユーザー・ストレージSPIでは、リンクされたローカルユーザーがローカル・データベースからロードされるたびに、コールバックがあります。

package org.keycloak.storage.user;
public interface ImportedUserValidation {
    /**
     * If this method returns null, then the user in local storage will be removed
     *
     * @param realm
     * @param user
     * @return null if user no longer valid
     */
    UserModel validate(RealmModel realm, UserModel user);
}

リンクされたローカルユーザーがロードされるたびに、ユーザー・ストレージ・プロバイダー・クラスがこのインターフェイスを実装すると、 validate() メソッドが呼び出されます。ここでは、パラメーターとして渡されたローカルユーザーをプロキシして返すことができます。それにより、新しい UserModel が使用されます。また、ユーザーが外部ストアにまだ存在するかどうかを任意で確認することもできます。 validate()null を返すと、ローカルユーザーはデータベースから削除されます。

11.9.2. ImportSynchronizationインターフェイス

インポート・ストラテジーを使用すると、ローカルユーザーのコピーが外部ストレージと同期しない可能性があるということがわかります。たとえば、ユーザーが外部ストアから削除されている可能性があります。ユーザー・ストレージSPIには、 これに対処するために実装可能な追加のインターフェイス、 org.keycloak.storage.user.ImportSynchronization があります。

package org.keycloak.storage.user;

public interface ImportSynchronization {
    SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
    SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
}

このインターフェイスは、プロバイダー・ファクトリーによって実装されます。このインターフェイスがプロバイダー・ファクトリーによって実装されると、プロバイダーのための管理コンソール・マネージメントページに追加のオプションが表示されます。ボタンをクリックして、手動で同期させることができます。こうして、 ImportSynchronization.sync() メソッドが呼び出されます。また、追加の設定オプションが表示され、自動的に同期実行が予定されます。自動同期によって、 syncSince() メソッドが呼び出されます。

11.10. ユーザー・キャッシュ

ID、ユーザー名、または電子メールのクエリーによってユーザーが読み込まれると、キャッシュされます。ユーザーがキャッシュされると、それは UserModel インタフェース全体を反復し、この情報をローカルのメモリー内専用キャッシュに保持します。 クラスターでは、このキャッシュはまだローカルですが、無効化キャッシュになります。ユーザーが変更されると、キャッシュは削除されます。このエビクション・イベントはクラスター全体に伝播され、他のノードのユーザー・キャッシュも無効になります。

11.10.1. ユーザー・キャッシュの管理

KeycloakSession.userCache() を呼び出すことで、ユーザー・キャッシュにアクセスできます。

/**
 * All these methods effect an entire cluster of Keycloak instances.
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public interface UserCache extends UserProvider {
    /**
     * Evict user from cache.
     *
     * @param user
     */
    void evict(RealmModel realm, UserModel user);

    /**
     * Evict users of a specific realm
     *
     * @param realm
     */
    void evict(RealmModel realm);

    /**
     * Clear cache entirely.
     *
     */
    void clear();
}

特定のユーザー、レルムに含まれるユーザー、またはキャッシュ全体を削除するメソッドがあります。

11.10.2. OnUserCacheコールバック・インターフェイス

プロバイダーの実装に固有の追加情報をキャッシュすることができます。User Storage SPIには、ユーザーがキャッシュされるたびにコールバックがあります( org.keycloak.models.cache.OnUserCache )。

public interface OnUserCache {
    void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
}

このコールバックが必要な場合、プロバイダー・クラスはこのインターフェイスを実装する必要があります。 UserModel デリゲート・パラメーターはプロバイダーから返された UserModel インスタンスです。 CachedUserModel は拡張された UserModel インターフェイスです。これは、ローカル・ストレージにローカルでキャッシュされるインスタンスです。

public interface CachedUserModel extends UserModel {

    /**
     * Invalidates the cache for this user and returns a delegate that represents the actual data provider
     *
     * @return
     */
    UserModel getDelegateForUpdate();

    boolean isMarkedForEviction();

    /**
     * Invalidate the cache for this model
     *
     */
    void invalidate();

    /**
     * When was the model was loaded from database.
     *
     * @return
     */
    long getCacheTimestamp();

    /**
     * Returns a map that contains custom things that are cached along with this model.  You can write to this map.
     *
     * @return
     */
    ConcurrentHashMap getCachedWith();
}

この CachedUserModel インターフェイスは、キャッシュからユーザーを削除し、プロバイダーの UserModel インスタンスを取得することを可能にします。 getCachedWith() メソッドは、ユーザーに関する追加情報をキャッシュするためのマップを返します。たとえば、クレデンシャルは UserModel インターフェイスの一部ではありません。メモリーにクレデンシャルをキャッシュしたい場合は、 OnUserCache を実装し、 getCachedWith() メソッドを使ってユーザーのクレデンシャルをキャッシュします。

11.10.3. キャッシュ・ポリシー

管理コンソールのユーザー・ストレージ・プロバイダーの管理ページで、一意なキャッシュ・ポリシーを指定できます。

11.11. Java EEの活用

プロバイダーを指し示す、 META-INF/services ファイルを正しく設定すると、ユーザー・ストレージ・プロバイダーは任意のJava EEコンポーネント内にパッケージ化することができます。たとえば、プロバイダーがサードパーティー・ライブラリーを使用する必要がある場合は、EAR内でプロバイダーをパッケージ化し、これらのサードパーティー・ライブラリーをEARの lib/ ディレクトリーに格納することができます。また、プロバイダーJARは、EJB、WAR、およびEARがWildFly環境で使用できる jboss-deployment-structure.xml ファイルを利用できます。このファイルの詳細については、WildFlyのドキュメントを参照してください。他のきめ細かなアクション間の外部依存関係を引き出すことができます。

UserStorageProviderFactory の実装はプレーンなJavaオブジェクトである必要があります。しかし、現在、 UserStorageProvider クラスをステートフルEJBとして実装することもサポートしています。これは、JPAを使用してリレーショナル・ストアに接続する場合に特に便利です。それを行う方法は以下のとおりです。

@Stateful
@Local(EjbExampleUserStorageProvider.class)
public class EjbExampleUserStorageProvider implements UserStorageProvider,
        UserLookupProvider,
        UserRegistrationProvider,
        UserQueryProvider,
        CredentialInputUpdater,
        CredentialInputValidator,
        OnUserCache
{
    @PersistenceContext
    protected EntityManager em;

    protected ComponentModel model;
    protected KeycloakSession session;

    public void setModel(ComponentModel model) {
        this.model = model;
    }

    public void setSession(KeycloakSession session) {
        this.session = session;
    }


    @Remove
    @Override
    public void close() {
    }
...
}

@Local アノテーションを定義して、そこにプロバイダー・クラスを指定する必要があります。これをしないと、EJBはユーザーを正しくプロキシしないので、プロバイダーは動作しません。

@Remove アノテーションは、プロバイダーの close() メソッドに付与する必要があります。そうしないと、ステートフルBeanはクリーンアップされず、最終的にエラーメッセージが表示されることがあります。

UserStorageProviderFactory の実装はプレーンなJavaオブジェクトである必要があります。ファクトリー・クラスは、create()メソッドでステートフルEJBのJNDIルックアップを実行します。

public class EjbExampleUserStorageProviderFactory
        implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {

    @Override
    public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
        try {
            InitialContext ctx = new InitialContext();
            EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup(
                     "java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName());
            provider.setModel(model);
            provider.setSession(session);
            return provider;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

また、この例では、プロバイダーと同じJAR内にJPAデプロイメントを定義していることを前提としています。これはJPAの @Entity クラスだけでなく persistence.xml ファイルも意味します。

JPAを使用する場合、追加のデータソースはXAデータソースでなければなりません。KeycloakデータソースはXAデータソースではありません。同じトランザクションで2つ以上の非XAデータソースと対話する場合、サーバーはエラーメッセージを返します。単一のトランザクションでは、非XAリソースは1つだけ許可されます。

XAデータソースの配備の詳細については、WildFlyのマニュアルを参照してください。

CDIはサポートされていません。

11.12. REST管理API

管理者のREST APIを使用して、ユーザー・ストレージ・プロバイダーの配備を作成、削除、更新できます。ユーザー・ストレージSPIは汎用コンポーネント・インターフェイスの上に構築されているため、汎用APIを使用してプロバイダーを管理します。

RESTコンポーネントAPIは、レルム管理リソースの下にあります。

/admin/realms/{realm-name}/components

このREST APIのJavaクライアントとの対話のみを示します。うまくいけば、このAPIから curl を使ってこれを行う方法を抽出できます。

public interface ComponentsResource {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<ComponentRepresentation> query();

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<ComponentRepresentation> query(@QueryParam("parent") String parent);

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<ComponentRepresentation> query(@QueryParam("parent") String parent, @QueryParam("type") String type);

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<ComponentRepresentation> query(@QueryParam("parent") String parent,
                                               @QueryParam("type") String type,
                                               @QueryParam("name") String name);

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    Response add(ComponentRepresentation rep);

    @Path("{id}")
    ComponentResource component(@PathParam("id") String id);
}

public interface ComponentResource {
    @GET
    public ComponentRepresentation toRepresentation();

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    public void update(ComponentRepresentation rep);

    @DELETE
    public void remove();
}

ユーザー・ストレージ・プロバイダーを作成するには、プロバイダID、文字列 org.keycloak.storage.UserStorageProvider のプロバイダー・タイプ、および設定を指定する必要があります。

import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RealmRepresentation;
...

Keycloak keycloak = Keycloak.getInstance(
    "http://localhost:8080/auth",
    "master",
    "admin",
    "password",
    "admin-cli");
RealmResource realmResource = keycloak.realm("master");
RealmRepresentation realm = realmResource.toRepresentation();

ComponentRepresentation component = new ComponentRepresentation();
component.setName("home");
component.setProviderId("readonly-property-file");
component.setProviderType("org.keycloak.storage.UserStorageProvider");
component.setParentId(realm.getId());
component.setConfig(new MultivaluedHashMap());
component.getConfig().putSingle("path", "~/users.properties");

realmResource.components().add(component);

// retrieve a component

List<ComponentRepresentation> components = realmResource.components().query(realm.getId(),
                                                                    "org.keycloak.storage.UserStorageProvider",
                                                                    "home");
component = components.get(0);

// Update a component

component.getConfig().putSingle("path", "~/my-users.properties");
realmResource.components().component(component.getId()).update(component);

// Remove a component

realmREsource.components().component(component.getId()).remove();

11.13. 以前のユーザー・フェデレーションSPIからの移行

この章は、以前の(および現在は削除された)ユーザー・フェデレーションSPIを使用してプロバイダーを実装した場合にのみ適用されます。

Keycloakバージョン2.4.0以前では、 ユーザー・フェデレーション SPIがありました。Red Hat Single Sign-Onバージョン7.0は、サポートされていませんが、以前のSPIも利用可能でした。 この以前のユーザー・フェデレーションSPIは、Keycloakバージョン2.5.0およびRed Hat Single Sign-Onバージョン7.1から削除されました。ただし、このSPIを使用してプロバイダーを作成した場合、この章では、SPIを移植するためのいくつかの方法について説明します。

11.13.1. インポート vs. 非インポート

以前のユーザー・フェデレーションSPIでは、Keycloakのデータベース内でユーザーのローカルコピーを作成し、外部ストアからの情報をローカルコピーにインポートする必要がありました。しかし、これはもう要件ではなくなりました。以前のプロバイダーをそのまま移植することもできますが、非インポート・ストラテジーの方がより良いアプローチになるかを検討する必要があります。

インポート方法の利点。

  • Keycloakは、基本的に外部ストアの永続ユーザーキャッシュになります。 ユーザーがインポートされると、外部ストアにアクセスしなくなり、その負荷を取り除きます。

  • Keycloakに移行して公式ユーザーストアとし、以前の外部ストアを廃止する場合、アプリケーションを徐々に移行してKeycloakを使用することができます。すべてのアプリケーションが移行されると、インポートされたユーザーのリンクを解除し、以前のレガシー外部ストアを廃止します。

インポート・ストラテジーの使用には明らかな欠点がいくつかあります。

  • 初めてユーザーを検索するには、Keycloakデータベースを複数回更新する必要があります。これは、負荷がかかった状態でパフォーマンスが大きく低下する可能性があり、Keycloakデータベースに多くの負担をかけることになります。ユーザー・フェデレーティッド・ストレージ・アプローチでは、必要以上のデータを保持し、外部ストアの機能に応じて使用されるということがありません。

  • インポートのアプローチでは、ローカルのkeycloakストレージと外部ストレージを同期させておく必要があります。ユーザー・ストレージSPIには、同期をサポートできるために実装できるケーパビリティー・インターフェイスがありますが、これはすぐにやっかいで面倒なものになります。

11.13.2. UserFederationProvider vs. UserStorageProvider

最初に気付くべきは、 UserFederationProvider が完成されたインターフェイスであったことです。このインターフェイスにはすべてのメソッドが実装されました。ただし、 UserStorageProvider では、代わりにこのインターフェイスを、必要に応じて実装可能な複数のケーパビリティー・インターフェイスに分割しています。

UserFederationProvider.getUserByUsername()getUserByEmail() は、新しいSPIに完全に同等のものを持ちます。この両者の違いはインポート方法にあります。インポート・ストラテジーを引き続き使用している場合、ユーザーをローカルで作成するために KeycloakSession.userStorage().addUser() を呼び出す必要はありません。その代わりに、 KeycloakSession.userLocalStorage().addUser() を呼び出します。 userStorage() メソッドは存在しません。

UserFederationProvider.validateAndProxy() メソッドは、オプションのケーパビリティー・インターフェイス、 ImportedUserValidation に移動されました。以前のプロバイダーをそのまま移植する場合は、このインターフェイスを実装します。また、以前のSPIでは、ローカルユーザーがキャッシュ内にあっても、ユーザーがアクセスされるたびにこのメソッドが呼び出されました。新しいSPIでは、このメソッドはローカルユーザーがローカル・ストレージからロードされたときにのみ呼び出されます。ローカルユーザーがキャッシュされている場合、 ImportedUserValidation.validate() メソッドはまったく呼び出されません。

UserFederationProvider.isValid() メソッドは、新しいモデルには存在しません。

UserFederationProvider のメソッド synchronizeRegistrations()registerUser()removeUser()UserRegistrationProvider のケーパビリティー・インターフェイスに移動されました。この新しいインターフェイスは実装するためにオプションです。プロバイダーがユーザーの作成と削除をサポートしていない場合は、実装する必要はありません。以前のプロバイダーが新しいユーザーの登録をサポートするように切り替えた場合、新しいSPIではこれがサポートされ、プロバイダーがユーザーの追加をサポートしていない場合は、 UserRegistrationProvider.addUser() から null が返されます。

クレデンシャルを中心とした以前の UserFederationProvider メソッドは CredentialInputValidatorCredentialInputUpdater インターフェイスにカプセル化されました。この実装は任意で、クレデンシャルの検証や更新をサポートするかどうかによって異なります。クレデンシャル管理は UserModel メソッドに存在しましたが、これらも CredentialInputValidatorCredentialInputUpdater インターフェイスに移行されました。 CredentialInputUpdater インターフェイスを実装しないと、プロバイダーが提供するクレデンシャルはKeycloakストレージ内でローカルにオーバーライドされる可能性があることに注意してください。したがって、クレデンシャルを読み取り専用にする場合は、 CredentialInputUpdater.updateCredential() メソッドを実装し、 ReadOnlyException を返します。

searchByAttributes()getGroupMembers() のような UserFederationProvider クエリーメソッドは、オプションのインターフェイス UserQueryProvider にカプセル化されました。このインターフェイスを実装しないと、管理コンソールでユーザーを表示できなくなります。なお、ログインすることはできます。

11.13.3. UserFederationProviderFactory vs. UserStorageProviderFactory

以前のSPIの同期メソッドは、現在、オプションの ImportSynchronization インターフェイスにカプセル化されています。同期ロジックを実装している場合は、新しい UserStorageProviderFactoryImportSynchronization インターフェイスを実装してください。

11.13.4. 新しいモデルへのアップグレード

ユーザーストレージSPIインスタンスは、異なる一連のリレーショナル・テーブルに格納されます。 Keycloakは自動的に移行スクリプトを実行します。レルムに対して以前のユーザー・フェデレーション・プロバイダーがデプロイされている場合、データの ID を含め、それ以降のストレージモデルにそのまま変換されます。この移行は、以前のユーザー・フェデレーション・プロバイダーと同じプロバイダーID("ldap"、"kerberos"など)を持つユーザー・ストレージ・プロバイダーが存在する場合にのみ発生します。

これを知ることにより、取ることができるさまざまなアプローチがあります。

  1. 以前のKeycloak配備で以前のプロバイダーを削除することができます。インポートされたユーザーのローカルリンクされたコピーがすべて削除されます。次に、Keycloakをアップグレードするときに、レルム用に新しいプロバイダーをデプロイして設定するだけです。

  2. 2つ目のオプションは、プロバイダーIDが同じであることを UserStorageProviderFactory.getId() で確認する新しいプロバイダーを実装することです。このプロバイダーが新しいKeycloakインストールの deploy/ ディレクトリーにあることを確認してください。サーバーを起動し、組み込みの移行スクリプトで以前のデータモデルを新しいデータモデルに変換します。この場合、以前にリンクされたインポート済みユーザーは、正常に動作し、すべて同じになります。

インポート・ストラテジーを廃止してユーザー・ストレージ・プロバイダーを実装し直すことに決めた場合は、Keycloakをアップグレードする前に以前のプロバイダーを削除することをお勧めします。これにより、インポートされたすべてのユーザーのリンクされたローカル・インポート済みのコピーが削除されます。