1. Client Scopes
Client Scopes là cơ chế quản lý nhóm protocol mappers và roles có thể được chia sẻ giữa nhiều clients. Thay vì thêm mappers vào từng client riêng lẻ, bạn tạo Client Scope và gán cho các clients cần.
1.1 Default Scopes vs Optional Scopes
| Loại | Mô tả | Khi nào claims được thêm vào token |
|---|---|---|
| Default Client Scopes | Tự động áp dụng cho mọi token request | Luôn luôn — không cần request explicit |
| Optional Client Scopes | Chỉ áp dụng khi client request explicit trong scope parameter | Chỉ khi client gửi scope=scope_name |
Ví dụ:
# Default scopes — luôn có trong token # profile, email, roles, web-origins, acr → tự động áp dụngOptional scopes — chỉ khi request
address, phone, offline_access, microprofile-jwt
Authorization request với optional scope
GET /auth?response_type=code& client_id=my-app& scope=openid profile email phone address offline_access& redirect_uri=...
1.2 Built-in Client Scopes
Keycloak cung cấp sẵn các client scopes theo chuẩn OIDC:
| Scope | Loại | Claims được thêm |
|---|---|---|
openid | Default | sub, iss, aud, exp, iat, auth_time, nonce, acr, session_state |
profile | Default | name, family_name, given_name, preferred_username, gender, birthdate, locale, updated_at |
email | Default | email, email_verified |
roles | Default | realm_access.roles, resource_access.{client}.roles |
web-origins | Default | allowed-origins (CORS) |
acr | Default | acr (Authentication Context Class Reference) |
address | Optional | address (formatted, street_address, locality, region, postal_code, country) |
phone | Optional | phone_number, phone_number_verified |
offline_access | Optional | Cho phép lấy offline refresh token |
microprofile-jwt | Optional | upn, groups (MicroProfile JWT spec) |
1.3 Tạo Client Scope mới
Vào Client scopes → Create client scope
Nhập thông tin:
- Name:
my-custom-scope - Description: Mô tả scope
- Type: Default / Optional / None
- Display on consent screen: ON nếu muốn hiển thị cho user
- Consent screen text: Text hiển thị trên consent screen
- Include in token scope: ON để scope name xuất hiện trong
scopeclaim của token - GUI order: Thứ tự hiển thị trên consent screen
- Name:
Thêm Protocol Mappers vào scope
Thêm Scope (role scope mappings) nếu cần giới hạn roles
# Ví dụ: Tạo scope "billing" chứa billing-related claims
Name: billing
Type: Optional
Display on consent screen: ON
Consent screen text: "Access your billing information"
Include in token scope: ON
# Thêm Protocol Mappers:
# 1. User Attribute Mapper: billing_plan → billing_plan claim
# 2. User Attribute Mapper: billing_email → billing_email claim
# 3. Hardcoded Claim: billing_api_version → "v2"
1.4 Gán Client Scope cho Client
Mở client → tab Client scopes
Click Add client scope
Chọn scope và gán là Default hoặc Optional
# Gán scope bằng Admin CLI
# Lấy client UUID
CLIENT_UUID=$(bin/kcadm.sh get clients -r my-company \
-q clientId=my-app --fields id --format csv --noquotes)
# Lấy client scope UUID
SCOPE_UUID=$(bin/kcadm.sh get client-scopes -r my-company \
-q name=billing --fields id --format csv --noquotes)
# Gán default scope
bin/kcadm.sh update clients/$CLIENT_UUID/default-client-scopes/$SCOPE_UUID \
-r my-company
# Gán optional scope
bin/kcadm.sh update clients/$CLIENT_UUID/optional-client-scopes/$SCOPE_UUID \
-r my-company
1.5 Realm Default Client Scopes
Realm Default Client Scopes tự động được gán cho mọi client mới khi tạo:
Vào Client scopes → xem danh sách
Scopes có Assigned type = Default hoặc Optional ở realm level sẽ tự động gán cho clients mới
Cấu hình qua Admin CLI:
# Thêm scope vào realm default scopes bin/kcadm.sh update realms/my-company/default-default-client-scopes/$SCOPE_UUIDThêm scope vào realm optional scopes
bin/kcadm.sh update realms/my-company/default-optional-client-scopes/$SCOPE_UUID
1.6 Consent Settings
Khi client có Consent required = ON, user phải đồng ý (consent) cho từng scope trước khi client nhận tokens:
Mỗi Client Scope có thể cấu hình Display on consent screen và Consent screen text
User có thể revoke consent trong Account Console → Applications
Consent entries được lưu per user per client
# Consent screen hiển thị:
# ┌────────────────────────────────────────────┐
# │ My Application muốn: │
# │ │
# │ ☑ Access your profile information │ ← scope: profile
# │ ☑ Access your email address │ ← scope: email
# │ ☐ Access your billing information │ ← scope: billing (optional)
# │ ☐ Access your phone number │ ← scope: phone (optional)
# │ │
# │ [Accept] [Cancel] │
# └────────────────────────────────────────────┘
1.7 Evaluating Scopes (Scope Evaluation)
Admin Console cung cấp tool để preview token contents dựa trên scopes:
Mở client → tab Client scopes → Evaluate
Nhập: User (chọn user test), Scope parameter (optional scopes)
Click Evaluate để xem:
- Effective protocol mappers: Mappers sẽ được áp dụng
- Effective role scope mappings: Roles có trong token
- Generated access token: Preview JSON của access token
- Generated ID token: Preview JSON của ID token
- Generated user info: Preview JSON của userinfo response
Đây là công cụ rất hữu ích để debug token contents mà không cần thực sự request token.
2. Token Management
2.1 Access Token
Access token là JWT chứa authorization information — xác định user nào có quyền truy cập resource nào.
Cấu trúc Access Token:
{
"exp": 1711800300, // Expiration time
"iat": 1711800000, // Issued at
"auth_time": 1711799900, // Authentication time
"jti": "token-id", // JWT ID (unique)
"iss": "http://localhost:8080/realms/my-company", // Issuer
"aud": ["my-app", "account"], // Audience
"sub": "user-uuid", // Subject (user ID)
"typ": "Bearer", // Token type
"azp": "my-app", // Authorized party (client ID)
"session_state": "session-id",
"acr": "1", // Authentication Context Class Reference
"scope": "openid profile email",
"sid": "session-id", // Session ID
"email_verified": true,
"name": "John Doe",
"preferred_username": "john",
"given_name": "John",
"family_name": "Doe",
"email": "[email protected]",
"realm_access": {
"roles": ["default-roles-my-company", "admin"]
},
"resource_access": {
"my-app": {
"roles": ["app-admin"]
},
"account": {
"roles": ["manage-account"]
}
}
}
2.2 ID Token
ID Token chứa identity information — xác nhận user là ai. Chỉ dành cho client (Relying Party), KHÔNG gửi đến resource server.
{
"exp": 1711800300,
"iat": 1711800000,
"auth_time": 1711799900,
"jti": "id-token-id",
"iss": "http://localhost:8080/realms/my-company",
"aud": "my-app", // Audience = client ID
"sub": "user-uuid",
"typ": "ID",
"azp": "my-app",
"nonce": "nonce-value", // Phải match với authorization request
"session_state": "session-id",
"at_hash": "access-token-hash", // Hash of access token
"acr": "1",
"sid": "session-id",
"email_verified": true,
"name": "John Doe",
"preferred_username": "john",
"email": "[email protected]"
}
2.3 Refresh Token
Refresh token dùng để lấy access token mới mà không cần user đăng nhập lại. Refresh token có lifespan dài hơn access token.
# Refresh access token
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=REFRESH_TOKEN&
client_id=my-app&
client_secret=CLIENT_SECRET
# Response — access token mới
{
"access_token": "new-access-token",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "new-refresh-token", // Refresh token mới (rotation)
"token_type": "Bearer"
}
2.4 Session và Token Timeouts
Cấu hình trong Realm Settings → Tokens tab và Sessions tab:
Token Lifespans:
| Setting | Mô tả | Giá trị khuyến nghị |
|---|---|---|
| Access Token Lifespan | Thời gian sống access token | 5 phút (production) |
| Client login timeout | Thời gian tối đa hoàn thành login flow | 5 phút |
| Login timeout | Thời gian tối đa trên login page | 30 phút |
| Login action timeout | Thời gian cho required actions (verify email,...) | 5 phút |
| User-Initiated Action Lifespan | Thời gian cho user-initiated actions | 5 phút |
| Default Admin-Initiated Action Lifespan | Thời gian cho admin-initiated actions (reset password link) | 12 giờ |
Session Lifespans:
| Setting | Mô tả | Giá trị khuyến nghị |
|---|---|---|
| SSO Session Idle | Session hết hạn sau khoảng thời gian không hoạt động | 30 phút |
| SSO Session Max | Session hết hạn tuyệt đối (bất kể hoạt động) | 10 giờ |
| SSO Session Idle Remember Me | Session idle khi "Remember Me" ON | 30 ngày |
| SSO Session Max Remember Me | Session max khi "Remember Me" ON | 30 ngày |
| Client Session Idle | Client session idle (ảnh hưởng refresh token) | Kế thừa SSO Session Idle |
| Client Session Max | Client session max (ảnh hưởng refresh token) | Kế thừa SSO Session Max |
Mối quan hệ giữa sessions và tokens:
# Refresh token expiration = MIN(Client Session Idle, Client Session Max) # Nếu Client Session = 0 → dùng SSO Session valuesVí dụ:
SSO Session Idle = 30 phút
SSO Session Max = 10 giờ
Access Token Lifespan = 5 phút
Client Session Idle = 0 (kế thừa SSO)
Client Session Max = 0 (kế thừa SSO)
→ Access token sống 5 phút
→ Refresh token sống tối đa 30 phút (idle) hoặc 10 giờ (max)
→ Nếu user hoạt động liên tục, session kéo dài đến SSO Session Max
Override ở Client level:
Mỗi client có thể override realm-level token settings trong tab Advanced:
Client → Advanced → Advanced Settings:
Access Token Lifespan: 60 # Override: 1 phút cho high-security API
Client Session Idle: 900 # Override: 15 phút idle
Client Session Max: 3600 # Override: 1 giờ max
2.5 Revoke Refresh Token (Rotation)
Khi bật Revoke Refresh Token, mỗi lần sử dụng refresh token để lấy access token mới, refresh token cũ bị thu hồi và phát hành refresh token mới.
# Realm Settings → Tokens tab
Revoke Refresh Token: ON
Refresh Token Max Reuse: 0 # Refresh token chỉ dùng 1 lần
# > 0: cho phép reuse N lần (cho network retry)
Tại sao cần Refresh Token Rotation?
Nếu refresh token bị đánh cắp, attacker chỉ có thể dùng 1 lần
Legitimate client dùng cùng refresh token → cả hai đều bị invalidate → phát hiện token theft
Đây là best practice được khuyến nghị trong OAuth 2.0 Security BCP
3. Offline Access
Offline tokens cho phép client truy cập resources ngay cả khi user không online (không có browser session). Offline tokens có lifespan rất dài và survive qua server restart.
3.1 Cấu hình Offline Access
Đảm bảo scope
offline_accessđược gán cho client (optional scope)Client request token với
scope=offline_accessCấu hình offline session timeouts trong Realm Settings → Sessions tab:
| Setting | Mô tả | Giá trị khuyến nghị |
|---|---|---|
| Offline Session Idle | Offline session hết hạn sau idle | 30 ngày |
| Offline Session Max Limited | Bật giới hạn max lifetime | ON |
| Offline Session Max | Offline session max lifetime | 60 ngày |
# Request offline token
GET /auth?response_type=code&
client_id=my-app&
scope=openid offline_access&
redirect_uri=...
# Token response — refresh_token là offline token
{
"access_token": "...",
"expires_in": 300,
"refresh_expires_in": 0, // 0 = offline token (không expire theo session)
"refresh_token": "offline-token",
"token_type": "Bearer",
"scope": "openid offline_access"
}
# Sử dụng offline token để refresh
POST /token
grant_type=refresh_token&
refresh_token=offline-token&
client_id=my-app&
client_secret=CLIENT_SECRET
3.2 Quản lý Offline Sessions
Admin Console → Sessions → tab Offline Sessions: xem tất cả offline sessions
User Account Console → Sessions: user có thể xem và revoke offline sessions
Admin REST API: revoke offline sessions programmatically
# Revoke offline session cho user cụ thể
DELETE /admin/realms/my-company/users/{user-id}/consents/{client-id}
# Revoke tất cả sessions (bao gồm offline) cho user
POST /admin/realms/my-company/users/{user-id}/logout
4. Token Revocation
4.1 Token Revocation Endpoint (RFC 7009)
Keycloak hỗ trợ Token Revocation endpoint cho phép client thu hồi access token hoặc refresh token:
# Revoke refresh token
POST /realms/my-company/protocol/openid-connect/revoke
Content-Type: application/x-www-form-urlencoded
token=REFRESH_TOKEN&
token_type_hint=refresh_token&
client_id=my-app&
client_secret=CLIENT_SECRET
# Revoke access token
POST /realms/my-company/protocol/openid-connect/revoke
Content-Type: application/x-www-form-urlencoded
token=ACCESS_TOKEN&
token_type_hint=access_token&
client_id=my-app&
client_secret=CLIENT_SECRET
Lưu ý:
Revoke refresh token → invalidate cả refresh token và associated SSO session (tùy cấu hình)
Revoke access token → với JWT-based tokens, revocation chỉ effective mà resource server thực hiện token introspection hoặc sử dụng Token Revocation Events
4.2 Not-Before Policy
Thu hồi tất cả tokens issued trước một thời điểm:
# Set not-before timestamp — tất cả tokens issued trước thời điểm này bị invalidate
PUT /admin/realms/my-company
{
"notBefore": 1711800000 // Unix timestamp
}
# Hoặc qua Admin Console:
# Realm Settings → Sessions → "Set to now" → "Push"
# "Push" gửi not-before policy đến tất cả clients có Admin URL
4.3 Token Introspection (RFC 7662)
Resource server sử dụng Token Introspection để xác minh token validity và lấy claims:
# Introspect token
POST /realms/my-company/protocol/openid-connect/token/introspect
Content-Type: application/x-www-form-urlencoded
token=ACCESS_TOKEN&
client_id=my-resource-server&
client_secret=RESOURCE_SERVER_SECRET
# Response — token hợp lệ
{
"active": true,
"sub": "user-uuid",
"email": "[email protected]",
"realm_access": { "roles": ["admin"] },
"client_id": "my-app",
"token_type": "Bearer",
"exp": 1711800300,
"iat": 1711800000,
"scope": "openid profile email"
}
# Response — token không hợp lệ
{
"active": false
}
Khi nào dùng Token Introspection vs JWT Verification:
| Phương pháp | Ưu điểm | Nhược điểm |
|---|---|---|
| JWT Verification (local) | Nhanh, không cần gọi Keycloak | Không real-time, revocation delay |
| Token Introspection (remote) | Real-time status, full claims | Network latency, dependency on Keycloak |
5. DPoP — Demonstration of Proof-of-Possession (RFC 9449)
DPoP giải quyết vấn đề Bearer Token theft — nếu access token bị đánh cắp, attacker có thể sử dụng ở bất kỳ đâu vì không có binding giữa token và client.
5.1 DPoP hoạt động thế nào?
DPoP ràng buộc token với một asymmetric key pair cụ thể do client sở hữu. Client phải chứng minh quyền sở hữu private key mỗi khi sử dụng token.
┌──────────┐ ┌──────────┐
│ Client │ │ Keycloak │
└────┬─────┘ └────┬─────┘
│ │
│ 1. Generate key pair │
│ (public + private key) │
│ │
│ 2. Token request + │
│ DPoP Proof (signed with │
│ private key) │
│───────────────────────────────>│
│ │
│ 3. DPoP-bound access token │
│ (contains cnf.jkt claim) │
│<──────────────────────────────│
│ │
│ ┌──────────┐
│ │ Resource │
│ │ Server │
│ └────┬─────┘
│ 4. API request + │
│ DPoP-bound token + │
│ DPoP Proof (new, signed │
│ with same private key) │
│──────────────────────────────>│
│ │
│ 5. Verify: token.cnf.jkt │
│ matches DPoP proof's │
│ public key │
│ 6. Response │
│<─────────────────────────────│
5.2 DPoP Proof JWT Structure
// DPoP Proof Header { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "base64url-encoded-x", "y": "base64url-encoded-y" } }
// DPoP Proof Payload { "jti": "unique-proof-id", // Unique ID, ngăn replay attacks "htm": "POST", // HTTP method "htu": "https://keycloak/token", // HTTP URI (token endpoint) "iat": 1711800000, // Issued at "ath": "access-token-hash" // Hash of access token (khi gọi resource server) }
5.3 Cấu hình DPoP trong Keycloak
DPoP được enforce thông qua Client Policies:
Tạo Client Profile:
- Realm Settings → Client Policies → Profiles tab → Create
- Name:
dpop-profile - Add Executor: DPoP Proof Verification
Tạo Client Policy:
- Policies tab → Create
- Name:
dpop-policy - Add Condition: Client Access Type hoặc Any Client
- Associate Profile:
dpop-profile
# Token request với DPoP
POST /realms/my-company/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik...
grant_type=authorization_code&
code=AUTH_CODE&
client_id=my-app&
redirect_uri=http://localhost:3000/callback
# Response — DPoP-bound token
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "DPoP", // Token type = "DPoP" thay vì "Bearer"
"expires_in": 300
}
# Access token chứa confirmation claim
{
"cnf": {
"jkt": "thumbprint-of-client-public-key" // JWK Thumbprint
}
}
# Gọi Resource Server với DPoP token
GET /api/resource
Authorization: DPoP eyJhbGciOiJSUzI1NiIs...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs... # DPoP proof mới (htm=GET, htu=API URL)
5.4 DPoP Nonce
Keycloak hỗ trợ server-issued DPoP nonce để tăng cường bảo mật chống replay attacks:
# Server response header khi DPoP nonce required
HTTP/1.1 401 Unauthorized
DPoP-Nonce: server-generated-nonce
# Client PHẢI include nonce trong DPoP proof tiếp theo
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": { ... }
}
{
"jti": "new-unique-id",
"htm": "POST",
"htu": "https://keycloak/token",
"iat": 1711800001,
"nonce": "server-generated-nonce" // Server-issued nonce
}
5.5 DPoP Implementation Example (JavaScript)
// Tạo DPoP key pair const keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"] );// Export public key cho DPoP proof const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
// Tạo DPoP proof JWT function createDPoPProof(method, url, accessToken = null, nonce = null) { const header = { typ: "dpop+jwt", alg: "ES256", jwk: { kty: publicKey.kty, crv: publicKey.crv, x: publicKey.x, y: publicKey.y, }, };
const payload = { jti: crypto.randomUUID(), htm: method, htu: url, iat: Math.floor(Date.now() / 1000), };
// Thêm access token hash khi gọi resource server if (accessToken) { const encoder = new TextEncoder(); const data = encoder.encode(accessToken); const hashBuffer = await crypto.subtle.digest("SHA-256", data); payload.ath = base64url(hashBuffer); }
// Thêm nonce nếu server yêu cầu if (nonce) { payload.nonce = nonce; }
return signJWT(header, payload, keyPair.privateKey); }
// Sử dụng const dpopProof = await createDPoPProof( "POST", "http://localhost:8080/realms/my-company/protocol/openid-connect/token" );
const tokenResponse = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", DPoP: dpopProof, }, body: "grant_type=authorization_code&code=AUTH_CODE&...", });
6. Client Policies cho Token Security
Client Policies cho phép enforce các security requirements liên quan đến tokens:
6.1 Token-related Executors
| Executor | Mô tả |
|---|---|
| DPoP Proof Verification | Bắt buộc DPoP cho token requests |
| Holder-of-key Enforcer | Bắt buộc token binding (MTLS hoặc DPoP) |
| Secure Signing Algorithm | Chỉ cho phép algorithms an toàn (RS256, ES256,...) |
| PKCE Enforcer | Bắt buộc PKCE cho authorization code flow |
| Confidential Client Enforcer | Bắt buộc client authentication |
| Secure Response Type | Chỉ cho phép response types an toàn |
| Reject Implicit Grant | Không cho phép implicit grant |
# Ví dụ: Policy enforce DPoP + PKCE + Secure Algorithm
Profile: high-security-profile
Executors:
- DPoP Proof Verification
- PKCE Enforcer (S256 only)
- Secure Signing Algorithm (RS256, ES256)
- Reject Implicit Grant
- Confidential Client Enforcer
Policy: high-security-policy
Conditions:
- Client Role: has role "high-security"
Profiles:
- high-security-profile
7. Bài tập thực hành
Lab 1: Client Scopes
Tạo Client Scope
organizationvới mappers: org_id, org_name, org_roleGán scope cho client trước tiên là Default, sau đó đổi sang Optional
Test: Request token không có scope parameter → không có org claims
Test: Request token với
scope=openid organization→ có org claimsSử dụng Evaluate tool để preview token
Lab 2: Token Lifecycle
Cấu hình Access Token Lifespan = 1 phút
Bật Revoke Refresh Token, Refresh Token Max Reuse = 0
Lấy token → chờ 1 phút → gọi API → nhận 401
Refresh token → nhận token mới → gọi API → thành công
Thử dùng refresh token cũ → nhận error (đã bị revoke)
Lab 3: Offline Access
Gán scope
offline_accesscho clientLấy token với
scope=openid offline_accessKiểm tra
refresh_expires_in= 0 (offline token)Restart Keycloak → sử dụng offline token để refresh → vẫn hoạt động
Xem offline sessions trong Admin Console
Lab 4: DPoP
Tạo Client Policy enforce DPoP cho client
dpop-clientGenerate key pair, tạo DPoP proof JWT
Request token với DPoP header → nhận DPoP-bound token
Gọi Resource Server với token nhưng không có DPoP proof → resource server reject
Gọi Resource Server với token và DPoP proof → thành công
Lab 5: Token Revocation
Lấy access token và refresh token
Revoke refresh token qua revocation endpoint
Thử refresh → nhận error
Set Not-Before policy qua Admin Console → Push
Thử dùng access token cũ → token introspection trả về
active=false