Skip to content
Published on

Keycloak 連携ハンズオン — Docker で起動し Realm・Client を設定、Spring Boot・Next.js 連携まで (2025 ハンズオンガイド)

Authors

プロローグ — 認証を自前で作るな

新規プロジェクトで最もよくある失敗: ログインを自前で実装すること。 パスワードハッシュ、セッション、トークン発行、パスワードリセット、ソーシャルログイン、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 つ
ClientKeycloak に登録されたアプリケーション。フロントエンド SPA、バックエンド API それぞれが Client
Userエンドユーザーアカウント。Realm に所属
Role権限のまとまり。Realm Role (グローバル) と Client Role (アプリ別) がある
GroupUser のまとまり。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-devstart
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 を新しく作る。

コンソールで作る

  1. 左上の realm ドロップダウン → Create realm
  2. Realm name: demo
  3. Create

これ以降のすべての作業は demo realm の中で行う。

知っておくべき Realm 設定 (Realm settings)

  • Tokens タブ
    • Access Token Lifespan — デフォルト 5 分。短いほど安全、長いほど楽。5〜15 分が一般的。
    • SSO Session Idle / Max — refresh token の寿命を左右する。
  • Login タブ
    • User registration — セルフ会員登録を許可するか
    • Forgot passwordRemember meEmail as username
  • Sessions タブ — セッション寿命の細かい調整
  • Security defenses タブBrute Force Detection をオンに (10 章)

今はデフォルト値のまま進める。


3章 · Client 設定 — 2 つのタイプ

Client は「Keycloak に登録されたアプリ」だ。今回のハンズオンでは 2 つ作る:

  • demo-frontendPublic Client (Next.js SPA)。シークレットなし、PKCE を使用。
  • demo-backendConfidential Client (API サーバー)。シークレットあり。

Public Client — フロントエンド用

左の ClientsCreate client:

  1. General settings
    • Client type: OpenID Connect
    • Client ID: demo-frontend
  2. Capability config
    • Client authentication: Off (← これが Public にする鍵)
    • Standard flow: On (Authorization Code Flow)
    • Direct access grants: Off (本番ではオフにする)
  3. 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:

  1. Client ID: demo-backend
  2. Capability config
    • Client authentication: On (← Confidential)
    • Standard flow: On
    • Service accounts roles: On (サーバー間通信用 — Client Credentials Grant)
  3. 作成後、Credentials タブClient Secret をコピーしておく。

バックエンドが純粋な「Resource Server」(トークン検証のみ) なら、実はシークレットは不要だ。JWKS で検証するだけでよいからだ。シークレットは、バックエンドが 自分でトークンを発行してもらう必要があるとき (Service Account、Token Exchange) に使う。

重要な Client 設定まとめ

設定意味よくある失敗
Valid redirect URIsログイン後に戻れる URL のホワイトリスト広すぎる * → open redirect のリスク
Web originsCORS 許可オリジン空にするとブラウザ呼び出しが CORS で弾かれる
Standard flowAuthorization Code Flow の有効化SPA・Web アプリは必須
Direct access grantsusername/password を直接交換本番ではオフにしろ (テスト専用)
Service accountsClient Credentials Grantサーバー間のみ

4章 · User・Role・Group を作る

User を作る

UsersCreate new user:

  • Username: alice
  • Email: alice@demo.testEmail verified: On
  • CreateCredentials タブSet passwordalice123Temporary: Off

(Temporary: On だと初回ログイン時にパスワード変更を強制する。ハンズオンでは Off。)

Realm Role を作る

Realm rolesCreate role:

  • admin を 1 つ、user を 1 つ作る。

Role を User にマッピング

UsersaliceRole mappingAssign roleadminuser を選択。

Group でまとめる (任意だが推奨)

ユーザーが増えると User ごとに Role を付けるのは地獄だ。Group を使う。

  1. GroupsCreate groupadministrators
  2. その group の Role mappingadmin を割り当て
  3. UsersaliceGroupsJoin groupadministrators

これで 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)役割
issuerhttp://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-backend Client の 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 を見つけて公開鍵をキャッシュする。トークン署名・issexp の検証が自動的にオンになる。

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_tokenAPI 呼び出しの認可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-jskeycloak.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) — 単一障害点を排除。
  • TLSauth.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_ENABLEDKC_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 originshttp://localhost:3000 を追加。(+ を入れると redirect URI から自動推論。)

