LabHub

ブログ

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

한국어English日本語

はじめに

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 つです。

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

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

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

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

トークンが大きくなるとすべての 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 が併存するような状態です。予防策は次のとおりです。

# 全クライアントの 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

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

既存でスクリプトマッパーを使っている場合は、前述のカスタム 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

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

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

おわりに

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

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

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます