Chuyển đến nội dung chính

Bài 15: Authorization Services - Phân quyền chi tiết

Authorization Services deep dive: Resource Server, Resources, Scopes, Permissions, Policies (Role-based, User-based, Group-based, Client-based, Time-based, JavaScript, Aggregated). UMA 2.0 support, Permission API, Policy Enforcer, Pushed Claims, Resource Attributes, Claim Information Points, Evaluation API và tích hợp Authorization vào ứng dụng Spring Boot / Node.js.

🔒 DevSecOps — Bài 15 Bài 15: Authorization Services - Phân quyền chi tiết

Keycloak từ Cơ bản đến Nâng cao

Phần 4: User Federation, Organizations và Authorization

xdev.asia

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

ConceptMô tảVí dụ
Resource ServerApplication 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
ScopeHành động có thể thực hiện trên resourceview, edit, delete, publish
PermissionKế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:

PatternScopes
CRUDcreate, read, update, delete
Content Managementview, edit, publish, archive
API Accessread, write, admin
File Operationsdownload, 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

StrategyMô tảKhi nào dùng
UnanimousTẤT CẢ policies phải PERMITStrict — tất cả điều kiện phải thỏa mãn
AffirmativeÍT NHẤT MỘT policy PERMITFlexible — chỉ cần một điều kiện thỏa mãn
ConsensusSỐ PERMIT > SỐ DENYVoting — đ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 — UNANIMOUS an toàn hơn nhưng restrictive hơn AFFIRMATIVE
  • 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