Invalid token issuer / バックエンドがトークンを拒否

  • 原因: トークンの iss とバックエンドの issuer-uri が文字まで正確に一致していない。定番は localhost vs コンテナホスト名 の不一致。
  • 例: バックエンドが Docker の中で http://keycloak:8080/realms/demo で検証しているのに、トークンの isshttp://localhost:8080/realms/demo
  • 解決: KC_HOSTNAME を固定し、すべての場所で 同じ issuer 文字列 を使う。トークンをデコードして iss を直接確認する (5 章)。

aud (audience) の検証失敗

  • 原因: バックエンドが特定の audience を要求するのに、Keycloak のデフォルトトークンの audaccount
  • 解決: 次のどちらか —
    1. バックエンドの audience 検証を account に合わせるか、
    2. Keycloak で Audience Mapper を作って demo-backendaud に入れる (Client scopes → 専用 scope → Mappers → Audience)。

トークンに role がない

  • 原因: Client に roles Client Scope が付いていないか、role を User にマッピングしていない。
  • 解決: Client → Client scopes タブに rolesDefault であるか確認。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-devdemo-prod を分離。master は絶対にアプリ用に使うな。
  • Client はアプリ単位で — フロントとバックエンドは別の Client。フロントは Public、バックエンドは必要に応じて Confidential。
  • role は Group で管理 — User に直接 role を付けるのは小規模だけ。大きくなったら Group + role mapping。
  • トークンを常にデコードして見る習慣 — 詰まったら jwt.iocut | base64 -d | jqissaudexproles を目で確認。推測するな。
  • Discovery ドキュメントを信頼する — エンドポイント URL をハードコードせず、.well-known/openid-configuration から読む。
  • ローカルは docker-compose + Postgres + --import-realm — realm JSON を Git に入れれば、チームメンバーが docker compose up 一度で同じ環境。
  • Access Token は短く、Refresh Token で更新 — 5〜15 分。漏洩しても寿命が短い。

アンチパターン 10 個

  1. 認証を自前で実装 — Keycloak があるのにパスワードハッシュから書く。
  2. master realm にアプリの Client・User を作る。
  3. start-dev で本番運用 (H2 DB、HTTPS なし)。
  4. Redirect URI を * で全開にしておく — open redirect。
  5. Web origins を埋めず CORS で詰まり続ける。
  6. バックエンドに id_token を送る (送るべきは access_token)。
  7. フロントエンドの Public Client で PKCE を使わない。
  8. Direct access grants (Password Grant) を本番でオンにしておく。
  9. KC_HOSTNAME を設定せず、トークンの iss 不一致で検証失敗。
  10. コンソールクリックだけで設定 — export/import なしで再現不可能な環境。

エピローグ — Keycloak は「終わり」ではなく「出発点」

この記事をやり切ったなら、今や手にしたもの:

  • Docker で起動した Keycloak 26 (+ Postgres)
  • demo realm、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 項目チェックリスト

  1. Keycloak を start-dev ではなく start モードで起動する準備ができているか?
  2. アプリ用 realm を master と分離したか?
  3. フロントは Public + PKCE、バックエンドは適切な Client タイプか?
  4. Redirect URI と Web origins を正確に (狭く) 設定したか?
  5. バックエンドが JWKS でローカル検証しているか (毎リクエストの introspection ではない)?
  6. iss · aud · exp · 署名をすべて検証しているか?
  7. role がトークンに入り、バックエンドの権限に変換されているか?
  8. バックエンドには access_token を、身元確認には id_token を使っているか?
  9. RP-Initiated Logout で Keycloak のセッションまで切っているか?
  10. Realm 設定を export/import (または IaC) で再現可能か?

次回予告

次回の候補: Keycloak Identity Provider Brokering — Google・GitHub ソーシャルログインと社内 LDAP の連携Keycloak のカスタムテーマ・拡張 (SPI) 開発Keycloak Authorization Services — UMA ベースの fine-grained な権限ハンズオン

「良い認証とは、ユーザーが意識しない認証だ。そして良い認証インフラとは、開発者が自前で作らなかったインフラだ。」

— Keycloak 連携ハンズオン、おわり。