まえがき

一部のリスト例では、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 を作成したことを前提としています。
手順
  1. ユーザー名 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 ドキュメントになります。

  2. API を呼び出すには、access_token プロパティの値​​を抽出する必要があります。

  3. API へのリクエストの Authorization ヘッダーに値を​​含めることで、API を呼び出します。

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

    curl \
      -H "Authorization: bearer eyJhbGciOiJSUz..." \
      "https://127.0.0.1:8080/admin/realms/master"

サービスアカウントによる認証

client_idclient_secret を使用して Admin REST API に対して認証するには、次の手順を実行します。

手順
  1. クライアントが次のように構成されていることを確認してください。

    • client_id は、レルム master に属するconfidential クライアントです。

    • client_id は、Service Accounts Enabled オプションが有効になっています。

    • client_id にはカスタムの「Audience」マッパーがあります。

      • 含まれるクライアントオーディエンス: security-admin-console

  2. 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 ページとメールのテーマサポートを提供します。これにより、エンドユーザー向けのページのデザインをカスタマイズして、アプリケーションと統合できます。

login sunrise
日の出の例のテーマを使用したログインページ

テーマタイプ

テーマは、Keycloak のさまざまな側面をカスタマイズするために、1 つ以上のタイプを提供できます。利用可能なタイプは次のとおりです。

  • アカウント - アカウントコンソール

  • Admin - Admin コンソール

  • メール - メール

  • ログイン - ログインフォーム

  • ようこそ - ウェルカムページ

テーマの設定

ウェルカムを除くすべてのテーマタイプは、Admin Console を介して構成されます。

手順
  1. Admin Console にログインします。

  2. 左上隅のドロップダウンボックスからレルムを選択します。

  3. メニューから レルム設定 をクリックします。

  4. テーマ タブをクリックします。

    master Admin Console のテーマを設定するには、master レルムの Admin Console テーマを設定する必要があります。
  5. Admin Console の変更を確認するには、ページを更新してください。

  6. ウェルカムテーマを変更するには、spi-theme-welcome-theme オプションを使用します。

  7. 例:

    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 ディレクトリからテーマリソースを直接編集できます。

手順
  1. 次のオプションを指定して Keycloak を実行します。

    bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false
  2. themes ディレクトリにディレクトリを作成します。

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

  3. テーマディレクトリ内で、テーマが提供するタイプごとにディレクトリを作成します。

    たとえば、ログインタイプを mytheme テーマに追加するには、ディレクトリ themes/mytheme/login を作成します。

  4. タイプごとに、テーマの設定を可能にするファイル theme.properties を作成します。

    たとえば、base テーマを拡張し、いくつかの共通リソースをインポートするようにテーマ themes/mytheme/login を構成するには、次の内容でファイル themes/mytheme/login/theme.properties を作成します。

    parent=base
    import=common/keycloak

    これで、ログインタイプをサポートするテーマが作成されました。

  5. Admin Console にログインして、新しいテーマを確認してください。

  6. レルムを選択します。

  7. メニューから レルム設定 をクリックします。

  8. テーマ タブをクリックします。

  9. ログインテーマmytheme を選択し、保存 をクリックします。

  10. レルムのログインページを開きます。

    これは、アプリケーションからログインするか、アカウントコンソール (/realms/{realm-name}/account) を開くことで実行できます。

  11. 親テーマの変更の効果を確認するには、theme.propertiesparent=keycloak を設定し、ログインページを更新します。

パフォーマンスに大きな影響を与えるため、本番環境ではキャッシュを再度有効にしてください。

テーマキャッシュの内容を手動で削除する場合は、サーバーディストリビューションの data/tmp/kc-gzip-cache ディレクトリを削除することで実行できます。これは、以前のサーバー実行でテーマのキャッシュを無効にせずにカスタムプロバイダーまたはカスタムテーマを再デプロイした場合などに役立ちます。

テーマのプロパティ

テーマプロパティは、テーマディレクトリのファイル <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 つ以上のスタイルシートを追加できます。

