✍️ 필사 모드: Keycloak 連携ハンズオン — Docker で起動し Realm・Client を設定、Spring Boot・Next.js 連携まで (2025 ハンズオンガイド)
日本語プロローグ — 認証を自前で作るな
新規プロジェクトで最もよくある失敗: ログインを自前で実装すること。 パスワードハッシュ、セッション、トークン発行、パスワードリセット、ソーシャルログイン、2FA、brute-force 防御… これを全部自前で作るのは、自分の手でセキュリティホールを掘るのと同じだ。
Keycloak は Red Hat が支援するオープンソースの IAM (Identity and Access Management) だ。OIDC・OAuth2・SAML を標準として実装しており、管理コンソール・ソーシャルログイン・2FA・LDAP 連携・brute-force 防御がすべて入っている。セルフホスティングで、ライセンスは Apache 2.0 だ。
2025 年の補足: Keycloak は 17 バージョンから Quarkus ベース に書き直された。かつての WildFly ディストリビューションは消えた。この記事は Keycloak 26.x が前提だ。
この記事は理論書ではなく ハンズオン だ。OAuth2・OIDC プロトコルそのものの深い説明は別記事 (OAuth2/OIDC 深掘りガイド) にあり、ここでは 実際に起動して連携すること に集中する。ターミナルを開いてついてくればいい。
流れはこうだ: Docker で起動 → Realm・Client・User の設定 → OIDC エンドポイントを直接叩く → Spring Boot・Node.js バックエンド連携 → Next.js フロント連携 → トークンフローの整理 → 本番チェックリスト → トラブルシューティング。
0章 · 事前準備と全体像
準備物
- Docker (Keycloak 実行用)
- JDK 21+ (Spring Boot ハンズオン時) または Node.js 20+ (Node・Next.js ハンズオン時)
curl,jq(エンドポイントを叩く用)
全体像
┌─────────┐ 1. ログインリダイレクト ┌──────────────┐
│ Browser │ ───────────────────────▶ │ Keycloak │
│ │ ◀─────────────────────── │ (IdP, 8080) │
└────┬────┘ 2. 認可コード + トークン └──────┬───────┘
│ │
│ 3. Access Token で API 呼び出し │ JWKS 公開鍵
▼ ▼
┌─────────┐ ┌──────────────┐
│ Backend │ ─── 4. トークン署名検証 ──▶ │ (JWKS キャッシュ) │
│ (API) │ └──────────────┘
└─────────┘
要点: バックエンドは Keycloak に毎リクエストを問い合わせない。 Keycloak の公開鍵 (JWKS) を一度受け取ってキャッシュし、その鍵で JWT 署名を ローカルで 検証する。Keycloak が落ちても、すでに発行されたトークンは検証できる。
Keycloak のコア概念 7 つ
| 概念 | 一行説明 |
|---|---|
| Realm | 隔離境界。User・Client・Role の独立したネームスペース。通常はサービス/環境ごとに 1 つ |
| Client | Keycloak に登録されたアプリケーション。フロントエンド SPA、バックエンド API それぞれが Client |
| User | エンドユーザーアカウント。Realm に所属 |
| Role | 権限のまとまり。Realm Role (グローバル) と Client Role (アプリ別) がある |
| Group | User のまとまり。Group に Role をマッピングすると所属 User が継承する |
| Client Scope | トークンにどの claim・role を入れるかを決める再利用単位 |
| Identity Provider | 外部 IdP (Google、GitHub、別の Keycloak) を接続するブローカリング |
1章 · Keycloak を起動する (Docker)
30 秒クイックスタート (dev モード)
docker run --name keycloak -p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.0 start-dev
バージョン注意: Keycloak 26 から初期管理者アカウントの環境変数が
KC_BOOTSTRAP_ADMIN_USERNAME/KC_BOOTSTRAP_ADMIN_PASSWORDに変わった。それ以前のバージョンはKEYCLOAK_ADMIN/KEYCLOAK_ADMIN_PASSWORDを使う。
ブラウザで http://localhost:8080 にアクセス → 「Administration Console」 → admin / admin でログイン。これで完了。
start-dev はインメモリ H2 DB を使い、HTTPS を強制しない。ハンズオン・ローカル専用だ。 本番は 10 章で扱う。
docker-compose — Postgres を付けた現実的な構成
ハンズオンでもデータが飛ぶとイライラする。Postgres を付けよう。
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keycloak"]
interval: 5s
timeout: 5s
retries: 5
keycloak:
image: quay.io/keycloak/keycloak:26.0
command: start-dev
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
KC_HEALTH_ENABLED: "true"
KC_METRICS_ENABLED: "true"
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
volumes:
pg_data:
docker compose up -d
docker compose logs -f keycloak # "Keycloak ... started" が出れば準備完了
dev モード vs 本番モード
start-dev | start | |
|---|---|---|
| HTTPS | 任意 (強制しない) | デフォルト必須 |
| キャッシュ | ローカル | 分散 (Infinispan) |
| hostname | 自動推論 | 明示必須 (KC_HOSTNAME) |
| 用途 | ローカル・ハンズオン | 運用 |
Health・Metrics エンドポイント
KC_HEALTH_ENABLED=true でオンにすると — Keycloak は管理用ポート (デフォルト 9000) で公開する:
http://localhost:9000/health/ready— 準備状態http://localhost:9000/health/live— 生存状態http://localhost:9000/metrics— Prometheus メトリクス
2章 · Realm を作る
Realm は 隔離境界 だ。master realm は Keycloak 自体の管理用なので 触らず、自分のサービス用 realm を新しく作る。
コンソールで作る
- 左上の realm ドロップダウン → Create realm
- Realm name:
demo - Create
これ以降のすべての作業は demo realm の中で行う。
知っておくべき Realm 設定 (Realm settings)
- Tokens タブ
Access Token Lifespan— デフォルト 5 分。短いほど安全、長いほど楽。5〜15 分が一般的。SSO Session Idle / Max— refresh token の寿命を左右する。
- Login タブ
User registration— セルフ会員登録を許可するかForgot password、Remember me、Email as username
- Sessions タブ — セッション寿命の細かい調整
- Security defenses タブ —
Brute Force Detectionをオンに (10 章)
今はデフォルト値のまま進める。
3章 · Client 設定 — 2 つのタイプ
Client は「Keycloak に登録されたアプリ」だ。今回のハンズオンでは 2 つ作る:
demo-frontend— Public Client (Next.js SPA)。シークレットなし、PKCE を使用。demo-backend— Confidential Client (API サーバー)。シークレットあり。
Public Client — フロントエンド用
左の Clients → Create client:
- General settings
- Client type:
OpenID Connect - Client ID:
demo-frontend
- Client type:
- Capability config
Client authentication: Off (← これが Public にする鍵)Standard flow: On (Authorization Code Flow)Direct access grants: Off (本番ではオフにする)
- Login settings
Valid redirect URIs:http://localhost:3000/*Valid post logout redirect URIs:http://localhost:3000/*Web origins:http://localhost:3000(← CORS 許可オリジン)
Public Client はシークレットを安全に保管できない (ブラウザのコードは全部見える) ので PKCE が必須 だ。Keycloak は Standard flow で PKCE を自動サポートする。
Confidential Client — バックエンド用
もう一度 Create client:
- Client ID:
demo-backend - Capability config
Client authentication: On (← Confidential)Standard flow: OnService accounts roles: On (サーバー間通信用 — Client Credentials Grant)
- 作成後、Credentials タブ で
Client Secretをコピーしておく。
バックエンドが純粋な「Resource Server」(トークン検証のみ) なら、実はシークレットは不要だ。JWKS で検証するだけでよいからだ。シークレットは、バックエンドが 自分でトークンを発行してもらう必要があるとき (Service Account、Token Exchange) に使う。
重要な Client 設定まとめ
| 設定 | 意味 | よくある失敗 |
|---|---|---|
| Valid redirect URIs | ログイン後に戻れる URL のホワイトリスト | 広すぎる * → open redirect のリスク |
| Web origins | CORS 許可オリジン | 空にするとブラウザ呼び出しが CORS で弾かれる |
| Standard flow | Authorization Code Flow の有効化 | SPA・Web アプリは必須 |
| Direct access grants | username/password を直接交換 | 本番ではオフにしろ (テスト専用) |
| Service accounts | Client Credentials Grant | サーバー間のみ |
4章 · User・Role・Group を作る
User を作る
Users → Create new user:
- Username:
alice - Email:
alice@demo.test、Email verified: On - Create → Credentials タブ → Set password →
alice123、Temporary: Off
(Temporary: On だと初回ログイン時にパスワード変更を強制する。ハンズオンでは Off。)
Realm Role を作る
Realm roles → Create role:
adminを 1 つ、userを 1 つ作る。
Role を User にマッピング
Users → alice → Role mapping → Assign role → admin、user を選択。
Group でまとめる (任意だが推奨)
ユーザーが増えると User ごとに Role を付けるのは地獄だ。Group を使う。
- Groups → Create group →
administrators - その group の Role mapping →
adminを割り当て - Users →
alice→ Groups → Join group →administrators
これで administrators group に入った人は自動的に admin role を持つ。
Role がトークンに入る仕組み
Realm Role は基本的に Access Token の realm_access.roles 配列に入る。Client Role は resource_access オブジェクトの下、クライアントごとの roles 配列に入る。これは roles Client Scope のデフォルト Mapper が処理する。
もしトークンに role が見えないなら — Client の Client scopes タブ で roles scope が Default として付いているか確認しよう (11 章のトラブルシューティング参照)。
5章 · OIDC エンドポイントの理解 — curl で直接叩いてみる
連携コードを書く前に、Keycloak が何を返すのかを 直接 見よう。これがハンズオンの核心だ。
Discovery ドキュメント — すべての地図
curl -s http://localhost:8080/realms/demo/.well-known/openid-configuration | jq
ここに主要なエンドポイントが全部出てくる:
| エンドポイント | URL (realm=demo) | 役割 |
|---|---|---|
| issuer | http://localhost:8080/realms/demo | トークン発行者の識別子 |
| authorization_endpoint | .../protocol/openid-connect/auth | ログイン画面に送る場所 |
| token_endpoint | .../protocol/openid-connect/token | コードをトークンに交換 |
| userinfo_endpoint | .../protocol/openid-connect/userinfo | ユーザー情報の照会 |
| jwks_uri | .../protocol/openid-connect/certs | 署名検証用の公開鍵 |
| end_session_endpoint | .../protocol/openid-connect/logout | ログアウト |
テスト用トークンを取得する (Password Grant)
警告: Password Grant (Direct Access Grants) は テスト専用 だ。実際のアプリは Authorization Code Flow を使う。ここではトークンの中身を素早く見るために一時的にオンにする —
demo-backendClient のDirect access grantsを少しだけ On に。
curl -s -X POST \
http://localhost:8080/realms/demo/protocol/openid-connect/token \
-d "client_id=demo-backend" \
-d "client_secret=PASTE_YOUR_SECRET" \
-d "grant_type=password" \
-d "username=alice" \
-d "password=alice123" | jq
レスポンス:
{
"access_token": "eyJhbGciOiJSUzI1Ni...",
"expires_in": 300,
"refresh_token": "eyJhbGciOiJIUzI1Ni...",
"token_type": "Bearer",
"scope": "profile email"
}
JWT の中に何が入っているか
access_token の真ん中の部分 (payload) をデコードしてみよう:
TOKEN="eyJhbGci..." # 上で受け取った access_token
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
主要な claim たち:
{
"iss": "http://localhost:8080/realms/demo",
"sub": "f7e3...-user-uuid",
"aud": "account",
"exp": 1763400000,
"iat": 1763399700,
"azp": "demo-backend",
"realm_access": { "roles": ["admin", "user", "default-roles-demo"] },
"resource_access": { "account": { "roles": ["view-profile"] } },
"scope": "profile email",
"email": "alice@demo.test",
"preferred_username": "alice"
}
バックエンドが検証すべきもの: iss (自分の issuer か)、exp (期限切れでないか)、aud (自分宛てのトークンか)、そして 署名 (JWKS 公開鍵で)。権限判定は realm_access.roles で行う。
ではコードに移そう。
6章 · バックエンド連携 (1) — Spring Boot Resource Server
Spring Boot で Keycloak 専用のアダプターは もはや不要だ。 (旧バージョンの keycloak-spring-boot-adapter は deprecated。) 標準の Spring Security OAuth2 Resource Server を使う。
依存関係
// build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
}
application.yml — これがほぼ全部だ
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/demo
issuer-uri を 1 つ与えるだけで、Spring が自動的に .well-known/openid-configuration を読み、jwks_uri を見つけて公開鍵をキャッシュする。トークン署名・iss・exp の検証が自動的にオンになる。
SecurityConfig — Keycloak の role を Spring の権限に変換する
デフォルトの状態では realm_access.roles を Spring は知らない。変換器を挟む。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // @PreAuthorize を使用
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("admin")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(converter())))
.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(csrf -> csrf.disable()); // ステートレス API なので CSRF 不要
return http.build();
}
// realm_access.roles -> ROLE_* 権限にマッピング
private JwtAuthenticationConverter converter() {
JwtAuthenticationConverter c = new JwtAuthenticationConverter();
c.setJwtGrantedAuthoritiesConverter(jwt -> {
Map<String, Object> realm = jwt.getClaim("realm_access");
if (realm == null || realm.get("roles") == null) {
return List.of();
}
Collection<String> roles = (Collection<String>) realm.get("roles");
return roles.stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.collect(Collectors.toList());
});
return c;
}
}
hasRole("admin") は内部的に ROLE_admin 権限を探す。だから変換器で ROLE_ プレフィックスを付けた。
保護されたエンドポイント
@RestController
@RequestMapping("/api")
public class DemoController {
@GetMapping("/public/ping")
public String publicPing() {
return "anyone can see this";
}
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal Jwt jwt) {
return Map.of(
"sub", jwt.getSubject(),
"username", jwt.getClaimAsString("preferred_username"));
}
@GetMapping("/admin/secret")
@PreAuthorize("hasRole('admin')")
public String adminSecret() {
return "only admins";
}
}
テスト
# 5章で受け取ったトークンで
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/me
# トークンなし → 401
curl -i http://localhost:8081/api/me
7章 · バックエンド連携 (2) — Node.js / Express
Node 陣営も同じだ — JWKS でローカル検証。Auth0 が作った express-oauth2-jwt-bearer が最もシンプルだ。
インストール & ミドルウェア
npm install express express-oauth2-jwt-bearer
import express from 'express'
import { auth } from 'express-oauth2-jwt-bearer'
const app = express()
// JWKS の自動取得・キャッシュ、iss・exp・署名の検証
const checkJwt = auth({
issuerBaseURL: 'http://localhost:8080/realms/demo',
audience: 'account', // Keycloak のデフォルト aud。専用 aud は 11 章参照
})
// realm_access.roles ベースの役割ガード
function requireRealmRole(role) {
return (req, res, next) => {
const roles = req.auth?.payload?.realm_access?.roles ?? []
if (!roles.includes(role)) {
return res.status(403).json({ error: 'forbidden' })
}
next()
}
}
app.get('/api/public/ping', (req, res) => {
res.json({ msg: 'anyone' })
})
app.get('/api/me', checkJwt, (req, res) => {
res.json({
sub: req.auth.payload.sub,
username: req.auth.payload.preferred_username,
})
})
app.get('/api/admin/secret', checkJwt, requireRealmRole('admin'), (req, res) => {
res.json({ msg: 'only admins' })
})
app.listen(8082, () => console.log('API on :8082'))
アダプターなしで jose で直接検証したいなら
ライブラリを減らして原理を見たいとき:
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('http://localhost:8080/realms/demo/protocol/openid-connect/certs')
)
async function verify(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'http://localhost:8080/realms/demo',
// audience: 'demo-backend', // 専用 aud を使うとき
})
return payload // { sub, realm_access, ... }
}
createRemoteJWKSet が公開鍵を取得してキャッシュし、鍵のローテーション (rotation) も自動で処理する。
8章 · フロントエンド連携 — Next.js
フロントエンドの役割: ユーザーを Keycloak のログインに送り、受け取った Access Token でバックエンドを呼び出す。2 つの方法がある。
方法 A — Auth.js (NextAuth v5)、Next.js に推奨
Next.js App Router なら Auth.js が最も滑らかだ。トークン交換・セッション・リフレッシュをサーバーサイドで処理するので、トークンがブラウザの JS に露出しない。
npm install next-auth@beta
// auth.ts
import NextAuth from 'next-auth'
import Keycloak from 'next-auth/providers/keycloak'
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Keycloak({
clientId: process.env.KEYCLOAK_CLIENT_ID, // demo-frontend
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, // Confidential のとき
issuer: process.env.KEYCLOAK_ISSUER, // http://localhost:8080/realms/demo
}),
],
callbacks: {
// 初回ログイン時に Keycloak のトークンをセッショントークンに保管
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token
token.refreshToken = account.refresh_token
token.expiresAt = account.expires_at
}
return token
},
async session({ session, token }) {
session.accessToken = token.accessToken
return session
},
},
})
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers
上のルートパスの角括弧部分は Next.js の catch-all ルート構文だ (
authの次に...nextauthが角括弧で囲まれたフォルダ)。
サーバーコンポーネントで使う:
// app/page.tsx
import { auth, signIn, signOut } from '@/auth'
export default async function Home() {
const session = await auth()
if (!session) {
return <form action={async () => { 'use server'; await signIn('keycloak') }}>
<button>Keycloak でログイン</button>
</form>
}
return (
<div>
<p>{session.user?.email} さん、ようこそ</p>
<form action={async () => { 'use server'; await signOut() }}>
<button>ログアウト</button>
</form>
</div>
)
}
バックエンド呼び出し — サーバーで Access Token を付ける:
// app/dashboard/page.tsx
import { auth } from '@/auth'
export default async function Dashboard() {
const session = await auth()
const res = await fetch('http://localhost:8082/api/me', {
headers: { Authorization: `Bearer ${session?.accessToken}` },
})
const me = await res.json()
return <pre>{JSON.stringify(me, null, 2)}</pre>
}
方法 B — keycloak-js アダプター (純粋な SPA)
Next.js ではなく純粋な React SPA だったり、クライアントサイドで直接トークンを扱いたいとき。Client は Public (Client authentication: Off) でなければならない。
import Keycloak from 'keycloak-js'
export const keycloak = new Keycloak({
url: 'http://localhost:8080',
realm: 'demo',
clientId: 'demo-frontend',
})
await keycloak.init({
onLoad: 'check-sso',
pkceMethod: 'S256', // PKCE 必須
})
if (keycloak.authenticated) {
// バックエンド呼び出し
await fetch('http://localhost:8082/api/me', {
headers: { Authorization: `Bearer ${keycloak.token}` },
})
}
// トークン期限が迫ったら更新
setInterval(() => keycloak.updateToken(30), 20_000)
どちらを使うか
| Auth.js (方法 A) | keycloak-js (方法 B) | |
|---|---|---|
| トークン露出 | サーバーサイド保管、より安全 | ブラウザのメモリ |
| Next.js への適合度 | 非常に高い | 普通 |
| 純粋な SPA | 不向き | 適合 |
| 推奨 | Next.js ならこれ | SPA・レガシー React |
9章 · トークンフローの整理 — Authorization Code + PKCE 全体像
8 章のコードが内部的にやっていることを 1 章にまとめる。SPA・Web アプリの標準フローだ。
1. ユーザーが「ログイン」をクリック
2. フロント → Keycloak の authorization_endpoint にリダイレクト
?response_type=code
&client_id=demo-frontend
&redirect_uri=http://localhost:3000/...
&scope=openid profile email
&code_challenge=... (PKCE: code_verifier の SHA-256)
&code_challenge_method=S256
3. Keycloak のログイン画面 → ユーザー認証
4. Keycloak → redirect_uri に戻す、?code=AUTH_CODE
5. フロント (または Auth.js サーバー) → token_endpoint で交換
grant_type=authorization_code
&code=AUTH_CODE
&code_verifier=... (PKCE: 元の verifier — コード横取りへの防御)
6. Keycloak → { access_token, id_token, refresh_token }
7. access_token でバックエンド API を呼び出し
8. 期限切れになったら refresh_token で token_endpoint に再リクエスト → 新しいトークン
3 つのトークンの役割
| トークン | 用途 | 受け取る側 |
|---|---|---|
access_token | API 呼び出しの認可 | Resource Server (バックエンド) |
id_token | 「誰がログインしたか」の身元証明 | Client (フロントエンド) |
refresh_token | 新しい access_token の発行 | Authorization Server (Keycloak) |
よくある失敗: id_token をバックエンド API に送ること。 バックエンドは access_token を受け取るべきだ。id_token はフロントが「ログイン済み」を知る用途だ。
ログアウト — セッションまで切る
フロントでトークンを捨てるだけでは本物のログアウトではない。Keycloak の SSO セッションが生きているので、再度ログインすると画面も出ずに通過してしまう。RP-Initiated Logout で Keycloak のセッションまで切らなければならない:
http://localhost:8080/realms/demo/protocol/openid-connect/logout
?id_token_hint=ID_TOKEN
&post_logout_redirect_uri=http://localhost:3000
Auth.js の signOut() はこれを処理してくれる (provider 設定による)。keycloak-js は keycloak.logout()。
10章 · 本番チェックリスト
start-dev で運用してはいけない。運用に移行する際の必須項目。
本番モードへ切り替える
# start-dev → start、hostname・HTTPS を明示
docker run ... quay.io/keycloak/keycloak:26.0 start \
--hostname https://auth.example.com \
--proxy-headers xforwarded # リバースプロキシの背後にいるとき
startモード — HTTPS をデフォルトで強制、分散キャッシュ。KC_HOSTNAME— 明示必須。そうしないと発行されるトークンのissがおかしくなる (11 章の定番バグ)。--proxy-headers— Nginx・ALB の背後にいるならX-Forwarded-*を信頼するように。
DB・インフラ
- Postgres/MySQL の外部 DB — H2 は絶対禁止。バックアップ・HA を設定。
- 複数インスタンス + 分散キャッシュ (Infinispan) — 単一障害点を排除。
- TLS —
auth.example.comは必ず HTTPS。
セキュリティ設定
- Brute Force Detection をオンに — Realm settings → Security defenses。パスワードの総当たりへの防御。
- Password policy — 最小長、複雑度、再利用禁止。
- Token lifespan の調整 — Access Token は 5〜15 分、Refresh Token はサービス特性に合わせて。
Direct access grantsをオフに — すべての本番 Client で。- Redirect URI を狭める —
*禁止。正確なパスで。
設定をコードに — Realm Export/Import
コンソールでクリックして作った設定は再現不可能だ。Realm を JSON に export して Git に入れ、起動時に import する (IaC)。
# export
docker exec keycloak /opt/keycloak/bin/kc.sh export \
--dir /tmp/export --realm demo
# import (コンテナ起動時)
docker run ... quay.io/keycloak/keycloak:26.0 \
start-dev --import-realm # /opt/keycloak/data/import/ の JSON を読む
より精緻には keycloak-config-cli のようなツールで宣言的な設定を管理するか、Terraform Keycloak provider を使う。
可観測性
KC_HEALTH_ENABLED、KC_METRICS_ENABLEDをオンにして Prometheus・k8s probe に接続。- ログイン失敗・管理作業は Events で残す (Realm settings → Events)。監査ログ。
11章 · トラブルシューティング — よく詰まる箇所
ハンズオンで 100% 遭遇するエラーたち。事前に知っておくと時間を節約できる。
Invalid redirect_uri
- 原因: Client の
Valid redirect URIsに登録されていない URL に戻ろうとしている。 - 解決: Client 設定に正確な URL を登録。ポート・パス・trailing slash まで正確に。ワイルドカードは
http://localhost:3000/*のように。
CORS エラー (ブラウザコンソール)
- 原因: Client の
Web originsが空、またはフロントのオリジンが入っていない。 - 解決:
Web originsにhttp://localhost:3000を追加。(+を入れると redirect URI から自動推論。)
Invalid token issuer / バックエンドがトークンを拒否
- 原因: トークンの
issとバックエンドのissuer-uriが文字まで正確に一致していない。定番はlocalhostvs コンテナホスト名 の不一致。 - 例: バックエンドが Docker の中で
http://keycloak:8080/realms/demoで検証しているのに、トークンのissはhttp://localhost:8080/realms/demo。 - 解決:
KC_HOSTNAMEを固定し、すべての場所で 同じ issuer 文字列 を使う。トークンをデコードしてissを直接確認する (5 章)。
aud (audience) の検証失敗
- 原因: バックエンドが特定の
audienceを要求するのに、Keycloak のデフォルトトークンのaudはaccount。 - 解決: 次のどちらか —
- バックエンドの audience 検証を
accountに合わせるか、 - Keycloak で Audience Mapper を作って
demo-backendをaudに入れる (Client scopes → 専用 scope → Mappers → Audience)。
- バックエンドの audience 検証を
トークンに role がない
- 原因: Client に
rolesClient Scope が付いていないか、role を User にマッピングしていない。 - 解決: Client → Client scopes タブに
rolesがDefaultであるか確認。User → Role mapping を確認。トークンをデコードしてrealm_access.rolesを直接確認。
Clock skew — Token is not active / expired
- 原因: Keycloak コンテナとバックエンドサーバーの時計がずれている。
- 解決: NTP 同期。検証ライブラリの
clockTolerance(通常は数秒〜1 分) を少し許容する。
start モードなのに起動しない — hostname エラー
- 原因: 本番モードは
KC_HOSTNAMEが必須。 - 解決:
--hostnameまたはKC_HOSTNAME環境変数を指定。リバースプロキシの背後なら--proxy-headersも。
12章 · ヒント & アンチパターン
実戦ヒント
- Realm は環境/サービス単位で —
demo-dev、demo-prodを分離。masterは絶対にアプリ用に使うな。 - Client はアプリ単位で — フロントとバックエンドは別の Client。フロントは Public、バックエンドは必要に応じて Confidential。
- role は Group で管理 — User に直接 role を付けるのは小規模だけ。大きくなったら Group + role mapping。
- トークンを常にデコードして見る習慣 — 詰まったら
jwt.ioかcut | base64 -d | jqでiss・aud・exp・rolesを目で確認。推測するな。 - Discovery ドキュメントを信頼する — エンドポイント URL をハードコードせず、
.well-known/openid-configurationから読む。 - ローカルは docker-compose + Postgres +
--import-realm— realm JSON を Git に入れれば、チームメンバーがdocker compose up一度で同じ環境。 - Access Token は短く、Refresh Token で更新 — 5〜15 分。漏洩しても寿命が短い。
アンチパターン 10 個
- 認証を自前で実装 — Keycloak があるのにパスワードハッシュから書く。
masterrealm にアプリの Client・User を作る。start-devで本番運用 (H2 DB、HTTPS なし)。- Redirect URI を
*で全開にしておく — open redirect。 Web originsを埋めず CORS で詰まり続ける。- バックエンドに
id_tokenを送る (送るべきはaccess_token)。 - フロントエンドの Public Client で PKCE を使わない。
Direct access grants(Password Grant) を本番でオンにしておく。KC_HOSTNAMEを設定せず、トークンのiss不一致で検証失敗。- コンソールクリックだけで設定 — export/import なしで再現不可能な環境。
エピローグ — Keycloak は「終わり」ではなく「出発点」
この記事をやり切ったなら、今や手にしたもの:
- Docker で起動した Keycloak 26 (+ Postgres)
demorealm、Public・Confidential Client、User・Role・Group- OIDC エンドポイントを直接叩いた経験 — Discovery、token、JWKS
- Spring Boot Resource Server、Node.js Express バックエンドの連携
- Next.js (Auth.js) +
keycloak-jsのフロント連携 - Authorization Code + PKCE 全体のトークンフローの理解
- 本番チェックリストとトラブルシューティングの感覚
ここからさらに進む道: Identity Provider Brokering (Google・GitHub ソーシャルログインの接続)、LDAP/AD User Federation (社内ディレクトリの連携)、カスタムテーマ (ログイン画面のブランディング)、Authorization Services (fine-grained な権限 — UMA)、Token Exchange (サービス間のトークン変換)、Organizations (B2B マルチテナンシー、Keycloak 26 の新機能)。
核心の教訓は 1 つだ。認証・認可は自前で作る領域ではない。 標準 (OIDC) を実装した検証済みのソリューションの上に乗り、あなたはビジネスロジックに集中しろ。Keycloak はその標準の上で最も普遍的なセルフホスティングの選択肢だ。
10 項目チェックリスト
- Keycloak を
start-devではなくstartモードで起動する準備ができているか? - アプリ用 realm を
masterと分離したか? - フロントは Public + PKCE、バックエンドは適切な Client タイプか?
- Redirect URI と Web origins を正確に (狭く) 設定したか?
- バックエンドが JWKS でローカル検証しているか (毎リクエストの introspection ではない)?
iss·aud·exp· 署名をすべて検証しているか?- role がトークンに入り、バックエンドの権限に変換されているか?
- バックエンドには
access_tokenを、身元確認にはid_tokenを使っているか? - RP-Initiated Logout で Keycloak のセッションまで切っているか?
- Realm 設定を export/import (または IaC) で再現可能か?
次回予告
次回の候補: Keycloak Identity Provider Brokering — Google・GitHub ソーシャルログインと社内 LDAP の連携、Keycloak のカスタムテーマ・拡張 (SPI) 開発、Keycloak Authorization Services — UMA ベースの fine-grained な権限ハンズオン。
「良い認証とは、ユーザーが意識しない認証だ。そして良い認証インフラとは、開発者が自前で作らなかったインフラだ。」
— Keycloak 連携ハンズオン、おわり。
현재 단락 (1/488)
新規プロジェクトで最もよくある失敗: **ログインを自前で実装すること。** パスワードハッシュ、セッション、トークン発行、パスワードリセット、ソーシャルログイン、2FA、brute-force 防御...