Skip to content
Published on

Keycloak トークンカスタマイズ — Protocol Mapper と Claims 設計の実践

Authors

はじめに

Keycloak を導入したチームが最初に直面する実務上の課題は、認証そのものではなくトークンの中に何を入れるかです。デフォルト設定で発行された access token には、自社サービスが必要とする部署コードも社内権限グレードも含まれていません。逆に数年運用を続けると、トークンが 8KB を超えて HTTP ヘッダーの上限を脅かすという正反対の問題に遭遇することもあります。

2026 年現在、Keycloak 26.6 は JWT Authorization Grant、Federated client authentication、FAPI 2.0 Security Profile Final 対応などによりトークン発行経路そのものが多様化し、OAuth Client ID Metadata Document(CIMD)の実験的機能により MCP(Model Context Protocol)ベースの AI エージェント向け authorization server の役割まで担えるようになりました。トークンの消費主体が人間のブラウザセッションを超えて AI エージェント、バッチワークロード、外部パートナーシステムへ拡大する中、クレーム設計はもはや副次的な設定ではなく、セキュリティアーキテクチャの中核になっています。

本記事では、Protocol Mapper と Client Scope の動作原理から、カスタム SPI の実装、audience の罠、トークンダイエット戦略、そして検証の自動化まで、実践の順序で整理します。

Client Scope と Protocol Mapper の関係

Keycloak でトークンに入るクレームはすべて Protocol Mapper が生成します。そして mapper は 2 つの場所に取り付けられます。

  1. Client Scope に付く mapper — 複数クライアントで共有する再利用単位
  2. クライアントに直接付く dedicated mapper — 当該クライアント専用

トークン発行時の評価順序を図にすると次のようになります。

+--------------------------------------------------------------+
|                      Token Issuance Pipeline                  |
+--------------------------------------------------------------+
   Authorization Request (scope=openid profile email teams)
        |
        v
 +--------------+     +---------------------------+
 | Default      |     | Optional Client Scopes    |
 | Client Scopes| <-- | (scope パラメータで要求時) |
 +--------------+     +---------------------------+
        |                     |
        +----------+----------+
                   v
        +---------------------+
        | Effective Scope Set |
        +---------------------+
                   |
                   v
        +---------------------+      +--------------------+
        | Protocol Mappers    | <--- | Dedicated Mappers  |
        | (scope に紐づくもの)|      | (client 専用)      |
        +---------------------+      +--------------------+
                   |
                   v
   +-----------------------------------------+
   | ID Token / Access Token / UserInfo 応答 |
   +-----------------------------------------+

重要なルールは 3 つです。

  • Default scope はクライアントが要求しなくても常に評価されます。profileemailrolesweb-origins が代表例です。
  • Optional scope は認可リクエストの scope パラメータに明示されたときのみ評価されます。部署情報のように一部のクライアントだけが必要とするクレームは optional scope に分離するのが、トークンダイエットの出発点です。
  • mapper ごとに Add to ID token / Add to access token / Add to userinfo のトグルが個別にあり、同じクレームでもトークン種別ごとに含めるかどうかを変えられます。

Keycloak 管理コンソールの Client Scopes メニューで realm 全体の scope を作成し、各クライアントの Client Scopes タブで default / optional として紐づけるのが標準的な運用パターンです。

組み込み Mapper の総まとめ

Keycloak が標準提供する mapper のうち、実務で使用頻度が高いものを整理すると次のとおりです。

Mapper タイプ用途主な設定項目
User Attributeユーザー attribute をクレームに属性名、クレーム名、JSON タイプ
User Propertyusername、email などの組み込みフィールドプロパティ名、クレーム名
Group Membership所属グループ一覧をクレームにfull path の有無
Role Name Mapperロール名を別名に置換元のロール、新しい名前
User Realm Rolerealm ロール一覧をクレームにクレーム名、マルチバリュー
User Client Role特定クライアントのロールのみ抽出client id、クレーム名
Audienceaud クレームに対象クライアントを追加included client audience
Audience Resolveロールに基づき aud を自動計算なし
Hardcoded Claim固定値クレームを挿入クレーム名、値、タイプ
Pairwise subject identifierクライアントごとに sub を匿名化sector identifier URI
Allowed Web OriginsCORS 許可 origin を注入なし

