サーバー開発者ガイド

Red Hat Single Sign-On 7.5

Red Hat Single Sign-On 7.5 向け

概要

本ガイドは、開発者が Red Hat Single Sign-On 7.5 をカスタマイズするための情報で設定されています。

多様性を受け入れるオープンソースの強化

Red Hat では、コード、ドキュメント、Web プロパティーにおける配慮に欠ける用語の置き換えに取り組んでいます。まずは、マスター (master)、スレーブ (slave)、ブラックリスト (blacklist)、ホワイトリスト (whitelist) の 4 つの用語の置き換えから始めます。この取り組みは膨大な作業を要するため、今後の複数のリリースで段階的に用語の置き換えを実施して参ります。詳細は、Red Hat CTO である Chris Wright のメッセージ をご覧ください。

第1章 はじめに

サンプルの一覧では、1 行に表示されるものが、利用可能なページ幅に収まらない場合があります。これらの行は、分割されています。行の末尾にある\は、ページに収まるように、改行が追加されていることを意味しています。つまり、

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

上記は、以下を指します。

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

第2章 管理 REST API

Red Hat Single Sign-On には、管理コンソールの全機能が含まれる、完全に機能する管理 REST API が同梱されています。

API を呼び出すには、適切なパーミッションでアクセストークンを取得する必要があります。必要なパーミッションは、サーバー管理ガイドで説明しています。

Red Hat Single Sign-On を使用してアプリケーションの認証を有効にすることで、トークンを取得できます。アプリケーションおよびサービスのセキュリティー保護ガイドを参照してください。直接アクセス許可を使用して、アクセストークンを取得することもできます。

2.1. CURL の使用例

2.1.1. ユーザー名とパスワードでの認証

手順

  1. ユーザー名 admin およびパスワード password を使用して、master レルムのユーザーのアクセストークンを取得します。

    curl \
      -d "client_id=admin-cli" \
      -d "username=admin" \
      -d "password=password" \
      -d "grant_type=password" \
      "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"
    注記

    デフォルトでは、このトークンは 1 分で有効期限が切れます。

    結果は JSON 形式のドキュメントになります。

  2. access_token プロパティーの値を抽出して、必要な API を呼び出します。
  3. API へのリクエストの Authorization ヘッダーに値を追加して、API を呼び出します。

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

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

2.1.2. サービスアカウントでの認証

client_id および client_secret を使用し、Admin REST API に対して認証するには、この手順を実行します。

手順

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

    • client_id は、レルム マスター に所属する 機密 クライアントです。
    • client_idService Accounts Enabled オプションを有効にしています。
    • client_id には、カスタムの "Audience" マッパーがあります。

      • 含まれるクライアントオーディエンス: security-admin-console
  2. Service Account Roles タブで client_id に admin というロールが割り当てられていることを確認します。
  3. client_idclient_secret を使用して Admin REST API のアクセストークンを取得します。

    curl \
      -d "client_id=<YOUR_CLIENT_ID>" \
      -d "client_secret=<YOUR_CLIENT_SECRET>" \
      -d "grant_type=client_credentials" \
      "http://localhost:8080/auth/realms/master/protocol/openid-connect/token"

2.2. 関連情報

第3章 テーマ

Red Hat Single Sign-On は、Web ページおよびメールのテーマをサポートします。これにより、エンドユーザーに表示されるページのルックアンドフィールをカスタマイズでき、アプリケーションと統合できます。

図3.1 sunrise のサンプルテーマを含むログインページ

sunrise へのログイン

3.1. テーマのタイプ

テーマには 1 つ以上あり、Red Hat Single Sign-On のさまざまな側面をカスタマイズできます。利用可能なタイプは以下のとおりです。

  • Account - アカウント管理
  • Admin - 管理コンソール
  • Email - メール
  • Login - ログインフォーム
  • Welcome - Welcome ページ

3.2. テーマの設定

Welcome を除くすべてのテーマタイプは、管理コンソールを介して設定されます。

手順

  1. 管理コンソールにログインします。
  2. 左上隅のドロップダウンボックスからレルムを選択します。
  3. メニューで Realm Settings をクリックします。
  4. Themes タブをクリックします。

    注記

    master 管理コンソールのテーマを設定するには、master レルムの管理コンソールテーマを設定する必要があります。

  5. 管理コンソールへの変更を表示するには、ページをリフレッシュします。
  6. standalone.xmlstandalone-ha.xml、または domain.xml を編集して、welcome テーマを変更します。
  7. welcomeTheme を theme 要素に追加します。以下に例を示します。

    <theme>
        ...
        <welcomeTheme>custom-theme</welcomeTheme>
        ...
    </theme>
  8. サーバーを再起動して welcome テーマへの変更を有効にします。

3.3. デフォルトのテーマ

Red Hat Single Sign-On は、サーバーのルート themes ディレクトリーにデフォルトのテーマがバンドルされています。アップグレードを簡素化するには、バンドルされたテーマを直接編集しないでください。代わりに、独自のテーマを作成して、バンドルされたテーマのいずれかを拡張します。

3.4. テーマの作成

テーマは、以下で設定されます。

  • HTML テンプレート (Freemarker テンプレート)
  • イメージ
  • メッセージバンドル
  • スタイルシート
  • スクリプト
  • テーマのプロパティー

すべてのページを置き換える予定がない限り、別のテーマを拡張してください。Red Hat Single Sign-On テーマを拡張する場合が多くですが、ページの外観と操作感を大幅に変更している場合は、ベーステーマの拡張も検討してみてください。ベーステーマは主に HTML テンプレートとメッセージバンドルで設定されており、Red Hat Single Sign-On テーマにはイメージとスタイルシートが含まれます。

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

テーマを作成する際に、Red Hat Single Sign-On を再起動せずに themes ディレクトリーから直接テーマリソースを編集できるため、キャッシングを無効にするとよいでしょう。

手順

  1. standalone.xml を編集します。
  2. theme の場合は、staticMaxAge-1 に設定し、cacheTemplates および cacheThemesfalse に設定します。

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

    パフォーマンスに大きく影響するため、必ず実稼働環境でキャッシュを再度有効にしてください。

  3. themes ディレクトリーにディレクトリーを作成します。

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

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

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

  5. それぞれのタイプに対して、テーマの設定を可能にする theme.properties ファイルを作成します。

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

    parent=base
    import=common/keycloak

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

  6. 管理コンソールにログインして、新しいテーマをチェックアウトします。
  7. レルムを選択します。
  8. メニューで Realm Settings をクリックします。
  9. Themes タブをクリックします。
  10. Login Theme については mytheme を選択し、Save をクリックします。
  11. レルムのログインページを開きます。

    これは、アプリケーションからログインするか、Account Management コンソール (/realms/{realm name}/account) を開いて実行できます。

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

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

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

  • parent - 拡張する親テーマ
  • import - 別のテーマからのリソースのインポート
  • styles - 追加するスタイルのスペース区切りリスト
  • locales - サポートされるロケールのコンマ区切りリスト

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

独自のカスタムプロパティーを追加して、カスタムテンプレートから使用することもできます。

これを行うには、次の形式を使用してシステムプロパティーまたは環境変数を置き換えることができます。

  • ${some.system.property} - システムプロパティーの場合。
  • ${env.ENV_VAR} - 環境変数の場合。

システムプロパティーまたは環境変数が ${foo:defaultValue} で見つからない場合は、デフォルト値を指定することもできます。

注記

デフォルト値が指定されておらず、対応するシステムプロパティーまたは環境変数がない場合は、何も置き換えられず、テンプレートの形式が使用されることになります。

以下は、実現できる例の一部です。

javaVersion=${java.version}

unixHome=${env.HOME:Unix home not found}
windowsHome=${env.HOMEPATH:Windows home not found}

3.4.2. テーマにスタイルシートを追加

