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.Authenticator と org.keycloak.authentication.AuthenticatorFactory の2つを最低限実装します。公式のServer Developer Guideにある通り、Factoryはサーバ起動時に一度だけ生成されるシングルトンで、Authenticator本体はリクエストごとに create(KeycloakSession) で作られる短命なオブジェクトです。
- Authenticator:1つの認証ステップのロジック本体。リクエストを検証し、成功・失敗・チャレンジ(画面表示)をフローへ通知します。
- AuthenticatorFactory:ステップの識別子(
getId())、管理コンソールでの表示名、設定項目(getConfigProperties())、許可するRequirement(REQUIRED/ALTERNATIVE/DISABLED)を定義します。
AuthenticationFlowContext と認証セッションのnoteに載せてください。02Authenticatorインターフェースを実装する
実装すべきメソッドは限られています。現行の26系Javadoc(Authenticator API)を基準に、主要なものを挙げます。
authenticate(AuthenticationFlowContext context):ステップに最初に入ったときに呼ばれます。ここでリクエストを検証し、画面が必要ならチャレンジを返します。action(AuthenticationFlowContext context):authenticate()が表示したフォームがPOSTで戻ってきたときに呼ばれます。入力の検証と最終判定はここで行います。requiresUser():このステップが「確定済みユーザー」を前提とするかを返します。パスワード入力より前に置くステップならfalseにします。configuredFor(KeycloakSession, RealmModel, UserModel):対象ユーザーがこのステップを利用できる状態か(例:OTPが登録済みか)を返します。setRequiredActions(KeycloakSession, RealmModel, UserModel):未設定ユーザーに対し、登録を促すRequired Actionを付与します。
authenticate() と action() の役割分担が肝です。画面を出さずに即判定できるステップ(バックエンドのAPI結果だけで通す/弾く)なら authenticate() の中で完結させ、context.success() か context.failure(...) を呼ぶだけで済みます。ユーザー入力を挟むなら、authenticate() でフォームを表示し、戻りを action() で受ける2段構えになります。
02bAuthenticationFlowContextの操作
フローとのやり取りはすべて AuthenticationFlowContext(サブインターフェース AuthenticationFlowContext)経由です。判定結果に応じて次のいずれかを必ず1回呼び、フローに制御を返します。
context.success():このステップを成功として次へ進めます。context.failure(AuthenticationFlowError.INVALID_CREDENTIALS):致命的失敗としてフローを止めます。context.challenge(Response)/context.forceChallenge(Response):ブラウザに画面(フォーム)を返します。context.attempted():このステップは判定できなかった(スキップ扱い)と通知します。ALTERNATIVEな並列ステップで「自分は該当しないので他に任せる」ときに使います。context.failureChallenge(...):失敗を示しつつエラー入りの画面を再表示します。
success() と attempted() の違いは統制上とても重要です。REQUIREDな並びで attempted() を返すと、そのステップは「実施されなかった」扱いになり、意図せず認証が素通りすることがあります。「実行したうえで問題なし」なら必ず success() を、「そもそも対象外」なら attempted() を、と明確に使い分けてください。ステップ間で値を引き継ぎたい場合は、context.getAuthenticationSession().setAuthNote("key", value) で認証セッションのnoteに保存します。設定値は context.getAuthenticatorConfig().getConfig() から取得し、Factoryの getConfigProperties() で宣言したキーで読み出します。
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テンプレートをレンダリングします。典型的には次のような流れです。
authenticate()内でResponse challenge = context.form().setAttribute("...", ...).createForm("my-step.ftl");を組み立て、context.challenge(challenge)で返す。- テンプレート(
.ftl)はJAR内のtheme-resources/templates/に置くと、追加テーマを作らずに認証画面から参照できます。 - フォームのPOST先URLはKeycloakが自動生成し、戻りは
action()で受けます。context.getHttpRequest().getDecodedFormParameters()で入力値を取得します。
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 のホットデプロイはありません。公式のプロバイダ設定ドキュメントにある通り、手順はシンプルです。
- Authenticatorを含むJARをビルドし、必ず
META-INF/services/org.keycloak.authentication.AuthenticatorFactoryにFactoryの完全修飾クラス名を記載する(これが無いとロードされません)。 - JARを配布物の
providers/ディレクトリにコピーする。 bin/kc.sh buildを実行してプロバイダレジストリを更新する。--optimized運用では特にビルド必須です。
providers/ に放り込むとバージョン衝突を起こしやすいです。依存はKeycloakが同梱するもので賄い、どうしても必要な追加ライブラリだけを厳選して置いてください。CI/CDでイメージにJARを焼き込み、起動時ではなくビルド時に kc.sh build を済ませる「optimizedイメージ」化が、コンテナ運用では定石です。詳細はbuildとruntime設定の分離もどうぞ。07バージョン間のAPI変化への備え
Custom Authenticator SPIはKeycloakのSPIの中では比較的安定している部類ですが、内部APIである以上、メジャーバージョンで変わり得ます。実際に注意が必要なのは主に次の点です。
- 配布基盤の転換:WildFly版からQuarkus版への移行で、デプロイ方法(
deployments廃止・providers+build)が根本的に変わりました。旧版向けの記事・手順をそのまま流用しないよう注意します。 - モデルAPIの整理:ユーザー・realm取得系のインターフェース(
UserModelや各種Provider)は、バージョンによってメソッドがStreamベース化されるなどの変更が入っています。コンパイル対象のKeycloakバージョンに合わせた依存でビルドするのが安全です。 - 非推奨の追跡:特定バージョンで
@Deprecatedになったメソッドは数バージョン後に削除されることがあります。アップグレード時は各バージョンのUpgrading Guideと、コンパイル時の警告を必ず確認してください。
いずれもバージョン依存の細部は「新しめのバージョン基準・公式ドキュメントで要確認」が原則です。実運用では、Keycloak本体のアップグレードと自作providerの再ビルド・回帰テストをワンセットで回す仕組みを用意しておくと、破壊的変更に慌てずに済みます。アップグレード運用の考え方はバージョンアップグレードの記事にまとめています。EMWでもエンタープライズのKeycloak認証基盤で独自ステップの実装・保守を行った実績があり、導入事例でも触れています。
—まとめ
Custom Authenticator SPIは、Authenticator と AuthenticatorFactory の2つを実装し、AuthenticationFlowContext で成功・失敗・チャレンジを通知するだけの、意外と素直な仕組みです。難所はむしろ、Factoryをステートレスに保つこと、success() と attempted() の統制的な使い分け、そしてQuarkus版のデプロイ(providers + kc.sh build)とバージョン差への備えにあります。標準機能では届かない要件を、フローの正式な部品として安全に組み込むための第一歩として、本記事が役立てば幸いです。