User Attribute mapper を kcadm CLI で作成してみます。department というユーザー attribute を access token の dept クレームとして出力する例です。

# client scope の作成
kcadm.sh create client-scopes -r myrealm \
  -s name=org-info \
  -s protocol=openid-connect \
  -s 'attributes."include.in.token.scope"=true'

# scope に user attribute mapper を追加
kcadm.sh create client-scopes/SCOPE_ID/protocol-mappers/models -r myrealm \
  -s name=dept-mapper \
  -s protocol=openid-connect \
  -s protocolMapper=oidc-usermodel-attribute-mapper \
  -s 'config."user.attribute"=department' \
  -s 'config."claim.name"=dept' \
  -s 'config."jsonType.label"=String' \
  -s 'config."access.token.claim"=true' \
  -s 'config."id.token.claim"=false' \
  -s 'config."userinfo.token.claim"=true'

# クライアントに optional scope として紐づけ
kcadm.sh update clients/CLIENT_UUID/optional-client-scopes/SCOPE_ID -r myrealm

Group Membership mapper で注意すべき点は Full group path オプションです。有効にすると /engineering/platform/sre のような完全パスが、無効にすると sre のような末端グループ名のみが入ります。消費側アプリケーションがパスをパースしている場合、このオプションを切り替えた瞬間に権限チェックがすべて壊れるため、最初からチーム標準を決めておくべきです。

Hardcoded Claim mapper は環境識別に便利です。たとえばステージング realm のすべてのトークンに env クレームを staging として刻んでおけば、トークンが環境を越えて誤用される事故をリソースサーバー側の 1 行の検証で遮断できます。

カスタム ProtocolMapper SPI の実装

組み込み mapper で解決できない要件 — たとえば外部 HR システムの照会結果をクレームに入れる、複数の attribute を組み合わせて 1 つの構造化クレームを作る、といったケース — には ProtocolMapper SPI を実装します。

Maven 依存関係は provided スコープで宣言します。

<dependencies>
  <dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-services</artifactId>
    <version>26.6.2</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-server-spi</artifactId>
    <version>26.6.2</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

実装クラスは AbstractOIDCProtocolMapper を継承し、3 つのマーカーインターフェースを実装します。

package com.example.keycloak.mapper;

import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class EmployeeGradeMapper extends AbstractOIDCProtocolMapper
        implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {

    public static final String PROVIDER_ID = "employee-grade-mapper";
    private static final List<ProviderConfigProperty> CONFIG = new ArrayList<>();

    static {
        // 管理コンソールに表示されるトークン包含トグル 3 種を自動追加
        OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG);
        OIDCAttributeMapperHelper.addIncludeInTokensConfig(CONFIG, EmployeeGradeMapper.class);
    }

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

    @Override
    public String getDisplayType() {
        return "Employee Grade Mapper";
    }

    @Override
    public String getDisplayCategory() {
        return TOKEN_MAPPER_CATEGORY;
    }

    @Override
    public String getHelpText() {
        return "Combines job-code and grade attributes into one structured claim.";
    }

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

    @Override
    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel,
                            UserSessionModel userSession, KeycloakSession session,
                            ClientSessionContext clientSessionCtx) {
        String jobCode = userSession.getUser().getFirstAttribute("jobCode");
        String grade = userSession.getUser().getFirstAttribute("grade");
        if (jobCode == null || grade == null) {
            return; // 値がなければクレーム自体を入れない
        }
        Map<String, Object> value = Map.of(
                "jobCode", jobCode,
                "grade", Integer.parseInt(grade));
        OIDCAttributeMapperHelper.mapClaim(token, mappingModel, value);
    }
}