テーマに 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=web_modules/@fortawesome/fontawesome-free/css/icons/all.css web_modules/@patternfly/react-core/dist/styles/base.css
    web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css
    node_modules/patternfly/dist/css/patternfly-additions.min.css css/login.css css/styles.css
    注記

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

3.4.3. テーマにスクリプトを追加

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

手順

  1. テーマの <THEMETYPE>/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

3.4.4. テーマにイメージを追加

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

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

次に、以下を使用して、カスタムスタイルシート内からこのイメージを使用できます。

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

または、HTML テンプレートで直接使用するには、以下をカスタム HTML テンプレートに追加します。

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

3.4.5. メッセージ

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

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

usernameOrEmail=Your Username

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

これらのメッセージバンドルのテキストは、レルム固有の値で上書きすることができます。レルム固有の値は、UI や API で管理できます。

3.4.6. レルムに言語を追加

前提条件

手順

  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. accountemail のテーマタイプにも同じものを追加します。そのためには、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

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

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

関連情報

3.4.7. カスタム ID プロバイダーアイコンの追加

Red Hat Single Sign-On は、ログイン画面に表示されるカスタム ID プロバイダーのアイコンの追加に対応しています。

手順

  1. ログイン theme.properties ファイル (themes/mytheme/login/theme.properties など) の中で、キーパターン kcLogoIdP-<alias> でアイコンクラスを定義します。
  2. myProvider というエイリアスを持つ Identity プロバイダーの場合は、カスタムテーマの theme.properties ファイルに行を追加できます。以下に例を示します。

    kcLogoIdP-myProvider = fa fa-lock

すべてのアイコンは、PatternFly4 の公式 Web サイトで公開されています。ソーシャルプロバイダー用のアイコンは、ベースとなるログインテーマのプロパティー (themes/keycloak/login/theme.properties) ですでに定義されており、自身で選択することができます。

3.4.8. カスタム HTML テンプレートの作成

Red Hat Single Sign-On は、HTML を生成するために Apache Freemarker を使用します。<THEME TYPE>/<TEMPLATE>.ftl を作成して、独自のテーマで個別のテンプレートを上書きできます。使用されるテンプレートの一覧は、themes/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. 変更したテンプレートをバックアップします。Red Hat Single Sign-On の新規バージョンにアップグレードする場合は、必要に応じて元のテンプレートに変更を適用できるようにカスタムテンプレートを更新する必要があるかもしれません。

関連情報

  • テンプレートの編集方法の詳細については、FreeMarker Manual を参照してください。

3.4.9. 電子メール

パスワードリカバリーメールなどのメールの件名および内容を編集するには、テーマの email タイプにメッセージバンドルをを追加します。メールごとに 3 つのメッセージがあります。このサブジェクトの 1 つ。1 つはプレーンテキストの本文用に、もう 1 つは 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>

3.5. テーマのデプロイ

テーマは、テーマディレクトリーを テーマ にコピーして Red Hat Single Sign-On にデプロイするか、アーカイブとしてデプロイできます。開発中にテーマを themes のディレクトリーにコピーできますが、実稼働環境では アーカイブ の使用を検討してください。アーカイブ により、特にクラスターリングなどの Red Hat Single Sign-On のインスタンスが複数ある場合に、テーマのバージョン化されたコピーが簡単になります。

手順

  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 つのアーカイブに複数のテーマを含めることができ、各テーマでは 1 つまたは複数のタイプをサポートできます。

  3. アーカイブを Red Hat Single Sign-On にデプロイする場合、Red Hat Single Sign-On の standalone/deployments/ ディレクトリーに追加すると、自動的にロードされます。

3.6. テーマセレクター

デフォルトでは、レルムに設定されたテーマが使用されます。ただし、クライアントがログインテーマをオーバーライドできる場合を除きます。この動作は Theme Selector SPI で変更できます。

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

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

3.7. テーマリソース

Red Hat Single Sign-On でカスタムプロバイダーを実装する場合は、テンプレート、リソース、およびメッセージバンドルを追加する必要がある場合があります。

追加のテーマリソースを読み込む最も簡単な方法は、theme-resources/resourcestheme-resources/templates リソースに JAR を作成し、theme-resources/messages のメッセージバンドルで Red Hat Single Sign-On の standalone/deployments/ ディレクトリーに置くことです。

テンプレートとリソースをさらに柔軟に読み込む方法が必要な場合には、ThemeResourceSPI で実現できます。ThemeResourceProviderFactory および ThemeResourceProvider を実装することで、テンプレートおよびリソースを読み込む方法を正確に決定できます。

3.8. ロケールセレクター

デフォルトでは、LocaleSelectorProvider インターフェイスを実装する DefaultLocaleSelectorProvider を使用してロケールが選択されます。国際化が無効になっている場合は、英語がデフォルト言語です。国際化を有効にした場合、ロケールは Server Administration Guide に記載されているロジックに従って解決されます。

この動作は、LocaleSelectorProvider および LocaleSelectorProviderFactory を実装して、LocaleSelectorSPI を介して変更できます。

LocaleSelectorProvider インターフェイスには単一のメソッド resolveLocale があり、RealmModel および Null が可能な UserModel を指定したロケールを返す必要があります。実際のリクエストは KeycloakSession#getContext メソッドから利用できます。

カスタム実装は、デフォルトの動作の一部を再利用できるように DefaultLocaleSelectorProvider を拡張できます。たとえば、Accept-Language リクエストヘッダーを無視する場合、カスタム実装はデフォルトのプロバイダーを拡張し、その getAcceptLanguageHeaderLocale をオーバーライドして Null 値を返すことができます。その結果、ロケールの選択はレルムのデフォルト言語にフォールバックします。

3.9. 関連情報

第4章 カスタムのユーザー属性

カスタムのユーザー属性は、カスタムテーマを使用して登録ページおよびアカウント管理コンソールに追加できます。

4.1. 登録ページ

この手順を使用して、登録ページにカスタム属性を入力します。

手順

  1. テンプレート themes/base/login/register.ftl をカスタムテーマのログインタイプにコピーします。
  2. コピーをエディターで開きます。

    たとえば、登録ページに携帯番号を追加するには、以下のスニペットをフォームに追加します。

    <div class="form-group">
       <div class="${properties.kcLabelWrapperClass!}">
           <label for="user.attributes.mobile" class="${properties.kcLabelClass!}">Mobile number</label>
       </div>
    
       <div class="${properties.kcInputWrapperClass!}">
           <input type="text" class="${properties.kcInputClass!}" id="user.attributes.mobile" name="user.attributes.mobile" value="${(register.formData['user.attributes.mobile']!'')}"/>
       </div>
    </div>
  3. 入力 html 要素の名前が user.attributes. で開始することを確認します。上記の例では、属性は Red Hat Single Sign-On によって mobile という名前で保存されます。
  4. 変更を確認するには、レルムがログインテーマにカスタムテーマを使用していることを確認し、登録ページを開きます。

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

この手順を使用して、アカウント管理コンソールのユーザープロファイルページでカスタム属性を管理します。

手順

  1. テンプレート themes/base/account/account.ftl をカスタムテーマのアカウントタイプにコピーします。
  2. コピーをエディターで開きます。

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

    <div class="form-group">
       <div class="col-sm-2 col-md-2">
           <label for="user.attributes.mobile" class="control-label">Mobile number</label>
       </div>
    
       <div class="col-sm-10 col-md-10">
           <input type="text" class="form-control" id="user.attributes.mobile" name="user.attributes.mobile" value="${(account.attributes.mobile!'')}"/>
       </div>
    </div>
  3. 入力 html 要素の名前が user.attributes. で開始することを確認します。
  4. 変更を確認するには、レルムがアカウントテーマのカスタムテーマを使用していることを確認し、アカウント管理コンソールでユーザープロファイルページを開きます。

4.3. 関連情報

  • カスタムテーマを作成する方法については、テーマ を参照してください。

第5章 ID ブローカー API