手順
  1. テーマの <THEME TYPE>/resources/css ディレクトリにファイルを作成します。

  2. このファイルを theme.propertiesstyles プロパティに追加します。

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

    .login-pf body {
        background: DimGrey none;
    }
  3. themes/mytheme/login/theme.properties を編集し、次を追加します。

    styles=css/styles.css
  4. 変更を確認するには、レルムのログインページを開きます。

    適用されているスタイルは、カスタムスタイルシートからのスタイルのみであることに気付くでしょう。

  5. 親テーマのスタイルを含めるには、そのテーマからスタイルをロードします。themes/mytheme/login/theme.properties を編集し、styles を次のように変更します。

    styles=css/login.css css/styles.css
    親スタイルシートのスタイルをオーバーライドするには、スタイルシートが最後にリストされていることを確認してください。

テーマへのスクリプトの追加

テーマに 1 つ以上のスクリプトを追加できます。

手順
  1. テーマの <THEME TYPE>/resources/js ディレクトリにファイルを作成します。

  2. このファイルを theme.propertiesscripts プロパティに追加します。

    たとえば、script.jsmytheme に追加するには、次の内容で 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 をテーマに追加することで個々のメッセージをオーバーライドできます。

たとえば、ログインフォームの UsernamemythemeYour Username に置き換えるには、次の内容でファイル themes/mytheme/login/messages/messages_en.properties を作成します。

usernameOrEmail=Your Username

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

これらのメッセージバンドルのテキストは、レルム固有の値で上書きできます。レルム固有の値は、UI および API を介して管理できます。

レルムへの言語の追加

前提条件
手順
  1. テーマのディレクトリにファイル <THEME TYPE>/messages/messages_<LOCALE>.properties を作成します。

  2. このファイルを <THEME TYPE>/theme.propertieslocales プロパティに追加します。言語をユーザーが利用できるようにするには、レルムの loginaccount、および email テーマが言語をサポートしている必要があるため、これらのテーマタイプに言語を追加する必要があります。

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

    usernameOrEmail=Brukernavn
    password=Passord

    メッセージの翻訳を省略すると、英語が使用されます。

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

    locales=en,no
  4. account および email テーマタイプについても同じことを追加します。これを行うには、themes/mytheme/account/messages/messages_no.properties および themes/mytheme/email/messages/messages_no.properties を作成します。これらのファイルを空のままにすると、英語のメッセージが使用されます。

  5. themes/mytheme/login/theme.propertiesthemes/mytheme/account/theme.properties および themes/mytheme/email/theme.properties にコピーします。

  6. 言語セレクターの翻訳を追加します。これは、英語の翻訳にメッセージを追加することで行われます。これを行うには、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 のアイコンの追加をサポートしています。

手順
  1. キーパターン kcLogoIdP-<alias> を使用して、ログイン theme.properties ファイル (例: themes/mytheme/login/theme.properties) でアイコンクラスを定義します。

  2. エイリアス 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> ディレクトリを参照してください。

