Chuyển đến nội dung chính

レッスン 6: OpenID Connect クライアント - A から Z までの構成

OIDC クライアント タイプ (パブリック、機密、ベアラーのみ)、管理コンソールを介したクライアントの作成と構成、OIDC 認証フロー (認証コード、暗黙的、クライアント資格情報、デバイス認証、CIBA)、PKCE、CIBA ポリシーの設定、および React および Spring Boot との実際的な統合について詳しく学びます。

🔒 DevSecOps — レッスン 6 レッスン 6: OpenID Connect クライアント - 構成 AからZまで

基本から上級までの Keycloak

パート 2: SSO プロトコル - OpenID Connect と SAML

xdev.asia

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/logoutRP によるログアウト
トークンのイントロスペクション/realms/{realm}/protocol/openid-connect/token/introspectトークンの有効性を確認する
トークンの取り消し/realms/{realm}/protocol/openid-connect/revokeトークンの取り消し
JWKS/realms/{realm}/protocol/openid-connect/certsJWT検証用の公開鍵
デバイスの認証/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 クライアントの作成手順

  1. アクセス管理コンソール→ レルムを選択 →クライアント → クライアントの作成

  2. 一般設定:

    • クライアントの種類: OpenID コネクト
    • クライアントID: 私のアプリ(一意の識別子)
    • 名前:マイアプリケーション(表示名)
    • 説明: クライアントの説明
    • 常に UI に表示: オフ
  3. 機能構成:

    • クライアント認証:ON(秘密)またはOFF(公開)
    • 認可: きめ細かい認証が必要な場合は ON
    • 認証の流れ: 適切なフローを選択します
  4. ログイン設定:

    • ルート 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 admin

Tạ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=false

Tạ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 フロントチャネルログアウト)
バックチャネルのログアウト URLURL が 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) は、認可コード フローを認可コード傍受攻撃から保護します。パブリッククライアントに必須そしてすべてのクライアントに推奨.

仕組み:

  1. クライアントが作成されましたコード検証者(ランダムな文字列 43 ~ 128 文字)

  2. クライアントが計算するコードチャレンジ= Base64URL(SHA256(コード検証者))

  3. 送信コードチャレンジ認可リクエストで

  4. 送信コード検証者トークンリクエスト内 — Keycloakはハッシュ化と比較によって検証します

KeycloakでPKCEを構成します。

「クライアント」→「タブ」に移動します高度な → 詳細設定:

設定価値説明する
コード交換コードチャレンジ方式の証明キーS256SHA-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: ON

Request (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 パイプライン

構成:

  1. 作成する機密クライアント (クライアント認証= オン)

  2. オンにするサービスアカウントの役割機能構成内

  3. サービス アカウントに役割を割り当てる: クライアント →サービスアカウントの役割タブ

# 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 ツール。ユーザーは別のデバイス (電話、ラップトップ) でコードを使用して認証します。

構成:

  1. クライアント → 機能設定 → 有効化OAuth 2.0 デバイス認証付与

  2. レルム設定 → 構成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 構成:

  1. クライアント → 機能設定 → 有効化OIDC CIBA 助成金

  2. レルム設定 → 認証 → タブ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 「サービスアカウントロール」タブ

サービス アカウントにロールを割り当てます (クライアント認証情報フロー):

  1. クライアント→タブを開くサービスアカウントの役割

  2. クリック役割の割り当て

  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 のパブリック クライアントを作成する

  1. クライアントの作成リアクトスパラボとクライアント認証= オフ

  2. 構成: 有効なリダイレクト URI =http://localhost:3000/*、Web オリジン =http://localhost:3000

  3. PKCE を有効にする: 詳細 → PKCE コード チャレンジ メソッド =S256

  4. Reactアプリを作成、インストールキークローク-js、統合されたログイン/ログアウト

  5. ブラウザの [DevTools] → [Application] → [Network] タブでトークンを確認します。

ラボ 2: Spring Boot API 用の Confidential クライアントを作成する

  1. クライアントの作成スプリングAPIラボとクライアント認証= オン

  2. オンにするサービスアカウントの役割

  3. 役割を割り当てる管理者。管理者サービスアカウント用

  4. Spring Boot プロジェクトを作成するスプリングブートスターターoauth2リソースサーバー

  5. エンドポイントの実装/api/私JWTからユーザー情報を返します

  6. でテストしますカールBearer トークンを送信する

ラボ 3: クライアント認証情報のフロー

  1. クライアントの作成バッチワーカークライアント資格情報のみのフロー

  2. トークンを取得しますカール

  3. 取得したトークンを使用して API エンドポイントを呼び出します

  4. トークンの内容を確認するにはjwt.io(開発専用)

ラボ 4: デバイス認証フロー

  1. パブリッククライアントを作成するクリツールDevice Authorization Grant が有効な場合

  2. 使用カールデバイスフローをシミュレートするには:

    • デバイスコードをリクエストする
    • ブラウザで認証URIを開き、ユーザーコードを入力します
    • トークンのポーリング
  3. 受け取ったトークンを確認する

# 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