Red Hat Single Sign-On は、親 IDP にログイン認証を委譲できます。典型的な例としては、Facebook や Google などのソーシャルプロバイダーを使用してユーザーがログインできるようにするケースがあります。既存のアカウントをブローカー化された IDP にリンクすることもできます。このセクションでは、アイデンティティーブローカーに関連するので、アプリケーションが使用する API についても一部説明します。

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

Red Hat Single Sign-On を使用すると、外部 IDP を使用して認証プロセスからのトークンと応答を保存できます。これには、IDP の設定ページで Store Token 設定オプションを使用できます。

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

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

アプリケーションが Red Hat Single Sign-On で認証され、アクセストークンを受信している必要があります。このアクセストークンには、ブローカー のクライアントレベルのロール read-token を設定する必要があります。そのため、ユーザーにはこのロールのロールマッピングが必要で、クライアントアプリケーションにそのスコープ内にそのロールがなければなりません。この場合、Red Hat Single Sign-On で保護されたサービスにアクセスする場合には、ユーザー認証時に Red Hat Single Sign-On が発行するアクセストークンを送信する必要があります。ブローカー設定ページでは、Stored Tokens Readable スイッチを有効にして、新たにインポートしたユーザーにこのロールを自動的に割り当てることができます。

これらの外部トークンについては、プロバイダー経由で再度ログインするか、クライアントが開始したアカウントリンク API を使用して再度確立できます。

5.2. クライアント開始アカウントリンク

アプリケーションによっては、Facebook などのソーシャルプロバイダーと統合しても、このようなソーシャルプロバイダー経由でログインするオプションを提供しないようにする場合もあります。Red Hat Single Sign-On は、アプリケーションが既存のユーザーアカウントを特定の外部 IDP にリンクするために使用できるブラウザーベースの API を提供します。これは、クライアント開始アカウントリンク と呼ばれます。アカウントリンクは OIDC アプリケーションによってのみ開始できます。

アプリケーションがユーザーのブラウザーを Red Hat Single Sign-On サーバーの URL に転送する方法は、ユーザーのアカウントを特定の外部プロバイダー (つまり Facebook) にリンクするように要求することです。サーバーは外部プロバイダーでログインを開始します。ブラウザーは外部プロバイダーにログインし、サーバーにリダイレクトされます。サーバーはリンクを確立し、確認でアプリケーションにリダイレクトします。

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

  • 必要なアイデンティティープロバイダーは、管理コンソールでユーザーのレルムに対して設定して有効にする必要があります。
  • ユーザーアカウントは、OIDC プロトコルを使用して既存のユーザーとしてログインしている必要があります。
  • ユーザーに account.manage-account または account.manage-account-links ロールマッピングが必要です。
  • アプリケーションには、アクセストークン内で上記のロールのスコープが付与される必要があります。
  • アプリケーションは、リダイレクト URL 生成にその情報を必要とするため、アクセストークンへのアクセスが必要です。

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

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

以下は、各パスおよびクエリーパラメーターの説明です。

provider
これは、管理コンソールの Identity Provider セクションで定義した外部 IDP のプロバイダーエイリアスです。
client_id
これは、アプリケーションの OIDC クライアント ID です。アプリケーションを管理コンソールでクライアントとして登録する場合は、このクライアント ID を指定する必要があります。
redirect_uri
これは、アカウントリンクの確立後にリダイレクトするアプリケーションのコールバック URL です。有効なクライアントのリダイレクト URI パターンである必要があります。つまり、管理コンソールでクライアント登録時に定義した有効な URL パターンのいずれかと一致する必要があります。
nonce
これは、アプリケーションが生成する必要のあるランダムな文字列です。
hash
これは、Base64 URL でエンコードされたハッシュです。このハッシュは、Base64 URL で nonce + token.getSessionState() + token.getIssuedFor() + provider の SHA_256 ハッシュをエンコードすることで生成されます。トークン変数は OIDC アクセストークンから取得されます。基本的に、アクセスするアイデンティティープロバイダーエイリアス、nonce、ユーザーセッション ID、クライアント ID、およびアイデンティティープロバイダーエイリアスを無作為にハッシュ化します。

以下は、アカウントリンクを確立するために URL を生成する Java Servlet コードの例になります。

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

このハッシュを含める理由認証サーバーで、クライアントアプリケーションが要求を開始したことを認識し、その他の不正なアプリが、特定のプロバイダーにリンクされるユーザーアカウントを無作為に要求しないように、ハッシュを含めます。認証サーバーは、最初にログイン時に設定された SSO クッキーをチェックしてユーザーがログインしているかどうかを確認します。その後、現在のログインに基づいてハッシュを再生成し、アプリケーションによって送信されるハッシュにマッチします。

アカウントのリンク後、認証サーバーは redirect_uri にリダイレクトします。リンク要求の処理に問題がある場合は、認証サーバーが redirect_uri にリダイレクトされない場合があります。ブラウザーは、アプリケーションにリダイレクトされるのではなく、エラーページで終了できます。エラー条件があり、認証サーバーがクライアントアプリケーションにリダイレクトするのに十分な安全でない場合には、追加の error クエリーパラメーターが redirect_uri に追加されます。

警告

この API は、アプリケーションで確実に要求を開始されるようにしますが、この操作に対する CSRF 攻撃を完全に防ぐことはできません。このアプリケーションは、CSRF 攻撃ターゲットに対して自己防衛します。

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

プロバイダーにログインして生成される外部トークン (つまり Facebook または GitHub トークン) を使用している場合は、アカウントリンク API を再度初期化してこのトークンを更新できます。

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

Red Hat Single Sign-On は、カスタムコードを必要とせずにほとんどのユースケースに対応するように設計されていますが、カスタマイズ可能となっています。Red Hat Single Sign-On には、独自のプロバイダーを実装することのできる数多くの SPI (サービスプロバイダーインターフェイス) があります。

6.1. SPI の実装

SPI を実装するには、その ProviderFactory および Provider インターフェイスを実装する必要があります。サービス設定ファイルも作成する必要があります。

たとえば、Theme Selector SPI を実装するには、ThemeSelectorProviderFactory および ThemeSelectorProvider を実装し、さらに META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory ファイルも指定する必要があります。

ThemeSelectorProviderFactory の例:

package org.acme.provider;

import ...

public class MyThemeSelectorProviderFactory implements ThemeSelectorProviderFactory {

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

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

    @Override
    public void postInit(KeycloakSessionFactory factory) {
    }

    @Override
    public void close() {
    }

    @Override
    public String getId() {
        return "myThemeSelector";
    }
}
注記

Red Hat Single Sign-On はプロバイダーファクトリーの単一のインスタンスを作成するので、複数の要求の状態を保存できます。プロバイダーのインスタンスは、各要求のファクトリーで create を呼び出すことで作成されるので、軽量オブジェクトでなければなりません。

ThemeSelectorProvider の例:

package org.acme.provider;

import ...

public class MyThemeSelectorProvider implements ThemeSelectorProvider {

    public MyThemeSelectorProvider(KeycloakSession session) {
    }


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

    @Override
	public void close() {
    }
}

サービス設定ファイルの例 (META-INF/services/org.keycloak.theme.ThemeSelectorProviderFactory):

org.acme.provider.MyThemeSelectorProviderFactory

standalone.xmlstandalone-ha.xml、または domain.xml を使用してプロバイダーを設定できます。

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

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

その後、init メソッド ProviderFactory で設定を取得できます。

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

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

プロバイダーに関する追加情報を Red Hat Single Sign-On 管理者に表示すると便利です。プロバイダーのビルド時間情報 (現在インストールされているカスタムプロバイダーのバージョンなど)、プロバイダーの現在の設定 (プロバイダーが通信するリモートシステムの URL)、または一部の運用情報 (プロバイダーが対話するリモートシステムからの応答時間) を表示できます。Red Hat Single Sign-On 管理コンソールは、サーバー情報のページを提供し、サーバー関連の情報を表示します。

プロバイダーからの情報を表示するには、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;
    }
}

6.2. 利用可能なプロバイダーの利用

