1. Client Scopes
Client Scopes is a mechanism for managing groups of protocol mappers and roles that can be shared among multiple clients. Instead of adding mappers to each individual client, you create a Client Scope and assign it to the necessary clients.
1.1 Default Scopes vs Optional Scopes
| Type | Description | When claims are added to token |
|---|---|---|
| Default Client Scopes | Automatically applied to all request tokens | Always — no need for explicit request |
| Optional Client Scopes | Only applies when the client requests explicitly in scope parameter | Only when the client sends scope=scope_name |
For example:
# 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 provides available client scopes according to OIDC standards:
| Scope | Type | Claims added |
|---|---|---|
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 | Allow to get offline refresh token |
microprofile-jwt | Optional | upn, groups (MicroProfile JWT spec) |
1.3 Create new Client Scope
Go to Client scopes → Create client scope
Enter information:
- Name:
my-custom-scope - Description: Description scope
- Type: Default / Optional / None
- Display on consent screen: ON if you want to display it to user
- Consent screen text: Text displayed on consent screen
- Include in token scope: ON so that the scope name appears in the
scopeclaim of token - GUI order: Order displayed on consent screen
- Name:
Add Protocol Mappers to scope
Add Scope (role scope mappings) if needed to limit 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 Assign Client Scope to Client
Open client → tab Client scopes
Click Add client scope
Select scope and assign it as Default or 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 are automatically assigned to every new client when created:
Go to Client scopes → see list
Scopes have Assigned type = Default or Optional at realm level will automatically assign to new clients
Configuration via 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
When the client has Consent required = ON, the user must consent for each scope before the client receives tokens:
Each Client Scope can configure Display on consent screen and Consent screen text
User can revoke consent in Account Console → Applications
Consent entries are saved 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 provides tools to preview token contents based on scopes:
Open client → tab Client scopes → Evaluate
Enter: User (select user test), Scope parameter (optional scopes)
Click Evaluate to view:
- Effective protocol mappers: Mappers will be applied
- Effective role scope mappings: Roles included in token
- Generated access token: Preview JSON of access token
- Generated ID token: Preview JSON of ID token
- Generated user info: Preview JSON of userinfo response
This is a very useful tool to debug token contents without actually requesting the token.
2. Token Management
2.1 Access Token
Access token is a JWT containing authorization information — determining which user has access to which resource.
Access Token structure:
{
"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 contains identity information — confirms who user is. Only for clients (Relying Party), NOT sent to 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 is used to get a new access token without needing the user to log in again. Refresh tokens have a longer lifespan than access tokens.
# 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 and Token Timeouts
Configuration in Realm Settings → Tokens tab and Sessions tab:
Token Lifespans:
| Setting | Description | Recommended value |
|---|---|---|
| Access Token Lifespan | Access token life time | 5 minutes (production) |
| Client login timeout | Maximum time to complete login flow | 5 minutes |
| Login timeout | Maximum time on login page | 30 minutes |
| Login action timeout | Time for required actions (verify email,...) | 5 minutes |
| User-Initiated Action Lifespan | Time for user-initiated actions | 5 minutes |
| Default Admin-Initiated Action Lifespan | Time for admin-initiated actions (reset password link) | 12 hours |
Session Lifespans:
| Setting | Description | Recommended value |
|---|---|---|
| SSO Session Idle | Session expires after inactivity period | 30 minutes |
| SSO Session Max | Session expires absolutely (regardless of activity) | 10 hours |
| SSO Session Idle Remember Me | Session idle when "Remember Me" ON | 30 days |
| SSO Session Max Remember Me | Session max when "Remember Me" ON | 30 days |
| Client Session Idle | Client session idle (affects refresh token) | Inherits SSO Session Idle |
| Client Session Max | Client session max (affects refresh token) | Inherits SSO Session Max |
Relationship between sessions and 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 at Client level:
Each client can override realm-level token settings in the Advanced:
tabClient → 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)
When Revoke Refresh Token is enabled, each time a refresh token is used to get a new access token, the old refresh token is revoked and a new refresh token is issued.
# 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)
Why is it necessary to Refresh Token Rotation?
If the refresh token is stolen, the attacker can only use it once
Legitimate client uses the same refresh token → both are invalidated → token theft detected
This is best practice recommended in OAuth 2.0 Security BCP
3. Offline Access
Offline tokens allow client to access resources even when the user is not online (no browser session). Offline tokens have a very long lifespan and survive server restarts.
3.1 Offline Access Configuration
Ensure scope
offline_accessis assigned to the client (optional scope)Client request token with
scope=offline_accessConfigure offline session timeouts in Realm Settings → Sessions tab:
| Setting | Description | Recommended value |
|---|---|---|
| Offline Session Idle | Offline session expires after idle | 30 days |
| Offline Session Max Limited | Turn on max lifetime limit | ON |
| Offline Session Max | Offline session max lifetime | 60 days |
# 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 Managing Offline Sessions
Admin Console → Sessions → tab Offline Sessions: see all offline sessions
User Account Console → Sessions: user can view and 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 supports Token Revocation endpoint allowing clients to revoke access tokens or refresh tokens:
# 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
Note:
Revoke refresh token → invalidate both refresh token and associated SSO session (depending on configuration)
Revoke access token → with JWT-based tokens, revocation is only effective if the resource server performs token introspection or uses Token Revocation Events
4.2 Not-Before Policy
Revoke all tokens issued before a time:
# 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 uses Token Introspection to verify token validity and retrieve 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
}
When to use Token Introspection vs JWT Verification:
| Method | Advantages | Disadvantages |
|---|---|---|
| JWT Verification (local) | Fast, no need to call Keycloak | No 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 solves the problem Bearer Token theft — if the access token is stolen, the attacker can use it anywhere because there is no binding between the token and the client.
5.1 How does DPoP work?
DPoP binds the token to a specific asymmetric key pair owned by the client. The client must prove ownership of the private key every time it uses the 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 Configure DPoP in Keycloak
DPoP is enforced through Client Policies:
Create Client Profile:
- Realm Settings → Client Policies → Profiles tab → Create
- Name:
dpop-profile - Add Executor: DPoP Proof Verification
Create Client Policy:
- Policies tab → Create
- Name:
dpop-policy - Add Condition: Client Access Type or 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 supports server-issued DPoP nonce to enhance security against 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 allow enforcement of security requirements related to tokens:
6.1 Token-related Executors
| Executor | Description |
|---|---|
| DPoP Proof Verification | DPoP required for token requests |
| Holder-of-key Enforcer | Required token binding (MTLS or DPoP) |
| Secure Signing Algorithm | Only allow secure algorithms (RS256, ES256,...) |
| PKCE Enforcer | Required PKCE for authorization code flow |
| Confidential Client Enforcer | Required client authentication |
| Secure Response Type | Only allowed secure response types |
| Reject Implicit Grant | Do not allow implicit grants |
# 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. Practice exercises
Lab 1: Client Scopes
Create Client Scope
organizationwith mappers: org_id, org_name, org_roleAssign scope to client first as Default, then change to Optional
Test: Request token has no scope parameter → no org claims
Test: Request token with
scope=openid organization→ yes org claimsUse Evaluate tool to preview token
Lab 2: Token Lifecycle
Configure Access Token Lifespan = 1 minute
Turn on Revoke Refresh Token, Refresh Token Max Reuse = 0
Get token → wait 1 minute → call API → receive 401
Refresh token → receive new token → API call → success
Try to refresh old token → get error (revoke)
Lab 3: Offline Access
Assign scope
offline_accessto clientGet token with
scope=openid offline_accessCheck
refresh_expires_in= 0 (offline token)Restart Keycloak → use offline token to refresh → still works
Xem offline sessions trong Admin Console
Lab 4: DPoP
Create Client Policy enforce DPoP for client
dpop-clientGenerate key pair, create DPoP proof JWT
Request token with DPoP header → receive DPoP-bound token
Call Resource Server with token but no DPoP proof → resource server reject
Call Resource Server with token and DPoP proof → success
Lab 5: Token Revocation
Get access token and refresh token
Revoke refresh token qua revocation endpoint
Try to refresh → get error
Set Not-Before policy qua Admin Console → Push
Try using old access token → token introspection returns
active=false