サービス登録ファイルを忘れると mapper はコンソールに表示されません。

src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
--------------------------------------------------------------------------
com.example.keycloak.mapper.EmployeeGradeMapper

ビルドした JAR は providers ディレクトリに配置し、build フェーズを再実行します。

cp target/employee-grade-mapper.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build
/opt/keycloak/bin/kc.sh start --optimized

運用の観点から 2 点を強調したいと思います。第一に、setClaim の中で外部 API を同期呼び出しする設計は避けるべきです。トークン発行はログインのクリティカルパスにあり、外部の遅延がそのまま全社的なログイン遅延になります。外部データはユーザー attribute へ事前に同期しておき、mapper は読み取りだけを行うパターンが安全です。第二に、Keycloak のメジャーアップグレードで SPI のシグネチャが変わることがあるため、カスタム mapper は必ずアップグレードチェックリストに含めるべきです。

Audience 設定の落とし穴 — aud 検証失敗の事例

トークンカスタマイズで最も頻繁に障害を引き起こすのが aud クレームです。典型的な事故シナリオは次のとおりです。

  1. フロントエンド SPA が web-app クライアントでログインし、access token を受け取ります。
  2. そのトークンでバックエンドの order-api を呼び出します。
  3. order-api は Spring Security の resource server として構成され、audience 検証が有効になっています。
  4. トークンの aud には account しか入っておらず、401 invalid_token が返ります。

Keycloak はデフォルトでは「このトークンをどのリソースサーバーが消費するか」を知らないため、aud を自動的に埋めてくれません。解決策は Audience mapper を明示的に追加することです。

# order-api を aud に追加する client scope + audience mapper
kcadm.sh create client-scopes -r myrealm \
  -s name=order-api-audience -s protocol=openid-connect

kcadm.sh create client-scopes/SCOPE_ID/protocol-mappers/models -r myrealm \
  -s name=order-api-aud \
  -s protocol=openid-connect \
  -s protocolMapper=oidc-audience-mapper \
  -s 'config."included.client.audience"=order-api' \
  -s 'config."access.token.claim"=true' \
  -s 'config."id.token.claim"=false'

検証側(Spring)のコードは次回の記事で詳しく扱いますが、核心部分だけ示します。

OAuth2TokenValidator<Jwt> audienceValidator = new JwtClaimValidator<List<String>>(
        "aud", aud -> aud != null && aud.contains("order-api"));

よくはまる罠をまとめると次のとおりです。

  • Audience Resolve mapper への誤解: この mapper はトークンに含まれる client role を根拠に aud を計算します。当該リソースサーバーの client role をユーザーが 1 つも持っていなければ aud には追加されません。
  • ID token に aud を入れて安心するケース: ID token の aud は常に要求クライアント自身です。リソースサーバーが検証するのは access token なので、mapper 設定で access token のトグルが有効になっているか確認すべきです。
  • azp と aud の混同: azp はトークンを要求したクライアント(authorized party)で、aud はトークンの消費対象です。ゲートウェイが azp だけを見て通過させる構成は、RFC 9700 OAuth Security BCP が警告するトークン再利用攻撃に対して脆弱です。
  • より厳密な代替案としては、RFC 8707 の resource indicator や、トークン交換(RFC 8693)でリソースサーバーごとにトークンを分離する方法があります。

トークン肥大化問題とダイエット戦略

運用 2〜3 年目の Keycloak でよくある症状は、access token が数 KB に膨れ上がることです。原因はおおむね次のとおりです。

  • 数百のグループが Group Membership mapper によってすべてトークンに含まれている
  • realm role とすべての client role が realm_accessresource_access に丸ごと含まれている
  • すべてのクレームが default scope に紐づき、すべてのクライアントのトークンに注入されている