プロバイダーの実装では、Red Hat Single Sign-On で利用可能な他のプロバイダーを使用することができます。既存のプロバイダーは、Implementing an SPI セクションで説明したように、プロバイダーが利用できる KeycloakSession を使用して一般的に検索することができます。

Red Hat Single Sign-On には 2 つのプロバイダータイプがあります。

  • 単一実装のプロバイダータイプ - Red Hat Single Sign-On ランタイムでは、特定のプロバイダータイプのアクティブな実装は 1 つしかありません。

    たとえば、HostnameProvider は Red Hat Single Sign-On で使用されるホスト名を指定し、Red Hat Single Sign-On サーバー全体で共有されます。そのため、Red Hat Single Sign-On サーバーで有効なこのプロバイダーの実装は 1 つだけです。サーバーランタイムで利用できるプロバイダーの実装が複数ある場合は、standalone.xml の keycloak サブシステム設定で、そのうちの 1 つをデフォルトのものとして指定する必要があります。

    たとえば、以下のようなものです。

    <spi name="hostname">
        <default-provider>default</default-provider>
        ...
    </spi>

    default-provider の値として使用される default は、特定のプロバイダーファクトリー実装の ProviderFactory.getId() が返す ID と一致する必要があります。コードでは、keycloakSession.getProvider(HostnameProvider.class) のようにプロバイダーを取得します。

  • 複数の実装プロバイダータイプ - Red Hat Single Sign-On ランタイムで複数の実装を利用し、一緒に動作させることができるプロバイダータイプです。

    たとえば、EventListener プロバイダーは、利用可能で登録された複数の実装を持つことができます。これは、特定のイベントがすべてのリスナー (jboss-logging、sysout など) に送信できることを意味します。コードでは、たとえば session.getProvider(EventListener.class, "jboss-logging") のように、プロバイダーの指定したインスタンスを取得することができます。前述のように、このプロバイダータイプのインスタンスは複数存在する可能性があるため、第 2 引数にプロバイダーの provider_id を指定する必要があります。

    プロバイダー ID は、特定のプロバイダーファクトリー実装の ProviderFactory.getId() が返す ID と一致する必要があります。プロバイダーの種類によっては、第 2 引数に ComponentModel を指定することで取得できるものもありますし、KeycloakSessionFactory を指定しなければならないものもあります (Authenticator など)。将来的に非推奨となる可能性があるため、この方法で独自のプロバイダーを実装することはお勧めできません。

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

プロバイダー実装を登録する方法は 2 つあります。ほとんどの場合、最も単純な方法では、Red Hat Single Sign-On デプロイヤーアプローチを使用します。これは、多数の依存関係を自動的に処理します。また、ホットデプロイメントも再デプロイメントもサポートします。

別の方法として、モジュールとしてデプロイできます。

カスタム SPI を作成する場合は、これをモジュールとしてデプロイする必要があります。そうでない場合は、Red Hat Single Sign-On デプロイヤーアプローチの使用を推奨します。

6.3.1. Red Hat Single Sign-On デプロイヤーの使用

プロバイダー jar を Red Hat Single Sign-On の standalone/deployments/ ディレクトリーにコピーする場合は、プロバイダーが自動的にデプロイされます。ホットデプロイメントも機能します。さらに、プロバイダーの jar は、jboss-deployment-structure.xml ファイルなどの機能を使用できるように、JBoss EAP 環境にデプロイされた他のコンポーネントと同じように機能します。このファイルでは、他のコンポーネントの依存関係を設定し、サードパーティーの jar およびモジュールを読み込むことができます。

プロバイダー jar は EAR や WAR などの他のデプロイ可能なユニットにも含めることができます。これらのライブラリーを EAR の lib/ ディレクトリーに配置できるため、EAR をデプロイすると、サードパーティーの jar を使用するのが非常に簡単になります。

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

手順

  1. jboss-cli スクリプトを使用してモジュールを作成するか、手動でフォルダーを作成します。

    1. たとえば、jboss-cli スクリプトを使用してイベントリスナーの sysout サンプルプロバイダーを追加するには、以下を実行します。

      KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.acme.provider --resources=target/provider.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi"
    2. または、KEYCLOAK_HOME/modules 内にモジュールを手動で作成し、jar と module.xml を追加することもできます。

      たとえば、KEYCLOAK_HOME/modules/org/acme/provider/main フォルダーを作成します。次に provider.jar をこのディレクトリーにコピーし、以下の内容で module.xml を作成します。

      <?xml version="1.0" encoding="UTF-8"?>
      <module xmlns="urn:jboss:module:1.3" name="org.acme.provider">
          <resources>
              <resource-root path="provider.jar"/>
          </resources>
          <dependencies>
              <module name="org.keycloak.keycloak-core"/>
              <module name="org.keycloak.keycloak-server-spi"/>
          </dependencies>
      </module>
  2. このモジュールを Red Hat Single Sign-On に登録するには、standalone.xmlstandalone-ha.xml、または domain.xml の keycloak-server サブシステムセクションを編集してプロバイダーに追加します。

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

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

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

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

6.4. Jakarta EE の活用

META-INF/services ファイルを適切に設定してプロバイダーを参照するように、サービスプロバイダーを Jakarta EE コンポーネント内にパッケージ化できます。たとえば、プロバイダーがサードパーティーのライブラリーを使用する必要がある場合は、プロバイダーを ear にパッケージ化して、サードパーティーライブラリーを ear の lib/ ディレクトリーに保存します。また、プロバイダー jars は EJB、WARS、および EAR が JBoss EAP 環境で使用できる jboss-deployment-structure.xml ファイルを利用できます。このファイルの詳細は、JBoss EAP のドキュメントを参照してください。これにより、他の細かいアクションから、外部の依存関係をプルすることができます。

ProviderFactory 実装はプレーンな Java オブジェクトである必要があります。ただし、現時点では、プロバイダークラスを Stateful EJB として実装することもできます。これは、以下の方法で行います。

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

    protected ComponentModel model;
    protected KeycloakSession session;

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

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


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

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

@Remove アノテーションをプロバイダーの close() メソッドに配置します。配置しない場合には、ステートフル Bean はクリーンアップされず、最終的にエラーメッセージが表示される可能性があります。

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

public class EjbExampleUserStorageProviderFactory
        implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {

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

6.5. JavaScript プロバイダー

Red Hat Single Sign-On には、管理者が特定の機能をカスタマイズできるように、ランタイム時にスクリプトを実行する機能があります。

  • オーセンティケーター
  • JavaScript ポリシー
  • OpenID Connect Script Protocol Mapper

6.5.1. オーセンティケーター

認証スクリプトは、Authenticator#action(AuthenticationFlowContext) から呼び出される Authenticator#authenticate(AuthenticationFlowContext) action(..) から呼び出される authenticate(..) 関数を少なくとも 1 つ提供する必要があります。

カスタム Authenticator は、最低でも authenticate(..) 関数を提供する必要があります。コード内で javax.script.Bindings スクリプトを使用できます。

script
スクリプトメタデータにアクセスするための ScriptModel
realm
RealmModel
user
現行の UserModel
session
アクティブな KeycloakSession
authenticationSession
現在の AuthenticationSessionModel
httpRequest
the current 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();
}

6.5.2. スクリプトで JAR を作成してデプロイ

注記

JAR ファイルは、.jar 拡張子を持つ通常の ZIP ファイルです。

Red Hat Single Sign-On でスクリプトを利用できるようにするには、サーバーにスクリプトをデプロイする必要があります。そのため、以下の構造で JAR ファイルを作成します。

META-INF/keycloak-scripts.json

my-script-authenticator.js
my-script-policy.js
my-script-mapper.js

META-INF/keycloak-scripts.json は、デプロイするスクリプトに関するメタデータ情報を提供するファイル記述子です。これは、次の構造が含まれる JSON ファイルです。

