1. Authorization Services — Tổng quan
Keycloak Authorization Services cung cấp khả năng phân quyền chi tiết (fine-grained authorization), cho phép kiểm soát truy cập ở mức resource và scope thay vì chỉ dựa vào roles. Hệ thống này tuân theo chuẩn UMA 2.0 (User-Managed Access) và hỗ trợ multiple policy types.
1.1 Các khái niệm chính
| Concept | Mô tả | Ví dụ |
|---|---|---|
| Resource Server | Application cần bảo vệ resources (là Keycloak client) | Backend API server |
| Resource | Đối tượng cần bảo vệ | Document, API endpoint, Page |
| Scope | Hành động có thể thực hiện trên resource | view, edit, delete, publish |
| Permission | Kết hợp Resource/Scope với Policies | "Ai được view Document?" |
| Policy | Điều kiện phải thỏa mãn để cho phép truy cập | "User phải có role 'editor'" |
1.2 Authorization Flow
Client Request → Resource Server → Keycloak Authorization
│
┌───────────────────┘
▼
Find Matching Permission
│
▼
Evaluate Associated Policies
│
┌─────┼─────┐
▼ ▼ ▼
Policy Policy Policy
(Role) (Time) (Group)
│ │ │
└─────┼─────┘
▼
Decision Strategy
(Unanimous/Affirmative/Consensus)
│
┌─────┴─────┐
▼ ▼
PERMIT DENY
2. Bật Authorization Services
Authorization Services được bật ở mức client (không phải realm):
2.1 Qua Admin Console
Clients → my-app → Settings:
Client authentication: ON
Authorization: ON
→ Save
2.2 Qua kcadm.sh
# Bật authorization trên client
kcadm.sh update clients/${CLIENT_ID} -r my-realm \
-s authorizationServicesEnabled=true \
-s serviceAccountsEnabled=true
Sau khi bật, tab Authorization sẽ xuất hiện trên client settings với các sub-tabs: Settings, Resources, Scopes, Policies, Permissions, Evaluate.
3. Resources
Resource đại diện cho đối tượng cần bảo vệ. Mỗi resource có thể có URIs, type, scopes, và attributes.
3.1 Tạo Resources
# Tạo resource qua REST API
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/resource" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Document Resource",
"type": "document",
"uris": ["/api/documents/*"],
"ownerManagedAccess": false,
"scopes": [
{ "name": "view" },
{ "name": "edit" },
{ "name": "delete" },
{ "name": "publish" }
],
"attributes": {
"department": ["engineering"],
"sensitivity": ["internal"]
}
}'
# Tạo thêm resource cho specific item
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/resource" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Admin Panel",
"type": "page",
"uris": ["/admin", "/admin/*"],
"scopes": [
{ "name": "access" }
]
}'
3.2 Resource Attributes
Attributes cho phép thêm metadata vào resources, có thể được sử dụng trong policies:
{
"name": "Project X Files",
"type": "project-files",
"attributes": {
"project_id": ["project-x"],
"classification": ["confidential"],
"allowed_regions": ["vietnam", "singapore"]
}
}
4. Scopes
Scopes định nghĩa các hành động có thể thực hiện trên resources:
# Tạo scopes
for scope in view edit delete publish manage; do
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/scope" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$scope\"}"
done
Common scope patterns:
| Pattern | Scopes |
|---|---|
| CRUD | create, read, update, delete |
| Content Management | view, edit, publish, archive |
| API Access | read, write, admin |
| File Operations | download, upload, share, delete |
5. Policies
Policies là điều kiện quyết định cho phép hoặc từ chối truy cập. Keycloak hỗ trợ nhiều loại policies:
5.1 Role-based Policy
# Tạo role-based policy
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/policy/role" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Editor Role Policy",
"description": "User phải có role editor",
"logic": "POSITIVE",
"roles": [
{
"id": "'${EDITOR_ROLE_ID}'",
"required": true
}
]
}'
5.2 User-based Policy
{
"name": "Specific Users Policy",
"description": "Chỉ cho phép users cụ thể",
"type": "user",
"logic": "POSITIVE",
"users": ["user-id-1", "user-id-2"]
}
5.3 Group-based Policy
{
"name": "Engineering Group Policy",
"description": "Members của group Engineering",
"type": "group",
"logic": "POSITIVE",
"groups": [
{
"id": "group-uuid",
"path": "/Engineering",
"extendChildren": true
}
],
"groupsClaim": "groups"
}
5.4 Client-based Policy
{
"name": "Trusted Client Policy",
"description": "Chỉ cho phép từ trusted clients",
"type": "client",
"logic": "POSITIVE",
"clients": ["trusted-frontend", "mobile-app"]
}
5.5 Time-based Policy
{
"name": "Business Hours Policy",
"description": "Chỉ cho phép trong giờ làm việc",
"type": "time",
"logic": "POSITIVE",
"notBefore": "2025-01-01 00:00:00",
"notOnOrAfter": "2030-12-31 23:59:59",
"dayMonth": null,
"dayMonthEnd": null,
"month": null,
"monthEnd": null,
"year": null,
"yearEnd": null,
"hour": 8,
"hourEnd": 18,
"minute": 0,
"minuteEnd": 0
}
5.6 JavaScript Policy
Lưu ý: JavaScript policies cần được bật qua --features=scripts hoặc upload JAR.
// Script-based policy (upload dưới dạng JAR provider)
// Filename: my-policy.js
var context = $evaluation.getContext();
var identity = context.getIdentity();
var attributes = identity.getAttributes();
// Kiểm tra custom attribute
var department = attributes.getValue('department');
if (department && department.asString(0) === 'engineering') {
$evaluation.grant();
} else {
$evaluation.deny();
}
Deploy JavaScript policy dưới dạng JAR:
# Tạo cấu trúc thư mục
mkdir -p META-INF/keycloak-scripts/
# Tạo file descriptor
cat > META-INF/keycloak-scripts/keycloak-scripts.json << 'EOF'
{
"policies": [
{
"name": "Engineering Department Policy",
"fileName": "engineering-policy.js",
"description": "Allow only engineering department"
}
]
}
EOF
# Package JAR
jar cf my-policies.jar META-INF/ engineering-policy.js
# Deploy
cp my-policies.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build
5.7 Aggregated Policy
Kết hợp nhiều policies thành một policy duy nhất với decision strategy:
{
"name": "Full Access Policy",
"description": "Kết hợp Role + Group + Time policies",
"type": "aggregate",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"policies": [
"Editor Role Policy",
"Engineering Group Policy",
"Business Hours Policy"
]
}
6. Decision Strategies
| Strategy | Mô tả | Khi nào dùng |
|---|---|---|
| Unanimous | TẤT CẢ policies phải PERMIT | Strict — tất cả điều kiện phải thỏa mãn |
| Affirmative | ÍT NHẤT MỘT policy PERMIT | Flexible — chỉ cần một điều kiện thỏa mãn |
| Consensus | SỐ PERMIT > SỐ DENY | Voting — đa số quyết định |
7. Permissions
Permissions kết hợp Resources/Scopes với Policies để tạo authorization rules.
7.1 Resource-based Permission
# Tạo resource-based permission
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/permission/resource" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Document Access Permission",
"description": "Ai có thể truy cập documents",
"type": "resource",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"resources": ["Document Resource"],
"policies": ["Editor Role Policy", "Business Hours Policy"]
}'
7.2 Scope-based Permission
# Tạo scope-based permission
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/permission/scope" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Document Delete Permission",
"description": "Chỉ admin mới được xóa documents",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"resources": ["Document Resource"],
"scopes": ["delete"],
"policies": ["Admin Role Policy"]
}'
# Permission cho publish scope
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/permission/scope" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Document Publish Permission",
"description": "Editor và Admin được publish",
"type": "scope",
"logic": "POSITIVE",
"decisionStrategy": "AFFIRMATIVE",
"resources": ["Document Resource"],
"scopes": ["publish"],
"policies": ["Editor Role Policy", "Admin Role Policy"]
}'
8. UMA 2.0
User-Managed Access (UMA) 2.0 cho phép resource owners quản lý quyền truy cập vào resources của họ. User có thể chia sẻ resources với users khác mà không cần admin can thiệp.
8.1 Bật UMA
Clients → my-app → Authorization → Settings:
Resource server settings:
Policy Enforcement Mode: ENFORCING
Decision Strategy: UNANIMOUS
Resources:
Resource → Owner Managed Access: ON
8.2 UMA Grant Flow
# 1. Client gọi Resource Server → bị deny → nhận permission ticket
# Response 401 với header:
# WWW-Authenticate: UMA realm="my-realm",
# as_uri="http://localhost:8080/realms/my-realm",
# ticket="permission-ticket-value"
# 2. Client exchange permission ticket lấy RPT (Requesting Party Token)
curl -X POST "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "ticket=permission-ticket-value" \
-d "client_id=my-app" \
-d "client_secret=my-secret"
# Response chứa RPT (access token with authorization data)
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"authorization": {
"permissions": [
{
"rsid": "resource-uuid",
"rsname": "Document Resource",
"scopes": ["view", "edit"]
}
]
}
}
9. Permission API
Permission API cho phép kiểm tra permissions programmatically mà không cần UMA flow:
# Kiểm tra quyền truy cập cho user hiện tại
curl -X POST "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=my-app" \
-d "permission=Document Resource#view" \
-d "response_mode=decision" \
-d "client_id=my-frontend" \
-d "subject_token=${USER_ACCESS_TOKEN}"
# Response:
# { "result": true } → PERMIT
# { "result": false } → DENY
# Kiểm tra nhiều permissions cùng lúc
curl -X POST "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=my-app" \
-d "permission=Document Resource#view" \
-d "permission=Document Resource#edit" \
-d "permission=Admin Panel#access" \
-d "response_mode=permissions" \
-d "client_id=my-frontend" \
-d "subject_token=${USER_ACCESS_TOKEN}"
10. Pushed Claims
Pushed Claims cho phép client gửi thêm context information khi request authorization, giúp policies có thêm dữ liệu để ra quyết định:
# Request với pushed claims
curl -X POST "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=my-app" \
-d "permission=Document Resource#edit" \
-d 'claim_token={"ip_address":["10.0.0.5"],"device_type":["desktop"],"risk_score":["low"]}' \
-d "claim_token_format=urn:ietf:params:oauth:token-type:jwt" \
-d "client_id=my-frontend" \
-d "subject_token=${USER_ACCESS_TOKEN}"
11. Claim Information Points
Claim Information Points cho phép tự động thu thập claims từ nhiều nguồn (HTTP request, external services) để sử dụng trong policies:
{
"name": "http-claim-info",
"claimInformationPoint": {
"claims": {
"ip_address": "{request.remoteAddr}",
"http_method": "{request.method}",
"request_uri": "{request.relativePath}",
"user_agent": "{request.headers[user-agent]}"
}
}
}
12. Evaluation API
Keycloak Admin Console cung cấp Evaluation Tool để test permissions trước khi deploy.
12.1 Sử dụng Evaluation Tool
Clients → my-app → Authorization → Evaluate:
1. Identity Information:
- User: chọn user cần test
- Roles: chọn roles (hoặc tự động từ user)
2. Resources:
- Thêm resources cần evaluate
3. Contextual Information:
- Pushed Claims (JSON)
4. Click "Evaluate"
Results:
┌─────────────────────────────┬────────┐
│ Permission │ Result │
├─────────────────────────────┼────────┤
│ Document Access Permission │ PERMIT │
│ Document Delete Permission │ DENY │
│ Admin Panel Permission │ DENY │
└─────────────────────────────┴────────┘
12.2 Evaluation qua API
# Evaluate permissions cho user cụ thể
curl -X POST "http://localhost:8080/admin/realms/my-realm/clients/${CLIENT_ID}/authz/resource-server/policy/evaluate" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"userId": "user-uuid",
"roleIds": [],
"resources": [
{
"name": "Document Resource",
"scopes": ["view", "edit", "delete"]
}
],
"context": {
"attributes": {
"ip_address": ["10.0.0.5"]
}
},
"entitlements": false
}'
13. Policy Enforcer
Policy Enforcer là Java library tích hợp vào application để tự động enforce authorization policies:
13.1 Spring Boot Integration
<!-- pom.xml -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>26.0.0</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-policy-enforcer</artifactId>
<version>26.0.0</version>
</dependency>
// AuthorizationConfig.java
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.representations.idm.authorization.*;
@Configuration
public class AuthorizationConfig {
@Bean
public AuthzClient authzClient() {
// Load từ keycloak.json hoặc cấu hình programmatically
return AuthzClient.create();
}
}
// DocumentService.java
@Service
public class DocumentService {
private final AuthzClient authzClient;
public DocumentService(AuthzClient authzClient) {
this.authzClient = authzClient;
}
public boolean canUserEditDocument(String userId, String documentId) {
AuthorizationRequest request = new AuthorizationRequest();
request.addPermission("Document Resource", "edit");
try {
AuthorizationResponse response = authzClient
.authorization(userId)
.authorize(request);
// Nếu thành công → user có quyền
return response.getToken() != null;
} catch (AuthorizationDeniedException e) {
return false;
}
}
}
// DocumentController.java
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
private final DocumentService documentService;
@PutMapping("/{id}")
public ResponseEntity<?> updateDocument(
@PathVariable String id,
@RequestBody DocumentDTO dto,
@AuthenticationPrincipal Jwt jwt) {
if (!documentService.canUserEditDocument(jwt.getSubject(), id)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "You don't have permission to edit this document"));
}
// Proceed with update
return ResponseEntity.ok(documentService.update(id, dto));
}
}
13.2 keycloak.json Configuration
{
"realm": "my-realm",
"auth-server-url": "http://localhost:8080",
"resource": "my-app",
"credentials": {
"secret": "client-secret-here"
},
"policy-enforcer": {
"enforcement-mode": "ENFORCING",
"paths": [
{
"path": "/api/documents/*",
"methods": [
{
"method": "GET",
"scopes": ["view"]
},
{
"method": "PUT",
"scopes": ["edit"]
},
{
"method": "DELETE",
"scopes": ["delete"]
}
]
},
{
"path": "/admin/*",
"enforcement-mode": "ENFORCING"
},
{
"path": "/public/*",
"enforcement-mode": "DISABLED"
}
]
}
}
13.3 Node.js Integration
// authorization.ts
import axios from 'axios';
interface PermissionResult {
rsid: string;
rsname: string;
scopes: string[];
}
class KeycloakAuthzClient {
private readonly baseUrl: string;
private readonly realm: string;
private readonly clientId: string;
private readonly clientSecret: string;
constructor(config: {
baseUrl: string;
realm: string;
clientId: string;
clientSecret: string;
}) {
this.baseUrl = config.baseUrl;
this.realm = config.realm;
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
}
async checkPermission(
userToken: string,
resource: string,
scope: string
): Promise<boolean> {
try {
const response = await axios.post(
`${this.baseUrl}/realms/${this.realm}/protocol/openid-connect/token`,
new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
audience: this.clientId,
permission: `${resource}#${scope}`,
response_mode: 'decision',
subject_token: userToken,
}),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
auth: {
username: this.clientId,
password: this.clientSecret,
},
}
);
return response.data.result === true;
} catch {
return false;
}
}
async getPermissions(userToken: string): Promise<PermissionResult[]> {
const response = await axios.post(
`${this.baseUrl}/realms/${this.realm}/protocol/openid-connect/token`,
new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
audience: this.clientId,
response_mode: 'permissions',
subject_token: userToken,
}),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
auth: {
username: this.clientId,
password: this.clientSecret,
},
}
);
return response.data;
}
}
// Express middleware
import { Request, Response, NextFunction } from 'express';
const authzClient = new KeycloakAuthzClient({
baseUrl: 'http://localhost:8080',
realm: 'my-realm',
clientId: 'my-app',
clientSecret: 'secret',
});
function enforcePermission(resource: string, scope: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const allowed = await authzClient.checkPermission(token, resource, scope);
if (!allowed) {
return res.status(403).json({
error: `Permission denied: ${resource}#${scope}`,
});
}
next();
};
}
// Usage
app.get('/api/documents',
enforcePermission('Document Resource', 'view'),
documentsController.list
);
app.put('/api/documents/:id',
enforcePermission('Document Resource', 'edit'),
documentsController.update
);
app.delete('/api/documents/:id',
enforcePermission('Document Resource', 'delete'),
documentsController.delete
);
14. kcadm.sh — Quản lý Authorization
# List resources
kcadm.sh get clients/${CLIENT_ID}/authz/resource-server/resource -r my-realm
# List policies
kcadm.sh get clients/${CLIENT_ID}/authz/resource-server/policy -r my-realm
# List permissions
kcadm.sh get clients/${CLIENT_ID}/authz/resource-server/permission -r my-realm
# List scopes
kcadm.sh get clients/${CLIENT_ID}/authz/resource-server/scope -r my-realm
# Export authorization settings
kcadm.sh get clients/${CLIENT_ID}/authz/resource-server -r my-realm > authz-export.json
# Import authorization settings
kcadm.sh create clients/${CLIENT_ID}/authz/resource-server \
-r my-realm -f authz-import.json
15. Best Practices
- Bắt đầu từ coarse-grained → fine-grained — dùng role-based trước, thêm resource-based khi cần
- Dùng resource types — nhóm resources cùng loại thay vì tạo permission cho từng resource riêng
- Test với Evaluation API — luôn test permissions trước khi deploy lên production
- Cẩn thận với Decision Strategy —
UNANIMOUSan toàn hơn nhưng restrictive hơnAFFIRMATIVE - Limit JavaScript policies — ưu tiên built-in policy types, chỉ dùng JavaScript khi thực sự cần
- Monitor permission evaluation performance — quá nhiều policies lồng nhau có thể gây chậm
- Export/import authorization config — dùng kcadm.sh để version control authorization settings
- Tách permission check khỏi business logic — enforce ở middleware/interceptor level