手順
  1. ベーステーマから独自のテーマにテンプレートをコピーします。

  2. 必要な変更を適用します。

    たとえば、mytheme テーマのカスタムログインフォームを作成するには、themes/base/login/login.ftlthemes/mytheme/login にコピーし、エディターで開きます。

    最初の行 (<#import …​>) の後に、次のように <h1>HELLO WORLD!</h1> を追加します。

    <#import "template.ftl" as layout>
    <h1>HELLO WORLD!</h1>
    ...
  3. 変更したテンプレートをバックアップします。Keycloak の新しいバージョンにアップグレードするときは、該当する場合、元のテンプレートへの変更を適用するために、カスタムテンプレートを更新する必要がある場合があります。

追加リソース

メール

パスワードリカバリーメールなどのメールの件名と内容を編集するには、テーマの 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 のインスタンスが複数ある場合に、テーマのバージョン管理されたコピーを簡単に作成できます。

手順
  1. テーマをアーカイブとしてデプロイするには、テーマリソースを含む JAR アーカイブを作成します。

  2. アーカイブ内の利用可能なテーマと、各テーマが提供するタイプをリストするファイル 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 で入手できます。

パッケージのインストール

パッケージをインストールするには、次のコマンドを実行します。

pnpm install @keycloak/keycloak-account-ui

パッケージの使用

これらのページを使用するには、コンポーネント階層に 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 を介して変更できます。

これは、たとえばユーザーエージェントヘッダーを参照して、デスクトップデバイスとモバイルデバイスで異なるテーマを選択するために使用できます。

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

テーマリソース

Keycloak でカスタムプロバイダーを実装する場合、追加のテンプレート、リソース、およびメッセージバンドルを追加する必要があることがよくあります。

ユースケースの例としては、追加のテンプレートとリソースを必要とする カスタム認証 があります。

追加のテーマリソースをロードする最も簡単な方法は、theme-resources/templates にテンプレート、theme-resources/resources にリソース、および theme-resources/messages にメッセージバンドルを含む JAR を作成することです。

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

ロケールセレクター

デフォルトでは、ロケールは 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攻撃から保護する責任を依然として負っています。

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

プロバイダー(つまり、FacebookまたはGitHubトークン)にログインすることによって生成された外部トークンを使用している場合、アカウント連携APIを再実行することでこのトークンをリフレッシュできます。

サービスプロバイダーインターフェース(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管理コンソールには、この種の情報を表示するサーバー情報ページが用意されています。

プロバイダーからの情報を表示するには、ProviderFactoryorg.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のリソースファイルのパス名にそのプロバイダーに固有のものが含まれていることを確認するか、"install root"/lib/lib/mainディレクトリの下のJARコンテンツにsome.fileが存在するかどうかを次のようなもので確認する必要があります。

find . -type f -name "*.jar" -exec unzip -l {} \; | grep some.file

削除されたプロバイダーJARに関連するNoSuchFileExceptionエラーが原因でサーバーが起動しないことが判明した場合は、次を実行してください。

./kc.sh -Dquarkus.launch.rebuild=true --help

これにより、Quarkusはクラスローディング関連のインデックスファイルを強制的に再構築します。そこから、例外なしで非最適化された起動またはビルドを実行できるはずです。

プロバイダーの無効化

プロバイダーのenabled属性をfalseに設定することで、プロバイダーを無効にできます。たとえば、Infinispanユーザーキャッシュプロバイダーを無効にするには、次を使用します。

bin/kc.[sh|bat] build --spi-user-cache-infinispan-enabled=false

JavaScriptプロバイダー

スクリプトはプレビューであり、完全にサポートされていません。この機能はデフォルトで無効になっています。

有効にするには、--features=previewまたは--features=scriptsでサーバーを起動します。

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

現在のUserModeluserは、スクリプト認証者が認証フローで、ユーザー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内のファイルにマップする必要があります。

スクリプトJARをデプロイする

記述子とデプロイするスクリプトを含むJARファイルを作成したら、JARをKeycloakのproviders/ディレクトリにコピーし、bin/kc.[sh|bat] buildを実行するだけです。

利用可能な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を追加するには、次の手順を使用します。

手順
  1. 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;
        }
    
    }
  2. ファイルMETA-INF/services/org.keycloak.provider.Spiを作成し、それにSPIのクラスを追加します。例:

    ExampleSpi
  3. ProviderFactoryから拡張されたExampleServiceProviderFactoryインターフェースと、Providerから拡張されたExampleServiceを作成します。ExampleServiceには通常、ユースケースに必要なビジネスメソッドが含まれます。ExampleServiceProviderFactoryインスタンスは常にアプリケーションごとにスコープ指定されますが、ExampleServiceはリクエストごと(またはより正確にはKeycloakSessionライフサイクルごと)にスコープ指定されることに注意してください。

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

KeycloakデータモデルにカスタムJPAエンティティを追加する

Keycloakデータモデルが目的のソリューションと正確に一致しない場合、またはKeycloakにいくつかのコア機能を追加したい場合、または独自のRESTエンドポイントがある場合、Keycloakデータモデルを拡張したい場合があります。Keycloak JPA EntityManagerに独自のJPAエンティティを追加できるようにします。

独自のJPAエンティティを追加するには、JpaEntityProviderFactoryJpaEntityProviderを実装する必要があります。JpaEntityProviderを使用すると、カスタムJPAエンティティのリストを返し、Liquibaseチェンジログの場所とIDを提供できます。実装例は次のようになります。