{
    "authenticators": [
        {
            "name": "My Authenticator",
            "fileName": "my-script-authenticator.js",
            "description": "My Authenticator from a JS file"
        }
    ],
    "policies": [
        {
            "name": "My Policy",
            "fileName": "my-script-policy.js",
            "description": "My Policy from a JS file"
        }
    ],
    "mappers": [
        {
            "name": "My Mapper",
            "fileName": "my-script-mapper.js",
            "description": "My Mapper from a JS file"
        }
    ]
}

このファイルは、デプロイする別のタイプのスクリプトプロバイダーを参照する必要があります。

  • authenticators

    OpenID Connect スクリプトオーセンティケーターの場合。同じ JAR ファイルに 1 つまたは複数のオーセンティケーターを設定できます。

  • policies

    Authorization Services を使用する場合の JavaScript ポリシー向け同じ JAR ファイルに 1 つまたは複数のポリシーを設定できます。

  • mappers

    OpenID Connect Script Protocol Mapper 用。同じ JAR ファイルに 1 つまたは複数のマッパーを設定できます。

JAR ファイルのスクリプトファイルごとに、スクリプトファイルを特定のプロバイダータイプにマッピングする META-INF/keycloak-scripts.json に対応するエントリーが必要です。そのためには、各エントリーに以下のプロパティーを指定する必要があります。

  • name

    Red Hat Single Sign-On 管理コンソールでのスクリプト表示に使用するための分かりやすい名前。指定がない場合は、代わりにスクリプトファイルを使用します。

  • description

    スクリプトファイルの意図をより適切に説明する任意のテキスト

  • fileName

    スクリプトファイルの名前。このプロパティーは 必須 であり、JAR 内のファイルにマップする必要があります。

6.5.3. スクリプト JAR のデプロイ

記述子とデプロイするスクリプトが含まれる JAR ファイルを追加したら、JAR を Red Hat Single Sign-On の standalone/deployments/ ディレクトリーにコピーする必要があります。

6.5.4. Red Hat Single Sign-On 管理コンソールを使用したスクリプトのアップロード

注記

管理コンソール経由でスクリプトをアップロードする機能は非推奨になり、今後の Red Hat Single Sign-On バージョンで削除されます。

管理者はスクリプトをサーバーにアップロードできません。この動作により、悪意のあるスクリプトが誤って実行されている場合にシステムに被害が加えられないようにします。管理者は常に、JAR ファイルを使用してサーバーに直接スクリプトをデプロイして、ランタイムのスクリプト実行時の攻撃を回避する必要があります。

スクリプトをアップロードする機能は、明示的に有効にできます。これは細心の注意を払って使用する必要があり、できるだけ早期にすべてのスクリプトを直接サーバーにデプロイするプランを作成する必要があります。

upload_scripts 機能を有効にする方法の詳細については、プロファイル を参照してください。

6.6. 利用可能な SPI

実行時に利用可能なすべての SPI のリストを見たい場合は、管理者コンソール のセクションで説明されているように、管理者コンソールの Server Info ページを確認することができます。

第7章 ユーザーストレージ SPI

ユーザーストレージ SPI を使用して拡張機能を Red Hat Single Sign-On に書き込み、外部のユーザーデータベースおよび認証情報ストアに接続することができます。組み込み LDAP および ActiveDirectory サポートは、この SPI 動作を実装したものです。追加設定なしで、Red Hat Single Sign-On はローカルデータベースを使用して、ユーザーおよびクレデンシャルの作成、更新、および検証を行います。多くの場合、組織には Red Hat Single Sign-On のデータモデルに移行できない既存の外部プロプライエタリーユーザーデータベースがあります。このような状況では、アプリケーション開発者は User Storage SPI の実装を作成し、外部ユーザーストアと、内部のユーザーオブジェクトモデルとの橋渡しをして、Red Hat Single Sign-On がユーザーのログインと管理を行えるようにします。

Red Hat Single Sign-On ランタイムがユーザーログイン時など、ユーザーを検索する必要がある場合は、複数の手順を実行してユーザーを特定します。まず、ユーザーキャッシュにユーザーが含まれているかどうかを確認します。ユーザーが見つかった場合にはユーザーがそのインメモリー表現を使用しているかを確認します。次に、Red Hat Single Sign-On のローカルデータベース内でユーザーを検索します。ユーザーが見つからない場合は、ランタイムが探しているユーザーをいずれかの実装が返すまで、User Storage SPI プロバイダー実装全体をループして、ユーザークエリーを実行し続けます。プロバイダーは、ユーザーの外部ユーザーストアをクエリーし、ユーザーの外部データ表現を Red Hat Single Sign-On のユーザーメタモデルにマップします。

User Storage SPI プロバイダー実装は、複雑な基準クエリーの実行、ユーザーへの CRUD 操作の実行、認証情報の検証および管理、または多数のユーザーの一括更新を実行することもできます。これは、外部ストアの機能により異なります。

User Storage SPI プロバイダー実装は、Jakarta EE コンポーネントと同様にパッケージ化およびデプロイされます。これらはデフォルトでは有効になっていませんが、管理コンソールの User Federation タブでレルムごとに有効および設定する必要があります。

警告

ユーザープロバイダーの実装で、ユーザー属性をユーザーアイデンティティーのリンク/確立のためのメタデータ属性として使用している場合は、ユーザーが属性を編集できないようにし、対応する属性が読み取り専用になっていることを確認してください。この例は LDAP_ID 属性で、内蔵の Red Hat Single Sign-On LDAP プロバイダーが LDAP サーバー側でユーザーの ID を保存するために使用しています。詳細については、脅威モデルの軽減の章 を参照してください。

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

User Storage 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() メソッドは、User Storage プロバイダーの名前を返します。この ID は、特定のレルムのプロバイダーを有効にする際に管理コンソールのユーザーフェデレーションページに表示されます。

create() メソッドは、プロバイダークラスのインスタンスを割り当てます。org.keycloak.models.KeycloakSession パラメーターを取ります。このオブジェクトは、その他の情報およびメタデータを検索し、ランタイム内の他のコンポーネントへのアクセスを提供するために使用できます。ComponentModel パラメーターは、プロバイダーが特定のレルム内でどのように有効および設定されたかを表します。これには、有効化されたプロバイダーのインスタンス ID と、管理コンソールで有効にした場合に指定できる設定が含まれます。

UserStorageProviderFactory には他の機能があり、この後、本章で取り上げます。

7.2. プロバイダー機能のインターフェイス

UserStorageProvider インターフェイスを詳しく調べると、ユーザーを検索または管理するための方法を定義しないことが分かります。これらの方法は、外部ユーザーストアが提供および実行可能な機能の範囲に応じて、実際に他の 機能インターフェイス で定義されます。たとえば、外部ストアの一部は読み取り専用で、単純なクエリーおよび認証情報の検証のみを実行できます。可能な機能の 機能インターフェイス を実装することのみが必要になります。これらのインターフェイスを実装することができます。

SPI説明

org.keycloak.storage.user.UserLookupProvider

この外部ストアのユーザーを使用してログインできるようにする場合には、このインターフェイスが必要です。大半 (またはすべて) プロバイダーはこのインターフェイスを実装します。

org.keycloak.storage.user.UserQueryProvider

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

org.keycloak.storage.user.UserRegistrationProvider

プロバイダーがユーザーの追加および削除に対応している場合は、このインターフェイスを実装します。

org.keycloak.storage.user.UserBulkUpdateProvider

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

org.keycloak.credential.CredentialInputValidator

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

org.keycloak.credential.CredentialInputUpdater

プロバイダーが 1 つ以上の異なる認証情報タイプの更新をサポートする場合は、このインターフェイスを実装します。

7.3. モデルインターフェイス

機能 インターフェイス で定義されるメソッドのほとんどは、ユーザーの表現を返すか、渡されます。これらの表現は、org.keycloak.models.UserModel インターフェイスで定義されます。アプリケーション開発者は、このインターフェイスを実装する必要があります。Red Hat Single Sign-On が使用する外部ユーザーストアと、ユーザーメタモデル間でマッピングを行います。

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 パッケージには、Red Hat Single Sign-On メタモデルの他の部分を表す他のモデルクラスがあります (RealmModelRoleModelGroupModel、および ClientModel)。

