1. Authorization Services — Overview
Keycloak Authorization Services provides fine-grained authorization, allowing access control at the resource and scope level instead of relying solely on roles. This system complies with the UMA 2.0 (User-Managed Access) standard and supports multiple policy types.
1.1 Main concepts
| Concept | Description | Example |
|---|---|---|
| Resource Server | Application needs to protect resources (Keycloak client) | Backend API server |
| Resource | Object to protect | Document, API endpoint, Page |
| Scope | Actions that can be performed on resource | view, edit, delete, publish |
| Permission | Combining Resource/Scope with Policies | "Who can view the Document?" |
| Policy | Conditions must be met to allow access | "User must have the 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. Enable Authorization Services
Authorization Services enabled at client level (not 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
Once enabled, the Authorization tab will appear on the client settings with sub-tabs: Settings, Resources, Scopes, Policies, Permissions, Evaluate.
3. Resources
Resource represents the object to be protected. Each resource can have URIs, types, scopes, and attributes.
3.1 Create 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 allows adding metadata to resources, which can be used in policies:
{
"name": "Project X Files",
"type": "project-files",
"attributes": {
"project_id": ["project-x"],
"classification": ["confidential"],
"allowed_regions": ["vietnam", "singapore"]
}
}
4. Scopes
Scopes defines actions that can be performed on 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 are conditions that decide to allow or deny access. Keycloak supports many types of 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
Note: JavaScript policies need to be enabled via --features=scripts or uploading 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 as 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
Combine multiple policies into a single policy with 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 | Description | When to use |
|---|---|---|
| Unanimous | ALL policies must PERMIT | Strict — all conditions must be satisfied |
| Affirmative | AT LEAST ONE PERMIT policy | Flexible — only one condition needs to be satisfied |
| Consensus | PERMIT NUMBER > DENY NUMBER | Voting — majority decides |
7. Permissions
Permissions combines Resources/Scopes with Policies to create 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 allows resource owners to manage access to their resources. Users can share resources with other users without admin intervention.
8.1 Enable 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 allows to check permissions programmatically without 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 allows the client to send additional context information when requesting authorization, helping policies have more data to make decisions:
# 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 allows to automatically collect claims from multiple sources (HTTP requests, external services) for use in 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 provides Evaluation Tool to test permissions before deploying.
12.1 Using 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 is a Java library integrated into the application to automatically 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 — Managing 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
- Start from coarse-grained → fine-grained — use role-based first, add resource-based as needed
- Use resource types — group resources of the same type instead of creating permissions for each individual resource
- Test with Evaluation API — always test permissions before deploying to production
- Be careful with Decision Strategy —
UNANIMOUSis safer but more restrictiveAFFIRMATIVE - Limit JavaScript policies — prioritize built-in policy types, only use JavaScript when really needed
- Monitor permission evaluation performance — too many nested policies can slow down
- Export/import authorization config — use kcadm.sh to version control authorization settings
- Separate permission check from business logic — enforce at middleware/interceptor level