1. Tổng quan OpenID Connect trong Keycloak
OpenID Connect (OIDC) là giao thức xác thực được xây dựng trên nền tảng OAuth 2.0. Keycloak hỗ trợ đầy đủ OIDC specification và mở rộng nhiều tính năng dành cho enterprise. Trong bài này, chúng ta sẽ đi sâu vào việc tạo, cấu hình và tích hợp OIDC clients.
OIDC Endpoints trong Keycloak
Keycloak cung cấp các endpoints chuẩn OIDC. Bạn có thể lấy toàn bộ thông tin endpoints qua Well-Known Configuration:
GET https://<keycloak-host>/realms/<realm-name>/.well-known/openid-configuration
Các endpoints quan trọng:
| Endpoint | URL Pattern | Mục đích |
|---|---|---|
| Authorization | /realms/{realm}/protocol/openid-connect/auth | Khởi tạo authentication flow |
| Token | /realms/{realm}/protocol/openid-connect/token | Lấy/refresh tokens |
| UserInfo | /realms/{realm}/protocol/openid-connect/userinfo | Lấy thông tin user |
| Logout | /realms/{realm}/protocol/openid-connect/logout | Đăng xuất (RP-Initiated Logout) |
| Token Introspection | /realms/{realm}/protocol/openid-connect/token/introspect | Kiểm tra token validity |
| Token Revocation | /realms/{realm}/protocol/openid-connect/revoke | Thu hồi token |
| JWKS | /realms/{realm}/protocol/openid-connect/certs | Public keys cho JWT verification |
| Device Authorization | /realms/{realm}/protocol/openid-connect/auth/device | Device Authorization Grant |
2. OIDC Client Types
Keycloak hỗ trợ ba loại client chính, mỗi loại phù hợp với kiến trúc ứng dụng khác nhau:
2.1 Public Client
Client không thể bảo mật client secret — thường là ứng dụng chạy hoàn toàn trên browser hoặc mobile device.
Đặc điểm: Không có client secret, xác thực qua redirect URI
Use cases: Single Page Applications (React, Angular, Vue), Mobile apps, Desktop apps
Auth flow: Authorization Code + PKCE (bắt buộc)
Cấu hình:
Client authentication= OFF
// 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 Confidential Client
Client có khả năng bảo mật client secret — thường là server-side applications.
Đặc điểm: Có client secret hoặc private key, xác thực khi gọi token endpoint
Use cases: Server-side web apps (Spring Boot, Django, .NET), Backend APIs, Service-to-service communication
Auth flow: Authorization Code, Client Credentials, hoặc cả hai
Cấu hình:
Client authentication= ON
// 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 Bearer-only Client (Legacy)
Client chỉ nhận và validate bearer tokens — không khởi tạo login flow.
Đặc điểm: Không có redirect URI, chỉ validate incoming tokens
Use cases: Pure API services, Microservices chỉ nhận requests đã xác thực
Lưu ý: Trong Keycloak 25+, bearer-only đã bị deprecated. Thay vào đó, tạo confidential client và chỉ bật
Service accounts roles
| Đặc điểm | Public | Confidential | Bearer-only (deprecated) |
|---|---|---|---|
| Client authentication | OFF | ON | N/A |
| Client secret | Không | Có | Không |
| Có thể khởi tạo login | Có | Có | Không |
| Redirect URI | Bắt buộc | Bắt buộc | Không |
| PKCE | Bắt buộc | Tùy chọn | N/A |
| Use case chính | SPA, Mobile | Server app | Pure API |
3. Tạo OIDC Client qua Admin Console
3.1 Các bước tạo Client
Truy cập Admin Console → chọn realm → Clients → Create client
General Settings:
- Client type: OpenID Connect
- Client ID:
my-app(unique identifier) - Name: My Application (tên hiển thị)
- Description: Mô tả client
- Always display in UI: OFF
Capability config:
- Client authentication: ON (confidential) hoặc OFF (public)
- Authorization: ON nếu cần fine-grained authorization
- Authentication flow: chọn các flows phù hợp
Login settings:
- Root URL, Home URL, Valid redirect URIs, Valid post logout redirect URIs, Web origins
3.2 Tạo Client bằng Admin 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 Tạo Client bằng Admin 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. Client Settings chi tiết
4.1 General Settings
| Setting | Mô tả | Ghi chú |
|---|---|---|
| Client ID | Unique identifier cho client | Không thể thay đổi sau khi tạo |
| Name | Tên hiển thị | Hỗ trợ localization key: ${my-client-name} |
| Description | Mô tả client | |
| Always display in UI | Luôn hiển thị trên Account Console | Dùng cho internal tools |
4.2 Access Settings
| Setting | Mô tả | Ví dụ |
|---|---|---|
| Root URL | URL gốc, prepend vào relative URLs | http://localhost:3000 |
| Home URL | URL mặc định khi redirect về client | /dashboard |
| Valid redirect URIs | Danh sách redirect URIs hợp lệ (wildcard *) | http://localhost:3000/* |
| Valid post logout redirect URIs | URIs hợp lệ sau logout | + (kế thừa redirect URIs) |
| Web origins | CORS allowed origins | + (kế thừa redirect URIs) |
| Admin URL | URL cho backchannel operations | URL backend (logout, policy enforcement) |
Lưu ý bảo mật cho redirect URIs:
KHÔNG BAO GIỜ sử dụng wildcard
*làm redirect URI trong production — đây là lỗ hổng Open RedirectKhai báo chính xác các redirect URIs cần thiết
Sử dụng HTTPS trong production
Tránh sử dụng localhost trong production redirect URIs
# ❌ 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 Capability Config
| Setting | Mô tả | Khi nào bật |
|---|---|---|
| Client authentication | ON = confidential, OFF = public | ON cho server apps |
| Authorization | Fine-grained authorization (UMA) | Khi cần resource-based permissions |
| Standard flow | Authorization Code Flow | Hầu hết use cases |
| Direct access grants | Resource Owner Password Credentials | Legacy apps (không khuyến nghị) |
| Implicit flow | Implicit Grant (deprecated) | Không nên dùng |
| Service accounts roles | Client Credentials Grant | Machine-to-machine auth |
| OAuth 2.0 Device Authorization Grant | Device Code Flow | Smart TV, CLI tools |
| OIDC CIBA Grant | Client-Initiated Backchannel Auth | Banking, telecom |
4.4 Login Settings
| Setting | Mô tả |
|---|---|
| Login theme | Theme cho trang đăng nhập của client này |
| Consent required | Hiển thị consent screen cho user |
| Display client on screen | Hiển thị tên client trên consent screen |
| Client consent screen text | Text tùy chỉnh cho consent |
4.5 Logout Settings
| Setting | Mô tả |
|---|---|
| Front channel logout | Logout qua browser redirect (OpenID Connect Front-Channel Logout) |
| Backchannel logout URL | URL nhận backchannel logout requests từ Keycloak |
| Backchannel logout session required | Bao gồm session ID trong logout token |
| Backchannel logout revoke offline sessions | Thu hồi offline sessions khi logout |
# 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 Authentication Flows chi tiết
5.1 Authorization Code Flow
Đây là flow được khuyến nghị nhất cho hầu hết use cases. User được redirect đến Keycloak login page, sau khi xác thực thành công, Keycloak trả về authorization code, client đổi code lấy tokens.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 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│ │
│<───────────────│ │
Request Authorization Code:
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
Exchange Code for Tokens:
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
Response:
{
"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 Authorization Code Flow + PKCE
PKCE (Proof Key for Code Exchange, RFC 7636) bảo vệ Authorization Code Flow khỏi authorization code interception attack. Bắt buộc sử dụng cho Public Clients và khuyến nghị cho tất cả clients.
Cách hoạt động:
Client tạo
code_verifier(random string 43-128 ký tự)Client tính
code_challenge= Base64URL(SHA256(code_verifier))Gửi
code_challengetrong authorization requestGửi
code_verifiertrong token request — Keycloak verify bằng cách hash và so sánh
Cấu hình PKCE trong Keycloak:
Vào client → tab Advanced → Advanced Settings:
| Setting | Giá trị | Mô tả |
|---|---|---|
| Proof Key for Code Exchange Code Challenge Method | S256 | Bắt buộc PKCE với SHA-256 (khuyến nghị) |
| plain | PKCE với plain text (không an toàn) | |
| (empty) | Không bắt buộc PKCE |
PKCE flow:
# 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 Implicit Flow (Deprecated)
KHÔNG NÊN sử dụng. OAuth 2.0 Security Best Current Practice (RFC 9700) khuyến nghị không dùng Implicit Flow vì tokens được trả về qua URL fragment, dễ bị đánh cắp qua browser history hoặc referrer header.
Thay thế: Sử dụng Authorization Code Flow + PKCE cho tất cả clients, kể cả SPAs.
Nếu buộc phải hỗ trợ legacy systems:
# 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 Client Credentials Flow
Dành cho machine-to-machine authentication — không có user interaction. Client tự xác thực bằng credentials của chính nó.
Use cases:
Microservice gọi microservice
Backend batch jobs
Scheduled tasks cần truy cập API
CI/CD pipelines
Cấu hình:
Tạo Confidential Client (
Client authentication= ON)Bật Service accounts roles trong Capability Config
Gán roles cho service account: Client → Service account roles tab
# 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 Device Authorization Grant (RFC 8628)
Dành cho thiết bị có hạn chế về input — Smart TV, IoT devices, CLI tools. User xác thực trên thiết bị khác (phone, laptop) bằng mã code.
Cấu hình:
Client → Capability Config → bật OAuth 2.0 Device Authorization Grant
Realm Settings → cấu hình OAuth Device Code lifespan (mặc định 600 giây)
# 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 — Client-Initiated Backchannel Authentication (OIDC CIBA)
CIBA cho phép client khởi tạo authentication mà không cần redirect user qua browser. Thay vào đó, Keycloak gửi authentication request đến user qua channel khác (push notification, SMS, email).
Use cases:
Banking: Point-of-sale xác thực thanh toán qua mobile app
Telecom: Xác thực SIM-based authentication
Call centers: Agent xác thực customer qua phone
Cấu hình CIBA:
Client → Capability Config → bật OIDC CIBA Grant
Realm Settings → Authentication → tab CIBA Policy:
| Setting | Mô tả | Giá trị mặc định |
|---|---|---|
| Backchannel Token Delivery Mode | poll, ping, hoặc push | poll |
| Expires In | Thời gian hết hạn authentication request | 120 giây |
| Interval | Khoảng cách giữa các polling requests | 5 giây |
| Authentication Requested User Hint | Loại user hint: login_hint, login_hint_token, id_token_hint | login_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...
Custom CIBA Authentication Channel Provider:
Mặc định Keycloak sử dụng CIBALoginUserResolver nội bộ. Để gửi push notification thực tế, bạn cần implement SPI custom:
// 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. Tích hợp OIDC Client với React (SPA)
6.1 Sử dụng keycloak-js adapter
Keycloak cung cấp JavaScript adapter chính thức cho SPAs:
# Cài đặt
npm install keycloak-js
Cấu hình Keycloak client cho React:
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
Khởi tạo Keycloak trong React:
// 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 Sử dụng react-oidc-context (thay thế keycloak-js)
Một lựa chọn khác là sử dụng thư viện react-oidc-context dựa trên oidc-client-ts — không phụ thuộc vào Keycloak-specific adapter:
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. Tích hợp OIDC Client với Spring Boot
7.1 Spring Boot OAuth2 Resource Server
Cấu hình Spring Boot làm Resource Server — validate JWT tokens từ Keycloak:
<!-- 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 Client (Server-side login)
Cấu hình Spring Boot làm OAuth2 Client — server-side login flow:
<!-- 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
Keycloak client settings cho Spring Boot OAuth2 Client:
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. Advanced Client Settings
8.1 Tab Advanced
Các cấu hình nâng cao trong tab Advanced của client:
| Setting | Mô tả | Giá trị khuyến nghị |
|---|---|---|
| Access Token Lifespan | Override realm-level token lifespan cho client này | Để trống = dùng realm level |
| Client Session Idle | Override client session idle timeout | Để trống = dùng realm level |
| Client Session Max | Override client session max lifespan | Để trống = dùng realm level |
| Client Offline Session Idle | Override offline session idle timeout | Để trống = dùng realm level |
| Client Offline Session Max | Override offline session max lifespan | Để trống = dùng realm level |
| PKCE Code Challenge Method | Bắt buộc PKCE method | S256 |
| Pushed Authorization Request Required | Bắt buộc PAR (RFC 9126) | ON cho high-security apps |
| ACR to LoA Mapping | Map ACR values → Level of Assurance | Cấu hình cho step-up auth |
8.2 Tab Credentials (Confidential Clients)
Quản lý client credentials:
Client Authenticator: Client ID and Secret (mặc định), Signed JWT (client_secret_jwt), Signed JWT with Private Key (private_key_jwt), X.509 Certificate
Client Secret: Regenerate nếu bị compromised
Registration access token: Dùng cho Dynamic Client Registration
8.3 Tab Service Account Roles
Gán roles cho service account (Client Credentials flow):
Mở client → tab Service account roles
Click Assign role
Chọn realm roles hoặc client roles cần gán
# 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. Bài tập thực hành
Lab 1: Tạo Public Client cho React SPA
Tạo client
react-spa-labvớiClient authentication= OFFCấu hình: Valid redirect URIs =
http://localhost:3000/*, Web origins =http://localhost:3000Bật PKCE: Advanced → PKCE Code Challenge Method =
S256Tạo React app, cài
keycloak-js, tích hợp login/logoutVerify token trong browser DevTools → Application → Network tab
Lab 2: Tạo Confidential Client cho Spring Boot API
Tạo client
spring-api-labvớiClient authentication= ONBật
Service accounts rolesGán role
admincho service accountTạo Spring Boot project với
spring-boot-starter-oauth2-resource-serverImplement endpoint
/api/metrả về thông tin user từ JWTTest với
curlgửi Bearer token
Lab 3: Client Credentials Flow
Tạo client
batch-workerchỉ có Client Credentials flowLấy token qua
curlGọi API endpoint với token vừa lấy
Kiểm tra token contents qua jwt.io (chỉ dùng cho development)
Lab 4: Device Authorization Flow
Tạo public client
cli-toolvới Device Authorization Grant enabledSử dụng
curlđể simulate device flow:- Request device code
- Mở verification URI trên browser, nhập user code
- Poll cho token
Verify token nhận được
# 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