トークンが大きくなるとすべての HTTP リクエストヘッダーも一緒に大きくなり、8KB を超えた瞬間に NGINX のデフォルト large_client_header_buffers や AWS ALB の 16KB ヘッダー上限に当たり、502 が散発的に発生します。ダイエット戦略を優先順位順に挙げます。

  1. scope の分離: 頻繁に使うクレームだけを default scope に残し、それ以外は optional scope へ移動します。クライアントは必要なときだけ scope パラメータで要求します。
  2. ロールのフィルタリング: クライアント設定の Scope タブで Full Scope Allowed を無効にし、当該クライアントに必要なロールだけを明示的に割り当てます。これだけで resource_access が劇的に縮小するケースが多くあります。
  3. グループの代わりに派生クレーム: グループの全リストの代わりに、カスタム mapper で「権限グレード」のような要約値を計算して入れます。
  4. userinfo への移動: 画面表示用のプロフィール情報は access token から外し、userinfo エンドポイントの照会に切り替えます。
  5. lightweight access token: Keycloak 24 から導入された軽量 access token 機能を有効にすると、access token から多数のデフォルトクレームが除外され、リソースサーバーは token introspection で詳細情報を照会するようになります。

クレーム配分の設計 — ID Token vs Access Token vs UserInfo

3 つの伝達チャネルの役割を明確に区別すると、設計はシンプルになります。

チャネル消費主体入れるべきもの入れてはいけないもの
ID Tokenクライアントアプリ認証の事実、画面用の最小プロフィールAPI 認可用の権限情報
Access Tokenリソースサーバーaud、ロール、認可判断用クレーム画面表示用の詳細プロフィール
UserInfoクライアントアプリ詳細プロフィール、変動の多い情報認可判断の根拠

原則は単純です。ID token は認証の証拠、access token は認可の入力、userinfo はプロフィールのソースです。ID token に権限を入れてクライアントが自前で認可を行うと、トークンの有効期間中は権限変更が反映されない問題と、クライアント改ざんのリスクを同時に抱え込むことになります。

実務でもう 1 つ重要なのは個人情報の置き場所です。access token はすべてのマイクロサービスとログパイプラインを巡回するため、マイナンバーの一部のような機微情報は access token に絶対に入れず、userinfo または専用 API に隔離すべきです。

マルチクライアント環境でのクレーム一貫性の維持

クライアントが数十個に増えると、「同じ意味のクレームがクライアントごとに違う名前」というエントロピーが発生します。deptdepartmentorg_code が併存するような状態です。予防策は次のとおりです。

  • 共有 client scope を単一の真実の源に: クレーム定義は必ず realm レベルの client scope だけに作成し、dedicated mapper の使用を禁止するチームルールを設けます。
  • クレーム命名規約のドキュメント化: クレーム辞書(claim dictionary)を作り、名前、タイプ、ソース attribute、含めるトークンを記録します。
  • Terraform / kcadm によるコード化: 管理コンソールの手作業ではなく、Keycloak Terraform provider や kcadm スクリプトで mapper を宣言的に管理すれば、環境間の drift を防げます。
# 全クライアントの mapper 現況をダンプして drift を監査するスクリプト
for c in $(kcadm.sh get clients -r myrealm --fields id --format csv --noquotes); do
  kcadm.sh get clients/$c/protocol-mappers/models -r myrealm \
    --fields name,protocolMapper,config
done > mappers-audit.json

スクリプトマッパーの危険性

Keycloak には JavaScript でクレームを計算する Script Mapper がありましたが、現在はデフォルト無効の preview 機能です。有効化するにはデプロイ時に明示的なフラグが必要です。

kc.sh start --features=scripts

スクリプトマッパーを避けるべき理由は明確です。

  • セキュリティ: 管理コンソールのアクセス権限が、そのままサーバー内のコード実行権限になります。管理者アカウントが奪取された場合、被害範囲が realm 設定の変更を超えて RCE に拡大します。
  • 移植性: Nashorn 系エンジンへの依存のため、Keycloak のバージョンアップで互換性が壊れやすくなります。
  • 観測不能: スクリプトのエラーがトークン発行の失敗につながっても、追跡が困難です。