これはサポートされていないAPIです。つまり、使用できますが、予告なしに削除または変更されないという保証はありません。
public class ExampleJpaEntityProvider implements JpaEntityProvider {

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

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

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

    ...
}

上記の例では、クラスCompanyで表される単一のJPAエンティティを追加しました。RESTエンドポイントのコードでは、次のようなものを使用してEntityManagerを取得し、DB操作を呼び出すことができます。

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

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にリダイレクトしてユーザーを認証するときからの手順を順に見ていきましょう。

  1. OpenID ConnectまたはSAMLプロトコルプロバイダーは、関連データをアンパックし、クライアントと署名を検証します。AuthenticationSessionModelを作成します。ブラウザーフローがどうあるべきかを調べ、フローの実行を開始します。

  2. フローはCookie実行を見て、それが代替であることを確認します。Cookieプロバイダーをロードします。Cookieプロバイダーが、ユーザーが認証セッションに既に関連付けられている必要があるかどうかを確認します。Cookieプロバイダーはユーザーを必要としません。もしそうであれば、フローは中止され、ユーザーはエラー画面を表示します。次に、Cookieプロバイダーが実行されます。その目的は、SSO Cookieが設定されているかどうかを確認することです。設定されている場合は、検証され、UserSessionModelが検証され、AuthenticationSessionModelに関連付けられます。Cookieプロバイダーは、SSO Cookieが存在し、検証された場合はsuccess()ステータスを返します。Cookieプロバイダーが成功を返し、フローのこのレベルでの各実行がALTERNATIVEであるため、他の実行は実行されず、これによりログインが成功します。SSO Cookieがない場合、Cookieプロバイダーはattempted()のステータスで戻ります。これは、エラー状態はなかったが、成功もなかったことを意味します。プロバイダーは試行しましたが、リクエストはこの認証者を処理するように設定されていませんでした。

  3. 次に、フローは Kerberos 実行に注目します。これも代替手段の一つです。Kerberos プロバイダーも、ユーザーがまだセットアップされておらず、AuthenticationSessionModel に関連付けられていないことを要求しないため、このプロバイダーが実行されます。Kerberos は SPNEGO ブラウザープロトコルを使用します。これには、サーバーとクライアント間でネゴシエーションヘッダーを交換する一連のチャレンジ/レスポンスが必要です。Kerberos プロバイダーはネゴシエーションヘッダーを認識しないため、サーバーとクライアント間の最初のインタラクションであると想定します。したがって、クライアントへの HTTP チャレンジレスポンスを作成し、forceChallenge() ステータスを設定します。forceChallenge() は、この HTTP レスポンスがフローによって無視できず、クライアントに返される必要があることを意味します。代わりに、プロバイダーが challenge() ステータスを返した場合、フローは他のすべての代替手段が試行されるまでチャレンジレスポンスを保持します。したがって、この初期フェーズでは、フローは停止し、チャレンジレスポンスがブラウザーに送り返されます。ブラウザーがネゴシエーションヘッダーの成功で応答した場合、プロバイダーはユーザーを AuthenticationSession に関連付け、フローのこのレベルでの残りの実行はすべて代替手段であるため、フローは終了します。それ以外の場合、再び Kerberos プロバイダーは attempted() ステータスを設定し、フローは続行されます。

  4. 次の実行は Forms と呼ばれるサブフローです。このサブフローの実行がロードされ、同じ処理ロジックが実行されます。

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

  6. 次の実行は Conditional OTP と呼ばれるサブフローです。このサブフローの実行がロードされ、同じ処理ロジックが実行されます。その Requirement は Conditional です。これは、フローが最初にそれに含まれるすべての条件付きエグゼキュータを評価することを意味します。条件付きエグゼキュータは、ConditionalAuthenticator を実装する認証器であり、メソッド boolean matchCondition(AuthenticationFlowContext context) を実装する必要があります。条件付きサブフローは、それに含まれるすべての条件付き実行の matchCondition メソッドを呼び出し、それらのすべてが true と評価された場合、必須サブフローであるかのように動作します。そうでない場合、無効化されたサブフローであるかのように動作します。条件付き認証器は、この目的でのみ使用され、認証器としては使用されません。これは、条件付き認証器が「true」と評価されたとしても、フローまたはサブフローが成功としてマークされないことを意味します。たとえば、条件付き認証器のみを持つ条件付きサブフローのみを含むフローは、ユーザーがログインすることを許可しません。

  7. Conditional OTP サブフローの最初の実行は Condition - User Configured です。このプロバイダーは、ユーザーがフローに関連付けられている必要があることを要求します。この要件は、UsernamePassword プロバイダーがすでにユーザーをフローに関連付けているため満たされています。このプロバイダーの matchCondition メソッドは、現在のサブフロー内の他のすべての認証器の configuredFor メソッドを評価します。サブフローに Requirement が required に設定されたエグゼキュータが含まれている場合、matchCondition メソッドは、すべての必須認証器の configuredFor メソッドが true と評価された場合にのみ true と評価されます。それ以外の場合、matchCondition メソッドは、任意の代替認証器が true と評価された場合に true と評価されます。

  8. 次の実行は OTP Form です。このプロバイダーも、ユーザーがフローに関連付けられている必要があることを要求します。この要件は、UsernamePassword プロバイダーがすでにユーザーをフローに関連付けているため満たされています。ユーザーはこのプロバイダーに必須であるため、プロバイダーはユーザーがこのプロバイダーを使用するように構成されているかどうかも尋ねられます。ユーザーが構成されていない場合、フローは認証完了後にユーザーが実行する必要がある必須アクションをセットアップします。OTP の場合、これは OTP セットアップページを意味します。ユーザーが構成されている場合、OTP コードを入力するように求められます。このシナリオでは、条件付きサブフローのため、Conditional OTP サブフローが Required に設定されていない限り、ユーザーは OTP ログインページを見ることはありません。

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

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

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

  12. 必須アクションが最終的に成功した場合、必須アクションはユーザーの必須アクションリストから削除されます。

  13. すべての必須アクションが解決されると、ユーザーは最終的にログインします。

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 にはクライアント認証のデフォルト実装が 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.ClientAuthenticatorFactoryorg.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 応答を生成します (たとえば、認証を続行したり、情報/エラーページを表示したりできます)。これらの手順を以下に詳しく説明します。

  1. 基本的なアクション トークンの検証。 署名と有効期間がチェックされ、typ フィールドに基づいてアクション トークン ハンドラーが決定されます。

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

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

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

  5. シングルユーストークンの無効化。 トークンがシングルユースに設定されている場合、認証フローが完了すると、アクション トークンは無効になります。

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

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

