はじめに
いくつかの例にあるように、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
管理REST API
Keycloakには、管理コンソールが提供するすべての機能を完全に備えた管理REST APIが付属しています。
APIを呼び出すには、適切な権限を持つアクセストークンを取得する必要があります。必要な権限については Server Administration Guide を参照してください。
トークンは、Keycloakを使用したアプリケーションへの認証を有効にすることで取得できます。 Securing Applications and Services Guide を参照してください。また、ダイレクト・アクセス・グラントを使用しても、アクセストークンを取得できます。
完全なドキュメントについては、 API Documentation を参照してください。
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"
サービス・アカウントで認証する
client_id
と client_secret
を使用して管理REST APIに対して認証できるようにする前に、クライアントが次のように設定されていることを確認する必要があります。
-
client_id
は、レルム master に属する confidential クライアントであること -
client_id
は、Service Accounts Enabled
オプションが有効になっていること -
client_id
は、カスタム "Audience" マッパーが設定されていること-
含まれるClient Audience:
security-admin-console
-
最後に、 client_id
にService Account Rolesタブで 'admin' のロールが割り当てられていることを確認します。
その後、 client_id
と client_secret
を使用して、以下のように管理REST APIのアクセストークンを取得できます。
curl \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "client_secret=<YOUR_CLIENT_SECRET>" \
-d "grant_type=client_credentials" \
"http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
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 を参照してください。
テーマ
Keycloakでは、webページと電子メール用のテーマがサポートされます。これによって、エンドユーザーが見るページのルック・アンド・フィールをカスタマイズすることができ、そのページをアプリケーションに統合することができます。
テーマの種類
テーマには1つ以上の種類が用意されており、Keycloakのさまざまな部分をカスタマイズすることができます。用意されている種類は以下のとおりです。
-
Account - アカウント管理
-
Admin - 管理コンソール
-
Email - 電子メール
-
Login - ログイン画面
-
Welcome - ウェルカムページ
テーマの設定
ウェルカムページを除く、すべてのテーマの種類は Admin Console
を通じて設定されます。レルム用に使用されるテーマを変更するには Admin Console
を開き、画面左上端にあるドロップダウン・ボックスからレルムを選択します。 Realm Settings
の下で Themes
をクリックします。
master の管理コンソール用にテーマをセットするには、 master レルム用の管理コンソール・テーマを設定する必要があります。管理コンソールへの変更を確認するには、ページをリフレッシュしてください。
|
ウェルカムテーマを変更するには、 standalone.xml
、 standalone-ha.xml
、または domain.xml
を編集する必要があります。
welcomeTheme
をテーマ要素に追加します。サンプルは以下のとおりになります。
<theme>
...
<welcomeTheme>custom-theme</welcomeTheme>
...
</theme>
サーバーが実行されている場合、サーバーを再起動してウェルカムページのテーマ変更を有効にする必要があります。
デフォルトのテーマ
Keycloakには、サーバーのルート themes
ディレクトリー内のデフォルト・テーマがバンドルされています。アップグレードを簡単にするために、バンドルされたテーマを直接編集しないでください。代わりに、バンドルされたテーマの1つを拡張する独自のテーマを作成してください。
テーマの作成
テーマは、以下の項目で構成されています。
-
HTML templates (Freemarker Templates)
-
画像
-
メッセージ・バンドル
-
スタイルシート
-
スクリプト
-
テーマ用のpropertiesファイル
ページをすべて置き換えるわけではない場合は、いずれかのテーマを1つ拡張する方法をお勧めします。通常の場合Keycloakのテーマを拡張したいでしょうが、ページのルック・アンド・フィールを大幅に変更する場合は基本テーマを拡張するということも検討対象となります。基本テーマは主にHTMLのテンプレートとメッセージ・バンドルで構成され、 Keycloakテーマには主にイメージとスタイルシートが含まれます。
テーマを拡張する場合、個々のリソース(テンプレート、スタイルシートなど)を上書きできます。HTMLのテンプレートを上書きする場合、新規リリースへのアップグレードの際に、カズタマイズしたテンプレートをアップグレードする必要があることに留意してください。
テーマを作成する際は、キャッシュを無効にすることをお勧めします。なぜなら、無効にしたことにより、Keycloakを再起動せずに、 themes
ディレクトリーからテーマ・リソースを直接編集することができるからです。これを実行するには standalone.xml
を編集します。以下のとおり、 theme
用に staticMaxAge
を -1
に設定し、 cacheTemplates
と cacheThemes
の両方は 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
を設定してログイン・ページをリフレッシュします。
テーマのプロパティー
テーマのプロパティーは、テーマ・ディレクトリー内にある <THEME TYPE>/theme.properties
ファイルで設定されています。
-
parent - 拡張が可能な親テーマ
-
import - 他テーマからリソースをインポートすること
-
styles - スペースで区切られた、インクルードするスタイルのリスト
-
locales - カンマで区切られた、サポートされるロケールのリスト
特定の要素タイプのために使用される、cssクラスの変更用として使用可能なプロパティーのリストがあります。これらのプロパティーのリストについては、keycloakテーマ( themes/keycloak/<THEME TYPE>/theme.properties
)に対応するタイプのtheme.propertiesファイルを参照してください。
カスタム・プロパティーを追加し、それらをカスタム・テンプレートから使用することができます。
その際、次の形式を使用してシステム・プロパティーまたは環境変数を置き換えることができます。
-
${some.system.property}
- システム・プロパティー用 -
${env.ENV_VAR}
- 環境変数用
システム・プロパティーまたは環境変数が ${foo:defaultValue}
で見つからない場合、デフォルト値を提供することもできます。
デフォルト値が提供されておらず、対応するシステム・プロパティーまたは環境変数がない場合は、何も置き換えられず、テンプレートの形式になります。 |
可能な例を次に示します。
javaVersion=${java.version}
unixHome=${env.HOME:Unix home not found}
windowsHome=${env.HOMEPATH:Windows home not found}
スタイルシート
テーマには、1つ以上のスタイルシートを持たせることができます。スタイルシートを追加するには、テーマの <THEME TYPE>/resources/css
ディレクトリー内でファイルを作成します。それから、 theme.properties
内の styles
プロパティーへ、そのファイルを追加します。
たとえば、 mytheme
へ styles.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=web_modules/@fontawesome/fontawesome-free/css/icons/all.css web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css css/login.css css/styles.css
親のスタイルシートからのスタイルを上書きするには、スタイルシートがリストの最後にあることが重要です。 |
スクリプト
テーマには、1つ以上のスクリプトを持たせることができます。スクリプトを追加するには、テーマの <THEME TYPE>/resources/js
ディレクトリー内でファイルを作成します。それから、 theme.properties
内の scripts
プロパティーへ、そのファイルを追加します。
たとえば、 mytheme
へ script.js
を追加するには、以下の内容で themes/mytheme/login/resources/js/script.js
を作成します。
alert('Hello');
次に、 themes/mytheme/login/theme.properties
を編集し、以下を追加します。
scripts=js/script.js
画像
テーマで画像を使用できるようにするには、テーマの <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">
メッセージ
テンプレート内のテキストは、メッセージ・バンドルからロードされます。他のテーマを拡張するテーマは、親メッセージ・バンドルからすべてのメッセージを引き継ぎ、テーマに <THEME TYPE>/messages/messages_en.properties
を追加することで個々のメッセージを上書きすることができます。
たとえば、 mytheme
でログイン画面上の Username
を Your Username
に置き換えるには、以下の内容で themes/mytheme/login/messages/messages_en.properties
ファイルを作成します。
usernameOrEmail=Your Username
メッセージ内では、メッセージが使用される際、 {0}
および {1}
のような値が引数に置き換えられます。たとえば、 Log in to {0}
内の {0} がレルム名に置き換えられます。
これらのメッセージ・バンドルのテキストは、レルム固有の値で上書きできます。レルム固有の値は、UIおよびAPIを介して管理できます。
国際化
Keycloakでは、国際化がサポートされています。レルム用に国際化を有効にするには、 Server Administration Guide を参照してください。このセクションでは、言語を追加する方法について説明します。
新しく言語を追加するには、テーマのディレクトリー内で <THEME TYPE>/messages/messages_<LOCALE>.properties
ファイルを作成します。次に、そのファイルを <THEME TYPE>/theme.properties
内の locales
プロパティーへ追加します。ユーザーが言語を使用できるようにするには、レルムの login
、 account
および email
のテーマによってその言語がサポートされなければなりません。したがって、それらのテーマ・タイプとしてその言語を追加する必要があります。
たとえば、 mytheme
テーマへノルウェー翻訳を追加するには、以下の内容で themes/mytheme/login/messages/messages_no.properties
ファイルを作成します。
usernameOrEmail=Brukernavn
password=Passord
翻訳していないメッセージにはすべて、デフォルトの英語翻訳が使用されます。
次に、 themes/mytheme/login/theme.properties
を編集し、以下を追加します。
locales=en,no
account
と email
のテーマ・タイプにも同じことを実行する必要があります。これを実行するには、 themes/mytheme/account/messages/messages_no.properties
と themes/mytheme/email/messages/messages_no.properties
を作成します。これらのファイルを空にしたままにすると、英語のメッセージが使用されます。次に、 themes/mytheme/login/theme.properties
をコピーして themes/mytheme/account/theme.properties
と themes/mytheme/email/theme.properties
へペーストします。
最後に、言語セレクターに翻訳を追加する必要があります。これは英語翻訳にメッセージを追加することによって実行されます。これを実行するには、以下を themes/mytheme/account/messages/messages_en.properties
と themes/mytheme/login/messages/messages_en.properties
に追加します。
locale_no=Norsk
デフォルトでは、メッセージ・プロパティー・ファイルはISO-8859-1を使用してエンコードされる必要があります。また、これは特別なヘッダーを使用してエンコーディングを指定する方法でも可能です。たとえば、UTF-8エンコーディングを使用するには、以下のとおりとなります。
# encoding: UTF-8
usernameOrEmail=....
現在のロケールの選択方法の詳細については、ロケール・セレクターを参照してください。
カスタム・アイデンティティー・プロバイダーのアイコン
Keycloakは、ログイン画面に表示されるカスタム・アイデンティティー・プロバイダーのアイコンの追加をサポートしています。ログインの theme.properties
ファイル(例: themes/mytheme/login/theme.properties
)でキーパターン kcLogoIdP-<alias>
を使用してアイコンクラスを定義する必要があります。エイリアスが myProvider
のアイデンティティー・プロバイダーの場合、カスタムテーマの theme.properties
に次のような行を追加できます。
kcLogoIdP-myProvider = fa fa-lock
すべてのアイコンは、PatternFly4の公式ウェブサイトで入手できます。ソーシャル・プロバイダーのアイコンは、標準提供のログイン・テーマ・プロパティー( themes/keycloak/login/theme.properties
)で既に定義されており、変更することができます。
HTMLのテンプレート
Keycloakでは、HTMLを生成するために Freemarkerテンプレート が使用されます。テーマ内で <THEME TYPE>/<TEMPLATE>.ftl
を作成すると、個々のテンプレートを上書きすることができます。使用したテンプレートのリストについては themes/base/<THEME TYPE>
を参照してください。
カスタム・テンプレートを作成する場合、基本テーマからのテンプレートをテーマへコピーし、必要な修正を適用する方法をお勧めします。Keycloakの新しいバージョンへアップグレードする際に、適用可能であれば、カスタム・テンプレートをアップグレードし、オリジナルのテンプレートへ変更を適用する必要があることを留意しておいてください。
たとえば、 mytheme
テーマにカスタム・ログイン画面を作成するには、 themes/base/login/login.ftl
を themes/mytheme/login
へコピーし、それをエディター内で開きます。以下のように最初の行(<#import …>)の次行に <h1>HELLO WORLD!</h1>
を追加します。
<#import "template.ftl" as layout>
<h1>HELLO WORLD!</h1>
...
テンプレートを編集する方法について、詳しくは、 FreeMarkerのマニュアル を参照してください。
電子メール
電子メールの題名と内容(たとえばパスワード・リカバリー電子メールなど)を編集するには、テーマの 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>
テーマのデプロイ
テーマは、テーマ・ディレクトリーを themes
へコピーすることによって、Keycloakへデプロイすることができ、アーカイブとしてデプロイすることも可能です。開発中は、テーマを themes
ディレクトリーへコピーできますが、プロダクション環境では archive
の使用を検討した方がいいかもしれません。 archive
を使用すると、テーマのコピー版の作成が簡単になります。たとえばクラスター構成のKeycloakなど、インスタンスが複数ある場合は特に便利です。
テーマをアーカイブとしてデプロイするには、テーマのリソースを使用してJARアーカイブを作成する必要があります。また、アーカイブで使用可能なテーマと各テーマが提供するタイプをリストするアーカイブに、 META-INF/keycloak-themes.json
ファイルを追加することも必要です。
たとえば、 mytheme
テーマの場合、以下の内容で mytheme.jar
を作成します。
-
META-INF/keycloak-themes.json
-
themes/mytheme/login/theme.properties
-
themes/mytheme/login/login.ftl
-
themes/mytheme/login/resources/css/styles.css
-
themes/mytheme/login/resources/img/image.png
-
themes/mytheme/login/messages/messages_en.properties
-
themes/mytheme/email/messages/messages_en.properties
このケースの META-INF/keycloak-themes.json
の内容は、以下のとおりです。
{
"themes": [{
"name" : "mytheme",
"types": [ "login", "email" ]
}]
}
1つのアーカイブには複数のテーマを含めることができ、各テーマは1つ以上のタイプをサポートすることができます。
アーカイブをKeycloakにデプロイするには、Keycloakの standalone/deployments/
ディレクトリーにドロップするだけで、それにより自動的にロードされます。
テーマセレクター
デフォルトでは、クライアントがログインテーマを上書きできる点を除き、レルムに設定されたテーマが使用されます。この動作は、テーマセレクターSPIによって変更できます。
これは、ユーザー・エージェント・ヘッダーを見ることにより、たとえばデスクトップおよびモバイルデバイス用の異なるテーマを選択するために使用することができます。
カスタム・テーマ・セレクターを作成するには、 ThemeSelectorProviderFactory
と ThemeSelectorProvider
を実装する必要があります。
カスタム・プロバイダーを作成してデプロイする方法の詳細については、サービス・プロバイダー・インターフェイスの手順に従ってください。
テーマリソース
Keycloakにカスタム・プロバイダーを実装する場合、テンプレート、リソースおよびメッセージバンドルを追加する必要がよくあります。
ユースケースの例は、追加のテンプレートとリソースを必要とするカスタム・オーセンティケーターです。
追加のテーマリソースをロードする最も簡単な方法は、 theme-resources/templates
のテンプレートと theme-resources/resources
のリソースと theme-resources/messages
のメッセージ・バンドルを持つJARを作成し、Keycloakの standalone/deployments/
ディレクトリーに格納することです。
テンプレートとリソースをより柔軟にロードする方法が必要な場合、ThemeResourceSPIを使用して実現できます。 ThemeResourceProviderFactory
と ThemeResourceProvider
を実装することで、テンプレートとリソースを読み込む方法を直に決めることができます。
カスタム・プロバイダーを作成してデプロイする方法の詳細については、サービス・プロバイダー・インターフェイスの手順に従ってください。
ロケール・セレクター
デフォルトでは、ロケールは LocaleSelectorProvider
インターフェイスを実装する DefaultLocaleSelectorProvider
を使用して選択されます。国際化が無効の場合は、英語がデフォルトの言語です。国際化を有効にすると、ロケールは Server Administration Guide で説明されているロジックに従って解決されます。
この動作は LocaleSelectorProvider
と LocaleSelectorProviderFactory
を実装することで LocaleSelectorSPI
を通して変更することができます。
デフォルトでは、ロケールは LocaleSelectorProvider
インターフェイスを実装する DefaultLocaleSelectorProvider
を使用して選択されます。国際化が無効の場合は、英語がデフォルトの言語です。国際化を有効にすると、ロケールは Server Administration Guide で説明されているロジックに従って解決されます。
この動作は LocaleSelectorProvider
と LocaleSelectorProviderFactory
を実装することで LocaleSelectorSPI
を通して変更することができます。
LocaleSelectorProvider
インターフェイスは resolveLocale
という単一のメソッドを持ちます。これは RealmModel
とnull許容の UserModel
を与えられたロケールを返さなければなりません。実際のリクエストは KeycloakSession#getContext
メソッドから利用できます。
カスタム実装ではデフォルトの振る舞いの一部を再利用するために DefaultLocaleSelectorProvider
を継承することができます。たとえば Accept-Language
リクエストヘッダーを無視するために、カスタム実装はデフォルト・プロバイダーを継承し、 getAcceptLanguageHeaderLocale
をオーバーライドし、そしてnull値を返すことができます。結果として、ロケールの選択はレルムのデフォルト言語に戻ります。
カスタム・プロバイダーを作成してデプロイする方法の詳細については、サービス・プロバイダー・インターフェイスの手順に従ってください。
カスタムユーザー属性
カスタムテーマを使用して、登録ページとアカウント管理コンソールにカスタムユーザー属性を追加できます。 この章では、カスタムテーマに属性を追加する方法について説明しますが、カスタムテーマの作成方法については、テーマの章を参照してください。
登録ページ
登録ページにカスタム属性を入力できるようにするには、テンプレート 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" value="${(register.formData['user.attributes.mobile']!'')}"/>
</div>
</div>
inputのhtml要素の名前が user.attributes
で始まっていることを確認してください。上記の例では、属性はKeycloakによって mobile
という名前で保存されます。
変更を参照するには、レルムがログインテーマにカスタムテーマを使用していることを確認し、登録ページを開きます。
アカウント管理コンソール
アカウント管理コンソールのユーザー・プロファイルページでカスタム属性を管理できるようにするには、テンプレート 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>
inputのhtml要素の名前が user.attributes
で始まっていることを確認してください。
変更を参照するには、レルムがアカウントテーマに対してカスタムテーマを使用していることを確認し、アカウント管理コンソールでユーザー・プロフィールのページを開いてください。
アイデンティティー・ブローカリングAPI
Keycloakはログインのために親IDPに認証を委譲できます。この典型的な例は、ユーザーがFacebookやGoogleなどのソーシャル・プロバイダーを介してログインできるようにしたい場合です。Keycloakでは、既存のアカウントを仲介されたIDPにリンクすることもできます。このセクションでは、アイデンティティー・ブローカリングに関連してアプリケーションが使用できるいくつかのAPIについて説明します。
外部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を使用して再確立できます。
Client Initiated Account Linking
アプリケーションの中には、Facebookなどのソーシャル・プロバイダーと統合したいが、これらのソーシャル・プロバイダーを介してログインするオプションを提供したくないものもあります。Keycloakは、既存のユーザー・アカウントを特定の外部IDPにリンクするためにアプリケーションが使用できる、ブラウザー・ベースのAPIを提供しています。これは、Client-Initiated Account Linkingと呼ばれます。アカウント・リンキングは、OIDCアプリケーションによってのみ開始できます。
これを動作させるには、アプリケーションがユーザーのブラウザーをKeycloakサーバーのURLに転送して、ユーザーのアカウントを特定の外部プロバイダー(Facebookなど)にリンクすることを要求します。Keycloakサーバーは、外部プロバイダーとのログインを開始します。ブラウザーは外部プロバイダーにログインし、Keycloakサーバーにリダイレクトされます。Keycloakサーバーはリンクを確立し、確認のためにアプリケーションにリダイレクトします。
このプロトコルを開始する上で、クライアント・アプリケーションが満たさなければならない、いくつかの前提条件があります。
-
管理コンソールで、必要なアイデンティティー・プロバイダーを設定し、ユーザーのレルムに対して有効にする必要がある。
-
ユーザー・アカウントは、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の攻撃のターゲットに対する防御の責任があります。 |
サービス・プロバイダー・インターフェイス(SPI)
Keycloakは、必要なカスタム・コードが無くても、ほとんどのユースケースをカバーできるように作られていますが、カスタマイズもできるようにする必要があります。これを実現するために、Keycloakには独自のプロバイダーを実装できる多数のサービス・プロバイダー・インタフェース(SPI)があります。
SPIの実装
SPIを実装するには、SPIのProviderFactoryとProviderインターフェイスを実装する必要があります。また、サービス設定ファイルを作成する必要があります。
たとえば、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.xml
、 standalone-ha.xml
、または domain.xml
で設定できます。
たとえば、下記を 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 ThemeSelectorProvider {
private KeycloakSession session;
public MyThemeSelectorProvider(KeycloakSession session) {
this.session = session;
}
@Override
public String getThemeName(Theme.Type type) {
return session.getContext().getRealm().getLoginTheme();
}
}
管理コンソールでのSPI実装の情報表示
Keycloak管理者にプロバイダーに関する追加情報を表示すると、便利なことがあります。 プロバイダー・ビルド・タイム情報(たとえば、現在インストール済みのカスタム・プロバイダーのバージョン)、プロバイダーの現在の設定(たとえば、プロバイダーが通信するリモートシステムのURL)、または動作情報(たとえば、プロバイダーが通信するリモートシステムからの平均レスポンス・タイム)を表示することができます。Keycloak管理コンソールでは、サーバーの情報ページが提供され、この種の情報が表示されます。
プロバイダーからの情報を表示するには、 ProviderFactory
内で org.keycloak.provider.ServerInfoAwareProviderFactory
インターフェイスを実装するだけです。
前のサンプルの MyThemeSelectorProviderFactory
のサンプル実装を次に示します。
package org.acme.provider;
import ...
public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory, ServerInfoAwareProviderFactory {
...
@Override
public Map<String, String> getOperationalInfo() {
Map<String, String> ret = new LinkedHashMap<>();
ret.put("theme-name", "my-theme");
return ret;
}
}
利用可能なプロバイダーを使用する
プロバイダーの実装では、Keycloak で利用可能な他のプロバイダーを使用できます。通常、既存のプロバイダーは KeycloakSession
を使用して取得できます。セクションSPI の実装で説明されているように、これをプロバイダーに利用できます。
Keycloak には次の2つのプロバイダーのタイプがあります。
-
単一実装のプロバイダータイプ - Keycloak ランタイムには、特定のプロバイダータイプの単一のアクティブな実装のみが存在できます。たとえば、
HostnameProvider
は、Keycloakによって使用されるホスト名を指定し、Keycloakサーバー全体で共有されます。したがって、Keycloakサーバーに対してアクティブなこのプロバイダーの実装は 1 つしかありません。サーバーランタイムで使用できるプロバイダー実装が複数ある場合、そのうちの1つは、standalone.xml
のkeycloakサブシステム設定でデフォルトとして指定する必要があります。たとえば、次のようなものです。
<spi name="hostname"> <default-provider>default</default-provider> ... </spi>
default-provider
の値として使用される default
の値は、特定のプロバイダー・ファクトリーの実装の ProviderFactory.getId()
によって返されるIDと一致する必要があります。コードでは、 keycloakSession.getProvider(HostnameProvider.class)
などのプロバイダーを取得できます。
-
複数の実装プロバイダー・タイプ - 複数の実装が利用可能で、Keycloakランタイムで一緒に動作できるプロバイダー・タイプです。たとえば、
EventListener
プロバイダーでは、複数の実装を使用可能にして登録できます。これは、特定のイベントをすべてのリスナー (jboss-logging、sysout など) に送信できることを意味します。コードでは、たとえばsession.getProvider(EventListener.class, "jboss-logging")
などのプロバイダーの指定されたインスタンスを取得できます。上記のように、このプロバイダー・タイプには複数のインスタンスが存在する可能性があるため、プロバイダーのprovider_id
を 2 番目の引数として指定する必要があります。プロバイダーIDは、特定のプロバイダー ファクトリ実装のProviderFactory.getId()
によって返されるIDと一致する必要があります。一部のプロバイダー・タイプは、2番目の引数としてComponentModel
を使用して取得でき、一部 (たとえばAuthenticator
) はKeycloakSessionFactory
を使用して取得する必要さえあります。将来的に廃止される可能性があるため、この方法で独自のプロバイダーを実装することはお勧めしません。
プロバイダー実装の登録
プロバイダーの実装を登録するには2通りの方法があります。ほとんどの場合、最も簡単な方法は、Keycloak deployerのアプローチを使用することです。なぜなら、この方法だと自動的にたくさんの依存関係が処理されるからです。また、リデプロイだけでなくホットデプロイもサポートされます。
代替のアプローチとしては、モジュールとしてデプロイするという方法があります。
カスタムSPIを作成する場合は、モジュールとして展開する必要があります。それ以外の場合は、Keycloak deployerのアプローチを使用することをお勧めします。
Keycloak Deployer の使用
プロバイダーのJARをKeycloakの standalone/deployments/
ディレクトリーにコピーすると、プロバイダーが自動的にデプロイされます。ホットデプロイも機能します。さらに、プロバイダーのJARファイルは jboss-deployment-structure.xml
ファイルのような機能を使うことができるという点で、WildFly環境にデプロイされた他のコンポーネントと同様に機能します。このファイルによって、他のコンポーネントへの依存関係を設定してサードパーティーのJARとモジュールを読み込むことができます。
また、プロバイダーJARを、EARおよびWARと同じように、デプロイ可能な他のユニット内に含めておくこともできます。EARでデプロイすると、実際、サードパーティーのJARを非常に簡単に使用できるようになります。なぜなら、これらのライブラリーをEARの lib/
ディレクトリーに置くだけで済むからです。
Modulesを使用したプロバイダーの登録
Modulesを使用してプロバイダーを登録するには、まずモジュールを作成します。これを実行するには、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.xml
、 standalone-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>
...
Jakarta EEの活用
サービス・プロバイダーは、プロバイダーを指すように META-INF/services
ファイルを正しくセットアップすれば、どのJakarta EEコンポーネント内でもパッケージ化することができます。たとえば、プロバイダーがサードパーティーのライブラリーを使用する必要がある場合、プロバイダーをEARの中にパッケージングして、EARの lib/
ディレクトリーにサードパーティーのライブラリーを格納することができます。また、プロバイダーのjarは、EJB、WAR、およびEARがWildFlyの環境で使用できる 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);
}
}
JavaScriptプロバイダー
Keycloakには、管理者が特定の機能をカスタマイズできるようにするために、起動中にスクリプトを実行する機能があります。
-
オーセンティケーター
-
JavaScriptポリシー
-
OpenID Connectプロトコル・マッパー
オーセンティケーター
認証スクリプトは、少なくとも以下の関数のうちの1つを提供しなければなりません。 Authenticator#authenticate(AuthenticationFlowContext)
から呼び出された authenticate(..)
。 Authenticator#action(AuthenticationFlowContext)
から呼び出された action(..)
。
カスタム Authenticator
は、少なくとも authenticate(..)
関数を提供する必要があります。コード内で javax.script.Bindings
スクリプトを使用できます。
script
-
スクリプトのメタデータにアクセスするための
ScriptModel
realm
-
RealmModel
user
-
現在の
UserModel
session
-
アクティブな
KeycloakSession
authenticationSession
-
現在の
AuthenticationSessionModel
httpRequest
-
現在の
org.jboss.resteasy.spi.HttpRequest
LOG
-
ScriptBasedAuthenticator
にスコープされたorg.jboss.logging.Logger
authenticate(context) や action(context) 関数に渡された context 引数から追加のコンテキスト情報を抽出できます。
|
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
function authenticate(context) {
LOG.info(script.name + " --> trace auth for: " + user.username);
if ( user.username === "tester"
&& user.getAttribute("someAttribute")
&& user.getAttribute("someAttribute").contains("someValue")) {
context.failure(AuthenticationFlowError.INVALID_USER);
return;
}
context.success();
}
デプロイするスクリプトを含むJARの作成
JARファイルは、拡張子が .jar の通常のZIPファイルです。
|
スクリプトをKeycloakで使用できるようにするには、それらをサーバーにデプロイする必要があります。そのためには、次の構造を持つ JAR
ファイルを作成する必要があります。
META-INF/keycloak-scripts.json
my-script-authenticator.js
my-script-policy.js
my-script-mapper.js
META-INF/keycloak-scripts.json
は、デプロイするスクリプトに関するメタデータ情報を提供するファイル・ディスクリプターです。次の構造を持つJSONファイルです。
{
"authenticators": [
{
"name": "My Authenticator",
"fileName": "my-script-authenticator.js",
"description": "My Authenticator from a JS file"
}
],
"policies": [
{
"name": "My Policy",
"fileName": "my-script-policy.js",
"description": "My Policy from a JS file"
}
],
"mappers": [
{
"name": "My Mapper",
"fileName": "my-script-mapper.js",
"description": "My Mapper from a JS file"
}
]
}
このファイルは、デプロイするさまざまなタイプのスクリプト・プロバイダーを参照する必要があります。
-
authenticators
OpenID Connectスクリプト・オーセンティケーター用です。同じJARファイルに1つ以上のオーセンティケーターを含めることができます。
-
policies
Keycloak認可サービスを使用する場合のJavaScriptポリシー用です。同じJARファイルに1つ以上のポリシーを含めることができます。
-
mappers
OpenID Connectスクリプト・プロトコル・マッパー用です。同じJARファイルに1つ以上のマッパーを含めることができます。
JAR
ファイル内の各スクリプト・ファイルに対して、スクリプト・ファイルを特定のプロバイダー・タイプにマッピングする META-INF/keycloak-scripts.json
に対応するエントリーが必要です。そのためには、各エントリーに次のプロパティーを提供する必要があります。
-
name
Keycloak管理コンソールでスクリプトを表示するために使用されるわかりやすい名前です。指定しない場合は、代わりにスクリプト・ファイルの名前が使用されます。
-
description
スクリプト・ファイルの意図をより詳しく説明するオプションのテキストです。
-
fileName
スクリプト・ファイルの名前です。このプロパティーは 必須 であり、JAR内のファイルにマップする必要があります。
スクリプトJARのデプロイ
ディスクリプターとデプロイしたいスクリプトを含むJARファイルを作成したら、JARをKeycloakの standalone/deployments/
ディレクトリーにコピーするだけです。
Keycloak管理コンソールを使用したスクリプトのアップロード
管理コンソールを介してスクリプトをアップロードする機能は廃止されており、Keycloakの将来のバージョンでは削除される予定です。 |
管理者はスクリプトをサーバーにアップロードできません。この動作により、悪意のあるスクリプトが誤って実行された場合にシステムに被害が及ぶ可能性を防ぎます。管理者は、スクリプトを実行する際の攻撃を防ぐために、常にJARファイルを使用してサーバーにスクリプトを直接デプロイする必要があります。
スクリプトをアップロードする機能を明示的に有効にすることができます。これは細心の注意を払って使用し、すべてのスクリプトをできるだけ早くサーバーに直接デプロイする計画を作成する必要があります。
upload_scripts
機能を有効にする方法の詳細については、Profilesを参照してください。
利用可能なSPI
利用可能なすべてのSPIのリストを実行時に確認する必要がある場合は、管理コンソールセクションでの説明通りに、管理コンソール内の Server Info
ページを確認します。
サーバーの拡張
Keycloak SPIフレームワークによって、特定のビルトイン・プロバイダーを実装、またはオーバーライドすることができます。ただし、Keycloak自身のコアの機能とドメインを拡張することもできます。これにより、以下も可能になります。
-
カスタムRESTエンドポイントをKeycloakサーバーに追加
-
独自のカスタムSPIを追加
-
カスタムJPAエンティティーをKeycloakデータモデルへ追加
カスタムRESTエンドポイントを追加
これは大変強力な拡張機能で、独自のRESTエンドポイントをKeycloakサーバーにデプロイすることができます。これによって、あらゆる種類の拡張が可能になります。たとえば、ビルトインのKeycloak RESTエンドポイントのデフォルトセットでは利用できないような機能をKeycloakサーバー上で起動することができます。
カスタムRESTエンドポイントを追加するには、 RealmResourceProviderFactory
と RealmResourceProvider
のインターフェイスを実装する必要があります。 RealmResourceProvider
には、次の重要なメソッドが1つあります。
Object getResource();
このメソッドにより、 JAX-RSリソース として機能するオブジェクトを返却できます。詳しくは、Javadocとサンプルを参照してください。 providers/rest
のサンプル配布物には非常に簡単なサンプルがあり、 providers/domain-extension
にはさらに高度なサンプルがあります。この高度なサンプルには、認証されるRESTエンドポイントと、独自のSPIの追加や独自のJPAエンティティーによるデータモデルの拡張のようなその他の機能を、追加する方法が示されます。
カスタム・プロバイダーをパッケージングしてデプロイする方法についての詳細は、サービス・プロバイダー・インターフェイスの章を参照してください。
独自のカスタム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
を継承する ProviderFactory
と ExampleService
を継承します。 ExampleService
には通常、ユースケースで必要なビジネスメソッドが含まれます。 ExampleServiceProviderFactory
インスタンスは常にアプリケーション毎にスコープされますが、 ExampleService
はリクエスト毎にスコープされます(より正確に言うと KeycloakSession
ライフサイクル毎にスコープされます)。
最後に、サービス・プロバイダー・インターフェイスの章で説明したのと同じ方法で、プロバイダーを実装する必要があります。
詳しくは、 providers/domain-extension
配布物のサンプルを参照してください。そこには、上記と同じようなSPIのサンプルが示されています。
カスタムJPAエンティティーをKeycloakデータモデルへ追加
Keycloakデータモデルが要求するソリューションとは厳密には違っていた場合やコアの機能をKeycloakに追加する場合、もしくは独自のRESTエンドポイントがある場合、Keycloakデータモデルの拡張が検討した方がいいかもしれません。独自のJPAエンティティーをKeycloakのJPA EntityManager
へ追加することが可能になりました。
独自のJPAエンティティーを追加するには、 JpaEntityProviderFactory
と JpaEntityProvider
を実装する必要があります。 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");
getChangelogLocation
と getFactoryId
のメソッドは、Liquibaseによるエンティティーの自動更新をサポートするために重要です。 Liquibase はデータベース・スキーマを更新するためのフレームワークです。これはKeycloakで内部的に使用され、DBスキーマを作成してバージョン間でDBスキーマを更新します。これを同じように使用して、エンティティーの変更履歴を作成する必要があるかもしれません。独自のLiquibaseの変更履歴のバージョニングはKeycloakのバージョンとは異なる独立したものであることに注意してください。つまり、新しいKeycloakバージョンへ更新した際、同時にスキーマを更新する必要はありません。また、その逆の場合でも、Keycloakバージョンを更新しなくてもスキーマを更新することができます。Liquibaseの更新は常にサーバー起動時に実行されるので、新しい変更セットをLiquibaseの変更履歴ファイル(上記のサンプルでは、これは META-INF/example-changelog.xml
ファイル(これはJPAエンティティーと ExampleJpaEntityProvider
と同じJAR内に含まれていなければなりません)になります)を追加して再起動するだけで、スキーマのDB更新のトリガーとなります。起動時に、DBスキーマが自動的に更新されます。
詳しくは、 providers/domain-extension
サンプル内のサンプル配布物を参照してください。そこでは、上記で説明された ExampleJpaEntityProvider
と example-changelog.xml
のサンプルが示されています。
Liquibase変更履歴に変更を加えたりDBの更新をトリガーにする前に、必ずデータベースをバックアップするようにしてください。 |
認証SPI
Keycloakには、ケルベロス、パスワード、OTPなどのさまざまな認証機構が用意されています。これらの機構は、要件をすべて満たしているわけではなく、独自のカスタムプラグインを必要とする場合もあります。Keycloakは、新しいプラグインの作成に使用できる認証SPIを提供します。管理コンソールは、これらの新しい機構の適用、順序、設定をサポートしています。
Keycloakでは簡単な登録フォームもサポートされます。このフォームのさまざまな要素を有効、無効にすることができます。つまり、reCAPTCHAのサポートをオフにすることができます。同じ認証SPIを使用して、他のページを登録フローに追加したり、それを完全に再実装することができます。また、追加のきめ細かいSPIを使用して、組み込みの登録フォームに特定のバリデーションやユーザー拡張機能を追加することもできます。
Keycloakでの必須アクションとは、認証後にユーザーが実行する必要のあるアクションのことです。アクションが実行された後、ユーザーはそのアクションを再実行する必要はありません。Keycloakには、"パスワードリセット"などの必須アクションがいくつか組み込まれています。たとえば、パスワードリセットは、ユーザーがログインした後にパスワードを変更するよう強制します。必須アクションを作成してプラグインすることができます。
オーセンティケーターまたは必須アクションの実装で、ユーザーのアイデンティティーをリンク/確立するためのメタデータ属性として一部のユーザー属性を使用している場合は、ユーザーが属性を編集できず、対応する属性が読み取り専用であることを確認してください。詳細については 脅威モデルの緩和の章 を参照してください。 |
用語
まず最初に、認証SPIについて学ぶには、それを説明するためのいくつかの用語を確認していきます。
- 認証フロー
-
フローは、ログインまたは登録中に必ず発生するすべての認証のためのコンテナーです。管理コンソールの認証ページに移動すると、システム内で定義されたすべてのフローと、どのようなオーセンティケーターで構成されているかが表示されます。フローには、他のフローを含めることができます。また、ブラウザーのログイン、ダイレクト・グラント・アクセス、および登録用に、新しい異なるフローをバインドすることもできます。
- オーセンティケーター
-
オーセンティケーターは、フロー内で認証またはアクションを実行するためのロジックを保持する、プラグイン可能なコンポーネントです。通常は、シングルトンです。
- エグゼキューション
-
エグゼキューションは、オーセンティケーターをフローにバインドしたり、オーセンティケーターの設定にオーセンティケーターをバインドするオブジェクトです。フローには、エグゼキューション・エントリーが含まれます。
- エグゼキューションのRequirement
-
エグゼキューションごとに、オーセンティケーターがフロー内でどのように動作するかを定義します。要件には、オーセンティケーターがenabled、disabled、conditional、required、またはalternativeのいずれであるかを定義します。alternativeの要件は、オーセンティケーターがそれが入っているフローを検証するのに十分であることを意味しますが、必須ではありません。たとえば、組み込みのブラウザーフローでは、Cookie認証、アイデンティティー・プロバイダー・リダイレクター、およびフォーム・サブフロー内のすべてのオーセンティケーターのセットがすべてalternativeです。これらは上から下へ順番に実行されるため、そのうちの1つが成功した場合、フローは成功し、フロー(またはサブフロー)での後続のエグゼキューションは評価されません。
- オーセンティケーター設定
-
このオブジェクトは、認証フロー内の特定のエグゼキューションに対してのオーセンティケーターの設定を定義します。エグゼキューションごとに異なる設定を持つことができます。
- 必須アクション
-
認証が完了した後、ユーザーは、ログインを許可される前に完了する必要がある1つ以上の1回限りのアクションを行う場合があります。ユーザーは、OTPトークン・ジェネレーターをセットアップするか、期限切れのパスワードをリセットするか、利用規約文書に同意する必要があります。
アルゴリズムの概要
これがブラウザー・ログインでどのように機能するか説明します。以下のフロー、エグゼキューション、サブフローを想定してみましょう。
Cookie - ALTERNATIVE
Kerberos - ALTERNATIVE
Forms subflow - ALTERNATIVE
Username/Password Form - REQUIRED
Conditional OTP subflow - CONDITIONAL
Condition - User Configured - REQUIRED
OTP Form - REQUIRED
フォームのトップレベルには、すべてが選択的に必要な3つのエグゼキューションがあります。これらのいずれかが成功した場合、ほかは実行する必要がないことを意味します。SSO Cookieセットまたはケルベロスのログインが成功した場合、Username/Passwordフォームは実行されません。クライアントが最初にKeycloakにリダイレクトし、ユーザーを認証するまでの手順を説明します。
-
OpenID ConnectまたはSAMLプロトコル・プロバイダーは、関連するデータを展開し、クライアントと署名を検証します。AuthenticationSessionModelを作成します。ブラウザーフローを検索し、フローの実行を開始します。
-
このフローは、Cookieのエグゼキューションを参照し、それがALTERNATIVEであることを確認します。そして、Cookieプロバイダーをロードし、ユーザーが認証セッションに関連付けられていることをCookieプロバイダーが要求しているかどうかを確認します。Cookieプロバイダーには、ユーザーは必要ありません。もしそうであれば、フローは中断され、ユーザーにはエラー画面が表示されます。その後、Cookieプロバイダーが実行されます。その目的は、SSO Cookieセットがあるかどうかを確認することです。1つのセットがある場合、そのSSO CookieとUserSessionModelが検証され、AuthenticationSessionModelに関連付けられます。SSO Cookieが存在し、検証が済むと、Cookieプロバイダーはsuccess()ステータスを返します。Cookieプロバイダーは成功を返すと、このフローのレベルでのそれぞれのエグゼキューションはALTERNATIVEであるため、他のエグゼキューションは実行されず、ログインに成功します。SSO Cookieが存在しない場合は、Cookieプロバイダーはattempted()のステータスを返します。この場合、エラー状態ではありませんが、成功でもないことを意味します。プロバイダーは試行しましたが、リクエストはこのオーセンティケーターを処理するようには設定されていませんでした。
-
次に、フローはケルベロス・エグゼキューションを参照します。これもALTERNATIVEです。ケルベロス・プロバイダーもまた、このプロバイダーを実行されるように、ユーザーに設定されていることと、AuthenticationSessionModelに関連付けられていることを要求しません。ケルベロスは、SPNEGOブラウザー・プロトコルを使用します。これは、サーバーとクライアントがネゴシエーション・ヘッダーを交換する一連のチャレンジ/レスポンスを必要とします。ケルベロス・プロバイダーは、ネゴシエーション・ヘッダーをまったく見ないため、これがサーバーとクライアント間の最初のやりとりであることを前提とします。したがって、クライアントへのHTTPチャレンジ・レスポンスを作成し、forceChallenge()ステータスを設定します。forceChallenge()は、このHTTPレスポンスがフローで無視できないため、クライアントに返す必要があることを意味します。代わりにプロバイダーがchallenge()ステータスを返した場合、フローは他のすべてのALTERNATIVEが試行されるまでチャレンジ・レスポンスを保持します。したがって、この初期フェーズでフローが停止し、チャレンジ・レスポンスがブラウザーに返されます。ブラウザーが成功のネゴシエーション・ヘッダーで応答すると、プロバイダーはユーザーをAuthenticationSessionに関連付け、フローが終了します(このフローのレベルの残りのエグゼキューションはALTERNATIVEであるため)。それ以外の場合は、ケルベロス・プロバイダーは、attempted()を設定し、フローを続行します。
-
次のエグゼキューションは、Formsと呼ばれるサブフローです。このサブフローのエグゼキューションがロードされ、同じ処理ロジックが発生します。
-
Formsサブフローの最初のエグゼキューションは、UsernamePasswordプロバイダーです。このプロバイダーも、ユーザーがフローに関連付けられていることを要求しません。このプロバイダーは、チャレンジHTTPレスポンスを作成し、そのステータスをchallenge()に設定します。このエグゼキューションは必須なので、フローはこのチャレンジを優先し、ブラウザーにHTTPレスポンスを返します。このレスポンスは、Username/Password HTMLページのレンダリングです。ユーザーは、ユーザー名とパスワードを入力し、送信をクリックします。このHTTPリクエストは、UsernamePasswordプロバイダーに送信されます。ユーザーが無効なユーザー名またはパスワードを入力した場合、新しいチャレンジ・レスポンスが作成され、このエグゼキューションにfailureChallenge()のステータスが設定されます。failureChallenge()は、チャレンジがあるが、エラーログにエラーとして記録する必要があることを意味します。このエラーログは、ログイン失敗回数の多いアカウントまたはIPアドレスをロックするのに使用できます。ユーザー名とパスワードが有効な場合、プロバイダーはUserModelをAuthenticationSessionModelに関連付け、success()ステータスを返します。
-
次のエグゼキューションは、Conditional OTPと呼ばれるサブフローです。このサブフローのエグゼキューションがロードされ、同じ処理ロジックが発生します。そのRequirementはConditionalです。これは、フローが最初に含まれるすべてのConditionalエグゼキューターを評価することを意味します。Conditionalエグゼキューターは
ConditionalAuthenticator
を実装するオーセンティケーターであり、メソッドboolean matchCondition(AuthenticationFlowContext context)
を実装する必要があります。Conditionalサブフローは、含まれるすべてのConditionalエグゼキューションのmatchCondition
メソッドを呼び出し、それらすべてがtrueと評価されると、必要なサブフローであるかのように動作します。そうでない場合は、無効なサブフローであるかのように動作します。Conditionalオーセンティケーターはこの目的にのみ使用され、オーセンティケーターとしては使用されません。これは、Conditionalオーセンティケーターが "true" と評価した場合でも、フローまたはサブフローが成功としてマークされないことを意味します。たとえば、Conditionalオーセンティケーターのみを持つConditionalサブフローのみを含むフローでは、ユーザーはログインできません。 -
Conditional OTPサブフローの最初のエグゼキューションは、Condition - User Configuredです。このプロバイダーでは、ユーザーがフローに関連付けられている必要があります。UsernamePasswordプロバイダーは既にユーザーをフローに関連付けているため、このRequirementは満たされています。このプロバイダーの
matchCondition
メソッドは、現在のサブフロー内の他のすべてのオーセンティケーターのconfiguredFor
メソッドを評価します。サブフローに、Requirementがrequiredに設定されたエグゼキューターが含まれている場合、必要なすべてのオーセンティケーターのconfiguredFor
メソッドがtrueに評価される場合にのみ、matchCondition
メソッドはtrueに評価されます。それ以外の場合、alternativeオーセンティケーターがtrueと評価されると、matchCondition
メソッドがtrueと評価されます。 -
次のエグゼキューションは、OTPフォームです。このプロバイダーでも、ユーザーがフローに関連付けられている必要があります。UsernamePasswordプロバイダーがすでにユーザーをフローに関連付けているため、この要件は満たされます。このプロバイダーは、ユーザーが必須であることから、ユーザーがこのプロバイダーを使用するように設定されているかを求められます。ユーザーが設定されていない場合、フローは認証が完了した後にユーザーが実行する必要のある必須アクションを設定します。OTPの場合、これはOTP設定ページを意味します。ユーザーが設定されている場合、ユーザーはOTPコードを入力するよう求められます。このシナリオでは、Conditional サブフローのため、Conditional OTPサブフローが必須に設定されていない限り、ユーザーにはOTPログインページが表示されません。
-
フローが完了すると、認証プロセッサーはUserSessionModelを作成し、それをAuthenticationSessionModelに関連付けます。その後、ユーザーはログイン前に必須アクションを完了する必要があるかどうかを確認します。
-
まず、それぞれの必須アクションのevaluateTriggers()メソッドが呼び出されます。これにより、必須アクション・プロバイダーは、アクションが実行されるトリガーとなる可能性があるかを判断できます。たとえば、レルムにパスワード有効期限ポリシーがある場合、このメソッドによってトリガーされる可能性があります。
-
ユーザーに関連付けられた各必須アクションにあるrequiredActionChallenge()メソッドが呼び出されます。ここでプロバイダーは、必須アクションのページをレンダリングするHTTPレスポンスをセットアップします。これは、チャレンジ・ステータスを設定することで実行されます。
-
必須アクションが最終的に成功すると、ユーザーの必須アクションリストから必須アクションが削除されます。
-
すべての必須アクションが解決した後、ユーザーはようやくログインしたことになります。
オーセンティケーターSPIのウォークスルー
このセクションでは、オーセンティケーター・インターフェイスについて説明します。これを説明するために、"あなたの母親の旧姓は何ですか?"のような秘密の質問にユーザーが回答を入力する必要のあるオーセンティケーターを実装していきます。この例は、完全に実装されており、Keycloakのデモ配布物のexamples/providers/authenticatorディレクトリーに含まれています。
オーセンティケーターを作成するには、少なくともorg.keycloak.authentication.AuthenticatorFactoryおよびAuthenticatorインターフェースを実装する必要があります。Authenticatorインターフェイスはロジックを定義します。AuthenticatorFactoryは、Authenticatorインスタンスの作成を担います。それらは両方とも、ユーザー・フェデレーションのような他のKeycloakコンポーネントが行う、より汎用的なProviderとProviderFactoryのインターフェイスのセットを継承します。
CookieAuthenticatorなどの一部のオーセンティケーターは、ユーザーを認証するためにユーザーが持っている、または知っているCredentialに依存しません。ただし、PasswordFormオーセンティケーターやOTPFormAuthenticatorなどの一部のオーセンティケーターは、ユーザーが何らかの情報を入力し、その情報をデータベース内のいくつかの情報と照合することに依存しています。たとえば、PasswordFormの場合、オーセンティケーターはデータベースに保存されているハッシュに対してパスワードのハッシュを検証し、OTPFormAuthenticatorはデータベースに保存されている共有シークレットから生成されたOTPに対して受信したOTPを検証します。
これらのタイプのオーセンティケーターはCredentialValidatorsと呼ばれ、さらに次のいくつかのクラスを実装する必要があります。
-
org.keycloak.credential.CredentialModelを継承し、データベースでクレデンシャルの正しい形式を生成できるクラス
-
org.keycloak.credential.CredentialProviderとインターフェイスを実装するクラス、およびそのCredentialProviderFactoryファクトリー・インターフェイスを実装するクラス。
このウォークスルーで確認するSecretQuestionAuthenticatorはCredentialValidatorであるため、これらのクラスをすべて実装する方法を確認します。
クラスのパッケージ化とデプロイ
1つのjarファイル内にクラスをパッケージ化します。このjarには、 org.keycloak.authentication.AuthenticatorFactory
という名前のファイルが、jarの META-INF/services/
ディレクトリーに含まれている必要があります。このファイルには、jarファイル内にある各AuthenticatorFactory実装の完全修飾クラス名が一覧化されている必要があります。たとえば、以下のとおりになります。
org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
org.keycloak.examples.authenticator.AnotherProviderFactory
このservices/ファイルは、システムにロードする必要があるプロバイダーをKeycloakがスキャンするために使用されます。
このjarをデプロイするには、これをprovidersディレクトリーにコピーします。
CredentialModelクラスの継承
Keycloakでは、クレデンシャルはデータベースのCredentialsテーブルに保存されます。次のテーブル定義になっています。
----------------------------- | ID | ----------------------------- | user_ID | ----------------------------- | credential_type | ----------------------------- | created_date | ----------------------------- | user_label | ----------------------------- | secret_data | ----------------------------- | credential_data | ----------------------------- | priority | -----------------------------
各列は以下のとおりです。
-
ID
は、Credentialsテーブルの主キーです。 -
user_ID
は、クレデンシャルをユーザーにリンクする外部キーです。 -
credential_type
は、作成中に設定される文字列で、既存のクレデンシャル・タイプを参照する必要があります。 -
created_date
は、クレデンシャルの作成タイムスタンプ(長い形式)です。 -
user_label
は、ユーザーによるクレデンシャルの編集可能な名前です。 -
secret_data
には、Keycloakの外部に送信できない情報を持つ静的JSONが含まれます。 -
credential_data
には、管理コンソールまたはREST APIを介して共有できるクレデンシャルの静的情報を含むJSONが含まれています。 -
priority
は、ユーザーが複数の選択肢を持っているときにどのクレデンシャルを提示するかを決定するために、ユーザーにとってクレデンシャルがどのように"優先される"かを定義します。
secret_dataフィールドとcredential_dataフィールドはjsonを含むように設計されているため、これらのフィールドの構成や、読み取り、書き込みの方法を柔軟に変更できます。
この例では、ユーザーに尋ねられた質問のみを含む非常に単純なクレデンシャル・データを使用します。
{
"question":"aQuestion"
}
以下は、同様に単純な秘密データを使用します(秘密の答えのみを含む)。
{
"answer":"anAnswer"
}
ここでは、簡単にするために、回答はデータベースにプレーンテキストで保存しますが、Keycloakのパスワードの場合のように、回答にソルトハッシュを使用することもできます。この場合、秘密データには、ソルトのフィールドと、使用されたアルゴリズムのタイプや使用された反復数などのアルゴリズムに関するクレデンシャル・データ情報も含まれている必要があります。詳細については、 org.keycloak.models.credential.PasswordCredentialModel
クラスの実装を参照してください。
この場合、以下のようなクラス SecretQuestionCredentialModel
を作成します。
public class SecretQuestionCredentialModel extends CredentialModel {
public static final String TYPE = "SECRET_QUESTION";
private final SecretQuestionCredentialData credentialData;
private final SecretQuestionSecretData secretData;
TYPE
は、データベースに書き込むcredential_typeです。一貫性を保つために、このクレデンシャルの型を取得するときにこのStringが常に参照されるようにします。クラス SecretQuestionCredentialData
と SecretQuestionSecretData
は、jsonのマーシャリングとアンマーシャリングに使用されます。
public class SecretQuestionCredentialData {
private final String question;
@JsonCreator
public SecretQuestionCredentialData(@JsonProperty("question") String question) {
this.question = question;
}
public String getQuestion() {
return question;
}
}
public class SecretQuestionSecretData {
private final String answer;
@JsonCreator
public SecretQuestionSecretData(@JsonProperty("answer") String answer) {
this.answer = answer;
}
public String getAnswer() {
return answer;
}
}
完全に使用可能にするために、 SecretQuestionCredentialModel
オブジェクトには、その親クラスからのそのままのjsonデータと、独自の属性のアンマーシャルされたオブジェクトの両方が含まれている必要があります。これにより、データベースから読み取るときに作成されるような単純なCredentialModelから読み取るメソッドを作成し、 SecretQuestionCredentialModel
を作成します。
private SecretQuestionCredentialModel(SecretQuestionCredentialData credentialData, SecretQuestionSecretData secretData) {
this.credentialData = credentialData;
this.secretData = secretData;
}
public static SecretQuestionCredentialModel createFromCredentialModel(CredentialModel credentialModel){
try {
SecretQuestionCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), SecretQuestionCredentialData.class);
SecretQuestionSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), SecretQuestionSecretData.class);
SecretQuestionCredentialModel secretQuestionCredentialModel = new SecretQuestionCredentialModel(credentialData, secretData);
secretQuestionCredentialModel.setUserLabel(credentialModel.getUserLabel());
secretQuestionCredentialModel.setCreatedDate(credentialModel.getCreatedDate());
secretQuestionCredentialModel.setType(TYPE);
secretQuestionCredentialModel.setId(credentialModel.getId());
secretQuestionCredentialModel.setSecretData(credentialModel.getSecretData());
secretQuestionCredentialModel.setCredentialData(credentialModel.getCredentialData());
return secretQuestionCredentialModel;
} catch (IOException e){
throw new RuntimeException(e);
}
}
そして、質問と回答から SecretQuestionCredentialModel
を生成するメソッドは次のようになります。
private SecretQuestionCredentialModel(String question, String answer) {
credentialData = new SecretQuestionCredentialData(question);
secretData = new SecretQuestionSecretData(answer);
}
public static SecretQuestionCredentialModel createSecretQuestion(String question, String answer) {
SecretQuestionCredentialModel credentialModel = new SecretQuestionCredentialModel(question, answer);
credentialModel.fillCredentialModelFields();
return credentialModel;
}
private void fillCredentialModelFields(){
try {
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setType(TYPE);
setCreatedDate(Time.currentTimeMillis());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
CredentialProviderの実装
すべてのプロバイダーで同様に、KeycloakがCredentialProviderを生成できるようにするには、CredentialProviderFactoryが必要です。この要件のために、SecretQuestionCredentialProviderFactoryを生成します。SecretQuestionCredentialProviderが要求されたときに、以下のように create
メソッドが呼び出されます。
public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory<SecretQuestionCredentialProvider> {
public static final String PROVIDER_ID = "secret-question";
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public CredentialProvider create(KeycloakSession session) {
return new SecretQuestionCredentialProvider(session);
}
}
CredentialProviderインターフェイスは、CredentialModelを継承するジェネリックなパラメーターを扱います。この場合、作成したSecretQuestionCredentialModelを使用します。
public class SecretQuestionCredentialProvider implements CredentialProvider<SecretQuestionCredentialModel>, CredentialInputValidator {
private static final Logger logger = Logger.getLogger(SecretQuestionCredentialProvider.class);
protected KeycloakSession session;
public SecretQuestionCredentialProvider(KeycloakSession session) {
this.session = session;
}
private UserCredentialStore getCredentialStore() {
return session.userCredentialManager();
}
このプロバイダーを使用してAuthenticatorのクレデンシャルを検証できることをKeycloakが知ることができるため、CredentialInputValidatorインターフェイスも実装します。CredentialProviderインターフェースの場合、実装する必要がある最初のメソッドは getType()
メソッドです。これは、次のように SecretQuestionCredentialModel
のTYPE文字列を返します。
@Override
public String getType() {
return SecretQuestionCredentialModel.TYPE;
}
2番目の方法は、 CredentialModel
から SecretQuestionCredentialModel
を作成することです。このメソッドは、 SecretQuestionCredentialModel
から既存の静的メソッドを呼び出します。
@Override
public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) {
return SecretQuestionCredentialModel.createFromCredentialModel(model);
}
最後に、クレデンシャルを作成するメソッドと、クレデンシャルを削除するメソッドがあります。これらのメソッドは、KeycloakSessionの userCredentialManager
を呼び出します。これらには、クレデンシャルの読み取りまたは書き込みの場所(たとえば、ローカル・ストレージやフェデレーション・ストレージなど)を把握する責任があります。
@Override
public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) {
if (credentialModel.getCreatedDate() == null) {
credentialModel.setCreatedDate(Time.currentTimeMillis());
}
return getCredentialStore().createCredential(realm, user, credentialModel);
}
@Override
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
return getCredentialStore().removeStoredCredential(realm, user, credentialId);
}
CredentialInputValidatorの場合、実装するメインメソッドは isValid
です。これは、特定のレルム内の特定のユーザーに対して認証情報が有効かどうかをテストします。これは、Authenticatorがユーザーの入力を検証しようとしたときに呼び出されるメソッドです。ここでは、入力文字列がクレデンシャルに記録されたものであることを確認するだけです。
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!(input instanceof UserCredentialModel)) {
logger.debug("Expected instance of UserCredentialModel for CredentialInput");
return false;
}
if (!input.getType().equals(getType())) {
return false;
}
String challengeResponse = input.getChallengeResponse();
if (challengeResponse == null) {
return false;
}
CredentialModel credentialModel = getCredentialStore().getStoredCredentialById(realm, user, input.getCredentialId());
SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel);
return sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse);
}
実装する他の2つの方法は、CredentialProviderが指定されたクレデンシャル・タイプをサポートするかどうかのテストと、クレデンシャル・タイプが特定のユーザーに対して設定されているかどうかを確認するテストです。私たちの場合、後者のテストは単に、ユーザーがSECRET_QUESTIONタイプのクレデンシャルを持っているかどうかを確認することを意味します。
@Override
public boolean supportsCredentialType(String credentialType) {
return getType().equals(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (!supportsCredentialType(credentialType)) return false;
return !getCredentialStore().getStoredCredentialsByType(realm, user, credentialType).isEmpty();
}
Authenticatorの実装
Credentialsを使用してユーザーを認証するオーセンティケーターを実装する場合、AuthenticatorにCredentialValidatorインターフェイスを実装させる必要があります。このインターフェイスは、CredentialProviderをパラメーターとして拡張するクラスを取り、KeycloakがCredentialProviderからメソッドを直接呼び出すことを許可します。実装する必要がある唯一のメソッドは getCredentialProvider
メソッドです。この例では、SecretQuestionAuthenticatorがSecretQuestionCredentialProviderを取得できます。
public SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession session) {
return (SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class, SecretQuestionCredentialProviderFactory.PROVIDER_ID);
}
Authenticatorインターフェースを実装する場合、実装する必要がある最初のメソッドはrequiresUser()メソッドです。この例では、ユーザーに関連付けられた秘密の質問を検証する必要があるため、このメソッドはtrueを返す必要があります。Kerberosのようなプロバイダーは、Negotiateヘッダーからユーザーを解決できるため、このメソッドからfalseを返します。ただし、この例では、特定のユーザーの特定のクレデンシャルを検証しています。
実装する次のメソッドはconfiguredFor()メソッドです。このメソッドは、ユーザーがこの特定の認証用に設定されているかどうかを判断します。この場合、SecretQuestionCredentialProviderに実装されているメソッドを呼び出すだけです。
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session));
}
Authenticatorに実装する次のメソッドはsetRequiredActions()です。configuredFor()がfalseを返し、サンプルのオーセンティケーターがフロー内で必要な場合、このメソッドが呼び出されますが、関連付けられたAuthenticatorFactoryの isUserSetupAllowed
メソッドがtrueを返す場合に限られます。setRequiredActions()メソッドは、ユーザーが実行する必要のある必要なアクションを登録します。この例では、秘密の質問への回答をユーザーに設定させる必要なアクションを登録する必要があります。この必須のアクション・プロバイダーは、この章の後半で実装します。setRequiredActions()メソッドの実装は次のとおりです。
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
user.addRequiredAction("SECRET_QUESTION_CONFIG");
}
Authenticatorの実装の要点を説明します。実装する次のメソッドはauthenticate()です。これは、実行が最初にアクセスされたときにフローが呼び出す最初のメソッドです。ユーザーがブラウザーのマシン上で秘密の質問にすでに答えている場合、ユーザーは質問に再度答える必要がなく、そのマシンを"信頼できる"ものにすることが望まれます。authenticate()メソッドは、秘密の質問フォームの処理を担当しません。その唯一の目的は、ページをレンダリングするか、フローを継続することです。
@Override
public void authenticate(AuthenticationFlowContext context) {
if (hasCookie(context)) {
context.success();
return;
}
Response challenge = context.form()
.createForm("secret-question.ftl");
context.challenge(challenge);
}
protected boolean hasCookie(AuthenticationFlowContext context) {
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
boolean result = cookie != null;
if (result) {
System.out.println("Bypassing secret question because cookie is set");
}
return result;
}
hasCookie()メソッドは、秘密の質問がすでに回答されたことを示すCookieがブラウザーに設定されているかどうかを確認します。それがtrueを返す場合、AuthenticationFlowContext.success()メソッドを使用してauthentication()メソッドから戻ることで、この実行のステータスを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リクエストをフローに送信します。フローは、Authenticatorの実装の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()を呼び出します。
検証自体は、フォームから受信したデータを取得し、SecretQuestionCredentialProviderからisValidメソッドを呼び出します。クレデンシャルIDの取得に関するコードのセクションがあることに気付くでしょう。これは、Keycloakが複数のタイプの代替オーセンティケーターを許可するように設定されている場合、またはユーザーがSECRET_QUESTIONタイプの複数のクレデンシャルを記録できる場合(たとえば、複数の質問から選択でき、ユーザーがこれらの質問の1つ以上に回答できるようにできる場合)、Keycloakはユーザーのログ記録に使用されているクレデンシャルを知る必要があります。複数のクレデンシャルがある場合、Keycloakを使用すると、ログイン時にユーザーがどのクレデンシャルを使用するかを選択できます。情報はフォームからオーセンティケーターに送信されます。フォームがこの情報を提示しない場合、使用されるクレデンシャルIDは、CredentialProviderの default getDefaultCredential
メソッドによって与えられます。このメソッドは、ユーザーの正しいタイプの"最も優先される"クレデンシャルを返します。
protected boolean validateAnswer(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String secret = formData.getFirst("secret_answer");
String credentialId = formData.getFirst("credentialId");
if (credentialId == null || credentialId.isEmpty()) {
credentialId = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId();
}
UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret);
return getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input);
}
次のメソッドは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"));
}
URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
addCookie(context, "SECRET_QUESTION_ANSWERED", "true",
uri.getRawPath(),
null, null,
maxCookieAge,
false, true);
}
AuthenticationFlowContext.getAuthenticatorConfig()メソッドからAuthenticatorConfigModelを取得します。設定が存在する場合は、その設定の最大有効期間を取り出します。何を設定するべきかを定義する方法については、AuthenticatorFactoryの実装について説明する際に確認していきます。AuthenticatorFactory実装で設定の定義をした場合、管理コンソール内で設定値を定義できます。
@Override
public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {
return CredentialTypeMetadata.builder()
.type(getType())
.category(CredentialTypeMetadata.Category.TWO_FACTOR)
.displayName(SecretQuestionCredentialProviderFactory.PROVIDER_ID)
.helpText("secret-question-text")
.createAction(SecretQuestionAuthenticatorFactory.PROVIDER_ID)
.removeable(false)
.build(session);
}
SecretQuestionCredentialProviderクラスに実装する最後のメソッドは、CredentialProviderインターフェイスの抽象メソッドであるgetCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext)です。各クレデンシャル・プロバイダーは、このメソッドを提供および実装する必要があります。このメソッドは、CredentialTypeMetadataのインスタンスを返します。これには、少なくともオーセンティケーターのタイプとカテゴリー、displayName、および取り外し可能なアイテムが含まれている必要があります。この例では、ビルダーはメソッドgetType()からオーセンティケーターのタイプを取り、カテゴリーは2要素(オーセンティケーターは認証の2番目の要素として使用できます)かつ取り外し可能で、falseに設定されています(ユーザーは以前に登録されたクレデンシャルを削除できません)。
ビルダーの他の項目は、helpText(さまざまな画面でユーザーに表示されます)、createAction(必要なアクションのプロバイダーID、ユーザーが新しいクレデンシャルを作成するために使用できます)、またはupdateAction(createActionと同じですが、新しいクレデンシャルを作成する代わりに、クレデンシャルを更新します)です。
AuthenticatorFactoryの実装
このプロセスでの次のステップは、AuthenticatorFactoryを実装することです。このファクトリーはAuthenticatorのインスタンス化を担当します。また、Authenticatorに関する配備と設定のメタデータも提供します。
getId()メソッドは、コンポーネントの一意の名前です。create()メソッドは、Authenticatorを割り当てて処理するために、ランタイムによって呼び出されます。
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、CONDITIONAL、DISABLEDの4種類の要件タイプがありますが、AuthenticatorFactoryの実装では、フローを定義する際に管理コンソールに表示されるオプションの要件を制限できます。CONDITIONALは常にサブフローにのみ使用する必要があり、そうする正当な理由がない限り、オーセンティケーターの要件はREQUIRED、ALTERNATIVE、およびDISABLEDである必要があります。
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.ALTERNATIVE,
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()は、オーセンティケーターが属するカテゴリーです。
オーセンティケーター・フォームの追加
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>
</form>
${}
で囲まれたテキストは、属性またはテンプレートの関数に対応します。フォームのアクションが表示された場合、それは ${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でも構いません。
ファイルの上部を見ると、テンプレートをインポートしていることがわかります。
<#import "select.ftl" as layout>
標準の template.ftl
の代わりにこのテンプレートをインポートすると、Keycloakはユーザーが別のクレデンシャルまたはエグゼキューションを選択できるドロップダウン・ボックスを表示できます。
フローにオーセンティケーターを追加
オーセンティケーターをフローに追加するには、管理コンソールで行う必要があります。Authenticationメニュー項目に移動してFlowタブに移動すると、現在定義されているフローを表示できます。組み込みフローを変更することはできないため、作成した認証システムを追加するには、既存のフローをコピーするか、独自のフローを作成する必要があります。ユーザー・インターフェイスが十分に明確であり、フローの作成方法とオーセンティケーターの追加方法を決定できることが期待されます。詳細については、 Server Administration Guide の Authentication Flows
の章を参照してください。
フローを作成したら、バインドさせたいログイン・アクションに、そのフローをバインドする必要があります。Authenticationメニューに移動して、Bindingsタブを選択すると、フローをBrowser、Registration、またはDirect Grantフローにバインドするオプションが表示されます。
必須アクションのウォークスルー
このセクションでは、必須アクションを定義する方法について説明します。オーセンティケーターのセクションでは、"どのようにしてシステムに入力された秘密の質問に対するユーザーの回答をもらえば良いのだろう"と疑問に思ったかもしれません。例で示したように、回答が設定されていない場合は、必須アクションがトリガーされます。このセクションでは、シークレット・クエスチョン・オーセンティケーターに必須アクションを実装する方法について説明します。
クラスのパッケージ化とデプロイ
1つのJARファイル内にクラスをパッケージ化します。このJARは、他のプロバイダーのクラスとは別にする必要はありませんが、 org.keycloak.authentication.RequiredActionFactory
という名前のファイルが、JARの META-INF/services/
ディレクトリーに含まれている必要があります。このファイルには、JARファイル内にある各RequiredActionFactory実装の完全修飾クラス名が一覧化されている必要があります。たとえば、以下のとおりになります。
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
このservices/ファイルは、システムにロードする必要があるプロバイダーをKeycloakがスキャンするために使用されます。
このjarをデプロイするには、これを standalone/deployments
ディレクトリーにコピーしてください。
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()は、必須アクションが成功したことをコンテナーに通知します。
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()メソッドは、必須アクションに対して分かりやすい名前を表示させたいときに、管理コンソールで使用されます。
登録フォームの変更または拡張
Keycloakでの登録方法を完全に変更するために、オーセンティケーターのセットを使用して独自のフローを実装することが可能です。しかし、独自の登録ページにちょっとしたバリデーションを加えたいことも普通にあるかと思います。これを行うために追加のSPIが作成されました。このSPIは、基本的には、ページにフォーム要素のバリデーションを追加するだけでなく、ユーザー登録後にUserModel属性とデータを初期化することもできます。ユーザー・プロファイルの登録処理の実装とGoogle Recaptchaプラグインの登録の両方を説明していきます。
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と同様に実装されるため、ここでは説明はしません。
アクションのパッケージ化
1つのJARファイル内にクラスをパッケージ化します。このJARには、 org.keycloak.authentication.FormActionFactory
という名前のファイルが、JARの META-INF/services/
ディレクトリーに含まれている必要があります。このファイルには、JARファイル内にある各FormActionFactory実装の完全修飾クラス名が一覧化されている必要があります。たとえば、以下のようになります。
org.keycloak.authentication.forms.RegistrationProfile
org.keycloak.authentication.forms.RegistrationRecaptcha
このservices/ファイルは、システムにロードする必要があるプロバイダーをKeycloakがスキャンするために使用されます。
このjarをデプロイするには、これを standalone/deployments
ディレクトリーにコピーしてください。
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フローにバインドするオプションが表示されます。
パスワード忘れおよびクレデンシャル・フローの変更
Keycloakには、パスワードを忘れた場合の特定の認証フロー、またはユーザーにより発行されたクレデンシャルのリセットもあります。管理コンソールのフローページに移動すると、"Reset Credentials"フローがあります。デフォルトでは、Keycloakはユーザーの電子メールまたはユーザー名を要求し、ユーザーに電子メールを送信します。ユーザーがリンクをクリックすると、パスワードとOTP(OTPが設定されている場合)の両方をリセットすることができます。フローの"Reset OTP"オーセンティケーターを無効にすると、自動のOTPリセットを無効にすることができます。
このフローにも機能を追加することができます。たとえば、多くのデプロイメントでは、リンク付きの電子メールを送信することに加え、ユーザーが1つ以上の秘密の質問に回答するようにしたいと考えています。配布物に付属している秘密の質問のサンプルを拡張し、それをReset Credentialフローに組み込むことができます。
Reset Credentialsフローを拡張する場合、1つ注意点があります。最初の"オーセンティケーター"は、ユーザー名または電子メールを取得するための単なるページです。ユーザー名または電子メールが存在する場合、AuthenticationFlowContext.getUser()はそのユーザーを返します。それ以外の場合はnullになります。このフォームは、以前の電子メールまたはユーザー名が存在しない場合、電子メールまたはユーザー名を入力するようにユーザーに再要求 しません 。攻撃者が有効なユーザーを推測できないようにする必要があるため、AuthenticationFlowContext.getUser()がnullを返す場合、有効なユーザーが選択されたかのように見せかけるために、フローを続行する必要があります。このフローに秘密の質問を追加する場合は、電子メールの送信後にこれらの質問をすることをお勧めします。つまり、"Send Reset Email"オーセンティケーターの後に、カスタム・オーセンティケーターを追加してください。
First Broker Loginフローの変更
First Broker Loginフローは、アイデンティティー・プロバイダーに最初にログインした時に使用されます。 First Login
という用語は、特定の認証されたアイデンティティー・プロバイダー・アカウントにリンクされているKeycloakアカウントがまだ存在していないことを意味します。このフローの詳細については、 Server Administration Guide の アイデンティティー・ブローカリング
の章を参照してください。
クライアントの認証
Keycloakは実際には、 OpenID Connect クライアント・アプリケーションのためのプラグイン可能な認証をサポートしています。クライアント(アプリケーション)の認証は、Keycloakアダプターがバックチャネル・リクエストをKeycloakサーバーに送信中に内部で使用されます(認証が成功した後にコードとアクセストークンを交換する要求をしたり、リフレッシュトークンを要求したりするなど)。しかし、 ダイレクト・アクセス・グラント
(OAuth2により表される Resource Owner Password Credentials Flow
)または サービス・アカウント
認証(OAuth2により表される Client Credentials Flow
)でクライアント認証を直接使用することも可能です。
KeycloakアダプターとOAuth2フローの詳細については、Securing Applications and Services Guideを参照してください。
デフォルト実装
実際、Keycloakには、以下の2つのクライアント認証の実装が組み込まれています。
- client_idとclient_secretによる従来の認証
-
これは、 OpenID Connect または OAuth2 の仕様で記載されているデフォルトのメカニズムであり、Keycloakは初期段階からサポートしています。パブリック・クライアントは、POSTリクエストに自身のIDを指定した
client_id
パラメーターが含まれている必要があり(実際は認証されていません)、コンフィデンシャル・クライアントは、Authorization: Basic
ヘッダーとともに、ユーザー名とパスワードとして使用されるクライアントIDとクライアント・シークレットが含まれている必要があります。 - 署名付きJWTによる認証
-
これは、 JWT Bearer Token Profiles for OAuth 2.0 仕様に基づいています。クライアントまたはアダプターは、 JWT を生成し、秘密鍵で署名します。Keycloakは、署名されたJWTをクライアントの公開鍵で検証し、それに基づいてクライアントを認証します。
署名付きJWTでクライアント認証を使用するアプリケーションを示すアプリケーションの例については、デモのサンプルと、特に examples/preconfigured-demo/product-app
を参照してください。
独自のクライアント・オーセンティケーターの実装
独自のクライアント・オーセンティケーターをプラグインするには、クライアント(アダプター)とサーバー側の両方にいくつかのインターフェイスを実装する必要があります。
- クライアント側
-
ここでは、
org.keycloak.adapters.authentication.ClientCredentialsProvider
を実装し、以下のいずれかにその実装を入れる必要があります。-
WARファイル内のWEB-INF/classes。ただし、この場合、実装はこの単一の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.ClientAuthenticatorFactory
とorg.keycloak.authentication.ClientAuthenticator
を実装する必要があります。また、META-INF/services/org.keycloak.authentication.ClientAuthenticatorFactory
ファイルに実装クラスの名前を追加する必要があります。詳細については、オーセンティケーター を参照してください。
アクション・トークンSPI
アクション・トークンとは、Json Web Token(JWT)の特別なインスタンスで、その持参者がいくつかの動作を実行できるようにするものです。たとえば、パスワードをリセットしたり、電子メールアドレスを検証したりすることができます。それらは通常、特定のレルムのアクション・トークンを処理するエンドポイントを指すリンクの形式でユーザーに送信されます。
Keycloakは、4つの基本のトークンタイプを提供して、持参者が以下を実行できるようにします。
-
クレデンシャルのリセット
-
電子メールアドレスの確認
-
必須アクションの実行
-
外部のアイデンティティー・プロバイダーのアカウントを使用した、アカウントのリンク付けの確認
これに加えて、アクショントークンSPIを使用して認証セッションを開始したり、変更したりする機能を実装することができます。これについて、詳しくは、以下で説明していきます。
アクション・トークンの解剖
アクション・トークンは、ペイロードに複数のフィールドが含まれている、アクティブなレルム鍵で署名された標準のJson Web Tokenです。
-
typ
- アクションの識別子(例:verify-email
) -
iat
とexp
- トークンの有効期限 -
sub
- ユーザーのID -
azp
- クライアント名 -
iss
- 発行者 - 発行するレルムのURL -
aud
- Audience - 発行するレルムのURLを含むリスト -
asid
- 認証セッションのID (optional) -
nonce
- 操作が1回しか実行できない場合の、使用の一意性を保証するためのランダムなノンス (optional)
さらに、アクション・トークンには、JSON内にシリアライズできるカスタム・フィールドをいくつか含めることができます。
アクション・トークン処理
アクション・トークンがKeycloakエンドポイント KEYCLOAK_ROOT/auth/realms/master/login-actions/action-token
に key
パラメーターを介して渡されると、それが検証され、適切なアクション・トークン・ハンドラーが実行されます。 この処理は常に認証セッションのコンテキストに沿って行われ 、新しいものかアクション・トークン・サービスが既存の認証セッションに加わります(詳しくは、後で説明します)。アクション・トークン・ハンドラーは、トークン(これで認証セッションを変更することが多い)によって規定されたアクションを実行し、HTTPレスポンスを返すことができます(たとえば、認証を続けるか、情報またはエラーページを表示することができます)。これらの手順について、詳しくは以下のとおりです。
-
基本的なアクショントークンの検証 。シグネチャーと時間の有効性がチェックされ、アクション・トークン・ハンドラーが
typ
フィールドに基づいて決定されます。 -
認証セッションの決定。 アクション・トークンURLが既存の認証セッションを使用するブラウザーで開かれ、トークンにブラウザーからの認証セッションと一致する認証セッションIDが含まれている場合、アクション・トークンの検証と処理により、この進行中の認証セッションが追加されます。 そうでない場合は、アクション・トークン・ハンドラーはその時点でブラウザーに存在する他の認証セッションを置き換える新しい認証セッションを作成します。
-
トークンタイプ固有のトークンの検証。 アクション・トークン・エンドポイント・ロジックは、トークンにユーザー(
sub
フィールド)とクライアント(azp
)が存在し、無効ではないことを検証します。次に、アクション・トークン・ハンドラー内で定義されたカスタム・バリデーションがすべて検証されます。さらに、トークン・ハンドラーは、トークンを使い捨てるように要求できます。これで、すでに使用済みのトークンは、アクション・トークン・エンドポイント・ロジックによって拒否されます。 -
アクションの実行。 これらの検証がすべて行われた後で、トークン内のパラメーターに応じて実際にアクションを実行する、アクション・トークン・ハンドラー・コードが呼び出されます。
-
使い捨てトークンの無効化。 トークンを使い捨てるように設定されている場合、認証フローが完了次第、アクション・トークンは無効になります。
独自のアクション・トークンとそのハンドラーの実装
アクション・トークンの作成方法
アクション・トークンは、必須フィールドがほとんどない署名付きJWTなので、Keycloakの JWSBuilder
クラスを使用してシリアライズして署名することができます(アクショントークンの解剖 を参照してください)。この方法は、 org.keycloak.authentication.actiontoken.DefaultActionToken
の serialize(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;
}
}
クラスのパッケージ化とデプロイ
独自のアクション・トークンとそのハンドラーをプラグインするには、サーバー側のインターフェイスを少し実装する必要があります。
-
org.keycloak.authentication.actiontoken.ActionTokenHandler
- 特定のアクションに対する(つまり、typ
トークン・フィールドに指定された値に対する)アクション・トークンの実際のハンドラー。そのインターフェイス内の中心的なメソッドは、アクション・トークンを受け取る際に実行される実際の操作を定義する
handleToken(token, context)
です。通常、それは認証セッション記録の変更ですが、一般的には任意です。このメソッドは、ベリファイア(getVerifiers(context)
で定義されたものを含む)がすべて成功し、token
がgetTokenClass()
メソッドによって返されるクラスであることが保証されている場合にのみ呼び出されます。上記の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
実装を、このガイドのサービス・プロバイダー・インターフェイスのセクションで説明するとおりに、登録しなければならないことに注意してください。
イベントリスナーSPI
イベントリスナー・プロバイダーの作成は、 EventListenerProvider
インタフェースと EventListenerProviderFactory
インタフェースを実装することから始まります。これを行う方法の詳細については、Javadocとサンプルを参照してください。
カスタム・プロバイダーをパッケージングしてデプロイする方法についての詳細は、サービス・プロバイダー・インターフェイスの章を参照してください。
SAMLロールマッピングSPI
Keycloakは、SP環境に存在するロールにSAMLロールをマッピングするためのSPIを定義します。サードパーティーのIDPによって返されるロールは、SPアプリケーション用に定義されたロールに常に対応するとは限らないため、SAMLロールを異なるロールにマッピングできるメカニズムが必要です。SAMLアサーションからロールを抽出してコンテナのセキュリティー・コンテキストをセットアップした後、SAMLアダプターによって使用されます。
org.keycloak.adapters.saml.RoleMappingsProvider
SPIは、実行可能なマッピングに制限を課しません。実装は、ロールを他のロールにマッピングするだけでなく、ユースケースに応じてロールを追加または削除することができます(したがって、SAMLプリンシパルに割り当てられたロールのセットを増やしたり減らしたりします)。
SAMLアダプターのロールマッピング・プロバイダーの設定の詳細、および利用可能なデフォルトの実装の説明については、Securing Applications and Services Guideを参照してください。
カスタム・ロールマッピング・プロバイダーの実装
カスタム・ロールマッピング・プロバイダーを実装するには、最初に org.keycloak.adapters.saml.RoleMappingsProvider
インターフェイスを実装する必要があります。次に、カスタム実装の完全修飾名を含む META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider
ファイルを、実装クラスも含むアーカイブに追加する必要があります。このアーカイブには次のものがあります。
-
プロバイダー・クラスがWEB-INF/classesに含まれているSPアプリケーションWARファイル。
-
SPアプリケーションのWARのWEB-INF/libに追加されるカスタムJARファイル。
-
(WildFly/JBoss EAPのみ)
jboss module
として設定され、SPアプリケーションのWARのjboss-deployment-structure.xml
で参照されるカスタムJARファイル。
SPアプリケーションがデプロイされると、使用されるロールマッピング・プロバイダーは、 keycloak-saml.xml
または keycloak-saml
サブシステムで設定されたIDによって選択されます。したがって、カスタム・プロバイダーを有効にするには、アダプター設定でそのIDが適切に設定されていることを確認するだけです。
ユーザー・ストレージSPI
ユーザー・ストレージSPIを使用して、外部ユーザー・データベースとクレデンシャル・ストアに接続するように、Keycloakの拡張機能を実装できます。組み込みのLDAPとActive Directoryのサポートは、このSPIを実際に実装したものです。設定などの作業をせず、すぐにKeycloakはローカル・データベースを使用して、ユーザーの作成、更新、検索とクレデンシャルの検証をします。しかし、多くの場合、組織はKeycloakのデータモデルに移行できない外部の独自ユーザー・データベースをすでに持っています。このような状況では、アプリケーション開発者は、ユーザー・ストレージSPIの実装をコーディングして、外部ユーザーストアと、Keycloakがユーザーのログインおよび管理に使用する内部ユーザー・オブジェクト・モデルをブリッジすることができます。
Keycloakランタイムは、ユーザーがログインしているときなどの、ユーザーを検索する必要があるときに、ユーザーを特定するためいくつかの手順を実行します。まず、ユーザーがユーザー・キャッシュに入っているかどうかを調べます。ユーザーが見つかった場合は、そのメモリー内表現を使用します。次に、Keycloakローカル・データベース内のユーザーを探します。ユーザーが見つからない場合は、ユーザー・ストレージSPIプロバイダーの実装をループして、そのうちの1つがランタイムが探しているユーザーを返すまでユーザークエリーを実行します。プロバイダーは外部ユーザーストアにユーザーを照会し、ユーザーの外部データ表現をKeycloakのユーザー・メタモデルにマップします。
ユーザー・ストレージSPIプロバイダーの実装では、複雑な条件のクエリーを実行したり、ユーザーに対してCRUD操作を実行したり、クレデンシャルを検証および管理したり、一度に多くのユーザーの一括更新を実行することもできます。これは外部ストアの機能に依存します。
ユーザー・ストレージSPIプロバイダーの実装は、Jakarta EEコンポーネントと同様にパッケージ化され、デプロイされます(しばしばJakarta EEコンポーネントです)。デフォルトでは有効になっていませんが、管理コンソールの User Federation
タブでレルムごとに有効にして設定する必要があります。
ユーザー・プロバイダーの実装で、ユーザーのアイデンティティーをリンク/確立するためのメタデータ属性として一部のユーザー属性を使用している場合は、ユーザーが属性を編集できず、対応する属性が読み取り専用であることを確認してください。この例は、組み込みのKeycloak LDAPプロバイダーがLDAPサーバー側でユーザーのIDを保存するために使用している LDAP_ID 属性です。詳細については 脅威モデルの緩和の章 を参照してください。
|
プロバイダー・インターフェイス
ユーザー・ストレージ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 shown 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は管理コンソールのUser Federationページ内に表示されます。
create()
メソッドは、プロバイダー・クラスのインスタンスの割り当てを担当します。これは org.keycloak.models.KeycloakSession
パラメーターを受け取ります。このオブジェクトは、ランタイム内で他のさまざまなコンポーネントへアクセスできるようにするだけでなく、他の情報やメタデータを参照することにも使用できます。 ComponentModel
パラメーターは、特定のレルム内でプロバイダーがどのように有効にされ設定されたかを表します。これには、有効なプロバイダーのインスタンスIDと、管理コンソールで有効にした際に指定した可能性がある設定が含まれます
UserStorageProviderFactory
には他にも機能がありますが、それはこの章で後ほど説明します。
プロバイダー・ケイパビリティー・インターフェイス
UserStorageProvider
インターフェイスを注意深く見てみると、ユーザーを検索したり管理したりするメソッドが定義されていないことに気がつくでしょう。これらのメソッドは ケイパビリティー・インターフェイス として定義されており、外部ユーザーストアが持つ機能に依存しています。たとえば、あるユーザーストアがリードオンリーで単純なクエリーとクレデンシャル検証機能のみ持つ場合、必要な作業はそれらの機能のみを ケイパビリティー・インターフェイス として実装することだけです。
SPI | 説明 |
---|---|
|
このインターフェイスは外部ストアからのユーザーをログインさせるのに必要です。ほとんど(全ての?)プロバイダーはこのインターフェイスを実装します。 |
|
1つ以上のユーザーを検索する複雑なクエリーを定義します。管理コンソールでユーザーを閲覧・管理する場合はこのインターフェイスを実装する必要があります。 |
|
プロバイダーがユーザーの削除をサポートする場合はこのインターフェイスを実装します。 |
|
プロバイダーがユーザーのバルク更新をサポートする場合はこのインターフェイスを実装します。 |
|
プロバイダーが1つ以上の異なるクレデンシャル・タイプを検証する(たとえば、プロバイダーがパスワード検証をする)場合はこのインターフェイスを実装します。 |
|
プロバイダーが1つ以上の異なるクレデンシャル・タイプを更新する場合はこのインターフェイスを実装します。 |
モデル・インターフェイス
ケイパビリティー・インターフェイス で定義されたメソッドのほとんどは、ユーザーの表現が返されるか、または渡されます。これらの表現は、 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メタモデルの他の部分を表す他のモデルクラス、 RealmModel
、 RoleModel
、 GroupModel
、および ClientModel
があります。
ストレージ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を再度解析し、それを外部ユーザー・ストレージにユーザーを配置するために使用します。
パッケージ化とデプロイ
ユーザー・ストレージ・プロバイダーは、WildFlyアプリケーション・サーバーに何かをデプロイするのと同じ方法で、JARにパッケージ化され、Keycloakランタイムにデプロイまたはアンデプロイされます。JARをサーバーの standalone/deployments/
ディレクトリーに直接コピーするか、または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のホット・デプロイメントをサポートしています。また、この章の後半では、Jakarta EEコンポーネント内およびJava EEコンポーネントとしてパッケージ化できることが分かります。
簡単な読み取り専用の参照のサンプル
ユーザー・ストレージSPIの基本実装を示すために、簡単なサンプルで説明します。この章では、簡単なプロパティー・ファイル内でユーザーを検索する簡単な UserStorageProvider
の実装を見ていきます。プロパティー・ファイルには、ユーザー名とパスワードの定義が含まれており、クラスパス上の特定のロケーションにハードコードされています。プロバイダーにより、IDとユーザー名でユーザーを検索し、パスワードを検証することもできるようになります。このプロバイダーに基づくユーザーは、読み取り専用になります。
プロバイダー・クラス
はじめに 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;
}
このプロバイダー・クラスのコンストラクターには、 KeycloakSession
、 ComponentModel
、およびプロパティー・ファイルへの参照が格納されます。後で、これらをすべて使用します。また、ロードされたユーザーのマップがあることにも注意してください。ユーザーを見つけるたびに、このマップに保存して、同じトランザクション内でそれを再度作成しないで済むようにします。多くのプロバイダがこれを行う必要があるので、従うことは良い習慣です(つまり、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(PasswordCredentialModel.TYPE) && password != null;
}
@Override
public boolean supportsCredentialType(String credentialType) {
return credentialType.equals(PasswordCredentialModel.TYPE);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType())) return false;
String password = properties.getProperty(user.getUsername());
if (password == null) return false;
return password.equals(input.getChallengeResponse());
}
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(PasswordCredentialModel.TYPE)) 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
がスローされます。
プロバイダー・ファクトリーの実装
プロバイダー・クラスに関しては完了したので、それでは次にプロバイダー・ファクトリー・クラスを見ていきましょう。
public class PropertyFileUserStorageProviderFactory
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
public static final String PROVIDER_NAME = "readonly-property-file";
@Override
public String getId() {
return PROVIDER_NAME;
}
まず最初に注意すべき点は、 UserStorageProviderFactory
クラスを実装する際、テンプレート・パラメーターとして具体的なプロバイダー・クラスの実装を渡す必要があるということです。ここでは、以前に定義したプロバイダー・クラス PropertyFileUserStorageProvider
を指定します。
テンプレート・パラメーターを指定しないと、プロバイダーは機能しません。ランタイムは、クラスのイントロスペクションが実行し、プロバイダーが実装する ケイパビリティー・インターフェース を定義します。 |
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.xml
、 standalone-ha.xml
または domain.xml
で設定できるファクトリー設定です。
たとえば、以下を 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メソッドはトランザクション毎に都度呼び出されます。
パッケージ化とデプロイ
プロバイダー実装のためのクラス・ファイルはjar内に置かれる必要があります。また、 META-INF/services/org.keycloak.storage.UserStorageProviderFactory
ファイル内でプロバイダー・ファクトリー・クラスを宣言する必要もあります。
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
一度JARを作成すれば、通常のWildFlyの方法を使って、それをデプロイすることができるようになります。その方法とは、JARスタンドアローンを standalone/deployments/
ディレクトリーにコピーするか、JBoss CLIを使用することです。
管理コンソール内でのプロバイダーの有効化
管理コンソールの User Federation
ページ内で、レルム毎にユーザー・ストレージ・プロバイダーを有効にします。
readonly-property-file
というリストから作成したプロバイダーを選択します。これで、プロバイダーの設定ページに移動します。何も設定する必要はないので Save をクリックします。
メインの User Federation
ページに戻ると、リストアップされたプロバイダーが表示されます。
これで、 users.properties
ファイル内で宣言されたユーザーを使用してログインすることができます。このユーザーは、ログイン後、アカウントページを見ることしかできません。
設定のテクニック
PropertyFileUserStorageProvider
のサンプルには少し工夫が必要です。プロバイダーのjarに組み込まれたプロパティー・ファイルにハードコードされていて、これはあまり便利ではありません。プロバイダーのインスタンス毎にこのファイルの場所を設定できるようにすることをお勧めします。つまり、このプロバイダーを複数の異なるレルムで複数回再利用して、まったく異なるユーザー・プロパティー・ファイルを指すようにしたい場合があります。また、この設定は管理コンソールのUI内でも変更できるようにすることをお勧めします。
UserStorageProviderFactory
には、プロバイダーの設定を処理する、実装可能な追加のメソッドがあります。プロバイダーごとに設定したい変数を記述すると、管理コンソールが自動的に汎用入力ページを表示してこの設定を収集します。実装されると、プロバイダーが初めて作成された時、および更新時に、callbackメソッドが設定を保存する前に検証します。 UserStorageProviderFactory
は org.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
インスタンスのリストを返します。これらのインスタンスは、プロバイダーの設定変数をそれぞれレンダリングして格納するのに必要なメタデータを宣言します。
設定のサンプル
プロバイダー・インスタンスがディスク上の特定のファイルを参照するように、 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
クラスは、設定プロパティーのリストを作成するための優れたヘルパークラスです。ここでは、String型の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);
}
このロジックは、すべてのトランザクションがディスクからユーザー・プロパティー・ファイル全体を読み込むので、もちろん非効率ですが、設定変数にフックする簡単な方法を示しているはずです。
ユーザーの追加/削除およびクエリーのケイパビリティー・インターフェイス
これまでのサンプルで行っていないことの1つは、ユーザーの追加と削除やパスワードの変更をできるようにすることです。これまでのサンプルで定義されたユーザーは、管理コンソールで照会することも表示することもできません。これらの拡張機能を追加するには、サンプル・プロバイダーで UserQueryProvider
と UserRegistrationProvider
インターフェイスを実装する必要があります。
UserRegistrationProviderの実装
特定のストアに対するユーザーの追加と削除を実装するためには、まずプロパティー・ファイルをディスクに保存できるようにする必要があります。
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()
メソッドの実装は単純になります。
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
を返すことで、このプロバイダーをスキップして、次を呼び出すようにできます。
@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());
}
これでプロパティー・ファイルを保存できるようになったので、パスワードも更新できるようになります。
@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;
}
また、パスワードを無効にすることができます。
@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;
}
これらのメソッドを実装すると、管理コンソールでユーザーのパスワードの変更や無効化ができるようになります。
UserQueryProviderの実装
UserQueryProvider
を実装しなければ、管理コンソールはサンプル・プロバイダーによってロードされたユーザーを表示および管理できません。このインターフェイスの実装を見てみましょう。
@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;
}
getUsers()
メソッドはプロパティー・ファイルのキーセットを反復し、 getUserByUsername()
に委譲してユーザーをロードします。 firstResult
と maxResults
パラメーターに基づいてこの呼び出しにインデックスを付けることに注目してください。外部ストアがページネーションをサポートしていない場合は、同様のロジックを実行する必要があります。
@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
パラメーターを受け取ります。これは、ユーザー名と電子メールの属性を検索してユーザーを見つけるために使用する文字列です。この文字列は部分文字列にすることができるので、検索を行うときに String.contains()
メソッドを使用しています。
@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()
に処理を委譲しています。
@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;
}
グループや属性は保存しないので、他のメソッドは空のリストを返します。
外部ストレージの拡張
PropertyFileUserStorageProvider
のサンプルは実際には限定されています。プロパティー・ファイルに保存されているユーザーでログインすることはできますが、他のことはあまりできません。このプロバイダーによってロードされたユーザーが、特定のアプリケーションに完全にアクセスするための特殊なロールまたはグループ・マッピングを必要とする場合、これらのユーザーにロールマッピングを追加する方法はありません。また、電子メール、姓名などの重要な属性を変更または追加することもできません。
このような状況の場合、KeycloakはKeycloakのデータベースに追加の情報を保存することで、外部ストアを拡張することができます。これはフェデレーテッド・ユーザー・ストレージと呼ばれ、 org.keycloak.storage.federated.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
が付属しています。外部ストレージ表現に委譲するため、オーバーライドする必要があるメソッドはオーバーライドします。このクラスは、オーバーライドしてもよい、より小さなprotectedのメソッドを持っているため、Javadocを読むことを強くお勧めします。特に、グループ・メンバーシップとロールマッピングのあたりを読んでください。
拡張の例
PropertyFileUserStorageProvider
の例では、 AbstractUserAdapterFederatedStorage
を使うためにプロバイダーに簡単な変更が必要です。
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()
メソッドはプロパティー・ファイルを変更して保存します。
インポート実装による方法
ユーザー・ストレージ・プロバイダーを実装する場合、選択可能な別の方法がもう1つあります。ユーザー・フェデレーティッド・ストレージを使用せずに、ローカルのKeycloakの組み込みユーザー・データベース内でユーザーを作成し、外部のストアからこのローカルコピーに属性をコピーすることができます。このアプローチにはたくさんの利点があります。
-
Keycloakは、基本的に外部ユーザーストア用の永続的なユーザー・キャッシュになります。ユーザーがインポートされると、外部ストアにヒットすることはなくなり、その負荷が取り除かれます。
-
公式のユーザーストアをKeycloakにし、従来の外部ストアは廃止する場合、アプリケーションを徐々に移行してKeycloakを使用することができます。すべてのアプリケーションが移行されたら、インポートされたユーザーのリンクを解除し、従来のレガシー外部ストアは廃止します。
インポートによる方法の使用には明らかな欠点がいくつかあります。
-
初回のユーザーの検索では、Keycloakのデータベースを複数回更新する必要があります。これは大きなパフォーマンス低下を招き、Keycloakのデータベースに多くの負担をかけることになります。ユーザー・フェデレーティッド・ストレージのアプローチでは、必要に応じて追加のデータのみが保存されるだけであり、外部ストアの機能によってはまったく使用されない可能性もあります。
-
インポートのアプローチでは、ローカルのKeycloakストレージと外部ストレージを同期させておく必要があります。ユーザー・ストレージSPIには、同期をサポートできるために実装できるケーパビリティー・インターフェイスがありますが、これはすぐにやっかいで面倒なものになります。
インポートによる方法を実装する場合は、ユーザーがローカルにインポートされているかどうかを初回のみ確認します。インポートされている場合は、ローカルユーザーを返します。インポートされていない場合は、ユーザーをローカルで作成して外部ストアからデータをインポートします。ほとんどの変更が自動的に同期されるように、ローカルユーザーをプロキシーすることもできます。
少し工夫が必要ですが、 PropertyFileUserStorageProvider
を拡張してこのアプローチをとることができます。まずは createAdapter()
メソッドを変更することからはじめます。
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つです。
|
もう1つ注意することは、ローカルユーザーがリンクされている場合、ストレージ・プロバイダーは CredentialInputValidator
インターフェイスと CredentialInputUpdater
インターフェイスから実装されているメソッドに委譲されることです。検証または更新から false
が返ると、Keycloakは、ローカル・ストレージを使用して検証または更新できるかどうかを確認します。
また、 org.keycloak.models.utils.UserModelDelegate
クラスを使用しているローカルユーザーをプロキシーしている点にも注意してください。このクラスは UserModel
の実装です。どのメソッドもインスタンス化された UserModel
に委譲するだけです。この委譲クラスの setUsername()
メソッドをオーバーライドし、自動的にプロパティー・ファイルと同期させます。プロバイダーの場合は、これを使用して、ローカルの UserModel
上の他のメソッドを インターセプト して、外部ストアと同期させることができます。たとえば、getメソッドは、ローカルストアが同期していることを確認することができます。setメソッドは、外部ストアをローカルストアと同期し続けることができます。注目すべきは、 getId()
メソッドは、ユーザーをローカルで作成したときに自動的に生成されたIDを常に返すべきだということです。他のインポートではないサンプルが示すように、フェデレーションIDを返すべきではありません。
プロバイダーが UserRegistrationProvider インターフェイスを実装している場合、 removeUser() メソッドはローカル・ストレージからユーザーを削除する必要はありません。ランタイムがこの操作を自動的に実行します。また、 removeUser() は、ローカル・ストレージから削除される前に呼び出される点に注意してください。
|
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
を返すと、ローカルユーザーはデータベースから削除されます。
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()
メソッドが呼び出されます。
ユーザー・キャッシュ
ID、ユーザー名、または電子メールのクエリーによってユーザー・オブジェクトが読み込まれると、キャッシュされます。ユーザー・オブジェクトがキャッシュされると、それは UserModel
インターフェイス全体を反復し、この情報をローカルのメモリー内専用キャッシュに保持します。クラスターでは、このキャッシュはまだローカルですが、無効化キャッシュになります。ユーザー・オブジェクトが変更されると、キャッシュは削除されます。このエビクション・イベントはクラスター全体に伝播され、他のノードのユーザー・キャッシュも無効になります。
ユーザー・キャッシュの管理
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();
}
特定のユーザー、レルムに含まれるユーザー、またはキャッシュ全体を削除するメソッドがあります。
OnUserCacheコールバック・インターフェイス
プロバイダーの実装に固有の追加情報をキャッシュすることができます。ユーザー・ストレージ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()
メソッドを使ってユーザーのクレデンシャルをキャッシュします。
Jakarta EEの活用
プロバイダーを指し示す、 META-INF/services
ファイルを正しく設定すると、ユーザー・ストレージ・プロバイダーは任意のJakarta EEコンポーネント内にパッケージ化することができます。たとえば、プロバイダーがサードパーティー・ライブラリーを使用する必要がある場合は、EAR内でプロバイダーをパッケージ化し、これらのサードパーティー・ライブラリーをEARの lib/
ディレクトリーに格納することができます。また、プロバイダーJARは、EJB、WAR、およびEARがWildFly環境で使用できる jboss-deployment-structure.xml
ファイルを利用できます。このファイルの詳細については、WildFlyのドキュメントを参照してください。他のきめ細かなアクション間の外部依存関係を引き出すことができます。
プロバイダーの実装はプレーンな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はクリーンアップされず、最終的にエラーメッセージが表示されることがあります。
UserStorageProvider
の実装はプレーンな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はサポートされていません。
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();
以前のユーザー・フェデレーション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を移植するためのいくつかの方法について説明します。
インポート vs. 非インポート
以前のユーザー・フェデレーションSPIでは、Keycloakのデータベース内でユーザーのローカルコピーを作成し、外部ストアからの情報をローカルコピーにインポートする必要がありました。しかし、これはもう要件ではなくなりました。以前のプロバイダーをそのまま移植することもできますが、インポートしない方法の方がより良いアプローチになるかを検討する必要があります。
インポートによる方法の利点は、以下のとおりです。
-
Keycloakは、基本的に外部ストアの永続ユーザーキャッシュになります。ユーザーがインポートされると、外部ストアにアクセスしなくなり、その負荷が無くなります。
-
公式のユーザーストアをKeycloakにし、従来の外部ストアは廃止する場合、アプリケーションを徐々に移行してKeycloakを使用することができます。すべてのアプリケーションが移行されたら、インポートされたユーザーのリンクを解除し、従来のレガシー外部ストアは廃止します。
インポートによる方法の使用には明らかな欠点がいくつかあります。
-
初回のユーザーの検索では、Keycloakのデータベースを複数回更新する必要があります。これは大きなパフォーマンス低下を招き、Keycloakのデータベースに多くの負担をかけることになります。ユーザー・フェデレーティッド・ストレージのアプローチでは、必要に応じて追加のデータのみが保存されるだけであり、外部ストアの機能によってはまったく使用されない可能性もあります。
-
インポートのアプローチでは、ローカルのKeycloakストレージと外部ストレージを同期させておく必要があります。ユーザー・ストレージSPIには、同期をサポートできるために実装できるケーパビリティー・インターフェイスがありますが、これはすぐにやっかいで面倒なものになります。
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()
メソッドは、新しいSPIには存在しません。
UserFederationProvider
のメソッド synchronizeRegistrations()
、 registerUser()
、 removeUser()
は UserRegistrationProvider
のケーパビリティー・インターフェイスに移動されました。この新しいインターフェイスは実装するためにオプションです。プロバイダーがユーザーの作成と削除をサポートしていない場合は、実装する必要はありません。以前のプロバイダーが新しいユーザーの登録をサポートするように切り替えた場合、新しいSPIではこれがサポートされ、プロバイダーがユーザーの追加をサポートしていない場合は、 UserRegistrationProvider.addUser()
から null
が返されます。
クレデンシャルを中心とした以前の UserFederationProvider
メソッドは CredentialInputValidator
と CredentialInputUpdater
インターフェイスにカプセル化されました。この実装は任意で、クレデンシャルの検証や更新をサポートするかどうかによって異なります。クレデンシャル管理は UserModel
メソッドに存在しましたが、これらも CredentialInputValidator
と CredentialInputUpdater
インターフェイスに移行されました。 CredentialInputUpdater
インターフェイスを実装しないと、プロバイダーが提供するクレデンシャルはKeycloakストレージ内でローカルにオーバーライドされる可能性があることに注意してください。したがって、クレデンシャルを読み取り専用にする場合は、 CredentialInputUpdater.updateCredential()
メソッドを実装し、 ReadOnlyException
を返します。
searchByAttributes()
や getGroupMembers()
のような UserFederationProvider
のクエリーメソッドは、オプションのインターフェイス UserQueryProvider
にカプセル化されました。このインターフェイスを実装しないと、管理コンソールでユーザーを表示できなくなります。なお、ログインすることはできます。
UserFederationProviderFactory vs. UserStorageProviderFactory
以前のSPIの同期メソッドは、現在、オプションの ImportSynchronization
インターフェイスにカプセル化されています。同期ロジックを実装している場合は、新しい UserStorageProviderFactory
に ImportSynchronization
インターフェイスを実装してください。
新しいモデルへのアップグレード
ユーザーストレージSPIインスタンスは、異なる一連のリレーショナル・テーブルに格納されます。 Keycloakは自動的に移行スクリプトを実行します。レルムに対して以前のユーザー・フェデレーション・プロバイダーがデプロイされている場合、データの ID
を含め、それ以降のストレージモデルにそのまま変換されます。この移行は、以前のユーザー・フェデレーション・プロバイダーと同じプロバイダーID("ldap"、"kerberos"など)を持つユーザー・ストレージ・プロバイダーが存在する場合にのみ発生します。
これを知ることにより、取ることができるさまざまなアプローチがあります。
-
以前のKeycloakのデプロイメントで、以前のプロバイダーを削除することができます。これにより、インポートした全ユーザーのローカルリンクされたコピーが削除されます。次に、Keycloakをアップグレードするときに、レルム用に新しいプロバイダーをデプロイして設定するだけです。
-
2つ目のオプションは、プロバイダーIDが同じであることを
UserStorageProviderFactory.getId()
で確認する新しいプロバイダーを実装することです。このプロバイダーが新しいKeycloakインストールのstandalone/deployments/
ディレクトリーにあることを確認してください。サーバーを起動し、組み込みの移行スクリプトで以前のデータモデルを新しいデータモデルに変換します。この場合、以前にリンクされたインポート済みユーザーは、正常に動作し、すべて同じになります。
インポートによる方法を廃止してユーザー・ストレージ・プロバイダーを実装し直すことに決めた場合は、Keycloakをアップグレードする前に以前のプロバイダーを削除することをお勧めします。これにより、インポートされたすべてのユーザーのリンクされたローカル・インポート済みのコピーが削除されます。
ストリームベースのインターフェイス
Keycloakのユーザー・ストレージ・インターフェイスの多くには、潜在的に大きなオブジェクトのセットを返す可能性のあるクエリーメソッドが含まれているため、メモリー消費と処理時間の点で重大な影響が生じる可能性があります。これは、オブジェクトの内部状態の小さなサブセットのみがクエリーメソッドのロジックで使用される場合に特に当てはまります。
これらのクエリーメソッドで大規模なデータセットを処理するためのより効率的な代替手段を開発者に提供するために、ユーザー・ストレージ・インターフェイスに Streams
サブ・インターフェイスが追加されました。これらの Streams
サブ・インターフェイスは、スーパー・インターフェイスの元のコレクションベースのメソッドをストリームベースの異なる形に置き換え、コレクション・ベースのメソッドをデフォルトにします。コレクション・ベースのクエリーメソッドのデフォルトの実装は、対応する Stream
を呼び出し、結果を適切なコレクション・タイプに収集します。
Streams
サブ・インターフェイスにより、実装はデータセットを処理するためのストリームベースのアプローチにフォーカスし、そのアプローチの潜在的なメモリーとパフォーマンスの最適化から利益を得ることができます。実装される Streams
サブ・インターフェイスを提供するインターフェイスには、いくつかの ケイパビリティー・インターフェイス ( org.keycloak.storage.federated
パッケージ内のすべてのインターフェイスと、カスタムストレージ実装のスコープに応じて実装される可能性のあるその他のいくつかのインターフェイス)が含まれます。
開発者に Streams
サブ・インターフェイスを提供するインターフェイスのリストを参照してください。
Package |
Classes |
|
|
|
|
|
|
|
すべてのインターフェイス |
|
|
(*) はインターフェイスが_ケイパビリティー・インターフェイス_であることを示します
ストリーム・アプローチの恩恵を受けたいカスタム・ユーザー・ストレージの実装では、元のインターフェイスではなく、単に Streams
サブ・インターフェイスを実装する必要があります。たとえば、次のコードは、 UserQueryProvider
インターフェイスの Streams
の異形を使用しています。
public class CustomQueryProvider extends UserQueryProvider.Streams {
...
@Override
Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults) {
// custom logic here
}
@Override
Stream<UserModel> searchForUserStream(String search, RealmModel realm) {
// custom logic here
}
...
}
ボールトSPI
ボールト・プロバイダー
Keycloakのカスタム拡張機能を記述し、任意のボールト実装に接続するために、 org.keycloak.vault
パッケージのボールトSPIを使用できます。
ビルトインの files-plaintext
プロバイダーは、このSPIの実装の一例です。一般的に、次のルールが適用されます。
-
シークレットがレルム間で漏洩するのを防ぐために、レルムによって取得できるシークレットを隔離または制限したい場合があります。その場合、プロバイダーは、たとえば、エントリーにプレフィックスとしてレルム名を付けるなどして、シークレットを検索するときにレルム名を考慮する必要があります。たとえば、式
${vault.key}
は、それがレルム A またはレルム B で使用されたかどうかに応じて、一般的に異なるエントリー名に評価されます。レルムを区別するには、KeycloakSession
パラメーターから利用できるVaultProviderFactory.create()
メソッドから作成されたVaultProvider
インスタンスにレルムを渡す必要があります。 -
ボールト・プロバイダーは、指定されたシークレット名に対して
VaultRawSecret
を返す単一のメソッドobtainSecret
を実装する必要があります。このクラスは、byte[]
またはByteBuffer
のいずれかでシークレットの表現を保持し、必要に応じて2つの間で変換することを期待されています。以下で説明するように、このバッファーは使用後に破棄されることに注意してください。
レルムの分離に関して、すべての組み込みのボールト・プロバイダー・ファクトリーでは、1つ以上のキーリゾルバーを設定できます。 VaultKeyResolver
インターフェイスで表されるキーリゾルバーは基本的に、レルム名とキー( ${vault.key}
式から得られる)を組み合わせて、ボールトからシークレットを取得するために使用される最終的なエントリー名を得るアルゴリズムまたは戦略を実装します。この設定を処理するコードは、抽象ボールト・プロバイダーおよびボールト・プロバイダー・ファクトリー・クラスに抽出されているため、キーリゾルバーのサポートを提供するカスタム実装は、SPIインターフェースを実装する代わりにこれらの抽象クラスを拡張して、シークレットを取得するときに試されるキーリゾルバーを設定する機能を継承できます。
カスタム・プロバイダーをパッケージングしてデプロイする方法についての詳細は、サービス・プロバイダー・インターフェイスの章を参照してください。
ボールトから取得した値を消費する
ボールトには機密データが含まれており、Keycloakはそれに応じてシークレットを扱います。シークレットにアクセスすると、シークレットはボールトから取得され、必要な時間だけJVMメモリーに保持されます。その後、JVMメモリーからコンテンツを破棄するすべての可能な試行が行われます。これは、以下で概説するように、 try-with-resources
ステートメント内でのみボールト・シークレットを使用することで実現されます。
char[] c;
try (VaultCharSecret cSecret = session.vault().getCharSecret(SECRET_NAME)) {
// ... use cSecret
c = cSecret.getAsArray().orElse(null);
// if c != null, it now contains password
}
// if c != null, it now contains garbage
この例では、シークレットにアクセスするためのエントリー・ポイントとして KeycloakSession.vault()
を使用しています。 VaultProvider.obtainSecret
メソッドを直接使用することもできます。しかし、 vault()
メソッドは、( vault().getRawSecret()
メソッドを介して)元の未解釈の値を取得することに加えて、文字配列( vault().getStringSecret()
経由)または String
( vault().getStringSecret()
)で平文のシークレット(通常はバイト配列)を解釈できる利点があります。
String
オブジェクトは不変であるため、ランダムなガーベージでオーバーライドしてもコンテンツを破棄できないことに注意してください。デフォルトの VaultStringSecret
の実装では String
の内部化を防ぐための対策が講じられていますが、 String
オブジェクトに保存されたシークレットは少なくとも次のGCラウンドまで存続します。したがって、プレーンなバイト配列と文字配列およびバッファーを使用することをお勧めします。