まえがき
一部のリスト例では、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
Admin REST API
Keycloak には、Admin Console で提供されるすべての機能を備えた、完全に機能する Admin REST API が付属しています。
API を呼び出すには、適切な権限を持つアクセストークンを取得する必要があります。必要な権限については、サーバー管理ガイドで説明されています。
トークンを取得するには、Keycloak を使用してアプリケーションの認証を有効にします。アプリケーションとサービスのセキュリティ保護ガイドを参照してください。ダイレクトアクセスグラントを使用してアクセストークンを取得することもできます。
CURL の使用例
ユーザー名とパスワードによる認証
以下の例では、入門ガイドチュートリアルで示されているように、master レルムにパスワード password を持つユーザー admin を作成したことを前提としています。 |
-
ユーザー名
admin
とパスワードpassword
を使用して、レルムmaster
のユーザーのアクセストークンを取得します。curl \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=password" \ -d "grant_type=password" \ "https://127.0.0.1:8080/realms/master/protocol/openid-connect/token"
デフォルトでは、このトークンは 1 分で期限切れになります。 結果は JSON ドキュメントになります。
-
API を呼び出すには、
access_token
プロパティの値を抽出する必要があります。 -
API へのリクエストの
Authorization
ヘッダーに値を含めることで、API を呼び出します。次の例は、master レルムの詳細を取得する方法を示しています。
curl \ -H "Authorization: bearer eyJhbGciOiJSUz..." \ "https://127.0.0.1:8080/admin/realms/master"
サービスアカウントによる認証
client_id
と client_secret
を使用して Admin REST API に対して認証するには、次の手順を実行します。
-
クライアントが次のように構成されていることを確認してください。
-
client_id
は、レルム master に属するconfidential クライアントです。 -
client_id
は、Service Accounts Enabled
オプションが有効になっています。 -
client_id
にはカスタムの「Audience」マッパーがあります。-
含まれるクライアントオーディエンス:
security-admin-console
-
-
-
client_id
に「サービスアカウントロール」タブで「admin」ロールが割り当てられていることを確認してください。
curl \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "client_secret=<YOUR_CLIENT_SECRET>" \
-d "grant_type=client_credentials" \
"https://127.0.0.1:8080/realms/master/protocol/openid-connect/token"
テーマ
Keycloak は、Web ページとメールのテーマサポートを提供します。これにより、エンドユーザー向けのページのデザインをカスタマイズして、アプリケーションと統合できます。