アクション トークンは、いくつかの必須フィールド (上記の アクション トークンの構造 を参照) を持つ署名付き JWT にすぎないため、Keycloak の JWSBuilder クラスを使用してシリアル化および署名できます。この方法は、org.keycloak.authentication.actiontoken.DefaultActionTokenserialize(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) で定義されたものを含む) が成功した場合にのみ呼び出され、tokengetTokenClass() メソッドによって返されるクラスのクラスであることが保証されます。

    上記の項目 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 クイックスタートの簡単な説明を示します。

表 1. ユーザー ストレージ SPI クイックスタート
名前 説明

user-storage-jpa

JPA を使用したユーザー ストレージプロバイダーの実装を示します。

user-storage-simple

ユーザー名/パスワードのキーペアを含む単純なプロパティファイルを使用したユーザー ストレージプロバイダーの実装を示します。

プロバイダーインターフェース

ユーザー ストレージ 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 説明

org.keycloak.storage.user.UserLookupProvider

このインターフェースは、この外部ストアのユーザーでログインできるようにする場合に必須です。ほとんど(すべて?)のプロバイダーがこのインターフェースを実装しています。

org.keycloak.storage.user.UserQueryMethodsProvider

1人以上のユーザーを検索するために使用される複雑なクエリを定義します。管理コンソールからユーザーを表示および管理する場合は、このインターフェースを実装する必要があります。

org.keycloak.storage.user.UserCountMethodsProvider

プロバイダーがカウントクエリをサポートしている場合は、このインターフェースを実装します。

org.keycloak.storage.user.UserQueryProvider

