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

レッスン 15: 認可サービス - 詳細な認可

認可サービスの詳細: リソース サーバー、リソース、スコープ、アクセス許可、ポリシー (ロールベース、ユーザーベース、グループベース、クライアントベース、時間ベース、JavaScript、集約)。 UMA 2.0 サポート、Permission API、Policy Enforcer、プッシュされたクレーム、リソース属性、クレーム情報ポイント、評価 API、および Spring Boot / Node.js アプリケーションへの認可の統合。

🔒 DevSecOps — レッスン 15 レッスン 15: 認可サービス - 部門 詳細な権限

基本から上級までの Keycloak

パート 4: ユーザー フェデレーション、組織、および認可

xdev.asia

1. 認可サービス — 概要

Keycloak認可サービスは機能を提供しますきめ細かい認可により、ロールのみに依存するのではなく、リソースおよびスコープ レベルでのアクセス制御が可能になります。このシステムは規格に準拠していますUMA 2.0(ユーザー管理アクセス) をサポートし、複数のポリシー タイプをサポートします。

1.1 主な概念

コンセプト説明する例えば
リソースサーバーアプリケーションはリソースを保護する必要があります (Keycloak クライアントです)バックエンドAPIサーバー
リソース保護が必要なオブジェクトドキュメント、API エンドポイント、ページ
範囲リソースに対して実行できるアクションビュー。ビュー, 編集, 消去, 公開
許可リソース/スコープとポリシーを組み合わせる「文書を閲覧できるのは誰ですか?」
ポリシーアクセスを許可するには条件を満たす必要があります「ユーザーは「編集者」の役割を持っている必要があります。」

1.2 認可フロー

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. 認証サービスをオンにする

認可サービスが有効になっているのは、クライアントレベル(レルムではありません):

2.1 管理コンソール経由

Clients → my-app → Settings:
  Client authentication: ON
  Authorization: ON
  → Save

2.2 kcadm.sh 経由

# Bật authorization trên client
kcadm.sh update clients/${CLIENT_ID} -r my-realm \
  -s authorizationServicesEnabled=true \
  -s serviceAccountsEnabled=true

有効にしたら、タブ認可クライアント設定には、設定、リソース、スコープ、ポリシー、権限、評価のサブタブが表示されます。

3. リソース

リソースが表す保護される対象。各リソースには、URI、タイプ、スコープ、属性を含めることができます。

3.1 リソースの作成

# 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 リソースの属性

属性を使用すると、ポリシーで使用できるメタデータをリソースに追加できます。

{
  "name": "Project X Files",
  "type": "project-files",
  "attributes": {
    "project_id": ["project-x"],
    "classification": ["confidential"],
    "allowed_regions": ["vietnam", "singapore"]
  }
}

4. スコープ

スコープの定義アクションリソースに対して実行できます。

# 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

一般的なスコープ パターン:

パターンスコープ
クラッド作成する, 読む, アップデート。アップデート, 消去
コンテンツ管理ビュー。ビュー, 編集, 公開, アーカイブ
APIアクセス読む, 書く。書く, 管理者。管理者
ファイル操作ダウンロード, アップロード, 共有, 消去

5. ポリシー

ポリシーは状態アクセスを許可するか拒否するかを決定します。 Keycloakはさまざまなタイプのポリシーをサポートしています。

5.1 役割ベースのポリシー

# 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 ユーザーベースのポリシー

{
  "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 グループベースのポリシー

{
  "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 クライアントベースのポリシー

{
  "name": "Trusted Client Policy",
  "description": "Chỉ cho phép từ trusted clients",
  "type": "client",
  "logic": "POSITIVE",
  "clients": ["trusted-frontend", "mobile-app"]
}

5.5 時間ベースのポリシー

{
  "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 ポリシー

注記:JavaScript ポリシーを有効にする必要があります--features=スクリプトまたは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();
}

JavaScript ポリシーを 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 集約ポリシー

意思決定戦略を使用して、複数のポリシーを 1 つのポリシーに結合します。

{
  "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. 意思決定戦略

戦略説明するいつ使用するか
全会一致すべてのポリシーは許可する必要があります厳密 — すべての条件が満たされる必要があります
肯定少なくとも 1 つの許可ポリシー柔軟性 - 必要な条件は 1 つだけです
コンセンサス許可番号 > 拒否番号投票 - 多数決で決定

7. 権限

結合された権限ポリシーを含むリソース/スコープ認可ルールを作成します。

7.1 リソースベースの権限

# 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 スコープベースの権限

# 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

ユーザー管理アクセス (UMA) 2.0 が有効になっていますリソース所有者がアクセス権を管理する彼らのリソースに。ユーザーは、管理者の介入なしに他のユーザーとリソースを共有できます。

8.1 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 助成金の流れ

# 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. 許可API

API で許可される権限プログラムで権限をチェックするUMA フローなし:

# 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. プッシュされたクレーム

プッシュされたクレームによりクライアントは許可されます追加のコンテキスト情報を送信する承認をリクエストする場合、ポリシーが意思決定を行うためのより多くのデータを取得できるようになります。

# 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. 請求情報ポイント

請求情報ポイントの許可多くの情報源から請求を自動的に収集します(HTTP リクエスト、外部サービス) ポリシーで使用するには:

{
  "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. 評価API

Keycloak管理コンソールが提供される評価ツール導入する前に権限をテストします。

12.1 評価ツールの使用

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 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. ポリシー執行者

ポリシー・エンフォーサは、Javaライブラリアプリケーションに統合して、認可ポリシーを自動的に適用します。

13.1 Spring Boot の統合

<!-- 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 の構成

{
  "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 の統合

// 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 — 認可管理

# 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. ベストプラクティス

  • 粗粒度から開始→細粒度へ— 最初にロールベースを使用し、必要に応じてリソースベースを追加します
  • リソースタイプを使用する— 個別のリソースごとに権限を作成するのではなく、同じタイプのリソースをグループ化します。
  • 評価APIを使用したテスト— 運用環境にデプロイする前に、常に権限をテストしてください
  • 意思決定戦略に注意する — 全会一致より安全ですが、より制限的です肯定的
  • JavaScript ポリシーを制限する— 組み込みポリシーの種類を優先し、本当に必要な場合にのみ JavaScript を使用します
  • 権限評価パフォーマンスを監視する— ネストされたポリシーが多すぎると速度が低下する可能性があります
  • 認可設定のエクスポート/インポート— kcadm.sh を使用して認証設定のバージョンを管理します
  • ビジネスロジックから権限チェックを分離する— ミドルウェア/インターセプターレベルで強制します