Keycloakの標準機能では届かない独自の認証ステップを、Custom Authenticator SPIで作り込む方法を整理します。Authenticator/AuthenticatorFactoryの実装からフローへの組込み、context操作、フォーム表示、providersディレクトリへのデプロイ、そしてバージョン間で変わるAPIまで、現場で詰まりやすい点を中心に解説します。

Keycloakは標準でパスワード、OTP、WebAuthn、条件分岐など豊富な認証ステップを備えていますが、エンタープライズの現場では「社内リスクスコアAPIの結果で追加認証を分岐したい」「特定の属性を持つユーザーだけに同意画面を挟みたい」といった、標準では埋まらない要件が必ず出てきます。こうした独自ステップを認証フローに正式な部品として組み込むための仕組みが Custom Authenticator SPI です。本記事では、実装・組込み・デプロイの一連を、現行のQuarkusベース配布を前提に整理します。

01Authenticator SPIの全体像

Keycloakのプロバイダは、実処理を担う Provider と、そのインスタンスを生成する ProviderFactory のペアで構成されます。認証ステップの場合は org.keycloak.authentication.Authenticatororg.keycloak.authentication.AuthenticatorFactory の2つを最低限実装します。公式のServer Developer Guideにある通り、Factoryはサーバ起動時に一度だけ生成されるシングルトンで、Authenticator本体はリクエストごとに create(KeycloakSession) で作られる短命なオブジェクトです。

現場のコツ:Authenticatorは短命・ステートレスに保つのが鉄則です。Factoryはシングルトンなので、Factoryのフィールドにリクエスト固有の状態を持たせると全ユーザーで共有され、深刻な情報漏洩やレースコンディションを引き起こします。状態はすべて AuthenticationFlowContext と認証セッションのnoteに載せてください。

02Authenticatorインターフェースを実装する

実装すべきメソッドは限られています。現行の26系Javadoc(Authenticator API)を基準に、主要なものを挙げます。

authenticate()action() の役割分担が肝です。画面を出さずに即判定できるステップ(バックエンドのAPI結果だけで通す/弾く)なら authenticate() の中で完結させ、context.success()context.failure(...) を呼ぶだけで済みます。ユーザー入力を挟むなら、authenticate() でフォームを表示し、戻りを action() で受ける2段構えになります。

02bAuthenticationFlowContextの操作

フローとのやり取りはすべて AuthenticationFlowContext(サブインターフェース AuthenticationFlowContext)経由です。判定結果に応じて次のいずれかを必ず1回呼び、フローに制御を返します。

現場のコツ:success()attempted() の違いは統制上とても重要です。REQUIREDな並びで attempted() を返すと、そのステップは「実施されなかった」扱いになり、意図せず認証が素通りすることがあります。「実行したうえで問題なし」なら必ず success() を、「そもそも対象外」なら attempted() を、と明確に使い分けてください。

ステップ間で値を引き継ぎたい場合は、context.getAuthenticationSession().setAuthNote("key", value) で認証セッションのnoteに保存します。設定値は context.getAuthenticatorConfig().getConfig() から取得し、Factoryの getConfigProperties() で宣言したキーで読み出します。

認証フロー ステップに到達 authenticate() 検証/画面判断 challenge() フォーム表示 action() POST受信・判定 success / failure フローへ通知 画面不要なら直接
図:authenticate()でチャレンジ→action()で受信→success/failureで通知する基本ライフサイクル。画面が不要な判定はauthenticate()で完結できます。

03AuthenticatorFactoryとconfig定義

Factoryは管理コンソールからの見え方を決めます。getId() が返す文字列はフロー設定やデプロイ後の識別に使う一意なIDなので、emw-risk-score-authenticator のように衝突しない命名にします。getDisplayType()getHelpText() はコンソール上の表示です。