7.3.1. ストレージ ID

UserModel‍ の重要なメソッドの 1 つとして、getId() メソッドがあります。UserModel の開発者は、ユーザー ID 形式を認識する必要があります。形式は以下のとおりです。

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

Red Hat Single Sign-On ランタイムは、多くの場合、ユーザー ID でユーザーを検索する必要があります。ユーザー ID には十分な情報が含まれているため、ランタイムは、システム内のすべての UserStorageProvider を紹介して、ユーザーを見つける必要はありません。

コンポーネント ID は、ComponentModel.getId() から返される ID です。ComponentModel はプロバイダークラスの作成時にパラメーターとして渡され、そこから取得できるようにします。外部 ID は、プロバイダークラスが外部ストアでのユーザー検索に必要な情報です。通常これはユーザー名または uid です。たとえば、以下のようになります。

f:332a234e31234:wburke

ランタイムが ID でルックアップを行う場合、ID はコンポーネント ID を取得するよう解析されます。コンポーネント ID は、ユーザーの読み込みに最初に使用された UserStorageProvider を見つけるために使用されます。その後、そのプロバイダーには ID が渡されます。プロバイダーが再び ID を解析して外部 ID を取得し、外部ユーザーストレージでのユーザー検索に使用されます。

7.4. パッケージおよびデプロイメント

ユーザーストレージプロバイダーは JAR でパッケージ化され、JBoss EAP アプリケーションサーバーへのデプロイと同じように、Red Hat Single Sign-On ランタイムにデプロイまたはアンデプロイされます。JAR を直接サーバーの standalone/deployments/ ディレクトリーにコピーするか、JBoss CLI を使用してデプロイメントを実行できます。

Red Hat Single Sign-On がプロバイダーを認識できるようにするには、ファイルを JAR (META-INF/services/org.keycloak.storage.UserStorageProviderFactory) に追加する必要があります。このファイルには、UserStorageProviderFactory 実装の完全修飾クラス名の行区切りリストが含まれている必要があります。

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

Red Hat Single Sign-On は、これらのプロバイダー JAR のホットデプロイメントをサポートします。また、本章の後半では、Jakarta EE コンポーネント内でパッケージ化可能なものも確認できます。

7.5. シンプルな読み取り専用、ルックアップの例

User Storage SPI を実装する際の基本事項について、簡単な例を追って説明します。本章では、簡単なプロパティーファイルでユーザーを検索する簡単な UserStorageProvider の実装を説明します。プロパティーファイルにはユーザー名とパスワードの定義が含まれ、クラスパス上の特定の場所にハードコーディングされます。プロバイダーは、ID およびユーザー名でユーザーを検索でき、パスワードを検証することもできます。このプロバイダーから発信されるユーザーは読み取り専用です。

7.5.1. プロバイダークラス

最初に説明するのは 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 度作成され、トランザクションの完了後に閉じられることにも注意してください。

7.5.1.1. UserLookupProvider 実装

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

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

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

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

getUserByUsername() メソッドは、ユーザーがログインすると Red Hat Single Sign-On ログインページにより呼び出されます。実装では最初に loadedUsers マップを確認し、ユーザーがこのトランザクション内にすでに読み込まれているかどうかを確認します。読み込まれていない場合は、ユーザー名のプロパティーファイルを探します。UserModel の実装を作成する場合は、将来のの参照のために loadedUsers に保存し、このインスタンスを返します。

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

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

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

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

メールは保存されないため、getUserByEmail() メソッドは null を返します。

7.5.1.2. CredentialInputValidator 実装

次に、CredentialInputValidator のメソッド実装を確認します。

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

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

    @Override
    public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
        if (!supportsCredentialType(input.getType())) 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 はパスワードが有効であることを意味します。

7.5.1.3. CredentialInputUpdater 実装

前述したように、CredentialInputUpdater インターフェイスを実装する唯一の理由は、ユーザーパスワードの変更を禁止することです。このようにする理由として、Red Hat Single Sign-On のローカルストレージでランタイムによりパスワードを上書きできるためです。本章の後半で詳しく説明します。

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

        return false;
    }

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

    }

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

updateCredential() メソッドは、認証情報タイプがパスワードであるかどうかを確認します。存在する場合は、ReadOnlyException が発生します。

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

プロバイダークラスが完成したので、プロバイダーファクトリークラスを見ていきます。

public class PropertyFileUserStorageProviderFactory
                 implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

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

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

最初に UserStorageProviderFactory クラスを実装する場合は、具体的なプロバイダークラス実装をテンプレートパラメーターとして渡す必要があります。ここで、以前に定義したプロバイダークラス (PropertyFileUserStorageProvider) を指定します。

警告

template パラメーターを指定しない場合には、プロバイダーは機能しません。ランタイムはクラスのイントロスペクションを実行し、プロバイダーが実装する 機能インターフェイス を判別します。

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

7.5.2.1. 初期化

    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() メソッドがあります。Red Hat Single Sign-On の起動時に、各プロバイダーファクトリーのインスタンスが 1 つだけ作成されます。また、システムの起動時に、これらの各ファクトリーのインスタンスでも init() メソッドが呼び出されます。同様に実装できる postInit() メソッドも実装できます。各ファクトリーの init() メソッドが呼び出されると、postInit() メソッドが呼び出されます。

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

Config.Scope パラメーターは、standalone.xmlstandalone-ha.xml、または domain.xml 内で設定できるファクトリー設定です。

たとえば、以下を standalone.xml に追加した場合は以下のようになります。

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

ハードコーディングするのではなく、ユーザープロパティーファイルのクラスパスを指定できます。次に、PropertyFileUserStorageProviderFactory.init() で設定を取得できます。

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

    ...
}

7.5.2.2. 作成方法

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

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

PropertyFileUserStorageProvider クラスを割り当てるだけです。この create メソッドはトランザクションごとに 1 回呼び出されます。

7.5.3. パッケージおよびデプロイメント

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

org.keycloak.examples.federation.properties.FilePropertiesStorageFactory

Jar を作成したら、通常の JBoss EAP を使用してデプロイできます。つまり、jar を standalone/deployments/ ディレクトリーにコピーするか、JBoss CLI を使用してデプロイできます。

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

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

手順

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

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

  2. 設定するものがないため、Save をクリックします。
  3. User Federation のメインページに戻ります。

    これで、プロバイダーが一覧表示されます。

これで、users.properties ファイルで宣言されたユーザーでログインできるようになります。このユーザーは、ログイン後にのみアカウントページを表示できます。

7.6. 設定方法

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 インスタンスの一覧を返します。これらのインスタンスは、プロバイダーの各設定変数のレンダリングおよび保存に必要なメタデータを宣言します。

7.6.1. 設定例

PropertyFileUserStorageProviderFactory の例を拡張して、プロバイダーインスタンスをディスク上の特定のファイルを指定できるようにします。

PropertyFileUserStorageProviderFactory