このインターフェースは、UserQueryMethodsProviderUserCountMethodsProvider の複合ケイパビリティです。

org.keycloak.storage.user.UserRegistrationProvider

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

org.keycloak.storage.user.UserBulkUpdateProvider

プロバイダーがユーザーセットの一括更新をサポートしている場合は、このインターフェースを実装します。

org.keycloak.credential.CredentialInputValidator

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

org.keycloak.credential.CredentialInputUpdater

プロバイダーが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 メタモデルの他の部分を表す他のモデル クラスがあります。RealmModelRoleModelGroupModel、および 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;
    }

このプロバイダー クラスのコンストラクターは、KeycloakSessionComponentModel、およびプロパティ ファイルへの参照を格納します。これらはすべて後で使用します。また、ロードされたユーザーのマップがあることにも注目してください。ユーザーが見つかるたびに、同じトランザクション内で再作成することを避けるために、このマップに格納します。これは、多くのプロバイダーがこれを行う必要があるため、従うべき良い習慣です(つまり、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 を実行します。

管理コンソールでプロバイダーを有効にする

ユーザー ストレージ プロバイダーは、管理コンソールの ユーザー フェデレーション ページ内のレルムごとに有効にします。

ユーザー フェデレーション

empty user federation page

手順
  1. リストから作成したばかりのプロバイダー readonly-property-file を選択します。

    プロバイダーの構成ページが表示されます。

  2. 構成するものが何もないため、保存 をクリックします。

    構成済みプロバイダー

    storage provider created

  3. メインの ユーザー フェデレーション ページに戻ります

    プロバイダーがリストされていることがわかります。

    ユーザー フェデレーション

    user federation page

これで、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 の例を拡張して、プロバイダー インスタンスをディスク上の特定のファイルを指し示すことができるようにしましょう。

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);
    }

このロジックは、もちろん非効率的です。トランザクションごとにディスクからユーザー プロパティ ファイル全体を読み取りますが、うまくいけば、構成変数をフックする方法を簡単な方法で説明できます。

管理コンソールでプロバイダーを構成する

構成が有効になったので、管理コンソールでプロバイダーを構成するときに path 変数を設定できます。

構成済みプロバイダー

readonly user storage provider with config

ユーザーの追加/削除およびクエリ ケイパビリティ インターフェース

例でまだ行っていないことの1つは、ユーザーの追加と削除、またはパスワードの変更を許可することです。例で定義されているユーザーは、管理コンソールでクエリ可能または表示可能でもありません。これらの拡張機能を追加するには、例のプロバイダーは UserQueryMethodsProvider(または UserQueryProvider)および UserRegistrationProvider インターフェースを実装する必要があります。

UserRegistrationProvider の実装

特定のストアからユーザーを追加および削除する実装を行うには、最初にプロパティ ファイルをディスクに保存できるようにする必要があります。

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

次に、addUser() および removeUser() メソッドの実装が簡単になります。

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

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

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

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

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

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

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

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

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

パスワードの無効化も実装できるようになりました。

