概要
Keycloak + OpenFGA + Kongで、認証と認可の責務を外部サービスに分離した構成について解説します。各コンポーネントが単一の責務を持ち、OIDC・OAuth 2.0 に基づいて連携します。アプリケーションコードに認証・認可ロジックを埋め込まないため、変更・監査・スケールを容易にします。
認可チェックの責務分担: Kong が JWKS 署名検証でトークンの真正性を確認(粗粒度チェック)し、バックエンドが OpenFGA の Check API でリソース単位のアクセス可否を判定(細粒度チェック)します。
| 要素 | 説明 |
|---|---|
| ユーザー | ブラウザからアプリケーションを操作するエンドユーザー |
| React SPA | PKCE 付き認可コードフローで Keycloak と連携し、Access Token を取得して API を呼び出すフロントエンド |
| Kong Gateway | OIDC プラグインで JWT 署名検証を行うポリシー実行点(Token 検証のみ担当) |
| Keycloak | OIDC 準拠の Identity Provider。ユーザー認証・SSO・Token 発行を担う |
| OpenFGA | Zanzibar ベース ReBAC による細粒度認可の判定点。ユーザーとリソースの関係に基づいてアクセス可否を決定する |
| Backend API | Kong を通過した検証済みリクエストのみを受け取るサービス群 |
認証と認可の責務分離
| 責務 | 担当コンポーネント | 問い |
|---|---|---|
| 認証 | Keycloak | このユーザーは誰か |
| Token 検証 | Kong | この Token は有効か |
| 認可 | OpenFGA | このユーザーはこのリソースに何ができるか |
| アプリ操作 | React SPA | ユーザーがどの操作を実行したか |
| ビジネスロジック | Backend | リクエストに対して何を返すか |
特徴
- オープン標準準拠: OIDC、OAuth 2.0、PKCE に完全準拠し、ベンダーロックインを回避できます
- 認証・認可の完全分離: 認証は Keycloak、認可は OpenFGA が専任で担うため、それぞれを独立して変更できます
- 細粒度アクセス制御: OpenFGA の ReBAC モデルにより、ロールだけでなくリソース間の関係でアクセスを制御できます
- ポリシー実行の一元化: Kong がゲートウェイレベルで Token 検証と認可チェックを実行するため、バックエンドに認証ロジックが不要になります
- SPA セキュリティ: React SPA は PKCE 付き認可コードフローを使用し、クライアントシークレットなしで安全に Token を取得できます
- OSS・自己ホスト: すべてのコンポーネントがオープンソースであり、データをオンプレミスまたは自社クラウドに保持できます
- イベント駆動の権限同期: keycloak-openfga-event-publisher を導入することで、Keycloak のロール変更イベントを OpenFGA に自動同期し、権限の一貫性を維持できます
他の構成との比較
| 比較項目 | Keycloak + OpenFGA + Kong | Auth0 + API Gateway | Okta + Envoy | 自前 JWT 検証 | Firebase Auth + Cloud Endpoints |
|---|---|---|---|---|---|
| 認証方式 | OIDC / OAuth 2.0(自己ホスト) | OIDC / OAuth 2.0(SaaS) | OIDC / OAuth 2.0(SaaS) | JWT(独自実装) | Firebase 独自 + OIDC |
| 認可モデル | ReBAC(Zanzibar)+ RBAC/ABAC | RBAC | RBAC + ABAC(OPA 連携) | アプリ内ロジック | Firebase Rules |
| カスタマイズ性 | 高(OSS・完全制御) | 中(SaaS 制約あり) | 中(SaaS 制約あり) | 高(全自前実装) | 低(Firebase 依存) |
| コスト | 低(OSS・インフラ費のみ) | 高(ユーザー数課金) | 高(ユーザー数課金) | 低(実装コストは高) | 中(使用量課金) |
| スケーラビリティ | 高(水平スケール可) | 高(SaaS 管理) | 高(SaaS 管理) | 低(自前管理) | 中(Firebase 制約) |
この構成を選ぶべきケース
この構成は以下の要件が重なる場合に特に効果を発揮します。
- データ主権の要件がある: SaaS に認証データを預けられない規制業種(金融・医療・公共)で、全コンポーネントを自社インフラに配置できます
- 細粒度認可が必要: 「組織 A のメンバーがプロジェクト B のドキュメント C を編集できる」のようなリソース単位の認可を、アプリケーションコードから分離して管理したい場合に OpenFGA の ReBAC モデルが有効です
- マルチサービス構成でゲートウェイ統制が必要: 複数のバックエンドサービスに対して Kong で一元的にトークン検証・レート制限・CORS を適用し、個々のサービスから認証ロジックを排除できます
一方、以下の場合はオーバースペックになる可能性があります。
- 認可が「管理者/一般ユーザー」程度の粗粒度で済む → Keycloak のロールのみで十分
- チームに IdP・API Gateway の運用経験がない → Auth0 + Vercel のようなマネージド構成のほうが立ち上がりが早い
- バックエンドが1〜2サービスのみ → Kong を挟む利点が薄い
構造
システムコンテキスト図
| 要素名 | 説明 |
|---|---|
| エンドユーザー | Web ブラウザ経由でシステムを利用するアクター |
| 管理者 | 認可ポリシーとユーザーを管理するアクター |
| 認証・認可システム | 調査対象の全体システム |
| 外部 IdP | SAML や OIDC で連携する外部 ID プロバイダー |
| 外部ディレクトリ | LDAP や Active Directory などの外部ユーザーストア |
コンテナ図
| 要素名 | 説明 |
|---|---|
| React SPA | ブラウザで動作するシングルページアプリケーション |
| Kong Gateway | API ゲートウェイ。リクエストのルーティングとトークン検証を担う |
| Keycloak | OIDC 準拠の ID プロバイダー。認証と JWT 発行を担う |
| OpenFGA | Zanzibar ベースの ReBAC エンジン。リソースへのアクセス可否を判定する |
| Backend API | ビジネスロジックを提供する API サーバー |
コンポーネント図
React SPA
| 要素名 | 説明 |
|---|---|
| Auth Provider | Keycloak との OIDC セッションを管理し、アクセストークンを保持する |
| Protected Routes | 認証済みユーザーのみアクセスを許可するルートガード |
| API Client | Kong Gateway にアクセストークン付きリクエストを送信する |
Kong Gateway
| 要素名 | 説明 |
|---|---|
| OIDC Plugin | Keycloak の JWKS エンドポイントを使ってアクセストークンを検証する |
| Rate Limiting | リクエスト数を制限し、過負荷を防止する |
| CORS | クロスオリジンリクエストのヘッダー制御を行う |
| Routing | 検証済みリクエストを Backend API へ転送する |
Keycloak
| 要素名 | 説明 |
|---|---|
| Realm | ユーザー、ロール、クライアントをまとめる独立した管理スペース |
| Client | React SPA や Kong Gateway が Keycloak に登録された認証クライアント |
| User Federation | LDAP や Active Directory と連携してユーザー情報を取得する |
| OpenFGA Event Publisher | 認証イベントを検知して OpenFGA にタプルを書き込むイベントリスナー |
OpenFGA
| 要素名 | 説明 |
|---|---|
| Store | 認可モデルと関係タプルをまとめるトップレベルのコンテナ |
| Authorization Model | オブジェクト型と関係のスキーマを定義する不変オブジェクト |
| Tuple Space | ユーザー・関係・オブジェクトの三要素からなる具体的な関係データの集合 |
Backend API
| 要素名 | 説明 |
|---|---|
| Auth Middleware | Kong が転送した X-Userinfo ヘッダーからユーザー情報を抽出する(トークン検証自体は Kong が実施済み) |
| OpenFGA Client | OpenFGA の Check API を呼び出してリソースへのアクセス可否を確認する |
| Business Logic | 認可済みリクエストに対してビジネス処理を実行する |
ネットワーク構成図
| 通信経路 | 説明 |
|---|---|
| ブラウザ → React SPA | ユーザーが SPA を初期ロードする |
| React SPA → Keycloak | OIDC Authorization Code Flow でログインし、アクセストークンを取得する |
| React SPA → Kong Gateway | アクセストークンを Authorization ヘッダーに付与して API を呼び出す |
| Kong Gateway → Keycloak | JWKS エンドポイントから公開鍵を取得・キャッシュし、JWT の署名をローカルで検証する |
| Kong Gateway → Backend API | 検証済みリクエストをバックエンドへ転送する |
| Backend API → OpenFGA | Check API でリソースへのアクセス可否を問い合わせる |
| Keycloak → OpenFGA | 認証イベントをトリガーとして関係タプルを書き込む |
データ
概念モデル
Keycloak
| 要素名 | 説明 |
|---|---|
| Realm | ユーザー・クライアント・ロール・グループを管理する独立した名前空間 |
| Client | 認証を依頼するアプリケーションまたはサービス |
| User | 認証対象のエンドユーザー |
| Session | ユーザーログイン時に生成される認証セッション |
| Role | ユーザーの種別や権限カテゴリを表すラベル |
| Group | ロールや属性をまとめて管理するユーザー集合 |
| ID Token | ユーザー識別情報を含む JWT |
| Access Token | API アクセス権限を表す JWT |
| Refresh Token | Access Token を更新するための認証情報 |
OIDC フロー
| 要素名 | 説明 |
|---|---|
| Authorization Code | トークン交換に使う一時的な認可コード |
| PKCE Challenge | Authorization Code の傍受攻撃を防ぐコード検証値 |
OpenFGA
| 要素名 | 説明 |
|---|---|
| Store | 認可モデルと Tuple を格納するトップレベルコンテナ |
| Authorization Model | 型定義と関係からなる権限スキーマ |
| Type Definition | オブジェクト型に対して定義可能な関係の集合 |
| Relation | オブジェクトとユーザー間の関係を表す名前付き定義 |
| Tuple | ユーザー・関係・オブジェクトの三つ組で表す実際の関係データ |
Kong
| 要素名 | 説明 |
|---|---|
| Upstream | 複数の Service インスタンスをまとめるロードバランサー定義 |
| Service | バックエンドの上流サービスを表すエンティティ |
| Route | リクエストを Service にマッピングするルーティングルール |
| Plugin | Service・Route・Consumer に適用する機能拡張モジュール |
| Consumer | Kong が管理する API を利用する外部クライアント識別子 |
情報モデル
Keycloak エンティティ
| 要素名 | 説明 |
|---|---|
| Realm | レルム全体の設定とセッション有効期間を管理する |
| Client | SPA 向けに publicClient を true に設定し、クライアントシークレットなしで動作する |
| User | メールアドレス・氏名・有効状態を持つ認証対象エンティティ |
| Role | 単純ロールと複合ロールを区別する composite フラグを持つ |
| Group | パス形式の階層構造と任意属性マップを持つ |
| Session | ユーザーのログイン開始時刻・最終アクセス時刻・IP アドレスを記録する |
| IDToken | ユーザー識別情報と認証時刻を含む JWT |
| AccessToken | スコープとロール情報を含み、API 認可に使用する JWT |
| RefreshToken | 有効期限切れの Access Token を更新するための認証情報 |
OIDC フローエンティティ
| 要素名 | 説明 |
|---|---|
| AuthorizationCode | 短命の認可コードで、トークンエンドポイントで Token に交換する |
| PKCEChallenge | code_challenge とチャレンジ方式を保持する。code_verifier はクライアント側のメモリにのみ存在する一時値で、サーバーには送信されない |
OpenFGA エンティティ
| 要素名 | 説明 |
|---|---|
| Store | 認可データの最上位コンテナで、ストア間でデータを共有しない |
| AuthorizationModel | スキーマバージョンと型定義で構成される権限スキーマ |
| TypeDefinition | オブジェクト型名と、その型に定義された関係の集合 |
| Relation | 関係名と計算方法(直接・委譲・セット演算)を持つ定義 |
| Tuple | ユーザー・関係・オブジェクトの三つ組と任意の条件式を持つ実データ |
Kong エンティティ
| 要素名 | 説明 |
|---|---|
| Service | バックエンドのホスト・ポート・プロトコルを定義する上流サービス表現 |
| Route | プロトコル・パス・メソッド・ホストでリクエストを Service にマッチングする |
| Plugin | 有効化状態・設定マップ・優先度を持つ機能拡張モジュール |
| Consumer | username と customId で外部クライアントを識別するエンティティ |
| Upstream | ロードバランシングアルゴリズムと複数 Service インスタンスを管理する |
構築方法
Docker Compose による環境構築
docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- auth-net
keycloak:
image: quay.io/keycloak/keycloak:26.1.0
command: start-dev # 開発用。本番では start を使用し、hostname-strict 等を設定する
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/postgres
KC_DB_USERNAME: postgres
KC_DB_PASSWORD: password
KC_HTTP_PORT: 8080
KC_SPI_EVENTS_LISTENER_OPENFGA_EVENTS_PUBLISHER_API_URL: http://openfga:8080
KC_SPI_EVENTS_LISTENER_OPENFGA_EVENTS_PUBLISHER_STORE_ID: ${FGA_STORE_ID}
KC_SPI_EVENTS_LISTENER_OPENFGA_EVENTS_PUBLISHER_AUTHORIZATION_MODEL_ID: ${FGA_MODEL_ID}
volumes:
- ./keycloak/providers:/opt/keycloak/providers
ports:
- "8080:8080"
depends_on:
- postgres
networks:
- auth-net
openfga:
image: openfga/openfga:v1.8
command: run
environment:
OPENFGA_DATASTORE_ENGINE: postgres
OPENFGA_DATASTORE_URI: postgres://postgres:password@postgres:5432/postgres?sslmode=disable
OPENFGA_LOG_FORMAT: json
ports:
- "8081:8080"
- "8082:8081"
- "3000:3000"
depends_on:
- postgres
networks:
- auth-net
kong:
image: kong/kong-gateway:3.9
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /kong/declarative/kong.yml
KONG_PROXY_LISTEN: "0.0.0.0:9000"
KONG_ADMIN_LISTEN: "0.0.0.0:9001"
KONG_LOG_LEVEL: info
volumes:
- ./kong:/kong/declarative
ports:
- "9000:9000"
- "9001:9001"
networks:
- auth-net
backend:
image: your-backend:latest
environment:
FGA_API_URL: http://openfga:8080
FGA_STORE_ID: ${FGA_STORE_ID}
FGA_MODEL_ID: ${FGA_MODEL_ID}
ports:
- "8090:8090"
networks:
- auth-net
networks:
auth-net:
volumes:
postgres_data:
環境変数一覧
| サービス | 変数名 | 説明 |
|---|---|---|
| Keycloak | KC_BOOTSTRAP_ADMIN_USERNAME |
初回管理者ユーザー名 |
| Keycloak | KC_BOOTSTRAP_ADMIN_PASSWORD |
初回管理者パスワード |
| Keycloak | KC_DB |
データベース種別 |
| Keycloak | KC_DB_URL |
PostgreSQL 接続 URL |
| Keycloak | KC_SPI_EVENTS_LISTENER_OPENFGA_EVENTS_PUBLISHER_API_URL |
OpenFGA サーバー URL |
| Keycloak | KC_SPI_EVENTS_LISTENER_OPENFGA_EVENTS_PUBLISHER_STORE_ID |
OpenFGA Store ID |
| OpenFGA | OPENFGA_DATASTORE_ENGINE |
データストア種別 |
| OpenFGA | OPENFGA_DATASTORE_URI |
PostgreSQL 接続文字列 |
| Kong | KONG_DATABASE |
DB モード(off で DB-less) |
| Kong | KONG_DECLARATIVE_CONFIG |
宣言的設定ファイルパス |
起動コマンド
# OpenFGA のデータベースマイグレーション
docker compose up -d postgres
docker compose run --rm openfga migrate
# 全サービス起動
docker compose up -d
Keycloak セットアップ
Realm 作成
# Admin CLI でログイン
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
config credentials \
--server http://localhost:8080 \
--realm master \
--user admin \
--password admin
# Realm 作成
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
create realms \
-s realm=myrealm \
-s enabled=true
SPA 用 Client 設定
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
create clients \
-r myrealm \
-s clientId=my-spa \
-s publicClient=true \
-s "redirectUris=[\"http://localhost:3000/*\"]" \
-s "webOrigins=[\"http://localhost:3000\"]" \
-s protocol=openid-connect \
-s attributes='{"pkce.code.challenge.method":"S256"}'
| パラメータ | 値 | 説明 |
|---|---|---|
clientId |
my-spa |
クライアント識別子 |
publicClient |
true |
クライアントシークレット不要 |
redirectUris |
http://localhost:3000/* |
認可コード返却先 URI |
webOrigins |
http://localhost:3000 |
CORS 許可オリジン |
pkce.code.challenge.method |
S256 |
PKCE チャレンジ方式 |
ロール・ユーザー作成
# ロール作成
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
create roles -r myrealm -s name=app-user
# ユーザー作成
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
create users -r myrealm -s username=alice -s enabled=true
# パスワード設定
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
set-password -r myrealm --username alice --new-password password
# ロール割り当て
docker exec -it <keycloak-container> /opt/keycloak/bin/kcadm.sh \
add-roles -r myrealm --uusername alice --rolename app-user
OpenFGA Event Publisher Extension のインストール
# JAR をダウンロード
curl -L -o keycloak/providers/keycloak-openfga-event-publisher.jar \
https://github.com/embesozzi/keycloak-openfga-event-publisher/releases/latest/download/keycloak-openfga-event-publisher.jar
# Keycloak を再起動して拡張機能を読み込む
docker compose restart keycloak
管理コンソールでの設定手順:
Realm settings>Events>Event listenersへ移動するopenfga-events-publisherを追加するSaveをクリックする
Kong セットアップ
Service と Route の作成
# Service 作成
curl -X POST http://localhost:9001/services \
-d name=backend-service \
-d url=http://backend:8090
# Route 作成
curl -X POST http://localhost:9001/services/backend-service/routes \
-d name=backend-route \
-d "paths[]=/api"
OpenID Connect プラグインの設定
curl -X POST http://localhost:9001/services/backend-service/plugins \
-H "Content-Type: application/json" \
-d '{
"name": "openid-connect",
"config": {
"issuer": "http://keycloak:8080/realms/myrealm",
"client_id": "kong-client",
"client_secret": "kong-client-secret",
"auth_methods": ["bearer"],
"bearer_token_param_type": ["header"],
"token_endpoint_auth_method": "client_secret_post",
"scopes": ["openid", "profile", "email"],
"verify_signature": true,
"verify_expiry": true,
"upstream_access_token_header": "Authorization",
"upstream_user_info_header": "X-Userinfo"
}
}'
| パラメータ | 値 | 説明 |
|---|---|---|
issuer |
Keycloak Realm の URL | OIDC Discovery エンドポイントのベース URL |
client_id |
kong-client |
Kong の Keycloak クライアント ID |
auth_methods |
["bearer"] |
Bearer トークン検証のみ有効化 |
token_endpoint_auth_method |
client_secret_post |
クライアント認証方式 |
scopes |
["openid","profile","email"] |
要求するスコープ |
upstream_access_token_header |
Authorization |
バックエンドへのトークン転送ヘッダー |
CORS プラグインの設定
curl -X POST http://localhost:9001/services/backend-service/plugins \
-H "Content-Type: application/json" \
-d '{
"name": "cors",
"config": {
"origins": ["http://localhost:3000"],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"headers": ["Content-Type", "Authorization"],
"exposed_headers": ["X-Auth-Token"],
"credentials": true,
"max_age": 3600
}
}'
OpenFGA セットアップ
Store 作成
# FGA CLI のインストール
brew install openfga/tap/fga
# Store 作成
fga store create --name "my-app-store"
# 環境変数を設定する
export FGA_API_URL=http://localhost:8081
export FGA_STORE_ID=<取得した store.id>
Authorization Model の定義と書き込み
cat > model.fga << 'EOF'
model
schema 1.1
type user
type role
relations
define member: [user]
type document
relations
define owner: [user]
define editor: [user, role#member]
define viewer: [user, role#member]
define can_write: editor or owner
define can_view: viewer or editor or owner
EOF
fga model write --store-id=$FGA_STORE_ID --file model.fga
初期 Tuple の投入
# ユーザーをロールメンバーとして追加する
fga tuple write --store-id=$FGA_STORE_ID user:alice member role:app-user
# ロールにドキュメント閲覧権限を付与する
fga tuple write --store-id=$FGA_STORE_ID role:app-user#member viewer document:readme
# ファイルで一括投入する
fga tuple write --store-id=$FGA_STORE_ID --file tuples.json
tuples.json の形式:
{
"writes": [
{ "user": "user:alice", "relation": "owner", "object": "document:readme" },
{ "user": "user:bob", "relation": "viewer", "object": "document:readme" }
]
}
React SPA セットアップ
oidc-spa ライブラリの導入
npm install oidc-spa
AuthProvider の設定
// src/oidc.ts
import { createOidc } from "oidc-spa/react";
export const { OidcProvider, useOidc, getOidc } = createOidc({
issuerUri: "http://localhost:8080/realms/myrealm",
clientId: "my-spa",
scopes: ["openid", "profile", "email"],
// PKCE は自動的に S256 で設定される
});
// src/main.tsx
import { OidcProvider } from "./oidc";
ReactDOM.createRoot(document.getElementById("root")!).render(
<OidcProvider fallback={<div>Loading...</div>}>
<App />
</OidcProvider>
);
Protected Route の実装
// src/components/ProtectedRoute.tsx
import { useOidc } from "../oidc";
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isUserLoggedIn, login } = useOidc();
if (!isUserLoggedIn) {
login({ doesCurrentHrefRequiresAuth: true });
return null;
}
return <>{children}</>;
}
// src/App.tsx
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
</Routes>
);
}
API Client へのトークン自動付与
// src/api/client.ts
import axios from "axios";
import { getOidc } from "../oidc";
const apiClient = axios.create({
baseURL: "http://localhost:9000/api",
});
apiClient.interceptors.request.use(async (config) => {
const oidc = await getOidc();
if (oidc.isUserLoggedIn) {
const { accessToken } = oidc.getTokens();
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
export default apiClient;
useAuth カスタムフック
oidc-spa の useOidc をラップし、ロールチェック機能を含むカスタムフックを実装します。
// src/hooks/useAuth.ts
import { useOidc } from "../oidc";
import { decodeJwt } from "oidc-spa/decode-jwt";
type KeycloakAccessTokenPayload = {
realm_access?: { roles: string[] };
resource_access?: Record<string, { roles: string[] }>;
sub: string;
};
type UseAuthReturn =
| {
isAuthenticated: false;
user: null;
hasRole: (_roleName: string) => false;
login: () => void;
}
| {
isAuthenticated: true;
user: { username: string; email: string; sub: string };
hasRole: (roleName: string) => boolean;
logout: (params?: { redirectTo?: string }) => void;
};
export function useAuth(): UseAuthReturn {
const oidc = useOidc();
if (!oidc.isUserLoggedIn) {
return {
isAuthenticated: false,
user: null,
hasRole: () => false,
login: () => oidc.login({ doesCurrentHrefRequiresAuth: true }),
};
}
const { decodedIdToken, oidcTokens, logout } = oidc;
const decoded = decodeJwt<KeycloakAccessTokenPayload>(
oidcTokens.accessToken
);
const realmRoles = decoded.realm_access?.roles ?? [];
return {
isAuthenticated: true,
user: {
username: decodedIdToken.preferred_username,
email: decodedIdToken.email,
sub: decodedIdToken.sub,
},
hasRole: (roleName: string) => realmRoles.includes(roleName),
logout,
};
}
decodeJwt は署名検証を行わないため、クライアント側の表示制御のみに使用します。認可の最終判断はバックエンドの OpenFGA で行います。
ロールベースのコンポーネントガード
// src/components/RequireRole.tsx
import { type ReactNode } from "react";
import { useAuth } from "../hooks/useAuth";
import { Navigate } from "react-router-dom";
export function RequireRole({
role,
children,
fallback = <Navigate to="/unauthorized" replace />,
}: {
role: string;
children: ReactNode;
fallback?: ReactNode;
}): ReactNode {
const auth = useAuth();
if (!auth.isAuthenticated) {
auth.login();
return null;
}
if (!auth.hasRole(role)) {
return fallback;
}
return children;
}
// ルーティングでの利用例
<Routes>
<Route path="/dashboard" element={
<RequireRole role="user"><DashboardPage /></RequireRole>
} />
<Route path="/admin" element={
<RequireRole role="admin" fallback={<Navigate to="/dashboard" replace />}>
<AdminPage />
</RequireRole>
} />
</Routes>
サイレントリフレッシュとタブ間同期
oidc-spa はオンデマンド方式でトークンをリフレッシュします。
getTokens()/oidcTokensアクセス時にトークン有効期限を確認する- 有効期限が近い場合、Refresh Token で自動的に再取得する
- Refresh Token も期限切れの場合、ログインページにリダイレクトする
- ユーザーの実際の操作を監視し、真の非アクティブ状態でのみログアウトする
複数タブ間の競合防止:
Navigator.locksAPI によるミューテックスでリフレッシュをシリアライズするBroadcastChannelAPI でリフレッシュ成功後の新トークンを全タブに通知する- oidc-spa がこれらの処理を内部で自動的に実行するため、開発者が実装する必要はない
利用方法
OIDC 認証フロー
Authorization Code Flow with PKCE
| 要素名 | 説明 |
|---|---|
| React SPA | PKCE の code_verifier と code_challenge を生成するクライアント |
| Kong Gateway | Bearer トークンを検証し、バックエンドにリクエストを転送するゲートウェイ |
| Keycloak | 認可コードとトークンを発行する Identity Provider |
| authorization_code | Keycloak が発行する一時的な認可コード |
| access_token | API アクセスに使用する JWT トークン |
| code_verifier | PKCE のランダム文字列(クライアント内で保持) |
| code_challenge | code_verifier の SHA-256 ハッシュ値 |
トークンリフレッシュ
oidc-spa はトークンの有効期限を監視し、期限切れ前に自動リフレッシュを実行します。Refresh Token も期限切れの場合はログイン画面にリダイレクトします。
# リフレッシュトークンによるトークン更新(手動確認用)
curl -X POST http://localhost:8080/realms/myrealm/protocol/openid-connect/token \
-d grant_type=refresh_token \
-d client_id=my-spa \
-d refresh_token=<refresh_token>
ログアウト
| ログアウト種別 | 説明 |
|---|---|
| フロントチャネルログアウト | SPA からリダイレクトで Keycloak のログアウトエンドポイントを呼び出す |
| バックチャネルログアウト | Keycloak がバックエンドサーバーに直接ログアウト通知を送信する |
// フロントチャネルログアウト
const { logout } = useOidc();
await logout({ redirectTo: "http://localhost:3000" });
バックチャネルログアウトのシーケンス
バックエンドでの logout_token 検証実装例:
import express from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
const KEYCLOAK_ISSUER = "https://keycloak.example.com/realms/myrealm";
const JWKS = createRemoteJWKSet(
new URL(`${KEYCLOAK_ISSUER}/protocol/openid-connect/certs`)
);
app.post("/backchannel-logout", async (req, res) => {
const logoutToken: string = req.body.logout_token;
const { payload } = await jwtVerify(logoutToken, JWKS, {
issuer: KEYCLOAK_ISSUER,
});
const events = payload.events as Record<string, unknown>;
if (events?.["http://schemas.openid.net/event/backchannel-logout"] == null) {
return res.status(400).send("Invalid logout token");
}
const sid = payload.sid as string | undefined;
const sub = payload.sub as string | undefined;
if (sid != null) {
await sessionStore.deleteBySessionId(sid);
} else if (sub != null) {
await sessionStore.deleteAllByUserId(sub);
}
return res.status(200).send("OK");
});
| 観点 | フロントチャネル | バックチャネル |
|---|---|---|
| 通信経路 | ブラウザ経由 | サーバー間の直接通信 |
| ブラウザ依存 | あり | なし |
| 信頼性 | 低い(広告ブロッカーの影響あり) | 高い(HTTP POST で確実に届く) |
| 実装の複雑さ | 低い | 高い(バックエンドにエンドポイント実装が必要) |
OpenFGA 認可チェック
Backend から OpenFGA への Check API 呼び出し
curl -X POST http://localhost:8081/stores/$FGA_STORE_ID/check \
-H "Content-Type: application/json" \
-d '{
"authorization_model_id": "'$FGA_MODEL_ID'",
"tuple_key": {
"user": "user:alice",
"relation": "can_view",
"object": "document:readme"
}
}'
# レスポンス: {"allowed": true}
Node.js バックエンドでの実装例:
import { OpenFgaClient } from "@openfga/sdk";
const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
});
async function checkPermission(
userId: string,
relation: string,
objectId: string
): Promise<boolean> {
const response = await fgaClient.check({
tuple_key: {
user: `user:${userId}`,
relation,
object: objectId,
},
});
return response.allowed ?? false;
}
Tuple の追加・削除
# Tuple 追加
fga tuple write --store-id=$FGA_STORE_ID user:alice editor document:report
# Tuple 削除
fga tuple delete --store-id=$FGA_STORE_ID user:alice editor document:report
API を使用した追加・削除:
curl -X POST http://localhost:8081/stores/$FGA_STORE_ID/write \
-H "Content-Type: application/json" \
-d '{
"authorization_model_id": "'$FGA_MODEL_ID'",
"writes": {
"tuple_keys": [
{ "user": "user:alice", "relation": "editor", "object": "document:report" }
]
},
"deletes": {
"tuple_keys": [
{ "user": "user:bob", "relation": "viewer", "object": "document:report" }
]
}
}'
Authorization Model の更新
モデルは更新ではなく新規作成として扱われます。各モデルに一意の authorization_model_id が付与されます。古いモデル ID を参照するタプルは引き続き動作します。
# 新しいモデルを書き込む
fga model write --store-id=$FGA_STORE_ID --file model_v2.fga
# モデル一覧を確認する
fga model list --store-id=$FGA_STORE_ID
マルチテナント階層モデルの例
組織・フォルダー・ドキュメントの階層関係を含む実用的なモデル定義です。
cat > model_multitenant.fga << 'EOF'
model
schema 1.1
type user
type organization
relations
define member: [user]
define admin: [user]
define can_manage: admin
type folder
relations
define parent_folder: [folder]
define owner: [user, organization#member] or owner from parent_folder
define writer: [user, organization#member] or owner or writer from parent_folder
define viewer: [user, organization#member] or writer or viewer from parent_folder
type document
relations
define organization: [organization]
define parent_folder: [folder]
define owner: [user, organization#member]
define editor: [user, organization#member] or owner
define viewer: [user, organization#member] or editor or viewer from parent_folder
define can_view: viewer or viewer from parent_folder
define can_edit: editor or admin from organization
EOF
fga model write --store-id=$FGA_STORE_ID --file model_multitenant.fga
Keycloak Event Publisher の Tuple 書き込みフォーマット
| Keycloak イベント | user | relation | object |
|---|---|---|---|
| ROLE_MAPPING_CREATE | user:<keycloak-user-id> |
assignee |
role:<role-name> |
| ROLE_MAPPING_DELETE | user:<keycloak-user-id> |
assignee |
role:<role-name> |
| GROUP_MEMBERSHIP(追加) | user:<user-id> |
assignee |
group:<group-name> |
| ロール間継承 | role:<parent-role> |
parent |
role:<child-role> |
Conditional Relation(条件付き関係)
IP アドレスや時刻条件でアクセスを制限する例です。
cat > model_conditional.fga << 'EOF'
model
schema 1.1
type user
type document
relations
define viewer: [user, user with non_expired_grant]
condition non_expired_grant(
current_time: timestamp,
grant_time: timestamp,
grant_duration: duration
) {
current_time < grant_time + grant_duration
}
EOF
条件付き Check リクエスト:
curl -X POST http://localhost:8081/stores/$FGA_STORE_ID/check \
-H "Content-Type: application/json" \
-d '{
"authorization_model_id": "'$FGA_MODEL_ID'",
"tuple_key": {
"user": "user:anne",
"relation": "viewer",
"object": "document:budget-2026"
},
"context": {
"current_time": "2026-04-03T14:00:00Z"
}
}'
Kong 経由の API アクセス
トークン付きリクエストの流れ
Kong OpenID Connect プラグインが実行する処理:
- Keycloak の Discovery エンドポイントから公開鍵を取得してキャッシュする
- Bearer トークンの署名・有効期限・クレームを検証する
- 検証済みトークンを
Authorizationヘッダーに付与してバックエンドに転送する X-Userinfoヘッダーにユーザー情報を付与する
バックエンドでのヘッダー取得例:
app.get("/resource", async (req, res) => {
const userInfo = req.headers["x-userinfo"];
const user = JSON.parse(Buffer.from(userInfo as string, "base64").toString());
const userId = user.sub;
const allowed = await checkPermission(userId, "can_view", "document:readme");
if (!allowed) {
return res.status(403).json({ error: "Forbidden" });
}
res.json({ data: "protected content" });
});
運用
Keycloak 運用
Realm エクスポート・インポート
- ユーザー数が 50,000 件を超える場合はディレクトリ形式を使用します
- インポート前にすべての Keycloak ノードを停止します
# エクスポート(ファイル)
bin/kc.sh export --file realm-export.json --realm myrealm
# エクスポート(ディレクトリ、ユーザー分割)
bin/kc.sh export --dir /export --realm myrealm \
--users different_files --users-per-file 100
# インポート(既存 Realm を上書きしない)
bin/kc.sh import --dir /export --override false
# 起動時インポート(data/import ディレクトリを参照)
bin/kc.sh start --import-realm
ユーザー・ロール管理の自動化
# サービスアカウント(admin-cli の Service Accounts Enabled が必要)
TOKEN=$(curl -s -X POST \
"http://keycloak:8080/realms/myrealm/protocol/openid-connect/token" \
-d "client_id=admin-cli&grant_type=client_credentials&client_secret=SECRET" \
| jq -r .access_token)
# ユーザー作成
curl -s -X POST \
"http://keycloak:8080/admin/realms/myrealm/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"alice","enabled":true,"email":"alice@example.com"}'
セッション管理とタイムアウト設定
| 設定項目 | 推奨値 | 説明 |
|---|---|---|
| Access Token Lifespan | 5 分 | アクセストークン有効期限 |
| SSO Session Idle | 30 分 | アイドル状態でのセッション有効期限 |
| SSO Session Max | 8〜24 時間 | セッションの最大有効期限 |
| Refresh Token Lifespan | SSO Session と連動 | リフレッシュトークン有効期限 |
バージョンアップ手順
# 1. 現行バージョンでメタデータ生成
bin/kc.sh update-compatibility metadata --file=/tmp/compat.json
# 2. 新バージョンで互換性チェック
bin/kc.sh update-compatibility check --file=/tmp/compat.json
# 終了コード 0: ローリングアップデート可能
# 終了コード 3: 全ノード停止が必要
| 変更種別 | アップデート戦略 |
|---|---|
| パッチリリース(同一 minor) | ローリングアップデート(ダウンタイムなし) |
| マイナー・メジャーバージョン | Recreate(全ノード停止後に起動) |
| DB スキーマ変更を含む | Recreate 必須 |
Kong 運用
declarative config によるバージョン管理
decK(declarative configuration toolkit)で Kong 設定を Git 管理します。Admin API ではなく YAML ファイルを唯一の正解とします(GitOps)。
# Kong 設定をエクスポート
deck gateway dump --output-file kong.yml
# 差分確認(ドライラン)
deck gateway diff kong.yml
# 設定同期(変更を Kong に適用)
deck gateway sync kong.yml
# OpenAPI Spec から kong.yml を生成
deck file openapi2kong --spec openapi.yml --output-file kong.yml
ヘルスチェックとアップストリーム管理
# kong.yml のアップストリーム定義例
upstreams:
- name: backend-upstream
algorithm: round-robin
healthchecks:
active:
http_path: /health
interval: 10
healthy:
successes: 2
unhealthy:
http_failures: 3
interval: 5
passive:
healthy:
successes: 5
unhealthy:
http_failures: 5
targets:
- target: backend-1:8080
weight: 100
- target: backend-2:8080
weight: 100
OpenFGA 運用
Authorization Model のバージョン管理
Authorization Model は一度書き込むと変更・削除できません(イミュータブル)。新しいモデルを書き込むたびに新しい authorization_model_id が生成されます。
# モデル一覧取得(新しい順)
curl -X GET "http://openfga:8080/stores/{store_id}/authorization-models"
Tuple の一括管理
1 回のリクエストで最大 100 件の Tuple を書き込み・削除できます。書き込みと削除は同一リクエスト内で組み合わせ可能です。
パフォーマンスモニタリング
openfga run \
--metrics-enabled \
--datastore-metrics-enabled \
--trace-enabled \
--trace-sample-ratio 0.1
| メトリクス | 説明 |
|---|---|
openfga_request_duration_ms |
Check / ListObjects のレイテンシ |
openfga_datastore_query_count |
データストアクエリ数 |
openfga_request_total |
リクエスト総数(エラーレート算出用) |
トークンライフサイクル管理
リフレッシュトークンのローテーション
Revoke Refresh Token を有効化すると、使用済みリフレッシュトークンは即時失効します。ローテーションにより、リフレッシュトークンの盗用を検知できます。
トークン失効
curl -X POST \
"http://keycloak:8080/realms/myrealm/protocol/openid-connect/revoke" \
-d "client_id=spa-client" \
-d "token=<refresh_token>" \
-d "token_type_hint=refresh_token"
監視・ログ
ヘルスチェックエンドポイント
| コンポーネント | エンドポイント | プロトコル |
|---|---|---|
| Keycloak | /health/ready |
HTTP |
| Keycloak | /health/live |
HTTP |
| Kong | /status |
HTTP(Admin: 9001) |
| OpenFGA | /healthz |
HTTP |
| OpenFGA | grpc.health.v1.Health/Check |
gRPC |
重要なメトリクスとアラート
| メトリクス | コンポーネント | アラートしきい値 |
|---|---|---|
| 認証成功率 | Keycloak | エラー率 > 1% |
| Check レイテンシ P99 | OpenFGA | > 200ms |
| HTTP 5xx エラー率 | Kong | > 0.1% |
| ヒープ使用率 | Keycloak | > 80% |
集中ログ管理
| コンポーネント | ログ形式 | 主要フィールド |
|---|---|---|
| Keycloak | JSON(events) | type, realmId, userId, sessionId, ipAddress |
| Kong | JSON | request_id, status, latency, upstream_uri, consumer |
| OpenFGA | JSON(zap) | store_id, model_id, user, relation, object, allowed |
ベストプラクティス
セキュリティ
PKCE S256 必須化
SPA(publicClient)では PKCE を必須化することで認可コード横取り攻撃を防ぎます。
curl -X PUT \
"http://keycloak:8080/admin/realms/myrealm/clients/{client-uuid}" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"attributes": {"pkce.code.challenge.method": "S256"}}'
トークンストレージ戦略
| ストレージ | トークン種別 | 備考 |
|---|---|---|
| メモリ(React State) | アクセストークン | ページリロードで消去されるが、実行中の XSS スクリプトからはアクセス可能 |
| HttpOnly Cookie | リフレッシュトークン | JS からアクセス不可。CSRF 対策に SameSite 必須 |
localStorage / sessionStorage へのトークン保存は XSS に対して脆弱なため使用しません。
CORS の最小限設定
plugins:
- name: cors
config:
origins:
- "https://app.example.com"
methods:
- GET
- POST
- PUT
- DELETE
headers:
- Authorization
- Content-Type
credentials: true
max_age: 3600
CSP ヘッダー設定
plugins:
- name: response-transformer
config:
add:
headers:
- "Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://keycloak.example.com; frame-ancestors 'none'"
- "X-Frame-Options: DENY"
- "X-Content-Type-Options: nosniff"
XSS 対策
React の JSX はデフォルトでエスケープするため、dangerouslySetInnerHTML の使用を禁止します。ユーザー入力を表示する箇所では DOMPurify でサニタイズします。
import DOMPurify from "dompurify";
const SafeContent = ({ html }: { html: string }) => (
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
);
Keycloak ブルートフォース対策
curl -X PUT \
"http://keycloak:8080/admin/realms/myrealm" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bruteForceProtected": true,
"failureFactor": 5,
"waitIncrementSeconds": 60,
"maxFailureWaitSeconds": 900,
"maxDeltaTimeSeconds": 43200
}'
| パラメータ | 推奨値 | 説明 |
|---|---|---|
bruteForceProtected |
true |
保護機能の有効化 |
failureFactor |
5 |
この回数を超えるとロックが開始される |
waitIncrementSeconds |
60 |
失敗回数が倍数に達するたびに加算される秒数 |
maxFailureWaitSeconds |
900 |
一時ロックの最大時間(15分) |
maxDeltaTimeSeconds |
43200 |
最後の失敗から失敗カウントをリセットするまでの秒数(12時間) |
OpenFGA API 認証
本番環境では OpenFGA API への認証を必須化します。
Pre-shared Key 方式:
export OPENFGA_AUTHN_METHOD=preshared
export OPENFGA_AUTHN_PRESHARED_KEYS=my-secret-key-1,my-secret-key-2
OIDC クライアントクレデンシャルズ方式(Keycloak 連携):
export OPENFGA_AUTHN_METHOD=oidc
export OPENFGA_AUTHN_OIDC_ISSUER=https://keycloak.example.com/realms/myrealm
export OPENFGA_AUTHN_OIDC_AUDIENCE=openfga-api
認可モデル設計
RBAC と ReBAC の使い分け
| ユースケース | 推奨モデル |
|---|---|
| 管理者 / 一般ユーザーの区別 | RBAC |
| 特定ドキュメントのオーナー・閲覧者管理 | ReBAC |
| 組織階層に応じた権限継承 | ReBAC |
| マルチテナント SaaS の権限管理 | ReBAC |
Contextual Tuple の活用
OIDC トークンのクレームを Contextual Tuple として Check API に渡します。ユーザーのグループ情報などをリアルタイムに反映でき、Tuple の同期コストを削減できます。
const checkResult = await fgaClient.check({
tuple_key: {
user: `user:${userId}`,
relation: "viewer",
object: `document:${docId}`,
},
contextual_tuples: {
tuple_keys: token.groups.map((group: string) => ({
user: `user:${userId}`,
relation: "member",
object: `group:${group}`,
})),
},
authorization_model_id: currentModelId,
});
| 特性 | 説明 |
|---|---|
| 永続化しない | Check / ListObjects / ListUsers のみで有効 |
| リアルタイム反映 | トークンクレームの変化を即座に権限チェックへ反映 |
| Read / Expand では使用不可 | 検索インデックスには反映されない |
インフラ
Keycloak の HA 構成
最低 2 ノード構成で運用し、Infinispan の分散キャッシュを共有します。Kubernetes では StatefulSet と Pod Anti-Affinity を設定してノードを分散します。
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: keycloak
topologyKey: kubernetes.io/hostname
Kong のクラスタリング
Hybrid モードでは Control Plane と Data Plane を分離し、Data Plane をスケールアウトします。Data Plane のみを公開ネットワークに配置し、Control Plane を内部ネットワークに隔離します。
OpenFGA のスケーリング
少数の高スペックサーバーに集約することでキャッシュヒット率を高めます。OpenFGA ノードと PostgreSQL を同一データセンターに配置します。
OPENFGA_MAX_CONCURRENT_READS_FOR_CHECK=100
OPENFGA_MAX_CONCURRENT_READS_FOR_LIST_OBJECTS=50
OPENFGA_RESOLVE_NODE_LIMIT=25
OPENFGA_LIST_OBJECTS_MAX_RESULTS=100
証明書管理
| コンポーネント | 証明書配置 |
|---|---|
| Keycloak | --https-certificate-file 起動オプション |
| Kong | Admin API の /certificates エンドポイント または kong.yml |
| OpenFGA | --grpc-tls-cert / --http-tls-cert 起動フラグ |
トラブルシューティング
認証系トラブル
| トラブル | 症状 | 原因 | 対処 |
|---|---|---|---|
| OIDC Discovery 失敗 | Kong OIDC プラグインが起動時または初回リクエスト時にエラーを返す | Kong から Keycloak の Discovery エンドポイントへの疎通不可、または URL 誤設定 | nc -vz keycloak 8080 で疎通確認する。Keycloak 20 以降は Discovery URL が /realms/{realm}/.well-known/openid-configuration に変更されているため URL を確認する |
| PKCE mismatch エラー | 認可コード交換時に invalid_grant または code_verifier mismatch エラーが返る |
code_verifier と code_challenge の生成ロジックに不整合がある | S256 の計算式 BASE64URL(SHA256(ASCII(code_verifier))) を確認する |
| トークンリフレッシュ失敗 | invalid_grant が返り、ユーザーが再ログインを求められる |
リフレッシュトークンの期限切れ、またはローテーション後に旧トークンを使用した | SSO Session Idle / Max の設定値を確認する。Revoke Refresh Token 有効時は複数タブの競合を考慮してリフレッシュ処理をシリアライズする |
| CORS エラー | ブラウザで CORS policy: No 'Access-Control-Allow-Origin' header が表示される |
Kong の CORS プラグイン未設定、または origins に SPA のオリジンが含まれていない |
deck gateway diff で設定を確認する。credentials: true 使用時は origins: ["*"] は無効なため、明示的なオリジンを指定する |
認可系トラブル
| トラブル | 症状 | 原因 | 対処 |
|---|---|---|---|
| OpenFGA Check が常に false を返す | Tuple が存在するにもかかわらず Check が {"allowed": false} を返す |
キャッシュによる整合性遅延、異なる authorization_model_id の使用、Tuple の user / object フォーマット不一致 |
consistency: "HIGHER_CONSISTENCY" を一時的に指定して再確認する |
| Tuple の不整合 | Keycloak でユーザーのロールが変更されても、OpenFGA の Check 結果が変わらない | Keycloak のロール変更イベントが OpenFGA の Tuple に反映されていない | Keycloak の Event Listener を確認する。Contextual Tuple でトークンクレームを渡す設計に切り替えることで同期コストを排除できる |
| Authorization Model マイグレーション失敗 | 新しいモデルへ切り替え後、一部の Check が予期しない結果を返す | 旧リレーション名の Tuple が残存、またはアプリコードの authorization_model_id が更新されていない |
旧モデル ID のまま Check を継続しながら、新リレーションへ Tuple をコピーする。コピー完了後にアプリの authorization_model_id を更新する |
インフラ系トラブル
| トラブル | 症状 | 原因 | 対処 |
|---|---|---|---|
| Kong ↔ Keycloak 接続タイムアウト | Kong から Keycloak への OIDC トークン検証でタイムアウトが発生する | ネットワーク遅延、または Discovery キャッシュの TTL 切れ時に集中リクエストが発生 | Discovery の cache_ttl を適切に設定する |
| Keycloak のメモリ・CPU 使用率高騰 | Keycloak のヒープ使用率が 80% を超え、GC が頻発する | アクティブセッション数の増大、または Infinispan キャッシュサイズの不足 | JVM ヒープを -Xmx で適切に設定する(目安: 2GB〜4GB) |
| OpenFGA のレスポンスタイム劣化 | Check API のレイテンシが増大し、P99 が 200ms を超える | DB 接続プールの枯渇、複雑な Authorization Model による深い再帰 | OPENFGA_DATASTORE_MAX_OPEN_CONNS を DB の最大接続数に合わせて調整する |
OSS 代替構成: suwa-sh/kong-plugin-oidc
これまでの手順は Kong Gateway Enterprise 版で提供される OpenID Connect プラグインと Session プラグインの組み合わせを前提としています。Kong OSS で同等の「OIDC 認証 + Bearer JWT 検証 + Redis セッション共有」を実現するために、筆者は suwa-sh/kong-plugin-oidc を開発しました。本セクションでは、既存の構成を維持したまま Kong 部分のみを OSS プラグインに差し替える方法を説明します。
置き換え対象のマッピング
| Enterprise 構成 | OSS 代替 |
|---|---|
| Kong Gateway Enterprise | Kong OSS(ghcr.io/suwa-sh/kong-plugin-oidc の Docker イメージ) |
openid-connect プラグイン |
oidc プラグイン(suwa-sh/kong-plugin-oidc) |
| Enterprise 内蔵の Session 管理 | lua-resty-session v4 + Redis |
| Kong Manager / Vitals 等の Enterprise 機能 | 対象外(必要なら別途 OSS ツールを併用) |
docker-compose.yml の差分
既存の postgres / keycloak / openfga / backend 定義はそのまま流用し、kong サービスを差し替え、新規に redis サービスを追加します。
services:
# 既存の postgres / keycloak / openfga / backend はそのまま
redis:
image: redis:7-alpine
networks:
- auth-net
kong:
image: ghcr.io/suwa-sh/kong-plugin-oidc:latest # Kong OSS にプラグインを同梱したイメージ
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /kong/declarative/kong.yml
KONG_PLUGINS: bundled,oidc # oidc プラグインを有効化
KONG_PROXY_LISTEN: "0.0.0.0:9000"
KONG_ADMIN_LISTEN: "0.0.0.0:9001"
KONG_LOG_LEVEL: info
volumes:
- ./kong:/kong/declarative
ports:
- "9000:9000"
- "9001:9001"
depends_on:
- keycloak
- redis
networks:
- auth-net
Kong プラグインの設定差分
Enterprise 版の openid-connect プラグイン設定の代わりに、oidc プラグインを設定します。Service と Route の作成手順(既存の「Service と Route の作成」)はそのまま流用できます。
curl -X POST http://localhost:9001/services/backend-service/plugins \
-H "Content-Type: application/json" \
-d '{
"name": "oidc",
"config": {
"client_id": "kong-client",
"client_secret": "kong-client-secret",
"discovery": "http://keycloak:8080/realms/myrealm/.well-known/openid-configuration",
"encryption_secret": "change-me-32bytes-base64xxxxxxxx",
"bearer_jwt_auth_enable": "yes",
"bearer_jwt_auth_allowed_auds": ["my-spa"],
"bearer_jwt_auth_signing_algs": ["RS256"],
"bearer_only": "yes",
"session_storage": "redis",
"session_redis_host": "redis",
"session_redis_port": 6379,
"groups_claim": "groups"
}
}'
主要パラメータの早見表:
| パラメータ | 値 | 説明 |
|---|---|---|
client_id / client_secret |
Keycloak クライアント情報 | IdP で発行したクライアント資格情報(必須) |
discovery |
Keycloak の .well-known/openid-configuration URL |
OIDC Discovery エンドポイント |
encryption_secret |
任意の文字列(全ノード共通) | セッション暗号化キー(必須) |
bearer_jwt_auth_enable |
"yes" |
JWKS による Bearer JWT 署名検証を有効化 |
bearer_jwt_auth_allowed_auds |
SPA の client_id を含む配列 |
JWT の aud 許可値 |
bearer_only |
"yes" |
Bearer トークンが無い場合に 401 を返す(Authorization Code Flow へフォールバックしない) |
session_storage |
"redis" |
Redis セッションを有効化 |
session_redis_host / session_redis_port |
redis / 6379 |
Redis 接続先 |
groups_claim |
"groups" |
JWT から取り出して kong.ctx.shared.authenticated_groups に格納するクレーム名 |
groups_claim で抽出されたグループは request-scoped な kong.ctx.shared.authenticated_groups に格納され、Kong の ACL プラグインなど他のプラグインから参照できます。全設定パラメータ一覧と詳細な挙動については 技術調査 - kong-plugin-oidc を参照してください。
宣言的設定(kong.yml)例
decK を用いた GitOps 運用(既存の「Kong 運用 > declarative config によるバージョン管理」の手順)はそのまま利用できます。kong.yml の完全形サンプル:
_format_version: "3.0"
_transform: true
services:
- name: backend-service
url: http://backend:8090
routes:
- name: backend-route
paths:
- /api
plugins:
- name: oidc
config:
client_id: kong-client
client_secret: kong-client-secret
discovery: http://keycloak:8080/realms/myrealm/.well-known/openid-configuration
encryption_secret: change-me-32bytes-base64xxxxxxxx
bearer_jwt_auth_enable: "yes"
bearer_jwt_auth_allowed_auds:
- my-spa
bearer_jwt_auth_signing_algs:
- RS256
bearer_only: "yes"
session_storage: redis
session_redis_host: redis
session_redis_port: 6379
groups_claim: groups
注意点・制約
encryption_secretは全 Kong ノードで同一値にしてください。Redis セッションを複数ノードで共有する構成では特に重要です。- セッションタイムアウトは明示設定を推奨します。
kong-plugin-oidcはlua-resty-sessionv4 を採用しており、session_idling_timeout/session_rolling_timeout/session_absolute_timeoutのデフォルトは 0(無期限)です。IdP のトークン有効期限と整合させるために明示的に指定してください。 - Bearer JWT 検証時は
bearer_jwt_auth_allowed_audsに SPA のclient_idを含める必要があります。未指定時はclient_idが暗黙のデフォルトになりますが、複数の aud を許可する場合や意図を明確にするため、明示指定を推奨します。 - Enterprise 版
openid-connectプラグイン固有の機能(一部の高度な Auth Method、Kong Consumer 自動マッピングの細かいオプションなど)は対応していないものがあります。
まとめ
Keycloak + OpenFGA + Kong + React SPA の構成は、認証・認可の責務をそれぞれ専任コンポーネントに分離し、アプリケーションコードに認証ロジックを持ち込まないゼロトラスト API セキュリティ基盤を実現します。OIDC / OAuth 2.0 / PKCE という標準に準拠し、ReBAC による細粒度認可と GitOps ベースのゲートウェイ管理を組み合わせることで、変更・監査・スケールに強い構成を構築できます。
この記事が少しでも参考になった、あるいは改善点などがあれば、ぜひリアクションやコメント、SNSでのシェアをいただけると励みになります!
参考リンク
- 公式ドキュメント
- Keycloak Server Administration Guide
- OpenFGA Concepts
- Kong Gateway Entities
- OpenID Connect Core 1.0
- Getting Started with Keycloak on Docker
- OpenFGA Setup with Docker
- OpenFGA Configure Authorization Model
- OpenFGA Use the FGA CLI
- Kong OpenID Connect Plugin
- oidc-spa Documentation
- Importing and exporting realms - Keycloak
- Checking if rolling updates are possible - Keycloak
- Multi-cluster deployments - Keycloak
- Running OpenFGA in Production
- Immutable Authorization Models - OpenFGA
- Use Token Claims As Contextual Tuples - OpenFGA
- Model Migrations - OpenFGA
- decK - Kong Docs
- Health checks and circuit breakers - Kong Gateway
- Query Consistency Modes - OpenFGA
- Conditional Tuples - OpenFGA
- OpenFGA Authentication Configuration
- oidc-spa JWT of the Access Token
- oidc-spa Tokens Renewal
- Keycloak Brute Force Protection
- GitHub
- keycloak-openfga-workshop
- oidc-spa
- keycloak-openfga-event-publisher
- openfga/sample-stores
- suwa-sh/kong-plugin-oidc - Kong OSS 向け OIDC プラグイン(本記事の OSS 代替構成で使用)
- 記事