public class PropertyFileUserStorageProviderFactory
                  implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {

    protected static final List<ProviderConfigProperty> configMetadata;

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

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

ProviderConfigurationBuilder クラスは、設定プロパティーのリストを作成するためのヘルパークラスです。ここでは、String タイプである path という名前の変数を指定します。このプロバイダーの管理コンソール設定ページでは、この設定変数は Path としてラベル付けされ、デフォルト値は ${jboss.server.config.dir}/example-users.properties です。この設定オプションのツールチップにカーソルを合わせると、ヘルプテキスト (File path to properties file (プロパティーファイルへのファイルパス)) が表示されます。

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

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

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

次に、古い init() メソッドを削除します。ユーザープロパティーファイルはプロバイダーインスタンスごとに一意であるため、これを行います。このロジックは create() メソッドに移動します。

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

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

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

全トランザクションでユーザープロパティーファイル全体をディスクから読み取るので、当然このロジックは効率的ではありませんが、今回の例で、設定変数をフックする方法をシンプルに説明できたかと思います。

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

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

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

今回の例で対応していない内容の 1 つとして、ユーザーやパスワードの追加および削除を可能にする操作です。また、この例で定義されたユーザーは、管理コンソールでクエリーや表示ができません。これらの拡張機能を追加するには、example プロバイダーが UserQueryProvider インターフェイスおよび UserRegistrationProvider インターフェイスを実装する必要があります。

7.7.1. UserRegistrationProvider の実装

この手順を使用して、特定のストアからユーザーの追加および削除を実装します。その場合、最初にプロパティーファイルをディスクに保存する必要があります。

PropertyFileUserStorageProvider

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

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

PropertyFileUserStorageProvider

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

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

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

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

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

PropertyFileUserStorageProvider

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

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

プロパティーファイルを保存できるようになったため、パスワードの更新を許可しても問題ありません。

PropertyFileUserStorageProvider

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

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

PropertyFileUserStorageProvider

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

    }

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

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

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

        return disableableTypes;
    }

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

7.7.2. UserQueryProvider の実装

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

PropertyFileUserStorageProvider

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

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

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

getUsers() メソッドはプロパティーファイルのキーセットを繰り返し処理し、getUserByUsername() にユーザーを読み込みます。firstResult パラメーターおよび maxResults パラメーターに基づいてこの呼び出しにインデックス化していることに留意してください。外部ストアがページネーションをサポートしない場合には、同様のロジックを実行する必要があります。

PropertyFileUserStorageProvider

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

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

searchForUser() の最初の宣言は String パラメーターを取ります。これは、username と email 属性を検索して、ユーザーを検出するために使用する文字列であると考えられます。この文字列は部分文字列にできます。そのため、検索時に String.contains() メソッドを使用します。

PropertyFileUserStorageProvider

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

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

Map パラメーターを取る searchForUser() メソッドは、姓名、ユーザー名、およびメールに基づいてユーザーを検索できます。ユーザー名のみを保存するため、ユーザー名に基づく検索のみを行います。これについては、searchForUser() を委譲します。

PropertyFileUserStorageProvider

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

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

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

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

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

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

このような状況では、Red Hat Single Sign-On では、Red Hat Single Sign-On のデータベースに追加情報を保存して外部ストアを拡張できます。これは フェデレーション (連合型) ユーザーストレージと呼ばれ、org.keycloak.storage.federated.UserFederatedStorageProvider 内でカプセル化されます。

UserFederatedStorageProvider

package org.keycloak.storage.federated;

public interface UserFederatedStorageProvider extends Provider {

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

...

UserFederatedStorageProvider インスタンスは、KeycloakSession.userFederatedStorage() メソッドで利用できます。これには、あらゆる種類のメソッドがあり、属性、グループおよびロールマッピング、さまざまな認証情報タイプ、および必要なアクションを保存できます。外部ストアのデータモデルが、Red Hat Single Sign-On 機能セットをすべてサポートできない場合は、このサービスで、その部分に対応できます。

Red Hat Single Sign-On にはヘルパークラス org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage が同梱されています。このクラスは、ユーザー名の get/set を除くすべての UserModel メソッドが、ユーザーがフェデレーションされたストレージに委譲します。外部ストレージ表現に委譲するのに必要なメソッドを上書きします。上書きする保護メソッドが少ないため、このクラスの javadoc を参照することを強くお勧めします。具体的には、グループメンバーシップおよびロールマッピング関連です。

7.8.1. 拡張例

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

PropertyFileUserStorageProvider

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

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

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

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

ユーザーストレージプロバイダーを実装する場合は、別のストラテジーを使用できます。ユーザーフェデレーションストレージを使用する代わりに、Red Hat Single Sign-On ビルトインユーザーデータベースでユーザーをローカルで作成し、外部ストアからこのローカルコピーに属性をコピーします。この方法には多くの利点があります。

  • Red Hat Single Sign-On は基本的に外部ストアの永続ユーザーキャッシュになります。ユーザーがインポートされると、外部ストアに到達できなくなるため、負荷はなくなります。
  • Red Hat Single Sign-On に正式なユーザーストアとして移行し、古い外部ストアの使用を終了する場合は、Red Hat Single Sign-On を使用するようにアプリケーションを徐々に移行できます。すべてのアプリケーションが移行されたら、インポートされたユーザーのリンクを解除し、古いレガシー外部ストアを破棄します。

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

  • ユーザーを初めて検索するには、Red Hat Single Sign-On データベースを複数回更新する必要があります。これは、負荷がある場合にパフォーマンスが大幅に低下し、Red Hat Single Sign-On データベースに大きな負担をかける可能性があります。ユーザーフェデレーションされたストレージアプローチは、必要に応じて追加のデータのみを保存し、外部ストアの機能によっては使用されない可能性があります。
  • このインポート方法では、ローカルの Red Hat Single Sign-On ストレージおよび外部ストレージを同期する必要があります。User Storage SPI には同期をサポートするために実装できる機能インターフェイスがありますが、この操作はすぐに面倒で複雑になる可能性があります。

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

これは少し複雑になりますが、PropertyFileUserStorageProvider を拡張してこのアプローチを取ることができます。最初に createAdapter() メソッドを修正します。

PropertyFileUserStorageProvider

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

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

注記

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

また、ローカルユーザーがリンクしている場合は、CredentialInputValidator インターフェイスおよび CredentialInputUpdater インターフェイスから実装するメソッドのために、ストレージプロバイダーが依然として委譲される点に留意してください。検証または更新から false を返すと、Red Hat Single Sign-On はローカルストレージを使用して検証または更新できるかどうかを確認できます。

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

注記

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

7.9.1. 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 を返すと、ローカルユーザーがこのデータベースから削除されます。

7.9.2. ImportSynchronization インターフェイス

インポートストラテジーにより、ローカルユーザーコピーが外部ストレージと同期できなくなっていることがわかります。おそらくユーザーは外部ストアから削除されてしまっているようです。ユーザーストレージ SPI には、これに対応するために実装できる追加のインターフェイス (org.keycloak.storage.user.ImportSynchronization) があります。

package org.keycloak.storage.user;

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

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

7.10. ユーザーキャッシュ

ユーザーオブジェクトが ID、ユーザー名、またはメールクエリーによって読み込まれると、キャッシュされます。ユーザーオブジェクトがキャッシュされている場合、これは UserModel インターフェイス全体を繰り返し処理し、この情報をローカルのインメモリーのみのキャッシュにプルします。クラスターでは、このキャッシュは引き続きローカルに存在しますが、インバリデーションキャッシュになります。ユーザーオブジェクトが変更されると、これはエビクトされます。このエビクションイベントはクラスター全体に伝播され、他のノードのユーザーキャッシュも無効になります。

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

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

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

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

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

特定のユーザー、特定のレルムに含まれるユーザー、またはキャッシュ全体をエビクトする方法があります。

7.10.2. OnUserCache Callback インターフェイス

プロバイダー実装に固有の追加情報をキャッシュすることもできます。ユーザーストレージ 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() メソッドを使用してユーザーの認証情報をキャッシュします。

7.10.3. キャッシュポリシー

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

7.11. Jakarta EE の活用

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

プロバイダー実装はプレーンな Java オブジェクトである必要があります。ただし、現時点では、UserStorageProvider クラスを Stateful EJB として実装することもできます。これは、JPA を使用してリレーショナルストアに接続する場合に便利です。これは、以下の方法で行います。

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