PropertyFileUserStorageProvider
    @Override
    public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
        if (!credentialType.equals(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 は、UserQueryMethodsProviderUserCountMethodsProvider の組み合わせです。UserQueryMethodsProvider を実装しないと、管理コンソールは、例のプロバイダーによってロードされたユーザーを表示および管理できなくなります。このインターフェースの実装を見てみましょう。

PropertyFileUserStorageProvider
    @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 パラメーターに基づいてこの呼び出しをインデックス付けしていることに注意してください。外部ストアがページネーションをサポートしていない場合は、同様のロジックを実行する必要があります。

PropertyFileUserStorageProvider
    @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) が使用されます。

PropertyFileUserStorageProvider
    @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 クラス内にカプセル化されています。

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 を使用するためにプロバイダーに簡単な変更を加えるだけです。

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

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

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

インポート実装戦略

ユーザー ストレージ プロバイダーを実装する場合、別の戦略を採用できます。ユーザー フェデレーション ストレージを使用する代わりに、Keycloak 組み込みユーザー データベースにローカル ユーザーを作成し、外部ストアから属性をこのローカル コピーにコピーできます。このアプローチには多くの利点があります。

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

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

ただし、インポート戦略を使用することには、明らかな欠点がいくつかあります。

  • ユーザーを初めてルックアップするには、Keycloak データベースへの複数の更新が必要です。これは、負荷がかかった状態では大きなパフォーマンスの低下になる可能性があり、Keycloak データベースに大きな負担をかける可能性があります。ユーザー フェデレーション ストレージ アプローチでは、必要に応じて追加データのみが格納され、外部ストアの機能によっては使用されない場合があります。

  • インポート アプローチでは、ローカル Keycloak ストレージと外部ストレージを同期させておく必要があります。User Storage SPI には、同期をサポートするために実装できるケイパビリティ インターフェースがありますが、これはすぐに苦痛で厄介になる可能性があります。

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

これは少し不自然になりますが、PropertyFileUserStorageProvider を拡張してこのアプローチを採用することができます。最初に createAdapter() メソッドを変更することから始めます。

PropertyFileUserStorageProvider
    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 インターフェース内にカプセル化されるようになりました。同期ロジックを実装している場合は、新しい UserStorageProviderFactoryImportSynchronization インターフェースを実装させます。

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

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

したがって、これを認識すると、さまざまなアプローチを取ることができます。

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

  2. 2 番目のオプションは、新しいプロバイダーを作成して、同じプロバイダー ID である UserStorageProviderFactory.getId() を持つようにすることです。このプロバイダーがサーバーにデプロイされていることを確認してください。サーバーを起動し、組み込みのマイグレーションスクリプトで以前のデータモデルから新しいデータモデルに変換します。この場合、以前にリンクされたインポートされたすべてのユーザーは動作し、同じになります。

インポート戦略を廃止し、ユーザー・ストレージ・プロバイダーを書き換えることを決定した場合は、Keycloak をアップグレードする前に以前のプロバイダーを削除することをお勧めします。これにより、インポートしたユーザーのリンクされたローカルコピーが削除されます。

ストリームベースのインターフェース

Keycloak のユーザー・ストレージ・インターフェースの多くには、潜在的に大きなオブジェクトセットを返す可能性のあるクエリメソッドが含まれており、メモリ消費量と処理時間に大きな影響を与える可能性があります。これは、クエリメソッドのロジックでオブジェクトの内部状態の小さなサブセットのみが使用される場合に特に当てはまります。

これらのクエリメソッドで大規模なデータセットを処理するためのより効率的な代替手段を開発者に提供するために、Streams サブインターフェースがユーザー・ストレージ・インターフェースに追加されました。これらの Streams サブインターフェースは、スーパーインターフェースの元のコレクションベースのメソッドをストリームベースのバリアントに置き換え、コレクションベースのメソッドをデフォルトにしています。コレクションベースのクエリメソッドのデフォルト実装は、その Stream カウンターパートを呼び出し、結果を適切なコレクションタイプに収集します。

Streams サブインターフェースを使用すると、実装はデータセットを処理するためのストリームベースのアプローチに焦点を当て、そのアプローチの潜在的なメモリとパフォーマンスの最適化の恩恵を受けることができます。Streams サブインターフェースを実装するために提供するインターフェースには、いくつかのケイパビリティ・インターフェースorg.keycloak.storage.federated パッケージのすべてのインターフェース、およびカスタム・ストレージ実装のスコープに応じて実装される可能性のある他のいくつかのインターフェースが含まれます。

Streams サブインターフェースを開発者に提供するインターフェースのリストを以下に示します。

パッケージ

クラス

org.keycloak.credential

CredentialInputUpdater(*)、UserCredentialStore

org.keycloak.models

GroupModelRoleMapperModelUserCredentialManagerUserModelUserProvider

org.keycloak.models.cache

CachedUserModelUserCache

org.keycloak.storage.federated

すべてのインターフェース

org.keycloak.storage.user

UserQueryProvider(*)

(*) は、インターフェースがケイパビリティ・インターフェースであることを示します

ストリームアプローチの恩恵を受けたいカスタム・ユーザー・ストレージ実装は、元のインターフェースの代わりに 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 ラウンドまで存続します。したがって、プレーンなバイト配列と文字配列、およびバッファーを使用する方が好ましいです。