既存でスクリプトマッパーを使っている場合は、前述のカスタム ProtocolMapper SPI(コンパイル・コードレビュー・バージョン管理が可能な Java コード)への移行が、2026 年時点の推奨パスです。

テスト方法 — kcadm と Token Introspection

クレーム設計は必ず自動化された検証とセットで進めるべきです。最初のステップは管理コンソールの Evaluate 機能(Client Scopes タブ)で、特定のユーザー・scope の組み合わせのトークンを発行せずにプレビューできます。CI では実際のトークンを取得して検査します。

# 1. password grant でテストトークンを発行(テスト専用 confidential client)
TOKEN=$(curl -s -X POST \
  "https://kc.example.com/realms/myrealm/protocol/openid-connect/token" \
  -d grant_type=password \
  -d client_id=ci-test-client \
  -d client_secret=$CLIENT_SECRET \
  -d username=testuser \
  -d password=$TEST_PASSWORD \
  -d scope="openid org-info" | jq -r .access_token)

# 2. ペイロードをデコードしてクレームをアサート
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null \
  | jq -e '.dept == "platform" and (.aud | index("order-api"))'

# 3. introspection エンドポイントでサーバー側の検証結果を確認
curl -s -X POST \
  "https://kc.example.com/realms/myrealm/protocol/openid-connect/token/introspect" \
  -u order-api:$RS_SECRET \
  -d token=$TOKEN | jq '{active, aud, scope, dept}'

introspection は opaque / lightweight トークン戦略を採るときに特に重要です。active フィールドが false なら、期限切れ・失効・署名不一致のいずれかであり、CI で introspection 応答までアサートしておけば、mapper の変更がリソースサーバーの検証を壊すリグレッションをデプロイ前に検出できます。

最後にトークンサイズのリグレッションテストもおすすめします。

SIZE=$(echo -n $TOKEN | wc -c)
if [ "$SIZE" -gt 4096 ]; then
  echo "FAIL: access token is $SIZE bytes (limit 4096)"; exit 1
fi

クレーム設計アンチパターン・チェックリスト

最後に、設計レビューでそのまま使えるアンチパターンの点検リストです。

  • すべてのクレームを default scope に配置 — トークン肥大化の第 1 の原因です。optional scope への分離が基本です。
  • 同じデータを access token と userinfo に重複して含める — チャネルごとの役割定義から合意し直すべきです。
  • 機微情報を access token に含める — access token はすべてのサービスとログパイプラインを通過するという事実を忘れがちです。
  • aud mapper だけ入れてリソースサーバーの検証を省略 — mapper の設定と検証コードは常にワンセットです。
  • dedicated mapper の乱用 — クライアントごとにクレーム名が分岐するエントロピーの始まりです。
  • スクリプトマッパーの新規導入 — SPI で実装できない理由をまず文書で証明すべきです。
  • mapper の変更をコンソールの手作業で処理 — コード化されていない変更は環境間の drift とロールバック不能を生みます。
  • トークンサイズの監視不在 — mapper 1 つの追加がゲートウェイの 502 につながる経路を断つには、サイズのリグレッションテストが必要です。

おわりに

Protocol Mapper は小さな設定に見えますが、その成果物であるクレームは、マイクロサービス全体の認可判断、ヘッダー予算、個人情報の露出範囲を決定します。まとめると次のとおりです。

  • クレームは client scope 単位で設計し、default / optional を区別してトークン肥大化を予防します。
  • aud は自動では埋まりません。Audience mapper を明示し、リソースサーバーで必ず検証します。
  • ID token、access token、userinfo の役割を区別してクレームを配分します。
  • スクリプトマッパーの代わりにカスタム ProtocolMapper SPI を使用します。
  • Evaluate、kcadm、introspection によるクレーム検証を CI に組み込みます。

次回の記事では、このように設計したトークンを Spring Security 6 のリソースサーバー側で消費する実装を扱います。

参考資料