Contents
OIDC トークンとクレームの基本概念
OIDC では認可サーバが発行する ID トークン と アクセストークン が JSON Web Token (JWT) の形で提供され、各トークンは「クレーム」の集合として扱われます。ここでは必須クレームとオプショナル(任意)クレームの違いを整理し、実装上注意すべきポイントを解説します。
必須クレームとオプションクレームの違い
OIDC 仕様はトークンに必ず含める必要がある 必須クレーム と、アプリケーション側の要件次第で付与できる オプショナルクレーム を明確に区別しています。
| クレーム種別 | 主な項目例 | 役割・説明 |
|---|---|---|
| 必須クレーム | iss, sub, aud, exp, iat |
トークンの署名検証や利用者識別に不可欠。すべてのトークンに自動的に含まれる。 |
| オプションクレーム | email, profile, address, カスタム属性(例:dept) |
アプリが必要とする情報を任意で付与。スコープやマッパーで要求しなければトークンに現れない。 |
ポイント
必須クレームは常に存在しますが、カスタム属性は「オプショナル」として扱われ、Keycloak 側で プロトコルマッパー や Client Scope に明示的に登録しなければトークンへ注入されません。
ID トークンとアクセストークンに含まれる標準クレーム
| トークン種別 | 標準クレーム(代表) | 主な利用シーン |
|---|---|---|
| ID トークン | sub, name, email, preferred_username |
シングルサインオン (SSO) 時にユーザー情報を取得 |
| アクセストークン | scope, client_id, realm_access, resource_access |
API 呼び出し時の認可情報(どのロールが付与されているか) |
まとめ
カスタムクレームは「オプショナル」領域に位置づけられ、Keycloak の UI または REST API で マッパー を作成し、必要に応じて Client Scope に紐付ける設計が必須です。
Keycloak 24.x でカスタムクレームを作成する手順
Keycloak 24 系は UI がモダン化され、左側メニューの「Protocol Mappers」からマッパー設定に直接アクセスできます。本節では UI 操作 と REST API の両方で「部署情報 (department) を dept クレームとしてトークンに付与する** 具体例を示します。
ユーザー属性を用いたマッピング – UI 操作
まずは Keycloak コンソール上で手動設定する流れです。以下の手順は 初回だけでも実行すれば、以降は UI が不要になる ことを意識してください。
- Realm → Users に移動し、対象ユーザー(例:
john.doe)を開く。 Attributesタブで新規属性department = Salesを追加し、Save ボタンで確定する。
ヒント:属性名は任意ですが、後述のマッパー設定と一致させる必要があります。
- Realm → Clients → myclient → Mappers に遷移し、Create mapper をクリック。
- 「Mapper type」から User Attribute を選択し、以下の項目を入力する(テーブルは UI での見た目を再現)。
| 項目 | 設定例 |
|---|---|
| Name | department-claim |
| User Attribute | department |
| Token Claim Name | dept |
| Claim JSON Type | String |
| Add to ID token | ✅ |
| Add to Access token | ✅ |
| Add to userinfo | ✅ |
- Save を押してマッパーを保存したら、Clients → myclient → Client Scopes に移動し、作成したスコープ(後述)を割り当てる。
導入文
UI で設定すると即座に結果が確認でき、属性名とクレーム名の違いも視覚的に把握できます。
REST API でプロトコルマッパーを自動登録
CI/CD パイプラインやインフラコード化(IaC)を行う場合は、REST API が便利です。以下は 具体的なプレースホルダー置換例 を含む完全スクリプトです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# 1. 管理者アクセストークン取得 (realm = master, client-id = admin-cli) ADMIN_TOKEN=$(curl -s -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \ -d "grant_type=password" \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=admin_password" | jq -r .access_token) # 2. マッパー作成対象の realm と client-id を具体的に指定 REALM="myrealm" CLIENT_ID="myclient" # 3. プロトコルマッパー作成リクエスト curl -s -X POST "http://localhost:8080/admin/realms/${REALM}/clients/${CLIENT_ID}/protocol-mappers/models" \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "name": "department-claim", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "config": { "user.attribute": "department", "claim.name": "dept", "jsonType.label": "String", "id.token.claim": "true", "access.token.claim": "true", "userinfo.token.claim": "true" } }' | jq . |
REALMとCLIENT_IDには 実際に使用する Realm 名(例:myrealm)と クライアント ID(例:myclient)を入力してください。- 上記スクリプトは jq がインストールされている環境前提です。出力が JSON 形式で整形され、作成結果の確認が容易になります。
導入文
REST API による自動化は設定ミスを防ぎ、同一構成を複数環境(開発・ステージング・本番)に再現できる点が最大の利点です。
ベストプラクティス:プロトコルマッパーとクライアントスコープの活用
カスタムクレームを 複数クライアントで共通利用 したい場合、個別にマッパーを配置すると管理負荷が急増します。ここでは Client Scope にマッパーを集約し、再利用性と保守性を高める手順をご紹介します。
マッパーとスコープの役割比較
| 項目 | プロトコルマッパー | Client Scope |
|---|---|---|
| 目的 | トークンに付与する単一クレームを定義 | 複数クライアントで共通利用できるクレーム集合・スコープ情報を管理 |
| 設定場所 | 各クライアント → Mappers | Realm → Client Scopes |
| 再利用性 | 低(クライアントごとに重複) | 高(スコープ割り当てだけで全クライアントに反映) |
| 運用負荷 | 設定変更時にすべてのクライアントを更新 | スコープ側のみ更新すれば自動的に全体へ適用 |
ポイント
スコープは「名前空間」のような役割を果たし、マッパー集合を一括管理できるため、組織規模が大きくなるほど効果が顕著です。
共通 Client Scope の作成手順
- Realm → Client Scopes → Create で
custom-claimsスコープを作成(名前は任意)。 - 作成したスコープを開き、Mappers タブから先ほどの
department-claimを Add Built-in または Create mapper で追加。 -
Clients → myclient → Client Scopes に遷移し、
custom-claimsを次のいずれかに割り当てる。 -
Default Client Scopes:全リクエストに自動付与(スコープ指定不要)。
- Optional Client Scopes:リクエスト時に
scope=custom-claimsが必要。
導入文
スコープ化したマッパーは、後から新しいクライアントを追加する際も「スコープだけ割り当てれば完了」というシンプルさが得られます。
Spring Boot 3 / Spring Security 6 と Quarkus 3.x でのカスタムクレーム取得例
Keycloak が発行した JWT に dept クレームが含まれることを前提に、代表的な Java フレームワークでの取得・活用パターンを示します。実装コードは Spring Security 6 と Quarkus smallrye‑jwt の両方で動作確認済みです。
Spring Security での JwtDecoder カスタマイズ
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri( "http://localhost:8080/realms/myrealm/protocol/openid-connect/certs") .build(); // カスタムクレームのデフォルト値や変換ロジックをここに追加 decoder.setClaimSetConverter(claims -> { claims.putIfAbsent("dept", "UNKNOWN"); return claims; }); return decoder; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/public/**").permitAll() .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(customAuthenticationConverter()))); return http.build(); } private JwtAuthenticationConverter customAuthenticationConverter() { JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setPrincipalClaimName("preferred_username"); // dept クレームを ROLE_DEPT_XXX という権限に変換 converter.setJwtGrantedAuthoritiesConverter(jwt -> { Collection<GrantedAuthority> authorities = new ArrayList<>(); String dept = jwt.getClaimAsString("dept"); if (dept != null && !dept.isBlank()) { authorities.add(new SimpleGrantedAuthority( "ROLE_DEPT_" + dept.toUpperCase())); } return authorities; }); return converter; } } |
JwtDecoderの claimSetConverter でデフォルト値を設定すれば、クレームが欠落しているリクエストでも安全に処理できます。JwtAuthenticationConverterにロジックを埋め込むことで、@PreAuthorize("hasRole('DEPT_SALES')")のような式で 部署別権限 を簡単に判定可能です。
導入文
Spring Security ではデコード・認可ロジックをコンポーネント化できるため、カスタムクレームの取り扱いが非常に柔軟です。
Quarkus smallrye‑jwt の設定と CDI ビーン例
application.properties
|
1 2 3 4 5 6 7 8 |
quarkus.oidc.auth-server-url=http://localhost:8080/realms/myrealm quarkus.oidc.client-id=myclient quarkus.smallrye-jwt.enabled=true mp.jwt.verify.publickey.location=classpath:META-INF/resources/public-key.pem # principal として使用したいクレーム(ユーザー名) quarkus.security.principal-claim=preferred_username |
DeptClaimProducer.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import jakarta.enterprise.context.RequestScoped; import org.eclipse.microprofile.jwt.JsonWebToken; @RequestScoped public class DeptClaimProducer { private final JsonWebToken jwt; public DeptClaimProducer(JsonWebToken jwt) { this.jwt = jwt; } /** 部署情報 (dept クレーム) を取得 */ public String getDept() { return jwt.getClaim("dept"); } } |
JsonWebTokenのインジェクションにより、全クレームへシンプルにアクセスできます。- 上記 CDI ビーンは REST エンドポイントやサービス層で
@Inject DeptClaimProducer dept;とすれば、そのままdept.getDept()が呼び出せます。
導入文
Quarkus は smallrye‑jwt がデフォルトで有効化されており、JWT の検証とクレーム取得が数行のコードで完結します。
Docker Compose でローカル Keycloak 環境を構築しテスト・トラブルシューティング
実務での動作確認は Docker Compose が最も手軽です。本節では永続化設定やポート競合への配慮、さらに典型的なトラブルと対処法を含めた完全ガイドを示します。
docker‑compose.yml の推奨構成(永続化・ポート注意)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
version: "3.8" services: keycloak: image: quay.io/keycloak/keycloak:24.0.4 command: start-dev --import-realm environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin_password KC_DB: dev-file # 開発向けのファイルベース DB(永続化が可能) ports: - "8080:8080" # ホスト側で 8080 が使用中の場合は "8181:8080" 等に変更してください volumes: - ./realm-export.json:/opt/keycloak/data/import/realm-export.json # 初回インポート用 - kc-data:/opt/keycloak/data # DB ファイル永続化 volumes: kc-data: |
重要ポイント
| 項目 | 説明 |
|---|---|
KC_DB: dev-file |
Keycloak が内部的に使用する H2/SQLite 相当のファイルデータベースです。コンテナ再起動後もデータが残ります。 |
volumes.kc-data |
永続化ボリュームを明示的に作成し、/opt/keycloak/data にマウントすることでユーザー・クライアント設定が失われません。 |
| ポート競合対策 | ホスト側で 8080 が既に占有されている場合は ports 行を "8181:8080" など別ポートへ変更してください。コンテナ内部のポートは変わりません。 |
導入文
永続化とポート競合への配慮だけで、開発環境の安定稼働が格段に楽になります。
起動手順
|
1 2 3 4 5 6 7 |
# リポジトリをクローン(例は GitHub の公開サンプル) git clone https://github.com/your-org/keycloak-oidc-custom-claim-demo.git cd keycloak-oidc-custom-claim-demo # Docker Compose でバックグラウンド起動 docker compose up -d |
コンテナが正常に立ち上がったら、http://localhost:8080(または設定したポート)へアクセスし、管理者 admin/admin_password でログインしてください。realm-export.json が正しくマウントされていれば、ユーザー属性・マッパー・スコープが自動的にロードされています。
JWT の取得と内容確認(curl + jq)
- アクセストークン取得(Resource Owner Password Credentials)
|
1 2 3 4 5 6 |
TOKEN=$(curl -s -X POST "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -d "grant_type=password" \ -d "client_id=myclient" \ -d "username=john.doe" \ -d "password=secret" | jq -r .access_token) |
- トークンのデコード(ローカルで確認)
|
1 2 3 |
# Base64 デコードして pretty‑print echo $TOKEN | cut -d '.' -f2 | base64 -d | jq . |
出力例:
|
1 2 3 4 5 6 7 8 |
{ "sub": "c5b8e4a7-...", "dept": "Sales", "preferred_username": "john.doe", "exp": 1728225600, ... } |
dept クレームが期待通り "Sales" と表示されていれば成功です。
よくある課題と対処法
| 症状 | 主な原因 | 推奨解決策 |
|---|---|---|
| カスタムクレームがトークンに現れない | マッパーを Client 配下に作成したが、対象スコープ未割当 | Client Scope → custom-claims を Default または Optional に設定し再取得 |
| 変更後も古いトークンが返る | Keycloak が内部キャッシュ(token‑store)を保持している | コンテナを docker compose restart keycloak、またはコンソール → Realm Settings → Tokens → Revoke Refresh Token を実行 |
| 起動時にポート競合エラー | ホスト側で 8080 が他サービスに使用中 | ports 行を "8181:8080" 等別ポートへ変更し、URL だけ置き換える |
| データがリセットされる | ボリューム未指定または docker compose down -v 実行 |
volumes.kc-data を必ず定義し、削除時は down のみ実行(-v は付けない) |
導入文
ここに挙げたチェックリストを踏むだけで、ほとんどの設定ミスや環境トラブルは即座に解決できます。
まとめ
| 項目 | 要点 |
|---|---|
| OIDC の基礎 | 必須クレームは常に付与され、カスタム属性はオプショナルとしてマッパー/スコープで明示的に追加する必要がある。 |
| Keycloak 24.x の設定 | UI と REST API 両方で department → dept クレームを作成し、Client Scope にまとめて再利用性を確保。 |
| ベストプラクティス | プロトコルマッパーはスコープ単位で管理し、クライアント増減時の設定変更を最小化する。 |
| Spring / Quarkus 側実装 | JwtDecoder や smallrye‑jwt にカスタムロジックを組み込むだけで、トークン内 dept クレームから権限情報やビジネスロジックに活用できる。 |
| Docker Compose | 永続化ボリュームとポート競合対策を施した docker‑compose.yml でローカル環境を即起動、curl と jwt.io でトークン内容を検証し、典型的な不具合はキャッシュやスコープ設定の見直しで解決できる。 |
これらの手順と注意点を実装・運用に取り込めば、Keycloak 24.x の OIDC カスタムクレーム を安全かつ効率的に導入でき、Spring Boot や Quarkus ベースのマイクロサービスでもシームレスに利用可能です。ぜひ本ガイドをプロジェクトのリファレンスとして活用し、認証基盤の柔軟性と拡張性を高めてください。