ステップをパラメータ化するには getConfigProperties()ProviderConfigProperty のリストを返します。例えば「リスクスコアの閾値」「呼び出し先APIのURL」を設定可能にしておくと、realmごと・環境ごとに値を変えられ、コード変更なしで運用チームが調整できます。getRequirementChoices() ではそのステップに許可するRequirement(REQUIRED/ALTERNATIVE/DISABLEDなど)を返し、isUserSetupAllowed()true にするとフローが setRequiredActions() を呼べるようになります。

04フォームを出す(FreeMarkerテーマ連携)

独自の入力画面を出す場合、context.form() が返す LoginFormsProvider でFreeMarkerテンプレートをレンダリングします。典型的には次のような流れです。

現場のコツ:フォーム系ステップでは必ずCSRF対策のためのアクション検証と、再表示時のエラー表示(context.form().setError("...").createForm(...))をセットで実装します。ここを省くと、入力ミス時に真っ白なエラー画面が出てユーザー体験が崩れます。認証フロー全体の設計思想は認証フローの設計の記事も併せてご覧ください。

05フローへの組込み

実装したステップは、そのままでは動きません。管理コンソールの Authentication → Flows で、対象フロー(例:browserフローのコピー)に「Add step」で自作Authenticatorを追加し、Requirementを設定します。ステップ単位でConfigを作成すれば、getConfigProperties() で宣言した項目を画面から入力できます。フローを組んだら、対象realmやクライアントにバインド(Bind flow)して初めて実際の認証に効きます。

ここは統制の要点でもあります。フローは realm 単位で管理されるため、マルチrealm・マルチテナントで同じステップを使い回す場合は、命名規則とConfigの棚卸しが煩雑になりがちです。組織横断の権限・統制設計についてはマルチアカウント統制マルチrealm設計の観点も参考にしてください。

06ビルドとproviersへのデプロイ

現行のQuarkusベース配布では、旧WildFly版にあった standalone/deployments のホットデプロイはありません。公式のプロバイダ設定ドキュメントにある通り、手順はシンプルです。

現場のコツ:Quarkus版では自作providerとKeycloak本体でクラスパスが共有されます。旧版のような分離ディレクトリは無いため、余計な依存JARを providers/ に放り込むとバージョン衝突を起こしやすいです。依存はKeycloakが同梱するもので賄い、どうしても必要な追加ライブラリだけを厳選して置いてください。CI/CDでイメージにJARを焼き込み、起動時ではなくビルド時に kc.sh build を済ませる「optimizedイメージ」化が、コンテナ運用では定石です。詳細はbuildとruntime設定の分離もどうぞ。

07バージョン間のAPI変化への備え

Custom Authenticator SPIはKeycloakのSPIの中では比較的安定している部類ですが、内部APIである以上、メジャーバージョンで変わり得ます。実際に注意が必要なのは主に次の点です。

いずれもバージョン依存の細部は「新しめのバージョン基準・公式ドキュメントで要確認」が原則です。実運用では、Keycloak本体のアップグレードと自作providerの再ビルド・回帰テストをワンセットで回す仕組みを用意しておくと、破壊的変更に慌てずに済みます。アップグレード運用の考え方はバージョンアップグレードの記事にまとめています。EMWでもエンタープライズのKeycloak認証基盤で独自ステップの実装・保守を行った実績があり、導入事例でも触れています。

まとめ

Custom Authenticator SPIは、AuthenticatorAuthenticatorFactory の2つを実装し、AuthenticationFlowContext で成功・失敗・チャレンジを通知するだけの、意外と素直な仕組みです。難所はむしろ、Factoryをステートレスに保つこと、success()attempted() の統制的な使い分け、そしてQuarkus版のデプロイ(providers + kc.sh build)とバージョン差への備えにあります。標準機能では届かない要件を、フローの正式な部品として安全に組み込むための第一歩として、本記事が役立てば幸いです。

参考(一次情報)

独自認証ステップの設計・実装からKeycloak基盤全体の統制まで、実装経験のあるエンジニアが伴走します。まずはお問い合わせください。

相談する
← ブログ一覧へ戻る