1. KeycloakのOpenID Connectの概要
OpenID Connect (OIDC) は、OAuth 2.0 プラットフォーム上に構築された認証プロトコルです。 KeycloakはOIDC仕様を完全にサポートし、企業向けに多くの機能を拡張します。この記事では、OIDC クライアントの作成、構成、統合について詳しく説明します。
KeycloakのOIDCエンドポイント
KeycloakはOIDC標準エンドポイントを提供します。すべてのエンドポイント情報は次の方法で取得できます。よく知られた構成:
GET https://<keycloak-host>/realms/<realm-name>/.well-known/openid-configuration
重要なエンドポイント:
| 終点 | URLパターン | 目的 |
|---|---|---|
| 認可 | /realms/{realm}/プロトコル/openid-connect/auth | 認証フローの初期化 |
| トークン | /realms/{realm}/プロトコル/openid-connect/token | トークンの取得/更新 |
| ユーザー情報 | /realms/{realm}/プロトコル/openid-connect/userinfo | ユーザー情報を取得する |
| ログアウト | /realms/{realm}/プロトコル/openid-connect/logout | RP によるログアウト |
| トークンのイントロスペクション | /realms/{realm}/protocol/openid-connect/token/introspect | トークンの有効性を確認する |
| トークンの取り消し | /realms/{realm}/protocol/openid-connect/revoke | トークンの取り消し |
| JWKS | /realms/{realm}/protocol/openid-connect/certs | JWT検証用の公開鍵 |
| デバイスの認証 | /realms/{realm}/protocol/openid-connect/auth/device | デバイス認証付与 |
2. OIDC クライアントの種類
Keycloakは、次の3つの主要なタイプのクライアントをサポートしており、それぞれが異なるアプリケーション・アーキテクチャに適しています。
2.1 パブリッククライアント
クライアントはクライアント シークレットを保護できません。通常、アプリケーションは完全にブラウザーまたはモバイル デバイス上で実行されます。
特性: クライアント シークレットなし、リダイレクト URI による認証
ユースケース: シングル ページ アプリケーション (React、Angular、Vue)、モバイル アプリ、デスクトップ アプリ
認証フロー: 認証コード + PKCE (必須)
構成:
クライアント認証= オフ
// Ví dụ: SPA không có backend — PHẢI dùng Public Client + PKCE
// Client KHÔNG lưu trữ secret, chỉ dùng code_verifier/code_challenge
Client ID: my-spa-app
Client authentication: OFF
Valid redirect URIs: http://localhost:3000/*
Web origins: http://localhost:3000
2.2 機密クライアント
クライアントには、クライアント シークレット (通常はサーバー側アプリケーション) を保護する機能があります。
特性: クライアント シークレットまたは秘密キーがあり、トークン エンドポイントの呼び出し時に認証されます
ユースケース: サーバーサイド Web アプリ (Spring Boot、Django、.NET)、バックエンド API、サービス間通信
認証フロー: 認証コード、クライアント資格情報、またはその両方
構成:
クライアント認証= オン
// Ví dụ: Spring Boot backend app — dùng Confidential Client
Client ID: my-backend-api
Client authentication: ON
Client secret: auto-generated hoặc custom
Valid redirect URIs: http://localhost:8081/login/oauth2/code/keycloak
2.3 ベアラー専用クライアント (レガシー)
クライアントはベアラー トークンを受信して検証するだけであり、ログイン フローは開始しません。
特性: リダイレクト URI なし、受信トークンのみを検証します
ユースケース: 純粋な API サービス、マイクロサービスは認証されたリクエストのみを受信します
注記: Keycloak 25 以降では、ベアラーのみが無効になっていました廃止された。代わりに、機密クライアントを作成し、それを有効にするだけです
サービスアカウントの役割
| 特性 | 公共 | 機密 | ベアラーのみ (非推奨) |
|---|---|---|---|
| クライアント認証 | オフ | の上 | 該当なし |
| クライアントシークレット | そうではない | 持っている | そうではない |
| ログインを初期化できる | 持っている | 持っている | そうではない |
| リダイレクトURI | 義務的 | 義務的 | そうではない |
| PKCE | 義務的 | オプション | 該当なし |
| 主な使用例 | SPA、モバイル | サーバーアプリ | 純粋なAPI |
3. 管理コンソールから OIDC クライアントを作成する
3.1 クライアントの作成手順
アクセス管理コンソール→ レルムを選択 →クライアント → クライアントの作成
一般設定:
- クライアントの種類: OpenID コネクト
- クライアントID:
私のアプリ(一意の識別子) - 名前:マイアプリケーション(表示名)
- 説明: クライアントの説明
- 常に UI に表示: オフ
機能構成:
- クライアント認証:ON(秘密)またはOFF(公開)
- 認可: きめ細かい認証が必要な場合は ON
- 認証の流れ: 適切なフローを選択します
ログイン設定:
- ルート URL、ホーム URL、有効なリダイレクト URI、有効なログアウト後のリダイレクト URI、Web オリジン
3.2 管理 CLI を使用したクライアントの作成
# Đăng nhập Admin CLI bin/kcadm.sh config credentials \ --server http://localhost:8080 \ --realm master \ --user admin \ --password adminTạo confidential client
bin/kcadm.sh create clients -r my-company
-s clientId=my-backend-app
-s name="My Backend Application"
-s enabled=true
-s protocol=openid-connect
-s publicClient=false
-s 'redirectUris=["http://localhost:8081/*"]'
-s 'webOrigins=["http://localhost:8081"]'
-s serviceAccountsEnabled=true
-s directAccessGrantsEnabled=falseTạo public client
bin/kcadm.sh create clients -r my-company
-s clientId=my-spa-app
-s name="My SPA Application"
-s enabled=true
-s protocol=openid-connect
-s publicClient=true
-s 'redirectUris=["http://localhost:3000/*"]'
-s 'webOrigins=["http://localhost:3000"]'
-s directAccessGrantsEnabled=false
3.3 管理REST APIを使用したクライアントの作成
# Lấy access token ACCESS_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/master/protocol/openid-connect/token" \ -d "client_id=admin-cli" \ -d "username=admin" \ -d "password=admin" \ -d "grant_type=password" | jq -r '.access_token')Tạo client
curl -s -X POST
"http://localhost:8080/admin/realms/my-company/clients"
-H "Authorization: Bearer $ACCESS_TOKEN"
-H "Content-Type: application/json"
-d '{ "clientId": "my-backend-app", "name": "My Backend Application", "enabled": true, "protocol": "openid-connect", "publicClient": false, "redirectUris": ["http://localhost:8081/*"], "webOrigins": ["http://localhost:8081"], "serviceAccountsEnabled": true, "directAccessGrantsEnabled": false, "attributes": { "pkce.code.challenge.method": "S256" } }'
4. クライアントの詳細設定
4.1 一般設定
| 設定 | 説明する | 注記 |
|---|---|---|
| クライアントID | クライアントの一意の識別子 | 作成後は変更できません |
| 名前 | 表示名 | ローカリゼーション キーのサポート:${私のクライアント名} |
| 説明 | クライアントの説明 | |
| 常に UI に表示 | アカウントコンソールに常に表示されます | 社内ツールに使用 |
4.2 アクセス設定
| 設定 | 説明する | 例えば |
|---|---|---|
| ルートURL | 元の URL、相対 URL の先頭に追加 | http://localhost:3000 |
| ホームURL | クライアントにリダイレクトするときのデフォルトの URL | /ダッシュボード |
| 有効なリダイレクト URI | 有効なリダイレクト URI のリスト (ワイルドカード *) | http://localhost:3000/* |
| 有効なログアウト後のリダイレクト URI | ログアウト後の有効な URI | +(リダイレクトURIを継承) |
| ウェブオリジン | CORS で許可されるオリジン | +(リダイレクトURIを継承) |
| 管理者URL | バックチャネル操作用の URL | バックエンド URL (ログアウト、ポリシーの適用) |
リダイレクト URI に関するセキュリティ上の注意:
一度もないワイルドカードを使用する
*運用環境でのリダイレクト URI — これは脆弱性ですオープンリダイレクト宣言するその通り必要なリダイレクト URI
実稼働環境で HTTPS を使用する
本番リダイレクト URI では localhost の使用を避ける
# ❌ KHÔNG NÊN — quá rộng, dễ bị Open Redirect attack
Valid redirect URIs: *
# ❌ KHÔNG NÊN — wildcard domain
Valid redirect URIs: https://*.example.com/*
# ✅ NÊN — khai báo chính xác
Valid redirect URIs:
https://myapp.example.com/callback
https://myapp.example.com/silent-renew
4.3 機能構成
| 設定 | 説明する | いつオンにするか |
|---|---|---|
| クライアント認証 | ON = 機密、OFF = 公開 | サーバーアプリの場合はオン |
| 認可 | きめ細かい認可 (UMA) | リソースベースの権限が必要な場合 |
| 標準流量 | 認可コードの流れ | ほとんどの使用例 |
| 直接アクセス許可 | リソース所有者のパスワード認証情報 | 従来のアプリ (非推奨) |
| 暗黙的なフロー | 暗黙的な許可 (非推奨) | 使用すべきではありません |
| サービスアカウントの役割 | クライアント資格情報の付与 | マシン間の認証 |
| OAuth 2.0 デバイス認証付与 | デバイスコードフロー | スマート TV、CLI ツール |
| OIDC CIBA 助成金 | クライアント開始のバックチャネル認証 | 銀行、通信 |
4.4 ログイン設定
| 設定 | 説明する |
|---|---|
| ログインテーマ | このクライアントのログインページのテーマ |
| 同意が必要です | ユーザーに同意画面を表示する |
| クライアントを画面に表示する | 同意画面にクライアント名を表示します |
| クライアントの同意画面のテキスト | 同意のためのカスタムテキスト |
4.5 ログアウト設定
| 設定 | 説明する |
|---|---|
| フロントチャネルのログアウト | ブラウザリダイレクトによるログアウト (OpenID Connect フロントチャネルログアウト) |
| バックチャネルのログアウト URL | URL が Keycloak からのバックチャネル ログアウト リクエストを受信します |
| バックチャネルのログアウトセッションが必要です | ログアウト トークンにセッション ID を含めます。 |
| バックチャネル ログアウトによりオフライン セッションが取り消される | ログアウト時にオフラインセッションを取り消す |
# Ví dụ Backchannel Logout URL cho Spring Boot
Backchannel logout URL: http://localhost:8081/logout/connect/back-channel/keycloak
# Ví dụ Front Channel Logout URL
Front channel logout URL: http://localhost:3000/logout-callback
5. OIDC 認証フローの詳細
5.1 認可コードの流れ
この流れでOKです一番おすすめほとんどのユースケースに対応します。ユーザーはKeycloakログインページにリダイレクトされます。認証が成功すると、Keycloakは認可コードを返し、クライアントはコードをトークンと交換します。
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Client │ │ Keycloak │
│ (Browser)│ │ (App) │ │ (IdP) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ 1. Click Login│ │
│───────────────>│ │
│ │ 2. Redirect │
│<───────────────│ /auth? │
│ │ response_type │
│ │ =code& │
│ │ client_id=... │
│ 3. Login page │ │
│───────────────────────────────>│
│ 4. Enter credentials │
│───────────────────────────────>│
│ 5. Redirect with code │
│<──────────────────────────────│
│───────────────>│ │
│ │ 6. Exchange │
│ │ code for │
│ │ tokens │
│ │───────────────>│
│ │ 7. Tokens │
│ │<──────────────│
│ 8. Authenticated│ │
│<───────────────│ │
リクエスト認証コード:
GET /realms/my-company/protocol/openid-connect/auth?
response_type=code&
client_id=my-app&
redirect_uri=http://localhost:3000/callback&
scope=openid profile email&
state=random-state-value&
nonce=random-nonce-value
トークンの交換コード:
POST /realms/my-company/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code& code=AUTH_CODE_FROM_CALLBACK& client_id=my-app& client_secret=CLIENT_SECRET& redirect_uri=http://localhost:3000/callback
応答:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIs...",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"not-before-policy": 0,
"session_state": "a-session-id",
"scope": "openid profile email"
}
5.2 認可コードフロー + PKCE
PKCE (Proof Key for Code Exchange、RFC 7636) は、認可コード フローを認可コード傍受攻撃から保護します。パブリッククライアントに必須そしてすべてのクライアントに推奨.
仕組み:
クライアントが作成されました
コード検証者(ランダムな文字列 43 ~ 128 文字)クライアントが計算する
コードチャレンジ= Base64URL(SHA256(コード検証者))送信
コードチャレンジ認可リクエストで送信
コード検証者トークンリクエスト内 — Keycloakはハッシュ化と比較によって検証します
KeycloakでPKCEを構成します。
「クライアント」→「タブ」に移動します高度な → 詳細設定:
| 設定 | 価値 | 説明する |
|---|---|---|
| コード交換コードチャレンジ方式の証明キー | S256 | SHA-256 を使用した PKCE が必要 (推奨) |
| 無地。無地 | プレーンテキストの PKCE (安全ではありません) | |
| (空の) | PKCEは必要ありません |
PKCE フロー:
# 1. Tạo code_verifier (client-side) code_verifier="dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"2. Tạo code_challenge = Base64URL(SHA256(code_verifier))
code_challenge="E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
3. Authorization request với code_challenge
GET /realms/my-company/protocol/openid-connect/auth? response_type=code& client_id=my-spa-app& redirect_uri=http://localhost:3000/callback& scope=openid profile email& state=random-state& code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& code_challenge_method=S256
4. Token request với code_verifier
POST /realms/my-company/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code& code=AUTH_CODE& client_id=my-spa-app& redirect_uri=http://localhost:3000/callback& code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
5.3 暗黙的なフロー (非推奨)
使用すべきではありません。OAuth 2.0 Security Best Practice (RFC 9700) では、トークンは URL フラグメントを介して返され、ブラウザ履歴やリファラー ヘッダーを介して簡単に盗まれる可能性があるため、暗黙的フローの使用を推奨しません。
交換する:SPA を含むすべてのクライアントに対して認証コード フロー + PKCE を使用します。
レガシー システムをサポートする必要がある場合:
# Bật Implicit Flow trong client settings Capability Config → Implicit flow: ONRequest (trả về token trực tiếp)
GET /realms/my-company/protocol/openid-connect/auth? response_type=id_token token& client_id=legacy-app& redirect_uri=http://localhost:3000/callback& scope=openid profile& state=random-state& nonce=random-nonce
5.4 クライアント認証情報のフロー
のためにマシン間の認証— ユーザーとの対話はありません。クライアントは、独自の資格情報を使用して自身を認証します。
使用例:
マイクロサービスがマイクロサービスを呼び出す
バックエンドのバッチジョブ
スケジュールされたタスクには API アクセスが必要です
CI/CD パイプライン
構成:
作成する機密クライアント (
クライアント認証= オン)オンにするサービスアカウントの役割機能構成内
サービス アカウントに役割を割り当てる: クライアント →サービスアカウントの役割タブ
# Request token
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=my-service&
client_secret=MY_CLIENT_SECRET&
scope=openid
# Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 300,
"token_type": "Bearer",
"not-before-policy": 0,
"scope": "openid profile email"
}
# Lưu ý: KHÔNG có refresh_token và id_token trong Client Credentials flow
5.5 デバイス認証付与 (RFC 8628)
入力が制限されているデバイス向け - スマート TV、IoT デバイス、CLI ツール。ユーザーは別のデバイス (電話、ラップトップ) でコードを使用して認証します。
構成:
クライアント → 機能設定 → 有効化OAuth 2.0 デバイス認証付与
レルム設定 → 構成OAuthデバイスコード寿命 (デフォルトは 600 秒)
# Bước 1: Device request — lấy device code và user code
POST /realms/my-company/protocol/openid-connect/auth/device
Content-Type: application/x-www-form-urlencoded
client_id=my-tv-app
# Response
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "http://localhost:8080/realms/my-company/device",
"verification_uri_complete": "http://localhost:8080/realms/my-company/device?user_code=WDJB-MJHT",
"expires_in": 600,
"interval": 5
}
# Bước 2: Hiển thị user_code và verification_uri trên TV/device
# User truy cập verification_uri trên phone/laptop, nhập user_code, đăng nhập
# Bước 3: Device polling — kiểm tra xem user đã xác thực chưa
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code&
client_id=my-tv-app&
device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
# Response khi user chưa xác thực
{
"error": "authorization_pending",
"error_description": "The authorization request is still pending"
}
# Response khi user đã xác thực — nhận tokens
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"id_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 300
}
5.6 CIBA — クライアント開始バックチャネル認証 (OIDC CIBA)
CIBA により、クライアントは認証を開始できるようになりますブラウザ経由でユーザーをリダイレクトする必要はありません。代わりに、Keycloakは別のチャネル(プッシュ通知、SMS、電子メール)経由でユーザーに認証リクエストを送信します。
使用例:
銀行業: POS はモバイルアプリ経由で支払いを認証します
電気通信: SIMベースの認証
コールセンター: エージェントが電話で顧客を認証します
CIBA 構成:
クライアント → 機能設定 → 有効化OIDC CIBA 助成金
レルム設定 → 認証 → タブCIBAポリシー:
| 設定 | 説明する | デフォルト値 |
|---|---|---|
| バックチャネルトークン配信モード | ポーリング、ping、またはプッシュ | 世論調査。世論調査 |
| 有効期限切れの印刷 | 認証リクエストの有効期限 | 120秒 |
| 間隔 | ポーリングリクエスト間の間隔 | 5秒 |
| 認証を要求されたユーザーのヒント | ユーザーヒントのタイプ:login_hint、login_hint_token、id_token_hint | ログインヒント |
# CIBA authentication request
POST /realms/my-company/protocol/openid-connect/ext/ciba/auth
Content-Type: application/x-www-form-urlencoded
client_id=my-pos-app&
client_secret=CLIENT_SECRET&
scope=openid&
[email protected]&
binding_message=Xac+nhan+thanh+toan+500k
# Response
{
"auth_req_id": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 120,
"interval": 5
}
# Polling for token (giống Device Auth)
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:openid:params:grant-type:ciba&
client_id=my-pos-app&
client_secret=CLIENT_SECRET&
auth_req_id=eyJhbGciOiJSUzI1NiIs...
カスタム CIBA 認証チャネル プロバイダー:
デフォルトではKeycloakが使用されますCIBAログインユーザーリゾルバー内部。実際のプッシュ通知を送信するには、カスタム SPI を実装する必要があります。
// Implement interface CIBAAuthenticationChannelProvider public class MyCIBAChannelProvider implements CIBAAuthenticationChannelProvider {@Override public void requestAuthentication( CIBALoginUserResolver.CIBALoginUser user, AuthenticationChannelRequest request) { // Gửi push notification đến user's device // binding_message: "Xác nhận thanh toán 500k" pushNotificationService.send( user.getDeviceToken(), request.getBindingMessage(), request.getAuthResultUrl() ); } @Override public boolean verifyAuthentication(String authResultId) { // Verify kết quả từ user's device return authResultStore.isApproved(authResultId); }
}
6. OIDC クライアントと React (SPA) を統合する
6.1 keycloak-jsアダプターの使用
Keycloak は SPA 用の公式 JavaScript アダプターを提供します。
# Cài đặt
npm install keycloak-js
React 用に Keycloak クライアントを構成します。
Client ID: my-react-app
Client authentication: OFF (public client)
Valid redirect URIs: http://localhost:3000/*
Valid post logout redirect URIs: http://localhost:3000/*
Web origins: http://localhost:3000
PKCE Code Challenge Method: S256
React で Keycloak を初期化します。
// src/keycloak.ts import Keycloak from "keycloak-js";const keycloak = new Keycloak({ url: "http://localhost:8080", realm: "my-company", clientId: "my-react-app", });
export default keycloak;
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import keycloak from "./keycloak";
keycloak
.init({
onLoad: "login-required", // hoặc 'check-sso'
pkceMethod: "S256",
checkLoginIframe: false, // tắt cho production tránh cookie issues
silentCheckSsoRedirectUri:
window.location.origin + "/silent-check-sso.html",
})
.then((authenticated) => {
if (authenticated) {
console.log("User is authenticated");
console.log("Token:", keycloak.token);
console.log("User info:", keycloak.tokenParsed);
// Auto-refresh token trước khi hết hạn
setInterval(() => {
keycloak
.updateToken(70) // refresh nếu token hết hạn trong 70 giây
.then((refreshed) => {
if (refreshed) {
console.log("Token was refreshed");
}
})
.catch(() => {
console.error("Failed to refresh token");
keycloak.login(); // redirect về login nếu refresh thất bại
});
}, 60000);
ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
).render(
<React.StrictMode>
<App keycloak={keycloak} />
</React.StrictMode>
);
} else {
console.warn("Not authenticated");
keycloak.login();
}
})
.catch((error) => {
console.error("Keycloak init failed:", error);
});
// src/App.tsx
import Keycloak from "keycloak-js";
interface AppProps {
keycloak: Keycloak;
}
function App({ keycloak }: AppProps) {
const handleLogout = () => {
keycloak.logout({
redirectUri: window.location.origin,
});
};
const callApi = async () => {
// Tự động gắn Bearer token vào API calls
const response = await fetch("http://localhost:8081/api/data", {
headers: {
Authorization: `Bearer ${keycloak.token}`,
},
});
const data = await response.json();
console.log(data);
};
return (
<div>
<h1>Welcome, {keycloak.tokenParsed?.preferred_username}</h1>
<p>Email: {keycloak.tokenParsed?.email}</p>
<p>Roles: {keycloak.tokenParsed?.realm_access?.roles?.join(", ")}</p>
<button onClick={callApi}>Call API</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
export default App;
6.2 React-oidc-context の使用 (keycloak-js を置き換える)
別のオプションはライブラリを使用することです反応-oidc-コンテキストに基づくoidc-クライアント-ts— Keycloak 固有のアダプターに依存しません。
npm install react-oidc-context oidc-client-ts
// src/main.tsx
import { AuthProvider } from "react-oidc-context";
const oidcConfig = {
authority: "http://localhost:8080/realms/my-company",
client_id: "my-react-app",
redirect_uri: "http://localhost:3000/callback",
post_logout_redirect_uri: "http://localhost:3000",
scope: "openid profile email",
automaticSilentRenew: true,
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<AuthProvider {...oidcConfig}>
<App />
</AuthProvider>
);
// src/App.tsx
import { useAuth } from "react-oidc-context";
function App() {
const auth = useAuth();
if (auth.isLoading) return <div>Loading...</div>;
if (auth.error) return <div>Error: {auth.error.message}</div>;
if (!auth.isAuthenticated) {
return <button onClick={() => auth.signinRedirect()}>Login</button>;
}
return (
<div>
<p>Welcome, {auth.user?.profile.preferred_username}</p>
<button onClick={() => auth.removeUser()}>Logout</button>
</div>
);
}
7. OIDC クライアントと Spring Boot を統合する
7.1 Spring Boot OAuth2 リソースサーバー
Spring Boot 構成は次のことを行いますリソースサーバー— Keycloak からの JWT トークンを検証します。
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/my-company
jwk-set-uri: http://localhost:8080/realms/my-company/protocol/openid-connect/certs
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public 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(jwtAuthenticationConverter())
)
);
return http.build();
}
// Custom converter để map Keycloak realm_access.roles → Spring Security authorities
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
List<GrantedAuthority> authorities = new ArrayList<>();
// Extract realm roles
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null) {
List<String> roles = (List<String>) realmAccess.get("roles");
if (roles != null) {
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}
}
// Extract client roles
Map<String, Object> resourceAccess = jwt.getClaimAsMap("resource_access");
if (resourceAccess != null) {
Map<String, Object> clientAccess =
(Map<String, Object>) resourceAccess.get("my-backend-app");
if (clientAccess != null) {
List<String> clientRoles = (List<String>) clientAccess.get("roles");
if (clientRoles != null) {
clientRoles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role))
);
}
}
}
return authorities;
});
return converter;
}
}
7.2 Spring Boot OAuth2 クライアント (サーバー側ログイン)
Spring Boot 構成は次のことを行いますOAuth2クライアント— サーバー側のログイン フロー:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
# application.yml
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-backend-app
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
provider:
keycloak:
issuer-uri: http://localhost:8080/realms/my-company
user-name-attribute: preferred_username
Spring Boot OAuth2 クライアントの Keycloak クライアント設定:
Client ID: my-backend-app
Client authentication: ON (confidential)
Valid redirect URIs: http://localhost:8081/login/oauth2/code/keycloak
Backchannel logout URL: http://localhost:8081/logout/connect/back-channel/keycloak
Web origins: http://localhost:8081
8. クライアントの詳細設定
8.1 詳細タブ
タブの詳細設定高度なクライアントの:
| 設定 | 説明する | 推奨値 |
|---|---|---|
| アクセストークンの有効期間 | このクライアントのレルムレベルのトークンの有効期間をオーバーライドします | 空白のままにします = レルムレベルを使用します |
| クライアントセッションアイドル状態 | クライアントセッションのアイドルタイムアウトをオーバーライドする | 空白のままにします = レルムレベルを使用します |
| クライアントセッション最大値 | クライアントセッションの最大存続期間を上書きする | 空白のままにします = レルムレベルを使用します |
| クライアントのオフライン セッションのアイドル状態 | オフライン セッションのアイドル タイムアウトをオーバーライドする | 空白のままにします = レルムレベルを使用します |
| クライアントのオフライン セッションの最大値 | オフラインセッションの最大存続期間を上書きする | 空白のままにします = レルムレベルを使用します |
| PKCEコードチャレンジ方式 | 必須のPKCEメソッド | S256 |
| プッシュされた承認リクエストが必要です | 必須の PAR (RFC 9126) | セキュリティの高いアプリの場合はオン |
| ACR から LoA へのマッピング | ACR値のマッピング → 保証レベル | ステップアップ認証を構成する |
8.2 [認証情報] タブ (機密クライアント)
クライアントの認証情報を管理します。
クライアント認証子: クライアント ID とシークレット (デフォルト)、署名付き JWT (client_secret_jwt)、秘密キー付き署名付き JWT (private_key_jwt)、X.509 証明書
クライアントシークレット: 侵害された場合は再生成します
登録アクセストークン: 動的クライアント登録に使用されます。
8.3 「サービスアカウントロール」タブ
サービス アカウントにロールを割り当てます (クライアント認証情報フロー):
クライアント→タブを開くサービスアカウントの役割
クリック役割の割り当て
割り当てるレルム ロールまたはクライアント ロールを選択します
# Ví dụ gán role bằng Admin CLI
# Lấy service account user ID
SERVICE_ACCOUNT_ID=$(bin/kcadm.sh get clients/$CLIENT_UUID/service-account-user \
-r my-company --fields id --format csv --noquotes)
# Gán realm role
bin/kcadm.sh add-roles -r my-company \
--uusername service-account-my-service \
--rolename admin
# Gán client role
bin/kcadm.sh add-roles -r my-company \
--uusername service-account-my-service \
--cclientid target-client \
--rolename manage-users
9. 練習問題
ラボ 1: React SPA のパブリック クライアントを作成する
クライアントの作成
リアクトスパラボとクライアント認証= オフ構成: 有効なリダイレクト URI =
http://localhost:3000/*、Web オリジン =http://localhost:3000PKCE を有効にする: 詳細 → PKCE コード チャレンジ メソッド =
S256Reactアプリを作成、インストール
キークローク-js、統合されたログイン/ログアウトブラウザの [DevTools] → [Application] → [Network] タブでトークンを確認します。
ラボ 2: Spring Boot API 用の Confidential クライアントを作成する
クライアントの作成
スプリングAPIラボとクライアント認証= オンオンにする
サービスアカウントの役割役割を割り当てる
管理者。管理者サービスアカウント用Spring Boot プロジェクトを作成する
スプリングブートスターターoauth2リソースサーバーエンドポイントの実装
/api/私JWTからユーザー情報を返しますでテストします
カールBearer トークンを送信する
ラボ 3: クライアント認証情報のフロー
クライアントの作成
バッチワーカークライアント資格情報のみのフロートークンを取得します
カール取得したトークンを使用して API エンドポイントを呼び出します
トークンの内容を確認するにはjwt.io(開発専用)
ラボ 4: デバイス認証フロー
パブリッククライアントを作成する
クリツールDevice Authorization Grant が有効な場合使用
カールデバイスフローをシミュレートするには:- デバイスコードをリクエストする
- ブラウザで認証URIを開き、ユーザーコードを入力します
- トークンのポーリング
受け取ったトークンを確認する
# Script test Device Authorization Flow
#!/bin/bash
REALM=my-company
CLIENT_ID=cli-tool
KC_URL=http://localhost:8080
# Bước 1: Request device code
RESPONSE=$(curl -s -X POST \
"$KC_URL/realms/$REALM/protocol/openid-connect/auth/device" \
-d "client_id=$CLIENT_ID")
DEVICE_CODE=$(echo $RESPONSE | jq -r '.device_code')
USER_CODE=$(echo $RESPONSE | jq -r '.user_code')
VERIFY_URI=$(echo $RESPONSE | jq -r '.verification_uri_complete')
INTERVAL=$(echo $RESPONSE | jq -r '.interval')
echo "========================================"
echo "Mở URL sau trên browser:"
echo "$VERIFY_URI"
echo "Hoặc truy cập: $(echo $RESPONSE | jq -r '.verification_uri')"
echo "Nhập code: $USER_CODE"
echo "========================================"
# Bước 2: Polling for token
while true; do
sleep $INTERVAL
TOKEN_RESPONSE=$(curl -s -X POST \
"$KC_URL/realms/$REALM/protocol/openid-connect/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "client_id=$CLIENT_ID" \
-d "device_code=$DEVICE_CODE")
ERROR=$(echo $TOKEN_RESPONSE | jq -r '.error // empty')
if [ -z "$ERROR" ]; then
echo "Xác thực thành công!"
echo "Access Token: $(echo $TOKEN_RESPONSE | jq -r '.access_token' | head -c 50)..."
break
elif [ "$ERROR" = "authorization_pending" ]; then
echo "Đang chờ user xác thực..."
elif [ "$ERROR" = "slow_down" ]; then
INTERVAL=$((INTERVAL + 5))
echo "Slow down, tăng interval lên ${INTERVAL}s"
else
echo "Error: $ERROR"
break
fi
done