テーマタイプ
テーマは、Keycloak のさまざまな側面をカスタマイズするために、1 つ以上のタイプを提供できます。利用可能なタイプは次のとおりです。
-
アカウント - アカウントコンソール
-
Admin - Admin コンソール
-
メール - メール
-
ログイン - ログインフォーム
-
ようこそ - ウェルカムページ
テーマの設定
ウェルカムを除くすべてのテーマタイプは、Admin Console を介して構成されます。
-
Admin Console にログインします。
-
左上隅のドロップダウンボックスからレルムを選択します。
-
メニューから レルム設定 をクリックします。
-
テーマ タブをクリックします。
master
Admin Console のテーマを設定するには、master
レルムの Admin Console テーマを設定する必要があります。 -
Admin Console の変更を確認するには、ページを更新してください。
-
ウェルカムテーマを変更するには、
spi-theme-welcome-theme
オプションを使用します。 -
例:
bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme
デフォルトテーマ
Keycloak には、サーバーディストリビューション内の JAR ファイル keycloak-themes-26.2.0.jar
にデフォルトテーマがバンドルされています。サーバーのルート themes
ディレクトリには、デフォルトではテーマは含まれていませんが、デフォルトテーマに関する追加の詳細を含む README ファイルが含まれています。アップグレードを簡素化するために、バンドルされたテーマを直接編集しないでください。代わりに、バンドルされたテーマのいずれかを拡張する独自のテーマを作成してください。
テーマの作成
テーマは以下で構成されます。
-
HTML テンプレート (Freemarker テンプレート)
-
画像
-
メッセージバンドル
-
スタイルシート
-
スクリプト
-
テーマのプロパティ
すべてのページを置き換える予定がない限り、別のテーマを拡張する必要があります。ほとんどの場合、既存のテーマを拡張することになるでしょう。あるいは、admin コンソールまたはアカウントコンソールの独自の実装を提供する場合は、base
テーマの拡張を検討してください。base
テーマはメッセージバンドルで構成されているため、そのような実装は、メインの index.ftl
Freemarker テンプレートの実装を含め、ゼロから開始する必要がありますが、メッセージバンドルから既存の翻訳を活用できます。
テーマを拡張するときは、個々のリソース (テンプレート、スタイルシートなど) をオーバーライドできます。HTML テンプレートをオーバーライドする場合は、新しいリリースにアップグレードするときにカスタムテンプレートを更新する必要がある場合があることに注意してください。
テーマを作成する際は、キャッシュを無効にすることをお勧めします。これにより、Keycloak を再起動せずに themes
ディレクトリからテーマリソースを直接編集できます。
-
次のオプションを指定して Keycloak を実行します。
bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false
-
themes
ディレクトリにディレクトリを作成します。ディレクトリの名前がテーマの名前になります。たとえば、
mytheme
というテーマを作成するには、ディレクトリthemes/mytheme
を作成します。 -
テーマディレクトリ内で、テーマが提供するタイプごとにディレクトリを作成します。
たとえば、ログインタイプを
mytheme
テーマに追加するには、ディレクトリthemes/mytheme/login
を作成します。 -
タイプごとに、テーマの設定を可能にするファイル
theme.properties
を作成します。たとえば、
base
テーマを拡張し、いくつかの共通リソースをインポートするようにテーマthemes/mytheme/login
を構成するには、次の内容でファイルthemes/mytheme/login/theme.properties
を作成します。parent=base import=common/keycloak
これで、ログインタイプをサポートするテーマが作成されました。
-
Admin Console にログインして、新しいテーマを確認してください。
-
レルムを選択します。
-
メニューから レルム設定 をクリックします。
-
テーマ タブをクリックします。
-
ログインテーマ で mytheme を選択し、保存 をクリックします。
-
レルムのログインページを開きます。
これは、アプリケーションからログインするか、アカウントコンソール (
/realms/{realm-name}/account
) を開くことで実行できます。 -
親テーマの変更の効果を確認するには、
theme.properties
でparent=keycloak
を設定し、ログインページを更新します。
パフォーマンスに大きな影響を与えるため、本番環境ではキャッシュを再度有効にしてください。 |
テーマキャッシュの内容を手動で削除する場合は、サーバーディストリビューションの |
テーマのプロパティ
テーマプロパティは、テーマディレクトリのファイル <THEME TYPE>/theme.properties
で設定されます。
-
parent - 拡張する親テーマ
-
import - 別のテーマからリソースをインポート
-
common - 共通リソースパスをオーバーライドします。デフォルト値は、指定されていない場合は
common/keycloak
です。この値は、通常 freemarker テンプレートで使用される${url.resourcesCommonPath}
のサフィックスの値として使用されます (${url.resoucesCommonPath}
値のプレフィックスはテーマルート URI です)。 -
styles - 含めるスタイルのスペース区切りリスト
-
locales - サポートされるロケールのコンマ区切りリスト
特定の要素タイプに使用される CSS クラスを変更するために使用できるプロパティのリストがあります。これらのプロパティのリストについては、keycloak テーマの対応するタイプの theme.properties ファイル (themes/keycloak/<THEME TYPE>/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
プロパティに追加します。たとえば、
styles.css
をmytheme
に追加するには、次の内容で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=css/login.css css/styles.css
親スタイルシートのスタイルをオーバーライドするには、スタイルシートが最後にリストされていることを確認してください。
テーマへのスクリプトの追加
テーマに 1 つ以上のスクリプトを追加できます。
-
テーマの
<THEME TYPE>/resources/js
ディレクトリにファイルを作成します。 -
このファイルを
theme.properties
のscripts
プロパティに追加します。たとえば、
script.js
をmytheme
に追加するには、次の内容で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" alt="My image description">
ログインテーマへのカスタムフッターの追加
カスタムフッターを使用するには、目的の内容でカスタムログインテーマに footer.ftl
ファイルを作成します。
カスタム footer.ftl
の例は次のようになります。
<#macro content>
<#-- footer at the end of the login box -->
<div>
<ul id="kc-login-footer-links">
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
<div>
</#macro>
メールテーマへの画像の追加
画像をテーマで使用できるようにするには、テーマの <THEME TYPE>/email/resources/img
ディレクトリに追加します。これらは、HTML テンプレートで直接使用できます。
たとえば、mytheme
に画像を追加するには、画像を themes/mytheme/email/resources/img/logo.jpg
にコピーします。
HTML テンプレートで直接使用するには、次のものをカスタム HTML テンプレートに追加します。
<img src="${url.resourcesUrl}/img/image.jpg" alt="My image description">
メッセージ
テンプレート内のテキストは、メッセージバンドルからロードされます。別のテーマを拡張するテーマは、親のメッセージバンドルからすべてのメッセージを継承し、<THEME TYPE>/messages/messages_en.properties
をテーマに追加することで個々のメッセージをオーバーライドできます。
たとえば、ログインフォームの Username
を mytheme
の Your Username
に置き換えるには、次の内容でファイル themes/mytheme/login/messages/messages_en.properties
を作成します。
usernameOrEmail=Your Username
メッセージ内では、{0}
や {1}
などの値は、メッセージが使用されるときに引数に置き換えられます。たとえば、{0}
in Log in to {0}
は、レルムの名前に置き換えられます。
これらのメッセージバンドルのテキストは、レルム固有の値で上書きできます。レルム固有の値は、UI および API を介して管理できます。
レルムへの言語の追加
-
レルムの国際化を有効にするには、サーバー管理ガイドを参照してください。
-
テーマのディレクトリにファイル
<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
デフォルトでは、メッセージプロパティファイルは UTF-8 を使用してエンコードする必要があります。Keycloak は、コンテンツを UTF-8 として読み取れない場合、ISO-8859-1 処理にフォールバックします。Unicode 文字は、Java の PropertyResourceBundle のドキュメントで説明されているようにエスケープできます。以前のバージョンの Keycloak では、# encoding: UTF-8
のようなコメントを含む最初の行でエンコーディングを指定できましたが、これは現在サポートされていません。
-
現在のロケールの選択方法の詳細については、ロケールセレクターを参照してください。
カスタム Identity Provider アイコンの追加
Keycloak は、ログイン画面に表示されるカスタム Identity provider のアイコンの追加をサポートしています。
-
キーパターン
kcLogoIdP-<alias>
を使用して、ログインtheme.properties
ファイル (例:themes/mytheme/login/theme.properties
) でアイコンクラスを定義します。 -
エイリアス
myProvider
を持つ Identity Provider の場合、カスタムテーマのtheme.properties
ファイルに行を追加できます。例:kcLogoIdP-myProvider = fa fa-lock
すべてのアイコンは、PatternFly4 の公式ウェブサイトで入手できます。ソーシャルプロバイダーのアイコンは、すでに base
ログインテーマプロパティ (themes/keycloak/login/theme.properties
) で定義されており、そこからインスピレーションを得ることができます。
カスタム HTML テンプレートの作成
Keycloak は、Apache Freemarker テンプレートを使用して HTML を生成し、ページをレンダリングします。
ページがレンダリングされる方法を完全に変更するカスタムテンプレートを作成することは可能ですが、可能な限り組み込みテンプレートを活用することをお勧めします。理由は次のとおりです。
-
アップグレード中に、新しいバージョンからの最新の更新を取得するために、カスタムテンプレートを更新する必要がある場合があります。
-
テーマへの CSS スタイルの構成 を使用すると、UI デザイン標準とガイドラインに合わせて UI を調整できます。
-
ユーザープロファイル を使用すると、カスタムユーザー属性をサポートし、それらがどのようにレンダリングされるかを構成できます。
ほとんどの場合、Keycloak をニーズに合わせるためにテンプレートを変更する必要はありませんが、<THEME TYPE>/<TEMPLATE>.ftl
を作成することで、独自のテーマで個々のテンプレートをオーバーライドできます。Admin コンソールとアカウントコンソールは、アプリケーションをレンダリングするために単一のテンプレート index.ftl
を使用します。
他のテーマタイプのテンプレートのリストについては、$KEYCLOAK_HOME/lib/lib/main/org.keycloak.keycloak-themes-<VERSION>.jar
の JAR ファイルの theme/base/<THEME_TYPE>
ディレクトリを参照してください。
-
ベーステーマから独自のテーマにテンプレートをコピーします。
-
必要な変更を適用します。
たとえば、
mytheme
テーマのカスタムログインフォームを作成するには、themes/base/login/login.ftl
をthemes/mytheme/login
にコピーし、エディターで開きます。最初の行 (<#import …>) の後に、次のように
<h1>HELLO WORLD!</h1>
を追加します。<#import "template.ftl" as layout> <h1>HELLO WORLD!</h1> ...
-
変更したテンプレートをバックアップします。Keycloak の新しいバージョンにアップグレードするときは、該当する場合、元のテンプレートへの変更を適用するために、カスタムテンプレートを更新する必要がある場合があります。
-
テンプレートの編集方法の詳細については、FreeMarker マニュアルを参照してください。
メール
パスワードリカバリーメールなどのメールの件名と内容を編集するには、テーマの email
タイプにメッセージバンドルを追加します。各メールには 3 つのメッセージがあります。件名用、プレーンテキスト本文用、および HTML 本文用です。
利用可能なすべてのメールを確認するには、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
ディレクトリにコピーできますが、本番環境では、アーカイブ
の使用を検討することをお勧めします。アーカイブ
を使用すると、特にクラスタリングなどで Keycloak のインスタンスが複数ある場合に、テーマのバージョン管理されたコピーを簡単に作成できます。
-
テーマをアーカイブとしてデプロイするには、テーマリソースを含む JAR アーカイブを作成します。
-
アーカイブ内の利用可能なテーマと、各テーマが提供するタイプをリストするファイル
META-INF/keycloak-themes.json
をアーカイブに追加します。たとえば、
mytheme
テーマの場合、次の内容でmytheme.jar
を作成します。-
META-INF/keycloak-themes.json
-
theme/mytheme/login/theme.properties
-
theme/mytheme/login/login.ftl
-
theme/mytheme/login/resources/css/styles.css
-
theme/mytheme/login/resources/img/image.png
-
theme/mytheme/login/messages/messages_en.properties
-
theme/mytheme/email/messages/messages_en.properties
この場合の
META-INF/keycloak-themes.json
の内容は次のようになります。{ "themes": [{ "name" : "mytheme", "types": [ "login", "email" ] }] }
単一のアーカイブに複数のテーマを含めることができ、各テーマは 1 つ以上のタイプをサポートできます。
-
アーカイブを Keycloak にデプロイするには、Keycloak の providers/
ディレクトリに追加し、サーバーがすでに実行中の場合は再起動します。
テーマの追加リソース
-
インスピレーションを得るには、Keycloak にバンドルされている デフォルトテーマ を参照してください。
-
Keycloak Quickstarts リポジトリ - quickstarts リポジトリの
extension
ディレクトリには、インスピレーションとしても使用できるテーマの例が含まれています。
React ベースのテーマ
admin コンソールとアカウントコンソールは React に基づいています。これらを完全にカスタマイズするには、React ベースの npm パッケージを使用できます。次の 2 つのパッケージがあります。
-
@keycloak/keycloak-admin-ui
: これは、admin コンソールのベーステーマです。 -
@keycloak/keycloak-account-ui
: これは、アカウントコンソールのベーステーマです。
どちらのパッケージも npm で入手できます。
パッケージの使用
これらのページを使用するには、コンポーネント階層に KeycloakProvider を追加して、使用するクライアント、レルム、および URL を設定する必要があります。
import { KeycloakProvider } from "@keycloak/keycloak-ui-shared";
//...
<KeycloakProvider environment={{
serverBaseUrl: "http://localhost:8080",
realm: "master",
clientId: "security-admin-console"
}}>
{/* rest of your application */}
</KeycloakProvider>
ページの翻訳
ページは i18next
ライブラリを使用して翻訳されます。([ウェブサイト](https://react.i18next.com/) で説明されているように設定できます。提供されている翻訳を使用する場合は、プロジェクトに i18next-fetch-backend
を追加し、次を追加する必要があります。
backend: {
loadPath: `http://127.0.0.1:8080/resources/master/account/{lng}}`,
parse: (data: string) => {
const messages = JSON.parse(data);
return Object.fromEntries(
messages.map(({ key, value }) => [key, value])
);
},
},
ページの使用
すべての「ページ」は、アプリケーションで使用できる React コンポーネントです。利用可能なコンポーネントを確認するには、[ソース](https://github.com/keycloak/keycloak/blob/main/js/apps/account-ui/src/index.ts) を参照してください。または、[クイックスタート](https://github.com/keycloak/keycloak-quickstarts/tree/main/extension/extend-account-console-node) を参照して、それらの使用方法を確認してください。
テーマセレクター
デフォルトでは、レルムに構成されたテーマが使用されます。ただし、クライアントはログインテーマをオーバーライドできます。この動作は、Theme Selector SPI を介して変更できます。
これは、たとえばユーザーエージェントヘッダーを参照して、デスクトップデバイスとモバイルデバイスで異なるテーマを選択するために使用できます。
カスタムテーマセレクターを作成するには、ThemeSelectorProviderFactory
と ThemeSelectorProvider
を実装する必要があります。
テーマリソース
Keycloak でカスタムプロバイダーを実装する場合、追加のテンプレート、リソース、およびメッセージバンドルを追加する必要があることがよくあります。
ユースケースの例としては、追加のテンプレートとリソースを必要とする カスタム認証 があります。
追加のテーマリソースをロードする最も簡単な方法は、theme-resources/templates
にテンプレート、theme-resources/resources
にリソース、および theme-resources/messages
にメッセージバンドルを含む JAR を作成することです。
テンプレートとリソースをロードするためのより柔軟な方法が必要な場合は、ThemeResourceSPI を使用して実現できます。ThemeResourceProviderFactory
と ThemeResourceProvider
を実装することで、テンプレートとリソースのロード方法を正確に決定できます。
ロケールセレクター
デフォルトでは、ロケールは LocaleSelectorProvider
インターフェースを実装する DefaultLocaleSelectorProvider
を使用して選択されます。国際化が無効になっている場合、英語がデフォルト言語です。
国際化が有効になっている場合、ロケールは サーバー管理ガイド で説明されているロジックに従って解決されます。
この動作は、LocaleSelectorSPI
を介して、LocaleSelectorProvider
および LocaleSelectorProviderFactory
を実装することで変更できます。
LocaleSelectorProvider
インターフェースには、RealmModel
と nullable な UserModel
が与えられたロケールを返す必要がある単一のメソッド resolveLocale
があります。実際のリクエストは、KeycloakSession#getContext
メソッドから利用できます。
カスタム実装は、デフォルトの動作の一部を再利用するために DefaultLocaleSelectorProvider
を拡張できます。たとえば、Accept-Language
リクエストヘッダーを無視するために、カスタム実装はデフォルトプロバイダーを拡張し、その getAcceptLanguageHeaderLocale
をオーバーライドして、null 値を返すことができます。その結果、ロケール選択はレルムのデフォルト言語にフォールバックします。
ロケールセレクターの追加リソース
-
カスタムプロバイダーの作成とデプロイの詳細については、Service Provider Interfaces を参照してください。
Identity Brokering API
Keycloak は、ログインのために親 IDP に認証を委任できます。この典型的な例は、Facebook や Google などのソーシャルプロバイダーを介してユーザーがログインできるようにしたい場合です。既存のアカウントをブローカー IDP にリンクすることもできます。このセクションでは、ID ブローカーに関連するアプリケーションで使用できるいくつかの API について説明します。
外部 IDP トークンの取得
Keycloak では、外部 IDP との認証プロセスからトークンと応答を保存できます。そのためには、IDP の設定ページで [トークンを保存] 構成オプションを使用できます。
アプリケーションコードは、これらのトークンと応答を取得して、追加のユーザー情報を取得したり、外部 IDP でリクエストを安全に呼び出したりできます。たとえば、アプリケーションは Google トークンを使用して、他の Google サービスと REST API で呼び出すことができます。特定の Identity Provider のトークンを取得するには、次のリクエストを送信する必要があります。
GET /realms/{realm-name}/broker/{provider_alias}/token HTTP/1.1
Host: localhost:8080
Authorization: Bearer <KEYCLOAK ACCESS TOKEN>
アプリケーションはKeycloakで認証済みであり、アクセストークンを受け取っている必要があります。このアクセストークンには、broker
クライアントレベルロールのread-token
が設定されている必要があります。これは、ユーザーがこのロールのマッピングを持っている必要があり、クライアントアプリケーションがそのスコープ内にそのロールを持っている必要があることを意味します。この場合、Keycloakの保護されたサービスにアクセスしているため、ユーザー認証中にKeycloakによって発行されたアクセストークンを送信する必要があります。ブローカー設定ページでは、Stored Tokens Readable
スイッチをオンにすることで、このロールを新しくインポートされたユーザーに自動的に割り当てることができます。
これらの外部トークンは、プロバイダーを介して再度ログインするか、クライアント起動型アカウント連携APIを使用することで再確立できます。
クライアント起動型アカウント連携
一部のアプリケーションは、Facebookのようなソーシャルプロバイダーと統合したいと考えていますが、これらのソーシャルプロバイダー経由でログインするオプションを提供したくありません。Keycloakは、アプリケーションが既存のユーザーアカウントを特定の外部IDPにリンクするために使用できるブラウザベースのAPIを提供します。これはクライアント起動型アカウント連携と呼ばれます。アカウント連携はOIDCアプリケーションによってのみ開始できます。
その仕組みは、アプリケーションがユーザーのブラウザをKeycloakサーバー上のURLに転送し、ユーザーのアカウントを特定の外部プロバイダー(つまりFacebook)にリンクしたいと要求することです。サーバーは外部プロバイダーとのログインを開始します。ブラウザは外部プロバイダーでログインし、サーバーにリダイレクトバックされます。サーバーはリンクを確立し、確認とともにアプリケーションにリダイレクトバックします。
クライアントアプリケーションがこのプロトコルを開始する前に満たす必要のあるいくつかの前提条件があります。
-
目的のアイデンティティプロバイダーは、管理コンソールでユーザーレルムに対して構成および有効になっている必要があります。
-
ユーザーアカウントは、OIDCプロトコルを介して既存のユーザーとして既にログインしている必要があります。
-
ユーザーは
account.manage-account
またはaccount.manage-account-links
ロールマッピングを持っている必要があります。 -
アプリケーションは、アクセストークン内でこれらのロールのスコープを許可されている必要があります。
-
アプリケーションは、リダイレクトURLを生成するために必要な情報がアクセストークン内にあるため、アクセストークンにアクセスできる必要があります。
ログインを開始するには、アプリケーションはURLを作成し、ユーザーのブラウザをこのURLにリダイレクトする必要があります。URLは次のようになります。
/{auth-server-root}/realms/{realm-name}/broker/{provider}/link?client_id={id}&redirect_uri={uri}&nonce={nonce}&hash={hash}
パスとクエリパラメーターのそれぞれの説明は次のとおりです。
- provider
-
これは、管理コンソールの
Identity Provider
セクションで定義した外部IDPのプロバイダーエイリアスです。 - client_id
-
これは、アプリケーションのOIDCクライアントIDです。管理コンソールでアプリケーションをクライアントとして登録したときに、このクライアントIDを指定する必要がありました。
- redirect_uri
-
これは、アカウントリンクが確立された後にリダイレクトしたいアプリケーションコールバックURLです。有効なクライアントリダイレクトURIパターンである必要があります。言い換えれば、管理コンソールでクライアントを登録したときに定義した有効なURLパターンのいずれかと一致する必要があります。
- nonce
-
これは、アプリケーションが生成する必要があるランダムな文字列です。
- hash
-
これは、Base64 URLエンコードされたハッシュです。このハッシュは、
nonce
+token.getSessionState()
+token.getIssuedFor()
+provider
のSHA_256ハッシュをBase64 URLエンコードすることによって生成されます。token変数は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("/realms/{realm-name}/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を実装するには、そのProviderFactoryおよびProviderインターフェースを実装する必要があります。また、サービス構成ファイルを作成する必要があります。
たとえば、テーマセレクター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";
}
}
プロバイダーファクトリーの実装では、getId()
メソッドで一意のIDを返すことをお勧めします。ただし、以下のプロバイダーのオーバーライドセクションで説明されているように、このルールにはいくつかの例外が存在する可能性があります。
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
プロバイダーを構成するには、プロバイダーの構成ガイドを参照してください。
たとえば、プロバイダーを構成するには、次のようにオプションを設定できます。
bin/kc.[sh|bat] --spi-theme-selector-my-theme-selector-enabled=true --spi-theme-selector-my-theme-selector-theme=my-theme
次に、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のpom.xmlファイルには、SPIの対象となるKeycloakバージョンへのインポート参照を含むdependencyManagement
セクションが必要です。この例では、VERSION
の出現箇所をKeycloakの現在のバージョンである26.2.0に置き換えます。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>test-lib</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
<version>VERSION</version> (1)
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
1 | VERSION をKeycloakの現在のバージョンに置き換えます。 |
組み込みプロバイダーのオーバーライド
上記のように、ProviderFactory
の実装では一意のIDを使用することをお勧めします。ただし同時に、Keycloakの組み込みプロバイダーの1つをオーバーライドすると便利な場合があります。これに対する推奨される方法は、依然として一意のIDを持つProviderFactoryの実装であり、たとえば、プロバイダーの構成ガイドで指定されているように、デフォルトプロバイダーを設定します。一方、これは常に可能とは限りません。
たとえば、デフォルトのOpenID Connectプロトコルの動作にいくつかのカスタマイズが必要で、OIDCLoginProtocolFactory
のデフォルトのKeycloak実装をオーバーライドしたい場合、同じproviderIdを保持する必要があります。たとえば、管理コンソール、OIDCプロトコルのwell-knownエンドポイント、およびその他のさまざまなものは、プロトコルファクトリーのIDがopenid-connect
であることに依存しています。
この場合、カスタム実装のorder()
メソッドを実装し、組み込み実装よりも高い順序を持つようにすること強くお勧めします。
public class CustomOIDCLoginProtocolFactory extends OIDCLoginProtocolFactory {
// Some customizations here
@Override
public int order() {
return 1;
}
}
同じプロバイダーIDを持つ複数の実装がある場合、Keycloakランタイムで使用されるのは順序が最も高い実装のみです。
管理コンソールで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で利用可能な他のプロバイダーを使用できます。既存のプロバイダーは通常、SPIの実装セクションで説明されているように、プロバイダーで使用可能なKeycloakSession
を使用して取得できます。
Keycloakには2つのプロバイダータイプがあります。
-
単一実装プロバイダータイプ - Keycloakランタイムで特定プロバイダータイプの単一のアクティブな実装のみが存在できます。
たとえば、
HostnameProvider
はKeycloakで使用されるホスト名を指定し、Keycloakサーバー全体で共有されます。したがって、Keycloakサーバーでアクティブなこのプロバイダーの実装は1つだけです。サーバーランタイムで使用可能な複数のプロバイダー実装がある場合、それらの1つをデフォルトとして指定する必要があります。
たとえば、次のようにします。
bin/kc.[sh|bat] build --spi-hostname-provider=default
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
を使用する必要さえあります。将来廃止される可能性があるため、この方法で独自のプロバイダーを実装することはお勧めしません。
プロバイダー実装の登録
プロバイダーは、JARファイルをproviders
ディレクトリにコピーするだけでサーバーに登録されます。
プロバイダーにKeycloakによってまだ提供されていない追加の依存関係が必要な場合は、これらをproviders
ディレクトリにコピーします。
新しいプロバイダーまたは依存関係を登録した後、Keycloakは非最適化された起動またはkc.[sh|bat] build
コマンドで再構築する必要があります。
プロバイダーJARは分離されたクラスローダーにロードされないため、組み込みリソースまたはクラスと競合するリソースまたはクラスをプロバイダーJARに含めないでください。特に、application.propertiesファイルを含めたり、commons-lang3依存関係をオーバーライドしたりすると、プロバイダーJARが削除された場合に自動ビルドが失敗します。競合するクラスを含めた場合、サーバーの起動ログに分割パッケージの警告が表示されることがあります。残念ながら、すべての組み込みlib JARが分割パッケージ警告ロジックによってチェックされるわけではないため、バンドルしたり、推移的な依存関係を含めたりする前に、libディレクトリJARを確認する必要があります。競合がある場合は、問題のあるクラスを削除または再パッケージ化することで解決できます。 競合するリソースファイルがある場合、警告は表示されません。JARのリソースファイルのパス名にそのプロバイダーに固有のものが含まれていることを確認するか、
削除されたプロバイダーJARに関連する
これにより、Quarkusはクラスローディング関連のインデックスファイルを強制的に再構築します。そこから、例外なしで非最適化された起動またはビルドを実行できるはずです。 |
JavaScriptプロバイダー
スクリプトはプレビューであり、完全にサポートされていません。この機能はデフォルトで無効になっています。 有効にするには、 |
Keycloakには、管理者runtime中にスクリプトを実行して、特定の機能をカスタマイズする機能があります。
-
Authenticator
-
JavaScriptポリシー
-
OpenID Connect プロトコルマッパー
-
SAMLプロトコルマッパー
Authenticator
認証スクリプトは、少なくとも次の関数のいずれかを提供する必要があります。authenticate(..)
(Authenticator#authenticate(AuthenticationFlowContext)
から呼び出されます)、action(..)
(Authenticator#action(AuthenticationFlowContext)
から呼び出されます)。
カスタムAuthenticator
は、少なくともauthenticate(..)
関数を提供する必要があります。コード内でjavax.script.Bindings
スクリプトを使用できます。
script
-
スクリプトメタデータにアクセスするための
ScriptModel
realm
-
RealmModel
user
-
現在の
UserModel
。user
は、スクリプト認証者が認証フローで、ユーザーIDを確立してユーザーを認証セッションに設定することに成功した別の認証者の後にトリガーされるように構成されている場合に使用できることに注意してください。 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();
}
スクリプト認証者の追加場所
スクリプト認証者の考えられる使用法は、認証の最後にいくつかのチェックを行うことです。スクリプト認証者を常にトリガーしたい場合(たとえば、アイデンティティCookieを使用したSSO再認証中であっても)、認証フローの最後にREQUIREDとして追加し、既存の認証者を個別のREQUIRED認証サブフローにカプセル化する必要がある場合があります。この必要性は、REQUIREDおよびALTERNATIVE実行が同じレベルであってはならないためです。たとえば、認証フロー構成は次のようになります。
- User-authentication-subflow REQUIRED
-- Cookie ALTERNATIVE
-- Identity-provider-redirect ALTERNATIVE
...
- Your-Script-Authenticator REQUIRED
OpenID Connectプロトコルマッパー
OpenID Connectプロトコルマッパースクリプトは、IDトークンまたはアクセストークンのコンテンツを変更できるJavaScriptスクリプトです。
コード内でjavax.script.Bindings
スクリプトを使用できます。
user
-
現在の
UserModel
realm
-
RealmModel
token
-
現在の
IDToken
。マッパーがIDトークン用に構成されている場合にのみ使用できます。 tokenResponse
-
現在の
AccessTokenResponse
。マッパーがアクセストークン用に構成されている場合にのみ使用できます。 userSession
-
アクティブな
UserSessionModel
keycloakSession
-
アクティブな
KeycloakSession
スクリプトのエクスポートは、トークンクレームの値として使用されます。
// prints can be used to log information for debug purpose.
print("STARTING CUSTOM MAPPER");
var inputRequest = keycloakSession.getContext().getHttpRequest();
var params = inputRequest.getDecodedFormParameters();
var output = params.getFirst("user_input");
exports = output;
上記のスクリプトを使用すると、認証リクエストからuser_input
を取得できます。これは、マッパーで構成されたToken Claim Name
にマップするために使用できます。
デプロイするスクリプトを含む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"
}
],
"saml-mappers": [
{
"name": "My Mapper",
"fileName": "my-script-mapper.js",
"description": "My Mapper from a JS file"
}
]
}
このファイルは、デプロイするさまざまなタイプのスクリプトプロバイダーを参照する必要があります。
-
authenticators
OpenID Connectスクリプト認証者用。同じJARファイルに1つまたは複数の認証者を含めることができます。
-
policies
Keycloak Authorization Servicesを使用する場合のJavaScriptポリシー用。同じJARファイルに1つまたは複数のポリシーを含めることができます。
-
mappers
OpenID Connectスクリプトプロトコルマッパー用。同じJARファイルに1つまたは複数のマッパーを含めることができます。
-
saml-mappers
SAMLスクリプトプロトコルマッパー用。同じJARファイルに1つまたは複数のマッパーを含めることができます。
JAR
ファイル内の各スクリプトファイルについて、スクリプトファイルを特定のプロバイダータイプにマッピングするMETA-INF/keycloak-scripts.json
に対応するエントリが必要です。そのためには、各エントリに次のプロパティを指定する必要があります。
-
name
Keycloak管理コンソールを介してスクリプトを表示するために使用されるフレンドリー名。指定しない場合、スクリプトファイルの名前が代わりに使用されます。
-
description
スクリプトファイルの意図をより適切に説明するオプションのテキスト。
-
fileName
スクリプトファイルの名前。このプロパティは必須であり、JAR内のファイルにマップする必要があります。
利用可能なSPI
runtime時に利用可能なすべてのSPIのリストを表示する場合は、管理コンソールセクションで説明されているように、管理コンソールのProvider Info
ページを確認できます。
サーバーの拡張
Keycloak SPIフレームワークは、特定の組み込みプロバイダーを実装またはオーバーライドする可能性を提供します。ただし、Keycloakは、コア機能とドメインを拡張する機能も提供します。これには、次の可能性が含まれます。
-
カスタムRESTエンドポイントをKeycloakサーバーに追加する
-
独自のカスタム SPI の追加
-
Keycloak データモデルへのカスタム JPA エンティティの追加
カスタムRESTエンドポイントの追加
これは非常に強力な拡張機能であり、独自のRESTエンドポイントをKeycloakサーバーにデプロイできます。これにより、デフォルトの組み込みKeycloak RESTエンドポイントのセットでは利用できない機能をKeycloakサーバーでトリガーする可能性など、あらゆる種類の拡張機能が実現します。
カスタムRESTエンドポイントを追加するには、RealmResourceProviderFactory
およびRealmResourceProvider
インターフェースを実装する必要があります。RealmResourceProvider
には、1つの重要なメソッドがあります。
Object getResource();
このメソッドを使用して、JAX-RSリソースとして機能するオブジェクトを返します。JAX-RSリソースは、次の構成が含まれている場合にのみサーバーによって認識され、有効なエンドポイントとして登録されます。 - META-INF
の下にbeans.xml
という名前の空のファイルを追加する - JAX-RSクラスにjakarta.ws.rs.ext.Provider
アノテーションを付ける。
カスタムプロバイダーのパッケージ化とデプロイの方法の詳細については、サービスプロバイダーインターフェースの章を参照してください。
プロバイダー拡張メカニズムを介して、フィルターやインターセプターなどの他のJAX-RSコンポーネントをインストールすることは可能ですが、これらは公式にはサポートされていません。 |
独自のカスタムSPIを追加する
カスタムSPIは、カスタムRESTエンドポイントで特に役立ちます。独自のSPIを追加するには、次の手順を使用します。
-
org.keycloak.provider.Spi
インターフェースを実装し、SPIのIDとProviderFactory
およびProvider
クラスを定義します。次のようになります。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のクラスを追加します。例:ExampleSpi
-
ProviderFactory
から拡張されたExampleServiceProviderFactory
インターフェースと、Provider
から拡張されたExampleService
を作成します。ExampleService
には通常、ユースケースに必要なビジネスメソッドが含まれます。ExampleServiceProviderFactory
インスタンスは常にアプリケーションごとにスコープ指定されますが、ExampleService
はリクエストごと(またはより正確にはKeycloakSession
ライフサイクルごと)にスコープ指定されることに注意してください。 -
最後に、サービスプロバイダーインターフェースの章で説明されているのと同じ方法でプロバイダーを実装する必要があります。
KeycloakデータモデルにカスタムJPAエンティティを追加する
Keycloakデータモデルが目的のソリューションと正確に一致しない場合、またはKeycloakにいくつかのコア機能を追加したい場合、または独自のRESTエンドポイントがある場合、Keycloakデータモデルを拡張したい場合があります。Keycloak JPA EntityManager
に独自のJPAエンティティを追加できるようにします。
独自の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の更新は常にサーバーの起動時に行われるため、スキーマのDB更新をトリガーするには、Liquibaseチェンジログファイルに新しいチェンジセットを追加するだけです(上記の例では、JPAエンティティおよびExampleJpaEntityProvider
と同じJARにパックする必要があるファイルMETA-INF/example-changelog.xml
)してから、サーバーを再起動します。DBスキーマは起動時に自動的に更新されます。
Liquibaseチェンジログで変更を加えたり、DB更新をトリガーしたりする前に、常にデータベースをバックアップすることを忘れないでください。 |
認証SPI
Keycloakには、Kerberos、パスワード、OTPなどのさまざまな認証メカニズムが含まれています。これらのメカニズムは、すべての要件を満たしているとは限らず、独自のカスタムメカニズムをプラグインしたい場合があります。Keycloakは、新しいプラグインを作成するために使用できる認証SPIを提供します。管理コンソールは、これらの新しいメカニズムの適用、順序付け、および構成をサポートしています。
Keycloakは、簡単な登録フォームもサポートしています。このフォームのさまざまな側面を有効または無効にできます。たとえば、reCAPTCHAサポートはオフまたはオンにできます。同じ認証SPIを使用して、登録フローに別のページを追加したり、完全に再実装したりできます。組み込みの登録フォームに特定の検証とユーザー拡張機能を追加するために使用できる、追加のきめ細かいSPIもあります。
Keycloakの必須アクションは、ユーザーが認証後に実行する必要があるアクションです。アクションが正常に実行されると、ユーザーはアクションを再度実行する必要はありません。Keycloakには、「パスワードのリセット」などの組み込みの必須アクションが付属しています。このアクションは、ユーザーがログイン後にパスワードを変更することを強制します。独自の必須アクションを作成してプラグインできます。
認証者または必須アクションの実装で、ユーザー属性をユーザーIDのリンク/確立のためのメタデータ属性として使用している場合は、ユーザーが属性を編集できず、対応する属性が読み取り専用であることを確認してください。脅威モデル軽減策の章で詳細を参照してください。 |
用語
最初に認証SPIについて学ぶために、それについて説明するために使用されるいくつかの用語を見ていきましょう。
- 認証フロー
-
フローは、ログインまたは登録中に行う必要のあるすべての認証のコンテナです。管理コンソールの認証ページに移動すると、システムで定義されているすべてのフローと、それらを構成する認証者を確認できます。フローには、他のフローを含めることができます。ブラウザーログイン、ダイレクトグラントアクセス、および登録用に、新しい別のフローをバインドすることもできます。
- Authenticator
-
認証者は、フロー内で認証またはアクションを実行するためのロジックを保持するプラグ可能なコンポーネントです。通常はシングルトンです。
- 実行
-
実行は、認証者をフローにバインドし、認証者を認証者の構成にバインドするオブジェクトです。フローには実行エントリが含まれています。
- 実行要件
-
各実行は、認証者がフロー内でどのように動作するかを定義します。要件は、認証者が有効、無効、条件付き、必須、または代替のいずれであるかを定義します。代替要件とは、認証者がそれが含まれているフローを検証するのに十分であるが、必須ではないことを意味します。たとえば、組み込みのブラウザーフローでは、Cookie認証、アイデンティティプロバイダーリダイレクター、およびフォームサブフロー内のすべての認証者のセットはすべて代替です。それらは順次トップダウン順に実行されるため、いずれかが成功した場合、フローは成功し、フロー(またはサブフロー)の後続の実行は評価されません。
- 認証者構成
-
このオブジェクトは、認証フロー内の特定の実行に対する認証者の構成を定義します。各実行は異なる構成を持つことができます。
- 必須アクション
-
認証が完了した後、ユーザーはログインを許可される前に、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が設定されているか、Kerberosログインが成功した場合、ユーザー名/パスワードフォームは実行されません。クライアントが最初にKeycloakにリダイレクトしてユーザーを認証するときからの手順を順に見ていきましょう。
-
OpenID ConnectまたはSAMLプロトコルプロバイダーは、関連データをアンパックし、クライアントと署名を検証します。AuthenticationSessionModelを作成します。ブラウザーフローがどうあるべきかを調べ、フローの実行を開始します。
-
フローはCookie実行を見て、それが代替であることを確認します。Cookieプロバイダーをロードします。Cookieプロバイダーが、ユーザーが認証セッションに既に関連付けられている必要があるかどうかを確認します。Cookieプロバイダーはユーザーを必要としません。もしそうであれば、フローは中止され、ユーザーはエラー画面を表示します。次に、Cookieプロバイダーが実行されます。その目的は、SSO Cookieが設定されているかどうかを確認することです。設定されている場合は、検証され、UserSessionModelが検証され、AuthenticationSessionModelに関連付けられます。Cookieプロバイダーは、SSO Cookieが存在し、検証された場合はsuccess()ステータスを返します。Cookieプロバイダーが成功を返し、フローのこのレベルでの各実行がALTERNATIVEであるため、他の実行は実行されず、これによりログインが成功します。SSO Cookieがない場合、Cookieプロバイダーはattempted()のステータスで戻ります。これは、エラー状態はなかったが、成功もなかったことを意味します。プロバイダーは試行しましたが、リクエストはこの認証者を処理するように設定されていませんでした。
-
次に、フローは Kerberos 実行に注目します。これも代替手段の一つです。Kerberos プロバイダーも、ユーザーがまだセットアップされておらず、AuthenticationSessionModel に関連付けられていないことを要求しないため、このプロバイダーが実行されます。Kerberos は SPNEGO ブラウザープロトコルを使用します。これには、サーバーとクライアント間でネゴシエーションヘッダーを交換する一連のチャレンジ/レスポンスが必要です。Kerberos プロバイダーはネゴシエーションヘッダーを認識しないため、サーバーとクライアント間の最初のインタラクションであると想定します。したがって、クライアントへの HTTP チャレンジレスポンスを作成し、forceChallenge() ステータスを設定します。forceChallenge() は、この HTTP レスポンスがフローによって無視できず、クライアントに返される必要があることを意味します。代わりに、プロバイダーが challenge() ステータスを返した場合、フローは他のすべての代替手段が試行されるまでチャレンジレスポンスを保持します。したがって、この初期フェーズでは、フローは停止し、チャレンジレスポンスがブラウザーに送り返されます。ブラウザーがネゴシエーションヘッダーの成功で応答した場合、プロバイダーはユーザーを AuthenticationSession に関連付け、フローのこのレベルでの残りの実行はすべて代替手段であるため、フローは終了します。それ以外の場合、再び Kerberos プロバイダーは attempted() ステータスを設定し、フローは続行されます。
-
次の実行は Forms と呼ばれるサブフローです。このサブフローの実行がロードされ、同じ処理ロジックが実行されます。
-
Forms サブフローの最初の実行は UsernamePassword プロバイダーです。このプロバイダーも、ユーザーがまだフローに関連付けられている必要はありません。このプロバイダーはチャレンジ HTTP レスポンスを作成し、そのステータスを challenge() に設定します。この実行は必須であるため、フローはこのチャレンジを尊重し、HTTP レスポンスをブラウザーに送り返します。このレスポンスは、ユーザー名/パスワード HTML ページのレンダリングです。ユーザーはユーザー名とパスワードを入力し、送信をクリックします。この HTTP リクエストは UsernamePassword プロバイダーに送信されます。ユーザーが無効なユーザー名またはパスワードを入力した場合、新しいチャレンジレスポンスが作成され、この実行に対して failureChallenge() のステータスが設定されます。failureChallenge() はチャレンジがあることを意味しますが、フローはこれをエラーログにエラーとして記録する必要があります。このエラーログは、ログイン失敗が多すぎるアカウントまたは IP アドレスをロックするために使用できます。ユーザー名とパスワードが有効な場合、プロバイダーは UserModel を AuthenticationSessionModel に関連付け、success() のステータスを返します。
-
次の実行は Conditional OTP と呼ばれるサブフローです。このサブフローの実行がロードされ、同じ処理ロジックが実行されます。その Requirement は Conditional です。これは、フローが最初にそれに含まれるすべての条件付きエグゼキュータを評価することを意味します。条件付きエグゼキュータは、
ConditionalAuthenticator
を実装する認証器であり、メソッドboolean matchCondition(AuthenticationFlowContext context)
を実装する必要があります。条件付きサブフローは、それに含まれるすべての条件付き実行のmatchCondition
メソッドを呼び出し、それらのすべてが true と評価された場合、必須サブフローであるかのように動作します。そうでない場合、無効化されたサブフローであるかのように動作します。条件付き認証器は、この目的でのみ使用され、認証器としては使用されません。これは、条件付き認証器が「true」と評価されたとしても、フローまたはサブフローが成功としてマークされないことを意味します。たとえば、条件付き認証器のみを持つ条件付きサブフローのみを含むフローは、ユーザーがログインすることを許可しません。 -
Conditional OTP サブフローの最初の実行は Condition - User Configured です。このプロバイダーは、ユーザーがフローに関連付けられている必要があることを要求します。この要件は、UsernamePassword プロバイダーがすでにユーザーをフローに関連付けているため満たされています。このプロバイダーの
matchCondition
メソッドは、現在のサブフロー内の他のすべての認証器のconfiguredFor
メソッドを評価します。サブフローに Requirement が required に設定されたエグゼキュータが含まれている場合、matchCondition
メソッドは、すべての必須認証器のconfiguredFor
メソッドが true と評価された場合にのみ true と評価されます。それ以外の場合、matchCondition
メソッドは、任意の代替認証器が true と評価された場合に true と評価されます。 -
次の実行は OTP Form です。このプロバイダーも、ユーザーがフローに関連付けられている必要があることを要求します。この要件は、UsernamePassword プロバイダーがすでにユーザーをフローに関連付けているため満たされています。ユーザーはこのプロバイダーに必須であるため、プロバイダーはユーザーがこのプロバイダーを使用するように構成されているかどうかも尋ねられます。ユーザーが構成されていない場合、フローは認証完了後にユーザーが実行する必要がある必須アクションをセットアップします。OTP の場合、これは OTP セットアップページを意味します。ユーザーが構成されている場合、OTP コードを入力するように求められます。このシナリオでは、条件付きサブフローのため、Conditional OTP サブフローが Required に設定されていない限り、ユーザーは OTP ログインページを見ることはありません。
-
フローが完了すると、認証プロセッサは UserSessionModel を作成し、それを AuthenticationSessionModel に関連付けます。次に、ユーザーがログインする前に必須アクションを完了する必要があるかどうかを確認します。
-
まず、各必須アクションの evaluateTriggers() メソッドが呼び出されます。これにより、必須アクションプロバイダーは、アクションをトリガーする可能性のある状態があるかどうかを把握できます。たとえば、レルムにパスワード有効期限ポリシーがある場合、このメソッドによってトリガーされる可能性があります。
-
requiredActionChallenge() メソッドが呼び出されたユーザーに関連付けられた各必須アクション。ここで、プロバイダーは必須アクションのページをレンダリングする HTTP レスポンスをセットアップします。これは、challenge ステータスを設定することで行われます。
-
必須アクションが最終的に成功した場合、必須アクションはユーザーの必須アクションリストから削除されます。
-
すべての必須アクションが解決されると、ユーザーは最終的にログインします。
Authenticator SPI のウォークスルー
このセクションでは、Authenticator インターフェースを見ていきます。これを行うために、「母親の旧姓は?」のような秘密の質問への回答をユーザーに入力させる認証器を実装します。この例は完全に実装されており、Keycloak Quickstarts リポジトリ の extension/authenticator
に含まれています。
認証器を作成するには、少なくとも org.keycloak.authentication.AuthenticatorFactory および Authenticator インターフェースを実装する必要があります。Authenticator インターフェースはロジックを定義します。AuthenticatorFactory は Authenticator のインスタンスを作成する役割を担います。これらは両方とも、ユーザーフェデレーションなどの他の Keycloak コンポーネントが行う、より一般的な Provider および ProviderFactory インターフェースのセットを拡張します。
CookieAuthenticator のような一部の認証器は、ユーザーを認証するためにユーザーが持っているまたは知っているクレデンシャルに依存していません。ただし、PasswordForm 認証器や OTPFormAuthenticator などの一部の認証器は、ユーザーが何らかの情報を入力し、その情報をデータベース内の何らかの情報と照合することに依存しています。たとえば、PasswordForm の場合、認証器はパスワードのハッシュをデータベースに保存されているハッシュと照合して検証しますが、OTPFormAuthenticator は受信した OTP をデータベースに保存されている共有シークレットから生成された OTP と照合して検証します。
これらのタイプの認証器は CredentialValidator と呼ばれ、さらにいくつかのクラスを実装する必要があります。
-
org.keycloak.credential.CredentialModel を拡張し、データベース内のクレデンシャルの正しい形式を生成できるクラス
-
org.keycloak.credential.CredentialProvider インターフェースを実装するクラス、およびその CredentialProviderFactory ファクトリーインターフェースを実装するクラス。
このウォークスルーで見ていく SecretQuestionAuthenticator は CredentialValidator であるため、これらのすべてのクラスを実装する方法を見ていきます。
クラスのパッケージングとデプロイメント
クラスは単一の 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
はクレデンシャルのプライマリキーです。 -
user_ID
はクレデンシャルをユーザーにリンクする外部キーです。 -
credential_type
は、作成中に設定される文字列であり、既存のクレデンシャルタイプを参照する必要があります。 -
created_date
はクレデンシャルの作成タイムスタンプ(long 形式)です。 -
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 です。一貫性を保つために、この文字列は常にこのクレデンシャルのタイプを取得するときに参照されるものであることを確認します。クラス 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 を作成します。SecretQuestionCredentialProviderFactory の create
メソッドは、SecretQuestionCredentialProvider が要求されたときに呼び出されます。
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;
}
また、CredentialInputValidator インターフェースを実装する必要があります。これにより、Keycloak はこのプロバイダーを Authenticator のクレデンシャルを検証するためにも使用できることを認識できます。CredentialProvider インターフェースの場合、最初に実装する必要があるメソッドは getType()
メソッドです。これは、`SecretQuestionCredentialModel’s TYPE String を単純に返します。
@Override
public String getType() {
return SecretQuestionCredentialModel.TYPE;
}
2 番目のメソッドは、CredentialModel
から SecretQuestionCredentialModel
を作成することです。このメソッドでは、SecretQuestionCredentialModel
から既存の静的メソッドを単純に呼び出します。
@Override
public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) {
return SecretQuestionCredentialModel.createFromCredentialModel(model);
}
最後に、クレデンシャルを作成および削除するメソッドがあります。これらのメソッドは UserModel のクレデンシャルマネージャーを呼び出します。クレデンシャルマネージャーは、ローカルストレージやフェデレーションストレージなど、クレデンシャルの読み取りまたは書き込み場所を認識する役割を担います。
@Override
public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) {
if (credentialModel.getCreatedDate() == null) {
credentialModel.setCreatedDate(Time.currentTimeMillis());
}
return user.credentialManager().createStoredCredential(credentialModel);
}
@Override
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
return user.credentialManager().removeStoredCredentialById(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();
}
認証器の実装
クレデンシャルを使用してユーザーを認証する認証器を実装する場合は、認証器に 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 のようなプロバイダーは、ネゴシエーションヘッダーからユーザーを解決できるため、このメソッドから 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() メソッドを使用してこの実行のステータスを SUCCESS としてマークし、authentication() メソッドから戻ります。
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 Response オブジェクトがブラウザーに送信されます。
したがって、秘密の質問への回答を求める 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 タイプの複数のクレデンシャルを記録できる場合(たとえば、いくつかの質問から選択できるようにし、ユーザーが複数の質問の回答を持つことを許可した場合)、Keycloak はユーザーのログインに使用されているクレデンシャルを知る必要があります。複数のクレデンシャルがある場合、Keycloak はユーザーがログイン中に使用するクレデンシャルを選択できるようにし、情報はフォームによって Authenticator に送信されます。フォームがこの情報を提示しない場合、使用されるクレデンシャル 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() です。これは、Authenticator の構成を提供することの例です。このケースでは、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 クラスで実装する最後のメソッドは getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) です。これは、CredentialProvider インターフェースの抽象メソッドです。各クレデンシャルプロバイダーは、このメソッドを提供および実装する必要があります。このメソッドは CredentialTypeMetadata のインスタンスを返します。CredentialTypeMetadata には、少なくとも認証器のタイプとカテゴリ、表示名、および削除可能なアイテムを含める必要があります。この例では、ビルダーは認証器のタイプを getType() メソッドから取得し、カテゴリは 2 要素(認証器は認証の 2 番目の要素として使用できます)、および削除可能は false に設定されています(ユーザーは以前に登録された一部のクレデンシャルを削除できません)。
ビルダーのその他の項目は、helpText(さまざまな画面でユーザーに表示されます)、createAction(ユーザーが新しいクレデンシャルを作成するために使用できる必須アクションの providerID)、または 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() メソッドが呼び出されるかどうかをフローマネージャーに伝えるフラグです。Authenticator がユーザーに対して構成されていない場合、フローマネージャーは isUserSetupAllowed() をチェックします。false の場合、フローはエラーで中止します。true を返す場合、フローマネージャーは Authenticator.setRequiredActions() を呼び出します。
@Override
public boolean isUserSetupAllowed() {
return true;
}
次のいくつかのメソッドは、Authenticator を構成する方法を定義します。isConfigurable() メソッドは、フロー内で Authenticator を構成できるかどうかを管理コンソールに指定するフラグです。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() は、実行にバインドする Authenticator を選択するときに表示されるツールチップテキストです。getDisplayType() は、Authenticator をリストするときに管理コンソールに表示されるテキストです。getReferenceCategory() は、Authenticator が属するカテゴリにすぎません。
認証器フォームの追加
Keycloak には Freemarker テーマおよびテンプレートエンジン が付属しています。Authenticator クラスの 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 ファイル) に対応します。英語のみを使用している場合は、loginSecretQuestion
の値を追加するだけで済みます。これは、ユーザーに尋ねたい質問である必要があります。
AuthenticationFlowContext.form() を呼び出すと、LoginFormsProvider インスタンスが提供されます。LoginFormsProvider.setAttribute("foo", "bar")
を呼び出した場合、「foo」の値はフォーム内で ${foo}
として参照できるようになります。属性の値は、任意の Java Bean にすることもできます。
ファイルの先頭を見ると、テンプレートをインポートしていることがわかります。
<#import "select.ftl" as layout>
標準の template.ftl
の代わりにこのテンプレートをインポートすると、Keycloak はユーザーが別のクレデンシャルまたは実行を選択できるドロップダウンボックスを表示できます。
フローへの認証器の追加
フローへの認証器の追加は、管理コンソールで行う必要があります。[認証] メニュー項目に移動し、[フロー] タブに移動すると、現在定義されているフローを表示できます。組み込みフローは変更できないため、作成した認証器を追加するには、既存のフローをコピーするか、独自のフローを作成する必要があります。ユーザーインターフェースが十分に明確であり、フローを作成して認証器を追加する方法を判断できることを願っています。詳細については、サーバー管理ガイド の「認証フロー
」の章を参照してください。
フローを作成したら、バインドするログインアクションにバインドする必要があります。[認証] メニューに移動し、[バインディング] タブに移動すると、フローをブラウザー、登録、またはダイレクトグラントフローにバインドするオプションが表示されます。
必須アクションのウォークスルー
このセクションでは、必須アクションを定義する方法について説明します。Authenticator セクションでは、「システムに入力された秘密の質問へのユーザーの回答をどのように取得するのだろうか?」と疑問に思ったかもしれません。例で示したように、回答が設定されていない場合、必須アクションがトリガーされます。このセクションでは、Secret Question Authenticator の必須アクションを実装する方法について説明します。
クラスのパッケージングとデプロイメント
クラスは単一の jar 内にパッケージ化します。この jar は他のプロバイダークラスとは別である必要はありませんが、org.keycloak.authentication.RequiredActionFactory
という名前のファイルが含まれている必要があり、jar の META-INF/services/
ディレクトリに格納されている必要があります。このファイルには、jar 内にある各 RequiredActionFactory 実装の完全修飾クラス名をリストする必要があります。例:
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
この services/ ファイルは、Keycloak がシステムにロードする必要があるプロバイダーをスキャンするために使用されます。
この jar をデプロイするには、providers/
ディレクトリにコピーし、bin/kc.[sh|bat] build
を実行します。
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();
}
回答はフォームポストからプルアウトされます。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 が作成されました。これは基本的に、ページ上のフォーム要素の検証を追加したり、ユーザー登録後に UserModel 属性とデータを初期化したりすることを可能にします。ユーザープロファイル登録処理の実装と、登録 Google reCAPTCHA Enterprise プラグインの両方を見ていきましょう。
FormAction インターフェースの実装
実装する必要があるコアインターフェースは、FormAction インターフェースです。FormAction は、ページの一部をレンダリングおよび処理する役割を担います。レンダリングは buildPage() メソッドで行い、検証は validate() メソッドで行い、検証後の操作は success() で行います。まず、Recaptcha プラグインの buildPage() メソッドを見てみましょう。
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
if (config == null
|| Stream.of(PROJECT_ID, SITE_KEY, API_KEY, ACTION)
.anyMatch(key -> Strings.isNullOrEmpty(config.get(key)))
|| parseDoubleFromConfig(config, SCORE_THRESHOLD) == null) {
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
return;
}
String userLanguageTag = context.getSession().getContext().resolveLocale(context.getUser())
.toLanguageTag();
boolean invisible = Boolean.parseBoolean(config.getOrDefault(INVISIBLE, "true"));
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", config.get(SITE_KEY));
form.setAttribute("recaptchaAction", config.get(ACTION));
form.setAttribute("recaptchaVisible", !invisible);
form.addScript("https://www.google.com/recaptcha/enterprise.js?hl=" + userLanguageTag);
}
Recaptcha の buildPage() メソッドは、ページをレンダリングするためにフォームフローによって呼び出されるコールバックです。LoginFormsProvider である form パラメータを受け取ります。フォームプロバイダーに追加の属性を追加して、登録 Freemarker テンプレートによって生成された HTML ページに表示できるようにすることができます。
上記のコードは、登録 recaptcha プラグインからのものです。Recaptcha には、構成から取得する必要がある特定の設定が必要です。FormAction は、Authenticator とまったく同じように構成されます。この例では、Google Recaptcha サイトキーとその他のオプションを Recaptcha 構成から取得し、フォームプロバイダーに属性として追加しています。これで、登録テンプレートファイル register.ftl からこれらの属性にアクセスできるようになりました。
Recaptcha には、JavaScript スクリプトをロードする必要もあります。これは、LoginFormsProvider.addScript() を呼び出し、URL を渡すことで実行できます。
ユーザープロファイル処理の場合、フォームに追加する必要がある追加情報はないため、buildPage() メソッドは空です。
このインターフェースの次の重要な部分は、validate() メソッドです。これは、フォームポストを受信するとすぐに呼び出されます。まず、Recaptcha のプラグインを見てみましょう。
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (!Validation.isBlank(captcha) && validateRecaptcha(context, captcha)) {
context.success();
} else {
List<FormMessage> errors = new ArrayList<>();
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
context.validationError(formData, errors);
}
}
ここでは、Recaptcha ウィジェットがフォームに追加するフォームデータを取得します。構成から Recaptcha シークレットキーを取得します。次に、reCaptcha を検証します。成功した場合、ValidationContext.success() が呼び出されます。formData.remove を使用してフォームから captcha トークンをクリアしますが、他のフォームデータはそのままにします。そうでない場合は、ValidationContext.validationError() を呼び出して formData(ユーザーがデータを再入力する必要がないように)を渡し、表示したいエラーメッセージも指定します。エラーメッセージは、国際化されたメッセージバンドルのメッセージバンドルプロパティを指している必要があります。他の登録拡張機能の場合、validate() は、たとえば代替メール属性などのフォーム要素の形式を検証している可能性があります。
登録時にメールアドレスやその他のユーザー情報を検証するために使用されるユーザープロファイルプラグインも見てみましょう。
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfile profile = getOrCreateUserProfile(context, formData);
try {
profile.validate();
} catch (ValidationException pve) {
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL));
}
if (pve.hasError(Messages.EMAIL_EXISTS)) {
context.error(Errors.EMAIL_IN_USE);
} else if (pve.hasError(Messages.USERNAME_EXISTS)) {
context.error(Errors.USERNAME_IN_USE);
} else {
context.error(Errors.INVALID_REGISTRATION);
}
context.validationError(formData, errors);
return;
}
context.success();
}
ご覧のとおり、このユーザープロファイル処理の validate() メソッドは、メールと他のすべての属性がフォームに入力されていることを確認します。これは、メールが正しい形式であることを確認し、他のすべての検証を実行するユーザープロファイル SPI に委譲します。これらの検証のいずれかが失敗した場合、エラーメッセージがレンダリングのためにキューに入れられます。これには、検証に失敗したすべてのフィールドのメッセージが含まれます。
ご覧のとおり、ユーザープロファイルは、登録フォームに必要なすべてのユーザープロファイルフィールドが含まれていることを確認します。ユーザープロファイルは、正しい検証が使用され、属性がページ上で正しくグループ化されていることも確認します。各フィールドに正しいタイプが使用されており(ユーザーが定義済みの値から選択する必要がある場合など)、フィールドは一部のスコープ(プログレッシブプロファイリング)などでのみ「条件付き」でレンダリングされます。したがって、通常は新しい FormAction または登録フィールドを実装する必要はありませんが、ユーザープロファイルを適切に構成してこれを反映させることができます。詳細については、ユーザープロファイルドキュメントを参照してください。一般に、新しい FormAction は、新しいユーザープロファイルフィールドではなく、登録フォームに新しい資格情報を追加する場合(ここで言及されている ReCaptcha サポートなど)に役立つ場合があります。 |
すべての検証が処理された後、フォームフローは FormAction.success() メソッドを呼び出します。recaptcha の場合、これは何もしないので、説明は省略します。ユーザープロファイル処理の場合、このメソッドは登録済みユーザーに値を入力します。
@Override
public void success(FormContext context) {
checkNotOtherUserAuthenticating(context);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String email = formData.getFirst(UserModel.EMAIL);
String username = formData.getFirst(UserModel.USERNAME);
if (context.getRealm().isRegistrationEmailAsUsername()) {
username = email;
}
context.getEvent().detail(Details.USERNAME, username)
.detail(Details.REGISTER_METHOD, "form")
.detail(Details.EMAIL, email);
UserProfile profile = getOrCreateUserProfile(context, formData);
UserModel user = profile.create();
user.setEnabled(true);
// This means that following actions can retrieve user from the context by context.getUser() method
context.setUser(user);
}
新しいユーザーが作成され、新しく登録されたユーザーの UserModel が FormContext に追加されます。UserModel データを初期化するために適切なメソッドが呼び出されます。独自の FormAction では、次のようなものを使用してユーザーを取得できる可能性があります。
@Override
public void success(FormContext context) {
UserModel user = context.getUser();
if (user != null) {
// Do something useful with the user here ...
}
}
最後に、FormActionFactory クラスも定義する必要があります。このクラスは AuthenticatorFactory と同様に実装されているため、説明は省略します。
アクションのパッケージ化
クラスを単一の jar 内にパッケージ化します。この jar には、org.keycloak.authentication.FormActionFactory
という名前のファイルが含まれている必要があり、jar の META-INF/services/
ディレクトリに含まれている必要があります。このファイルには、jar 内にある各 FormActionFactory 実装の完全修飾クラス名をリストする必要があります。例:
org.keycloak.authentication.forms.RegistrationUserCreation
org.keycloak.authentication.forms.RegistrationRecaptcha
この services/ ファイルは、Keycloak がシステムにロードする必要があるプロバイダーをスキャンするために使用されます。
この jar をデプロイするには、providers/
ディレクトリにコピーし、bin/kc.[sh|bat] build
を実行します。
登録フローへの FormAction の追加
登録ページフローへの FormAction の追加は、管理コンソールで行う必要があります。[認証] メニュー項目に移動し、[フロー] タブに移動すると、現在定義されているフローを表示できます。組み込みフローは変更できないため、作成した Authenticator を追加するには、既存のフローをコピーするか、独自のフローを作成する必要があります。UI は十分に直感的であるため、フローを作成し、FormAction を追加する方法を自分で理解できることを願っています。
基本的には、登録フローをコピーする必要があります。次に、[登録フォーム] の右側にある [アクション] メニューをクリックし、[実行の追加] を選択して新しい実行を追加します。選択リストから FormAction を選択します。[FormAction] がまだ [ユーザー作成の登録] の後にリストされていない場合は、下ボタンを使用して移動して、[FormAction] が [ユーザー作成の登録] の後に来るようにします。FormAction はユーザー作成の後に来るようにする必要があります。[ユーザー作成の登録] の success() メソッドが新しい UserModel の作成を担当するためです。
フローを作成したら、登録にバインドする必要があります。[認証] メニューに移動し、[バインディング] タブに移動すると、フローをブラウザ、登録、または直接許可フローにバインドするオプションが表示されます。
パスワード/資格情報忘れフローの変更
Keycloak には、パスワード忘れ、またはユーザーによって開始された資格情報リセットのための特定の認証フローもあります。管理コンソールのフローページに移動すると、「資格情報のリセット」フローがあります。デフォルトでは、Keycloak はユーザーのメールまたはユーザー名を要求し、メールを送信します。ユーザーがリンクをクリックすると、パスワードと OTP(OTP が設定されている場合)の両方をリセットできます。フローで「OTP のリセット」認証基盤を無効にすることで、OTP の自動リセットを無効にできます。
このフローに追加機能を追加することもできます。たとえば、多くのデプロイメントでは、リンク付きのメールを送信するだけでなく、ユーザーに 1 つまたは複数の秘密の質問に答えてもらいたいと考えています。ディストリビューションに付属している秘密の質問の例を拡張して、資格情報リセットフローに組み込むことができます。
資格情報リセットフローを拡張する場合に注意すべき点が 1 つあります。最初の「認証基盤」は、ユーザー名またはメールを取得するためのページにすぎません。ユーザー名またはメールが存在する場合、AuthenticationFlowContext.getUser() は、特定されたユーザーを返します。それ以外の場合は null になります。このフォームは、以前のメールまたはユーザー名が存在しなかった場合、ユーザーにメールまたはユーザー名の入力を再度求めません。攻撃者が有効なユーザーを推測できないようにする必要があります。したがって、AuthenticationFlowContext.getUser() が null を返す場合は、有効なユーザーが選択されたように見せかけるためにフローを続行する必要があります。このフローに秘密の質問を追加する場合は、メールが送信された後にこれらの質問をすることを提案します。言い換えれば、「リセットメールの送信」認証基盤の後にカスタム認証基盤を追加します。
初回ブローカーログインフローの変更
初回ブローカーログインフローは、一部のアイデンティティプロバイダーを使用した初回ログイン中に使用されます。用語 初回ログイン
は、特定の認証済みアイデンティティプロバイダーアカウントにリンクされた既存の Keycloak アカウントがまだないことを意味します。
-
サーバー管理ガイド の
アイデンティティブローカリング
の章を参照してください。
クライアントの認証
Keycloak は実際には、OpenID Connect クライアントアプリケーションのプラグ可能な認証をサポートしています。クライアント(アプリケーション)の認証は、Keycloak アダプターが、Keycloak サーバーにバックチャネルリクエストを送信する際(認証成功後のアクセストークンへのコード交換リクエストや、トークンを更新するリクエストなど)に内部的に使用されます。ただし、クライアント認証は、ダイレクトアクセス許可
(OAuth2 リソースオーナーパスワード資格情報フロー
で表される)中、または サービスアカウント
認証(OAuth2 クライアント資格情報フロー
で表される)中に直接使用することもできます。
-
Keycloak アダプターと OAuth2 フローの詳細については、アプリケーションの保護ガイド を参照してください。
デフォルトの実装
実際には、Keycloak にはクライアント認証のデフォルト実装が 2 つあります。
- client_id と client_secret を使用した従来の認証
-
これは、OpenID Connect または OAuth2 仕様で言及されているデフォルトのメカニズムであり、Keycloak は初期の頃からサポートしています。パブリッククライアントは、POST リクエストに ID を含む
client_id
パラメータを含める必要があり(したがって、事実上認証されていません)、コンフィデンシャルクライアントは、clientId と clientSecret をユーザー名とパスワードとして使用したAuthorization: Basic
ヘッダーを含める必要があります。 - 署名付き JWT による認証
-
これは、OAuth 2.0 の JWT ベアラートークンプロファイル 仕様に基づいています。クライアント/アダプターは 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
- 対象者 - 発行レルムの URL を含むリスト -
asid
- 認証セッションの ID (オプション) -
nonce
- 操作を 1 回のみ実行できる場合に、使用の一意性を保証するためのランダムなナンス (オプション)
さらに、アクション トークンには、JSON にシリアル化可能な任意の数のカスタムフィールドを含めることができます。
アクション トークンの処理
アクション トークンが KEYCLOAK_ROOT/realms/master/login-actions/action-token
Keycloak エンドポイントに key
パラメータを介して渡されると、検証され、適切なアクション トークン ハンドラーが実行されます。処理は常に認証セッションのコンテキストで実行されます。新しいセッションであるか、アクション トークン サービスが既存の認証セッションに参加します (詳細は以下で説明します)。アクション トークン ハンドラーは、トークンによって規定されたアクションを実行でき (多くの場合、認証セッションを変更します)、HTTP 応答を生成します (たとえば、認証を続行したり、情報/エラーページを表示したりできます)。これらの手順を以下に詳しく説明します。
-
基本的なアクション トークンの検証。 署名と有効期間がチェックされ、
typ
フィールドに基づいてアクション トークン ハンドラーが決定されます。 -
認証セッションの決定。 アクション トークン URL が既存の認証セッションを持つブラウザで開かれ、トークンにブラウザからの認証セッションと一致する認証セッション ID が含まれている場合、アクション トークンの検証と処理はこの進行中の認証セッションをアタッチします。それ以外の場合、アクション トークン ハンドラーは、その時点でブラウザに存在する他の認証セッションを置き換える新しい認証セッションを作成します。
-
トークンタイプに固有のトークン検証。 アクション トークン エンドポイントロジックは、トークンからのユーザー (
sub
フィールド) とクライアント (azp
) が存在し、有効で、無効になっていないことを検証します。次に、アクション トークン ハンドラーで定義されたすべてのカスタム検証を検証します。さらに、トークン ハンドラーは、このトークンがシングルユースであることを要求できます。すでに使用されたトークンは、アクション トークン エンドポイントロジックによって拒否されます。 -
アクションの実行。 これらすべての検証の後、トークン内のパラメータに従って実際のアクションを実行するアクション トークン ハンドラーコードが呼び出されます。
-
シングルユーストークンの無効化。 トークンがシングルユースに設定されている場合、認証フローが完了すると、アクション トークンは無効になります。
独自のアクション トークンとそのハンドラーの実装
アクション トークンの作成方法
アクション トークンは、いくつかの必須フィールド (上記の アクション トークンの構造 を参照) を持つ署名付き JWT にすぎないため、Keycloak の JWSBuilder
クラスを使用してシリアル化および署名できます。この方法は、org.keycloak.authentication.actiontoken.DefaultActionToken
の serialize(session, realm, uriInfo)
メソッドで既に実装されており、プレーンな JsonWebToken
の代わりにトークンにそのクラスを使用することで、実装者が活用できます。
次の例は、単純なアクション トークンの実装を示しています。クラスには引数のないプライベートコンストラクタが必要であることに注意してください。これは、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.representations.JsonWebToken
クラスの子孫であり、org.keycloak.models.ActionTokenKeyModel
インターフェースを実装することを検討する必要があります。その場合、既存の org.keycloak.authentication.actiontoken.DefaultActionToken
クラスを利用できます。これは既に両方の条件を満たしており、直接使用するか、その子を実装することができます。そのフィールドには、JSON にシリアル化するために適切な Jackson アノテーション (例: 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.SingleUseObjectProvider
を使用する必要があります。ほとんどの
ActionTokenHandler
メソッドのデフォルト実装は、keycloak-services
モジュールのorg.keycloak.authentication.actiontoken.AbstractActionTokenHandler
抽象クラスです。実装する必要がある唯一のメソッドは、実際のアクションを実行するhandleToken(token, context)
です。 -
org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
- アクション トークン ハンドラーをインスタンス化するファクトリ。実装は、getId()
をオーバーライドして、アクション トークンのtyp
フィールドの値と正確に一致する必要がある値を返す必要があります。カスタム
ActionTokenHandlerFactory
実装を登録する必要があることに注意してください。これについては、このガイドの サービスプロバイダーインターフェース セクションで説明します。
イベントリスナー SPI
イベントリスナープロバイダーの作成は、EventListenerProvider
および EventListenerProviderFactory
インターフェースを実装することから始まります。その方法の詳細については、Javadoc と例を参照してください。
カスタムプロバイダーのパッケージ化とデプロイの方法の詳細については、サービスプロバイダーインターフェース の章を参照してください。
SAML ロールマッピング SPI
Keycloak は、SAML ロールを SP 環境に存在するロールにマッピングするための SPI を定義します。サードパーティ IDP によって返されるロールは、常に SP アプリケーションに定義されたロールに対応するとは限らないため、SAML ロールを別のロールにマッピングできるメカニズムが必要です。これは、SAML アダプターが SAML アサーションからロールを抽出した後、コンテナのセキュリティコンテキストを設定するために使用されます。
org.keycloak.adapters.saml.RoleMappingsProvider
SPI は、実行できるマッピングに制限を課していません。実装は、ロールを他のロールにマッピングするだけでなく、ユースケースに応じてロールを追加または削除することもできます(したがって、SAML プリンシパルに割り当てられたロールのセットを拡張または削減することもできます)。
SAML アダプターのロールマッピングプロバイダーの構成の詳細、および利用可能なデフォルト実装の説明については、アプリケーションの保護ガイド を参照してください。
カスタム ロールマッピングプロバイダーの実装
カスタム ロールマッピングプロバイダーを実装するには、まず 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 および ActiveDirectory サポートは、この SPI の実装例です。Keycloak は、ローカルデータベースをそのまま使用して、ユーザーの作成、更新、検索、および資格情報の検証を行います。ただし多くの場合、組織には、Keycloak のデータモデルに移行できない既存の独自のユーザーデータベースがあります。そのような状況のために、アプリケーション開発者は、ユーザー ストレージ SPI の実装を記述して、外部ユーザー ストアと、Keycloak がユーザーのログインと管理に使用する内部ユーザーオブジェクトモデルをブリッジできます。
Keycloak ランタイムがユーザーを検索する必要がある場合(ユーザーがログインしている場合など)、ユーザーを特定するためにいくつかの手順を実行します。最初に、ユーザーがユーザーキャッシュにあるかどうかを確認します。ユーザーが見つかった場合は、そのインメモリ表現を使用します。次に、Keycloak ローカルデータベース内でユーザーを検索します。ユーザーが見つからない場合は、ユーザー ストレージ SPI プロバイダーの実装をループ処理して、ランタイムが探しているユーザーを返すまでユーザークエリを実行します。プロバイダーは、ユーザーの外部ユーザー ストアをクエリし、ユーザーの外部データ表現を Keycloak のユーザーメタモデルにマッピングします。
ユーザー ストレージ SPI プロバイダーの実装は、複雑な条件クエリを実行したり、ユーザーに対して CRUD 操作を実行したり、資格情報を検証および管理したり、多数のユーザーの一括更新を実行したりすることもできます。これは、外部ストアの機能によって異なります。
ユーザー ストレージ SPI プロバイダーの実装は、Jakarta EE コンポーネントと同様に(そして多くの場合そうであるように)パッケージ化およびデプロイされます。これらはデフォルトでは有効になっていませんが、管理コンソールの [ユーザーフェデレーション] タブでレルムごとに有効にして構成する必要があります。
ユーザープロバイダーの実装で、ユーザー属性をユーザー ID をリンク/確立するためのメタデータ属性として使用している場合は、ユーザーが属性を編集できず、対応する属性が読み取り専用であることを確認してください。例として、組み込みの Keycloak LDAP プロバイダーが LDAP サーバー側のユーザーの ID を格納するために使用している LDAP_ID 属性があります。詳細については、脅威モデルの軽減の章 を参照してください。 |
Keycloak クイックスタートリポジトリ には 2 つのサンプルプロジェクトがあります。各クイックスタートには、サンプルプロジェクトのビルド、デプロイ、およびテストの方法に関する手順が記載された README
ファイルがあります。次の表に、利用可能なユーザー ストレージ SPI クイックスタートの簡単な説明を示します。
名前 | 説明 |
---|---|
JPA を使用したユーザー ストレージプロバイダーの実装を示します。 |
|
ユーザー名/パスワードのキーペアを含む単純なプロパティファイルを使用したユーザー ストレージプロバイダーの実装を示します。 |
プロバイダーインターフェース
ユーザー ストレージ 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
インスタンスは、トランザクションごとに 1 回作成されます。トランザクションが完了すると、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 は、特定のレルムのプロバイダーを有効にするときに、管理コンソールの [ユーザーフェデレーション] ページに表示されます。
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 を取得し、それを使用して外部ユーザー ストレージでユーザーを検索します。
この形式には、外部ストレージ ユーザーに対して長い ID を生成できるという欠点があります。これは、WebAuthn 認証と組み合わせると特に重要です。WebAuthn 認証では、ユーザー ハンドル ID が 64 バイトに制限されています。そのため、ストレージ ユーザーが WebAuthn 認証を使用する場合は、フル ストレージ ID を 64 文字に制限することが重要です。メソッド validateConfiguration
を使用すると、プロバイダー コンポーネントの作成時に短い ID を割り当てて、64 バイトの制限内でユーザー ID にある程度のスペースを与えることができます。
@Override
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
throws ComponentValidationException
{
// ...
if (model.getId() == null) {
// On creation use short UUID of 22 chars, 40 chars left for the user ID
model.setId(KeycloakModelUtils.generateShortId());
}
}
パッケージングとデプロイメント
Keycloak がプロバイダーを認識するためには、JAR にファイルを追加する必要があります。META-INF/services/org.keycloak.storage.UserStorageProviderFactory
です。このファイルには、UserStorageProviderFactory
実装の完全修飾クラス名の行区切りリストが含まれている必要があります。
org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
この jar をデプロイするには、providers/
ディレクトリにコピーし、bin/kc.[sh|bat] build
を実行します。
単純な読み取り専用、ルックアップの例
User Storage SPI の実装の基本を示すために、簡単な例を見ていきましょう。この章では、単純なプロパティ ファイルでユーザーをルックアップする単純な UserStorageProvider
の実装について説明します。プロパティ ファイルには、ユーザー名とパスワードの定義が含まれており、クラスパス上の特定の場所にハードコードされています。プロバイダーは、ID とユーザー名でユーザーをルックアップでき、パスワードを検証できます。このプロバイダーから発生したユーザーは読み取り専用になります。
プロバイダー クラス
最初に説明するのは、UserStorageProvider
クラスです。
public class PropertyFileUserStorageProvider implements
UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
CredentialInputUpdater
{
...
}
プロバイダー クラス PropertyFileUserStorageProvider
は、多くのインターフェースを実装しています。SPI の基本要件である UserStorageProvider
を実装しています。このプロバイダーによって格納されたユーザーでログインできるようにするために、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 と統合するプロバイダー)。また、プロバイダー クラスのインスタンスはトランザクションごとに1回作成され、トランザクションが完了すると閉じられることも覚えておいてください。
UserLookupProvider の実装
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
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(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(realm, username);
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
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 Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
return Stream.empty();
}
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
パラメーターは、サーバー構成を通じて構成されたファクトリー構成です。
たとえば、次の引数を指定してサーバーを実行すると
kc.[sh|bat] start --spi-storage-readonly-property-file-path=/other-users.properties
ユーザー プロパティ ファイルのクラスパスをハードコーディングする代わりに指定できます。次に、PropertyFileUserStorageProviderFactory.init()
で構成を取得できます。
public void init(Config.Scope config) {
String path = config.get("path");
InputStream is = getClass().getClassLoader().getResourceAsStream(path);
...
}
Create メソッド
プロバイダー ファクトリーを作成する最後のステップは、create()
メソッドです。
@Override
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
return new PropertyFileUserStorageProvider(session, model, properties);
}
PropertyFileUserStorageProvider
クラスを割り当てるだけです。この create メソッドは、トランザクションごとに1回呼び出されます。
パッケージングとデプロイメント
プロバイダー実装のクラス ファイルは、jar に配置する必要があります。また、プロバイダー ファクトリー クラスを META-INF/services/org.keycloak.storage.UserStorageProviderFactory
ファイル内で宣言する必要があります。
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
この jar をデプロイするには、providers/
ディレクトリにコピーし、bin/kc.[sh|bat] build
を実行します。
管理コンソールでプロバイダーを有効にする
ユーザー ストレージ プロバイダーは、管理コンソールの ユーザー フェデレーション ページ内のレルムごとに有効にします。
-
リストから作成したばかりのプロバイダー
readonly-property-file
を選択します。プロバイダーの構成ページが表示されます。
-
構成するものが何もないため、保存 をクリックします。
構成済みプロバイダー -
メインの ユーザー フェデレーション ページに戻ります
プロバイダーがリストされていることがわかります。
ユーザー フェデレーション
これで、users.properties
ファイルで宣言されたユーザーでログインできるようになります。このユーザーは、ログイン後にアカウント ページのみを表示できます。
構成テクニック
PropertyFileUserStorageProvider
の例は、少し不自然です。プロバイダーの jar に埋め込まれているプロパティ ファイルにハードコードされており、それほど役立ちません。このファイルの場所をプロバイダーのインスタンスごとに構成可能にしたい場合があります。言い換えれば、このプロバイダーを複数の異なるレルムで複数回再利用し、完全に異なるユーザー プロパティ ファイルを指し示したい場合があります。また、管理コンソール UI 内でこの構成を実行したいと考えています。
UserStorageProviderFactory
には、プロバイダー構成を処理する追加のメソッドを実装できます。プロバイダーごとに構成する変数を記述すると、管理コンソールは自動的に汎用入力ページをレンダリングしてこの構成を収集します。実装すると、コールバック メソッドは、プロバイダーが初めて作成されたとき、および更新されたときに、保存する前に構成を検証します。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
クラスは、構成プロパティのリストを作成するための優れたヘルパー クラスです。ここでは、path
という名前の変数を String タイプとして指定します。このプロバイダーの管理コンソール構成ページでは、この構成変数は 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}
文字列はサーバーの conf/
ディレクトリに対応し、この例では非常に役立ちます。
次にやらなければならないことは、古い 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つは、ユーザーの追加と削除、またはパスワードの変更を許可することです。例で定義されているユーザーは、管理コンソールでクエリ可能または表示可能でもありません。これらの拡張機能を追加するには、例のプロバイダーは UserQueryMethodsProvider
(または 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
メソッドを変更する必要があります。
addUser()
メソッドは、プロバイダーが UserRegistrationProvider
インターフェースを実装している場合に呼び出されます。プロバイダーにユーザーの追加をオフにする構成スイッチがある場合、このメソッドから 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(PasswordCredentialModel.TYPE)) 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(PasswordCredentialModel.TYPE)) return;
synchronized (properties) {
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
save();
}
}
private static final Set<String> disableableTypes = new HashSet<>();
static {
disableableTypes.add(PasswordCredentialModel.TYPE);
}
@Override
public Stream<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
return disableableTypes.stream();
}
これらのメソッドを実装すると、管理コンソールでユーザーのパスワードを変更および無効にできるようになります。
UserQueryProvider の実装
UserQueryProvider
は、UserQueryMethodsProvider
と UserCountMethodsProvider
の組み合わせです。UserQueryMethodsProvider
を実装しないと、管理コンソールは、例のプロバイダーによってロードされたユーザーを表示および管理できなくなります。このインターフェースの実装を見てみましょう。
@Override
public int getUsersCount(RealmModel realm) {
return properties.size();
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
Predicate<String> predicate = "*".equals(search) ? username -> true : username -> username.contains(search);
return properties.keySet().stream()
.map(String.class::cast)
.filter(predicate)
.skip(firstResult)
.map(username -> getUserByUsername(realm, username))
.limit(maxResults);
}
searchForUserStream()
の最初の宣言は、String
パラメーターを受け取ります。この例では、パラメーターは検索するユーザー名を表します。この文字列は部分文字列である可能性があり、検索を実行するときに String.contains()
メソッドを選択した理由を説明しています。すべてのユーザーのリストをリクエストすることを示すために *
を使用していることに注意してください。メソッドは、プロパティ ファイルのキー セットを反復処理し、getUserByUsername()
に委譲してユーザーをロードします。firstResult
および maxResults
パラメーターに基づいてこの呼び出しをインデックス付けしていることに注意してください。外部ストアがページネーションをサポートしていない場合は、同様のロジックを実行する必要があります。
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult, Integer maxResults) {
// only support searching by username
String usernameSearchString = params.get("username");
if (usernameSearchString != null)
return searchForUserStream(realm, usernameSearchString, firstResult, maxResults);
// if we are not searching by username, return all users
return searchForUserStream(realm, "*", firstResult, maxResults);
}
Map
パラメーターを受け取る searchForUserStream()
メソッドは、姓、名、ユーザー名、およびメールに基づいてユーザーを検索できます。ユーザー名のみが格納されるため、検索はユーザー名のみに基づいています。ただし、Map
パラメーターに username
属性が含まれていない場合は例外です。この場合、すべてのユーザーが返されます。そのような状況では、searchForUserStream(realm, search, firstResult, maxResults)
が使用されます。
@Override
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
return Stream.empty();
}
@Override
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
return Stream.empty();
}
グループまたは属性は格納されないため、他のメソッドは空のストリームを返します。
外部ストレージの拡張
PropertyFileUserStorageProvider
の例は、非常に制限されています。プロパティ ファイルに格納されているユーザーでログインできますが、それ以外はあまりできません。このプロバイダーによってロードされたユーザーが特定のアプリケーションに完全にアクセスするために特別なロールまたはグループ マッピングが必要な場合、これらのユーザーに追加のロール マッピングを追加する方法はありません。また、メール、姓、名などの重要な追加属性を変更または追加することもできません。
これらのタイプの状況では、Keycloak では、Keycloak のデータベースに追加情報を格納することで、外部ストアを拡張できます。これはフェデレーション ユーザー ストレージと呼ばれ、org.keycloak.storage.federated.UserFederatedStorageProvider
クラス内にカプセル化されています。
package org.keycloak.storage.federated;
public interface UserFederatedStorageProvider extends Provider,
UserAttributeFederatedStorage,
UserBrokerLinkFederatedStorage,
UserConsentFederatedStorage,
UserNotBeforeFederatedStorage,
UserGroupMembershipFederatedStorage,
UserRequiredActionsFederatedStorage,
UserRoleMappingsFederatedStorage,
UserFederatedUserCredentialStore {
...
}
UserFederatedStorageProvider
インスタンスは、UserStorageUtil.userFederatedStorage(KeycloakSession)
メソッドで利用できます。属性、グループとロールのマッピング、さまざまなクレデンシャル タイプ、および必要なアクションを格納するためのさまざまな種類のメソッドがあります。外部ストアのデータモデルが Keycloak のフル機能セットをサポートできない場合、このサービスはギャップを埋めることができます。
Keycloak には、org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage
というヘルパー クラスが付属しています。これは、ユーザー名get/setを除くすべての UserModel
メソッドをユーザー フェデレーション ストレージに委譲します。外部ストレージ表現に委譲するためにオーバーライドする必要があるメソッドをオーバーライドします。このクラスの javadoc を読むことを強くお勧めします。グループ メンバーシップとロール マッピングを中心に、オーバーライドしたい小さな protected メソッドがあります。
拡張例
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()
メソッドは、プロパティ ファイルに変更を加え、保存します。
インポート実装戦略
ユーザー ストレージ プロバイダーを実装する場合、別の戦略を採用できます。ユーザー フェデレーション ストレージを使用する代わりに、Keycloak 組み込みユーザー データベースにローカル ユーザーを作成し、外部ストアから属性をこのローカル コピーにコピーできます。このアプローチには多くの利点があります。
-
Keycloak は基本的に、外部ストアの永続ユーザー キャッシュになります。ユーザーがインポートされると、外部ストアにアクセスする必要がなくなり、負荷が軽減されます。
-
Keycloak を公式ユーザー ストアとして移行し、古い外部ストアを非推奨にする場合は、アプリケーションを Keycloak を使用するように徐々に移行できます。すべてのアプリケーションが移行されたら、インポートされたユーザーのリンクを解除し、古いレガシー外部ストアを廃止します。
ただし、インポート戦略を使用することには、明らかな欠点がいくつかあります。
-
ユーザーを初めてルックアップするには、Keycloak データベースへの複数の更新が必要です。これは、負荷がかかった状態では大きなパフォーマンスの低下になる可能性があり、Keycloak データベースに大きな負担をかける可能性があります。ユーザー フェデレーション ストレージ アプローチでは、必要に応じて追加データのみが格納され、外部ストアの機能によっては使用されない場合があります。
-
インポート アプローチでは、ローカル Keycloak ストレージと外部ストレージを同期させておく必要があります。User Storage SPI には、同期をサポートするために実装できるケイパビリティ インターフェースがありますが、これはすぐに苦痛で厄介になる可能性があります。
インポート戦略を実装するには、最初にユーザーがローカルにインポートされているかどうかを確認するだけです。そうである場合はローカル ユーザーを返し、そうでない場合はローカルにユーザーを作成し、外部ストアからデータをインポートします。ローカル ユーザーをプロキシして、ほとんどの変更が自動的に同期されるようにすることもできます。
これは少し不自然になりますが、PropertyFileUserStorageProvider
を拡張してこのアプローチを採用することができます。最初に createAdapter()
メソッドを変更することから始めます。
protected UserModel createAdapter(RealmModel realm, String username) {
UserModel local = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, username);
if (local == null) {
local = UserStoragePrivateUtil.userLocalStorage(session).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);
}
};
}
このメソッドでは、UserStoragePrivateUtil.userLocalStorage(session)
メソッドを呼び出して、ローカル 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 メソッドは、外部ストアをローカル ストアと同期させます。注意すべきことの1つは、getId()
メソッドは常に、ローカルでユーザーを作成したときに自動生成された ID を返す必要があるということです。他のインポート以外の例に示すように、フェデレーション ID を返すことは避けてください。
プロバイダーが UserRegistrationProvider インターフェースを実装している場合、removeUser() メソッドはローカル ストレージからユーザーを削除する必要はありません。ランタイムがこの操作を自動的に実行します。また、removeUser() はローカル ストレージから削除される前に呼び出されることにも注意してください。 |
ImportedUserValidation インターフェース
この章の前半で、ユーザーのクエリがどのように機能するかについて説明したことを覚えているかもしれません。ローカル ストレージが最初にクエリされ、そこでユーザーが見つかった場合、クエリは終了します。これは、ユーザー名を同期させておくことができるように、ローカル UserModel
をプロキシしたい上記の実装では問題になります。User Storage 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 インターフェース
インポート戦略を使用すると、ローカル ユーザー コピーが外部ストレージと同期しなくなる可能性があることがわかります。たとえば、ユーザーが外部ストアから削除された可能性があります。User Storage 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.getProvider(UserCache.class)
を呼び出すことでアクセスできます。
/**
* 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 コールバック インターフェース
プロバイダー実装に固有の追加情報をキャッシュしたい場合があります。User Storage SPI には、ユーザーがキャッシュされるたびにコールバックがあります。org.keycloak.models.cache.OnUserCache
です。
public interface OnUserCache {
void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
}
プロバイダー クラスは、このコールバックが必要な場合は、このインターフェースを実装する必要があります。UserModel
デリゲート パラメーターは、プロバイダーによって返される UserModel
インスタンスです。CachedUserModel
は、拡張された UserModel
インターフェースです。これは、ローカル ストレージにローカルにキャッシュされるインスタンスです。
public interface CachedUserModel extends UserModel {
/**
* Invalidates the cache for this user and returns a delegate that represents the actual data provider
*
* @return
*/
UserModel getDelegateForUpdate();
boolean isMarkedForEviction();
/**
* Invalidate the cache for this model
*
*/
void invalidate();
/**
* When was the model was loaded from database.
*
* @return
*/
long getCacheTimestamp();
/**
* Returns a map that contains custom things that are cached along with this model. You can write to this map.
*
* @return
*/
ConcurrentHashMap getCachedWith();
}
このCachedUserModel
インターフェースを使用すると、キャッシュからユーザーを削除し、プロバイダーのUserModel
インスタンスを取得できます。getCachedWith()
メソッドは、ユーザーに関する追加情報をキャッシュできるマップを返します。たとえば、認証情報はUserModel
インターフェースの一部ではありません。認証情報をメモリにキャッシュしたい場合は、OnUserCache
を実装し、getCachedWith()
メソッドを使用してユーザーの認証情報をキャッシュします。
Jakarta EE の活用
バージョン 20 以降、Keycloak は Quarkus のみに依存しています。WildFly とは異なり、Quarkus はアプリケーションサーバーではありません。詳細については、https://keycloak.dokyumento.jp/migration/migrating-to-quarkus#_quarkus_is_not_an_application_server を参照してください。
したがって、ユーザー・ストレージ・プロバイダーは、Jakarta EE コンポーネント内にパッケージ化したり、以前のバージョンの Keycloak が WildFly 上で実行されていた場合のように EJB にしたりすることはできません。
プロバイダーの実装は、以前のセクションで説明したように、適切なユーザー・ストレージ SPI インターフェースを実装するプレーンな Java オブジェクトである必要があります。それらは、移行ガイドに記載されているようにパッケージ化およびデプロイする必要があります。「カスタムプロバイダーの移行」を参照してください。
この例に示すように、JPA エンティティ・マネージャーによって外部データベースを統合できるカスタムの UserStorageProvider
クラスを実装することもできます。
CDI はサポートされていません。
REST 管理 API
管理者 REST API を介して、ユーザー・ストレージ・プロバイダーのデプロイメントを作成、削除、および更新できます。ユーザー・ストレージ SPI は、汎用コンポーネント・インターフェースの上に構築されているため、その汎用 API を使用してプロバイダーを管理します。
REST コンポーネント API は、レルム管理者リソースの下に存在します。
/admin/realms/{realm-name}/components
Java クライアントを使用したこの REST API のインタラクションのみを示します。この 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(
"https://127.0.0.1:8080",
"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 では、Keycloak のデータベースにユーザーのローカルコピーを作成し、外部ストアからローカルコピーに情報をインポートする必要がありました。ただし、これはもはや要件ではありません。以前のプロバイダーをそのまま移植することもできますが、非インポート戦略の方がより良いアプローチになるかどうかを検討する必要があります。
インポート戦略の利点
-
Keycloak は基本的に外部ストアの永続ユーザーキャッシュになります。ユーザーがインポートされると、外部ストアにアクセスする必要がなくなり、負荷が軽減されます。
-
Keycloak を公式ユーザー・ストアとして使用し、以前の外部ストアを非推奨にする場合は、アプリケーションを Keycloak に徐々に移行できます。すべてのアプリケーションが移行されたら、インポートされたユーザーのリンクを解除し、以前のレガシー外部ストアを廃止します。
ただし、インポート戦略を使用することには、明らかな欠点がいくつかあります。
-
ユーザーを初めて検索すると、Keycloak データベースへの複数の更新が必要になります。これは、負荷がかかると大きなパフォーマンスの低下につながり、Keycloak データベースに大きな負担をかける可能性があります。ユーザー・フェデレーション・ストレージ・アプローチでは、必要に応じて追加データのみが保存され、外部ストアの機能によっては使用されない場合があります。
-
インポート アプローチでは、ローカル Keycloak ストレージと外部ストレージを同期させておく必要があります。User Storage SPI には、同期をサポートするために実装できるケイパビリティ インターフェースがありますが、これはすぐに苦痛で厄介になる可能性があります。
UserFederationProvider 対 UserStorageProvider
最初に気づくことは、UserFederationProvider
が完全なインターフェースであったことです。このインターフェースのすべてのメソッドを実装しました。ただし、UserStorageProvider
は代わりに、このインターフェースを複数のケイパビリティ・インターフェースに分割し、必要に応じて実装します。
UserFederationProvider.getUserByUsername()
および getUserByEmail()
には、新しい SPI に正確な同等のものが存在します。2 つの違いは、インポート方法です。インポート戦略を継続する場合は、ユーザーをローカルに作成するために 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
インターフェースに移動されました。注意すべき点の 1 つは、CredentialInputUpdater
インターフェースを実装しない場合、プロバイダーによって提供される認証情報は Keycloak ストレージ内でローカルに上書きできることです。したがって、認証情報を読み取り専用にしたい場合は、CredentialInputUpdater.updateCredential()
メソッドを実装し、ReadOnlyException
を返します。
UserFederationProvider
クエリメソッド (searchByAttributes()
や getGroupMembers()
など) は、オプションのインターフェース UserQueryProvider
にカプセル化されるようになりました。このインターフェースを実装しない場合、管理コンソールでユーザーを表示できなくなります。ただし、ログインは引き続き可能です。
UserFederationProviderFactory 対 UserStorageProviderFactory
以前の SPI の同期メソッドは、オプションの ImportSynchronization
インターフェース内にカプセル化されるようになりました。同期ロジックを実装している場合は、新しい UserStorageProviderFactory
に ImportSynchronization
インターフェースを実装させます。
新しいモデルへのアップグレード
ユーザー・ストレージ SPI インスタンスは、異なるリレーショナルテーブルのセットに格納されます。Keycloak は、自動的にマイグレーションスクリプトを実行します。以前のユーザー・フェデレーション・プロバイダーがレルムにデプロイされている場合、データの id
を含め、それらは後期のストレージモデルにそのまま変換されます。このマイグレーションは、以前のユーザー・フェデレーション・プロバイダーと同じプロバイダー ID (つまり、「ldap」、「kerberos」) を持つユーザー・ストレージ・プロバイダーが存在する場合にのみ発生します。
したがって、これを認識すると、さまざまなアプローチを取ることができます。
-
以前の Keycloak デプロイメントで以前のプロバイダーを削除できます。これにより、インポートしたすべてのユーザーのローカルリンクされたコピーが削除されます。次に、Keycloak をアップグレードするときに、レルム用の新しいプロバイダーをデプロイして構成するだけです。
-
2 番目のオプションは、新しいプロバイダーを作成して、同じプロバイダー ID である
UserStorageProviderFactory.getId()
を持つようにすることです。このプロバイダーがサーバーにデプロイされていることを確認してください。サーバーを起動し、組み込みのマイグレーションスクリプトで以前のデータモデルから新しいデータモデルに変換します。この場合、以前にリンクされたインポートされたすべてのユーザーは動作し、同じになります。
インポート戦略を廃止し、ユーザー・ストレージ・プロバイダーを書き換えることを決定した場合は、Keycloak をアップグレードする前に以前のプロバイダーを削除することをお勧めします。これにより、インポートしたユーザーのリンクされたローカルコピーが削除されます。
ストリームベースのインターフェース
Keycloak のユーザー・ストレージ・インターフェースの多くには、潜在的に大きなオブジェクトセットを返す可能性のあるクエリメソッドが含まれており、メモリ消費量と処理時間に大きな影響を与える可能性があります。これは、クエリメソッドのロジックでオブジェクトの内部状態の小さなサブセットのみが使用される場合に特に当てはまります。
これらのクエリメソッドで大規模なデータセットを処理するためのより効率的な代替手段を開発者に提供するために、Streams
サブインターフェースがユーザー・ストレージ・インターフェースに追加されました。これらの Streams
サブインターフェースは、スーパーインターフェースの元のコレクションベースのメソッドをストリームベースのバリアントに置き換え、コレクションベースのメソッドをデフォルトにしています。コレクションベースのクエリメソッドのデフォルト実装は、その Stream
カウンターパートを呼び出し、結果を適切なコレクションタイプに収集します。
Streams
サブインターフェースを使用すると、実装はデータセットを処理するためのストリームベースのアプローチに焦点を当て、そのアプローチの潜在的なメモリとパフォーマンスの最適化の恩恵を受けることができます。Streams
サブインターフェースを実装するために提供するインターフェースには、いくつかのケイパビリティ・インターフェース、org.keycloak.storage.federated
パッケージのすべてのインターフェース、およびカスタム・ストレージ実装のスコープに応じて実装される可能性のある他のいくつかのインターフェースが含まれます。
Streams
サブインターフェースを開発者に提供するインターフェースのリストを以下に示します。
パッケージ |
クラス |
|
|
|
|
|
|
|
すべてのインターフェース |
|
|
(*) は、インターフェースがケイパビリティ・インターフェースであることを示します
ストリームアプローチの恩恵を受けたいカスタム・ユーザー・ストレージ実装は、元のインターフェースの代わりに 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
}
...
}
Vault SPI
Vault プロバイダー
org.keycloak.vault
パッケージの Vault SPI を使用して、任意の Vault 実装に接続するための Keycloak のカスタム拡張機能を記述できます。
組み込みの files-plaintext
プロバイダーは、この SPI の実装例です。一般に、次のルールが適用されます
-
シークレットがレルム間でリークするのを防ぐために、レルムが取得できるシークレットを分離または制限することができます。その場合、プロバイダーは、シークレットを検索するときにレルム名を考慮する必要があります。たとえば、エントリにレルム名をプレフィックスとして付けるなどです。たとえば、式
${vault.key}
は、レルム *A* またはレルム *B* で使用されているかどうかに応じて、一般的に異なるエントリ名に評価されます。レルムを区別するには、レルムをVaultProviderFactory.create()
メソッドから作成されたVaultProvider
インスタンスに渡す必要があります。これは、KeycloakSession
パラメーターから利用できます。 -
Vault プロバイダーは、指定されたシークレット名の
VaultRawSecret
を返す単一のメソッドobtainSecret
を実装する必要があります。そのクラスは、byte[]
またはByteBuffer
のいずれかでシークレットの表現を保持し、要求に応じて 2 つの間で変換することが期待されます。このバッファーは、以下で説明するように、使用後に破棄されることに注意してください。
レルムの分離に関して、すべての組み込み Vault プロバイダーファクトリーは、1 つ以上のキーリゾルバーの構成を許可します。VaultKeyResolver
インターフェースによって表されるキーリゾルバーは、本質的に、レルム名とキー (${vault.key}
式から取得) を結合して、Vault からシークレットを取得するために使用される最終的なエントリ名にするためのアルゴリズムまたは戦略を実装します。この構成を処理するコードは、抽象 Vault プロバイダーおよび Vault プロバイダーファクトリークラスに抽出されているため、キーリゾルバーのサポートを提供したいカスタム実装は、SPI インターフェースを実装する代わりにこれらの抽象クラスを拡張して、シークレットを取得するときに試行する必要があるキーリゾルバーを構成する機能を継承できます。
カスタムプロバイダーのパッケージ化とデプロイの方法の詳細については、サービスプロバイダーインターフェース の章を参照してください。
Vault からの値の消費
Vault には機密データが含まれており、Keycloak はそれに応じてシークレットを扱います。シークレットにアクセスすると、シークレットは Vault から取得され、必要な時間だけ JVM メモリに保持されます。次に、JVM メモリからその内容を破棄するための可能なすべての試行が行われます。これは、以下に示すように、try
-with-resources ステートメント内でのみ Vault シークレットを使用することで実現されます。
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()
メソッド経由) を取得することに加えて、raw シークレット (通常はバイト配列) を文字配列 (vault().getCharSecret()
経由) または String
(vault().getStringSecret()
経由) として解釈する機能があるという利点があります。
String
オブジェクトは不変であるため、その内容はランダムなガベージで上書きしても破棄できないことに注意してください。デフォルトの VaultStringSecret
実装では String
の内部化を防ぐための対策が講じられていますが、String
オブジェクトに格納されたシークレットは、少なくとも次の GC ラウンドまで存続します。したがって、プレーンなバイト配列と文字配列、およびバッファーを使用する方が好ましいです。