    protected ComponentModel model;
    protected KeycloakSession session;

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

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


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

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

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

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

public class EjbExampleUserStorageProviderFactory
        implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {

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

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

警告

JPA を使用する場合には、追加のデータソースは XA データソースでなければなりません。Red Hat Single Sign-On データソースは、XA データソースではありません。同じトランザクションで 2 つ以上の非 XA データソースと対話する場合には、サーバーはエラーメッセージを返します。1 つのトランザクションでは、非 XA リソース 1 つのみが許可されます。XA データソースのデプロイの詳細は、JBoss EAP のマニュアルを参照してください。

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

7.12. REST 管理 API

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

REST Component API は、レルム管理リソース下に存在します。

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

ここでは、この REST API と Java クライアントとのやり取りのみを紹介します。この API から curl からこの実行方法を抽出できます。

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

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

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

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

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

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

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

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

    @DELETE
    public void remove();
}

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

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

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

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

realmResource.components().add(component);

// retrieve a component

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

// Update a component

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

// Remove a component

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

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

注記

本章では、以前の (すでに廃止された)User Federation SPI を使用してプロバイダーを実装している場合のみを対象としています。

Keycloak バージョン 2.4.0 以前のバージョンでは、User Federation SPI がありました。Red Hat Single Sign-On バージョン 7.0 はサポート対象外ですが、この以前の SPI も使用できます。この以前の User Federation SPI は、Keycloak バージョン 2.5.0 および Red Hat Single Sign-On バージョン 7.1 から削除されました。ただし、本章では、この SPI でプロバイダーを作成している場合の移植に使用できるストラテジーについて説明します。

7.13.1. インポートと非インポート

以前の User Federation SPI は、Red Hat Single Sign-On のデータベースでユーザーのローカルコピーを作成し、情報を外部ストアからローカルコピーにインポートする必要がありました。ただし、要件ではなくなりました。以前のプロバイダーもそのままの状態で移植できますが、インポートなしのストラテジーの方が適しているかどうかを検討する必要があります。

インポートストラテジーの利点:

  • Red Hat Single Sign-On は基本的に外部ストアの永続ユーザーキャッシュになります。ユーザーがインポートされると、外部ストアに到達できなくなるため、負荷がなくなります。
  • Red Hat Single Sign-On に正式なユーザーストアとして移行し、古い外部ストアの使用を終了する場合は、Red Hat Single Sign-On を使用するようにアプリケーションを徐々に移行できます。すべてのアプリケーションが移行されたら、インポートされたユーザーのリンクを解除し、古いレガシー外部ストアを破棄します。

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

  • ユーザーを初めて検索するには、Red Hat Single Sign-On データベースを複数回更新する必要があります。これは、負荷がある場合にパフォーマンスが大幅に低下し、Red Hat Single Sign-On データベースに大きな負担をかける可能性があります。ユーザーフェデレーションされたストレージアプローチは、必要に応じて追加のデータのみを保存し、外部ストアの機能によっては使用されない可能性があります。
  • このインポート方法では、ローカルの Red Hat Single Sign-On ストレージおよび外部ストレージを同期する必要があります。User Storage SPI には同期をサポートするために実装できる機能インターフェイスがありますが、この操作はすぐに面倒で複雑になる可能性があります。

7.13.2. 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 インターフェイスに移動されています。CredentialInputUpdater インターフェイスを実装していない場合は、Red Hat Single Sign-On ストレージでプロバイダーが提供する認証情報をローカルで上書きできます。そのため、認証情報を読み取り専用にするには、CredentialInputUpdater.updateCredential() メソッドを実装し、ReadOnlyException を返します。

searchByAttributes()getGroupMembers() などの UserFederationProvider クエリーメソッドは、任意のインターフェイス UserQueryProvider でカプセル化されるようになりました。このインターフェイスを実装しないと、ユーザーは管理コンソールでは表示されません。ただし、ログインは引き続き可能です。

7.13.3. UserFederationProviderFactory と UserStorageProviderFactory

以前の SPI の同期メソッドは、任意の ImportSynchronization インターフェイス内でカプセル化されるようになりました。同期ロジックを実装している場合、新しい UserStorageProviderFactoryImportSynchronization インターフェイスを実装します。

7.13.4. 新規モデルへのアップグレード

User Storage SPI インスタンスは異なるリレーショナルテーブルのセットに保存されます。Red Hat Single Sign-On は、移行スクリプトを自動的に実行します。以前のユーザーフェデレーションプロバイダーがレルムにデプロイされると、データの id を含む、後のストレージモデルにそのまま変換されます。この移行は、以前のユーザーフェデレーションプロバイダーのプロバイダー ID ("ldap", "kerberos") と同じユーザーストレージプロバイダーが存在する場合のみ行われます。

そのため、この知識をもとに、さまざまなアプローチを取ることができます。

  1. 以前の Red Hat Single Sign-On デプロイメントでは、以前のプロバイダーを削除できます。これにより、インポートした全ユーザーのローカルリンクコピーが削除されます。その後は、Red Hat Single Sign-On のアップグレード時、レルムに新しいプロバイダーをデプロイして設定するだけです。
  2. 2 つ目のオプションは、UserStorageProviderFactory.getId() という同じプロバイダー ID を持つように新規プロバイダーを作成します。このプロバイダーは、Red Hat Single Sign-On の新規インストールの standalone/deployments/ ディレクトリーにあることを確認してください。サーバーを起動し、組み込み移行スクリプトが以前のデータモデルから後のデータモデルに変換されるようにします。この場合、以前にリンクしたインポート済みのユーザーはすべて機能し、同じ状態になります。

インポートストラテジーを廃止して、ユーザーストレージプロバイダーを書き換えた場合には、Red Hat Single Sign-On をアップグレードする前に以前のプロバイダーを削除することを推奨します。これにより、インポートしたユーザーにリンクされたローカルのインポートコピーが削除されます。

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

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

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

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

開発者向けに Streams サブインターフェイスを提供しているインターフェイスの一覧を参照してください。

パッケージ

クラス

org.keycloak.credential

CredentialInputUpdater(*)

org.keycloak.models

GroupModelRoleMapperModelUserModel

org.keycloak.storage.federated

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

org.kecyloak.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
    }
...
}

第8章 Vault SPI

8.1. Vault プロバイダー

org.keycloak.vault パッケージの Vault SPI を使用して、Red Hat Single Sign-On のカスタム拡張を作成し、任意の Vault 実装に接続することができます。

組み込みの files-plaintext プロバイダーは、この SPI の実装例です。一般的に以下のルールが適用されます。

  • レルム間でシークレットのリークを防ぐために、レルムで取得できるシークレットを分離したり、制限したりすることができます。この場合、プロバイダーはシークレットの検索時に、エントリーの前にレルム名を付けるなど、レルム名を考慮する必要があります。たとえば、式 ${vault.key} は、レルム A またはレルム B で使用されるかに応じて、一般的に異なるエントリー名に評価されます。レルムを区別するには、レルムを、KeycloakSession パラメーターから利用できる VaultProviderFactory.create() メソッドから、作成された VaultProvider インスタンスに渡す必要があります。
  • Vault プロバイダーは、指定したシークレット名に VaultRawSecret 返す単一のメソッド obtainSecret を実装します。そのクラスは、シークレットの表現を byte[] または ByteBuffer のいずれかに保持し、必要に応じて 2 つの間で変換することが期待されます。このバッファーは、以下で説明されているように使用後に破棄されることに注意してください。

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

8.2. Vault からの値の使用

vault には機密データが含まれ、Red Hat Single Sign-On はシークレットを適宜処理します。シークレットにアクセスする場合には、シークレットは 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() メソッド経由) を取得するのに加え、(通常はバイト配列である) 未編集のシークレットを文字アレイ (vault().getCharSecret() 経由) または String (via vault().getStringSecret() 経由) として解釈できるという利点があります。

String オブジェクトは変更できないため、ランダムなガベージでコンテンツを破棄できないことに注意してください。デフォルトの VaultStringSecret の実装では、String の内面化を防ぐための対策が取られていますが、String オブジェクトに格納されているシークレットは、少なくとも次の GC ラウンドまで存続します。そのため、プレーンなバイト配列、文字配列、およびバッファーを使用することが推奨されます。