Skip to content

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

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

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

2026 年現在、[Keycloak 26.6](https://www.keycloak.org/docs/latest/release_notes/index.html) は 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** はクライアントが要求しなくても常に評価されます。`profile`、`email`、`roles`、`web-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 Property | username、email などの組み込みフィールド | プロパティ名、クレーム名 |

| Group Membership | 所属グループ一覧をクレームに | full path の有無 |

| Role Name Mapper | ロール名を別名に置換 | 元のロール、新しい名前 |

| User Realm Role | realm ロール一覧をクレームに | クレーム名、マルチバリュー |

| User Client Role | 特定クライアントのロールのみ抽出 | client id、クレーム名 |

| Audience | aud クレームに対象クライアントを追加 | included client audience |

| Audience Resolve | ロールに基づき aud を自動計算 | なし |

| Hardcoded Claim | 固定値クレームを挿入 | クレーム名、値、タイプ |

| Pairwise subject identifier | クライアントごとに sub を匿名化 | sector identifier URI |

| Allowed Web Origins | CORS 許可 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 スコープで宣言します。

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

package com.example.keycloak.mapper;

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](https://datatracker.ietf.org/doc/html/rfc9700) が警告するトークン再利用攻撃に対して脆弱です。

- より厳密な代替案としては、RFC 8707 の resource indicator や、トークン交換(RFC 8693)でリソースサーバーごとにトークンを分離する方法があります。

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

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

- 数百のグループが Group Membership mapper によってすべてトークンに含まれている

- realm role とすべての client role が `realm_access` と `resource_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 に隔離すべきです。

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

クライアントが数十個に増えると、「同じ意味のクレームがクライアントごとに違う名前」というエントロピーが発生します。`dept`、`department`、`org_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 のリソースサーバー側で消費する実装を扱います。

参考資料

- [Keycloak Server Administration Guide — Protocol Mappers](https://www.keycloak.org/docs/latest/server_admin/index.html)

- [Keycloak Server Developer Guide — Service Provider Interfaces](https://www.keycloak.org/docs/latest/server_development/index.html)

- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)

- [Keycloak 26.6.0 Released](https://www.keycloak.org/2026/04/keycloak-2660-released)

- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)

- [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [RFC 8693 — OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

- [RFC 7662 — OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)

- [RFC 8707 — Resource Indicators for OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc8707)

- [OAuth 2.1 draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)

현재 단락 (1/233)

Keycloak を導入したチームが最初に直面する実務上の課題は、認証そのものではなく**トークンの中に何を入れるか**です。デフォルト設定で発行された access token には、自社サービスが...

작성 글자: 0원문 글자: 13,353작성 단락: 0/233