---
title: 搭配 GitHub Actions 使用 WIF
url: https://platform.claude.com/docs/zh-TW/manage-claude/wif-providers/github-actions
description: 使用短期身分權杖而非長期 API 金鑰，讓 GitHub Actions 工作流程向 Claude API 進行驗證。
---

每一次 GitHub Actions 工作流程執行都可以向 GitHub 託管的簽發者 `https://token.actions.githubusercontent.com` 請求一個已簽署的身分權杖。透過「Workload Identity Federation」（工作負載身分聯合），即 WIF，您的工作流程可將該權杖交換為短期的 Anthropic 存取權杖，如此一來您的 CI 作業便能呼叫 Claude API，而無需在儲存庫中存放 `ANTHROPIC_API_KEY` 密鑰。

權杖的 `sub` 宣告（claim）編碼了儲存庫與觸發情境。對於推送至分支的情況，其格式為 `repo:<owner>/<repo>:ref:refs/heads/<branch>`。Pull request 執行使用 `repo:<owner>/<repo>:pull_request`，而受環境管控的部署則使用 `repo:<owner>/<repo>:environment:<name>`。您的聯合規則會比對此宣告（以及其他宣告，例如 `repository_owner` 與 `ref`），以決定哪些工作流程執行被允許進行驗證。

## 先決條件

* 熟悉 [WIF 概念](https://platform.claude.com/docs/zh-TW/manage-claude/workload-identity-federation#concepts)：服務帳戶（service accounts）、聯合簽發者（federation issuers）與聯合規則（federation rules）。
* 一個您可以編輯工作流程檔案並授予 `id-token: write` 權限的 GitHub 儲存庫。
* 在 Claude Console 中為您的 Anthropic 組織建立服務帳戶、聯合簽發者與聯合規則的權限。
* 您的 Anthropic 組織 ID。您可以在 Claude Console 的 [Organization](https://platform.claude.com/settings/organization) 頁面（`platform.claude.com/settings/organization`）找到它。

## 設定您的工作流程

GitHub 只會向明確請求的作業簽發身分權杖。請在工作流程或作業層級新增 `id-token: write` 權限：

```yaml
permissions:
  id-token: write
  contents: read
```

在作業內部，執行器（runner）會公開兩個環境變數：`ACTIONS_ID_TOKEN_REQUEST_URL` 與 `ACTIONS_ID_TOKEN_REQUEST_TOKEN`。以請求權杖作為 bearer 憑證、並以您選擇的 audience 作為查詢參數來呼叫請求 URL，然後將回傳的「JSON Web Token」（JSON 網路權杖），即 JWT，寫入檔案：

```yaml
- name: Fetch GitHub OIDC token
  run: |
    curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
      | jq -r .value > /tmp/gha-jwt
```

如果您偏好 JavaScript，`actions/github-script` 透過 `core.getIDToken(audience)` 提供相同的功能：

```yaml
- name: Fetch GitHub OIDC token
  uses: actions/github-script@v8
  with:
    script: |
      const fs = require('fs');
      const token = await core.getIDToken('https://api.anthropic.com');
      fs.writeFileSync('/tmp/gha-jwt', token);
```

解碼後的權杖帶有描述該次工作流程執行的宣告。您的聯合規則會比對這些宣告：

```json
{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:your-org/your-repo:ref:refs/heads/main",
  "aud": "https://api.anthropic.com",
  "repository": "your-org/your-repo",
  "repository_owner": "your-org",
  "ref": "refs/heads/main",
  "sha": "abc123...",
  "workflow": "CI",
  "actor": "octocat",
  "event_name": "push"
}
```

請參閱 [GitHub 的 OIDC subject 宣告參考文件](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims)以取得 `sub` 格式的完整清單。

## 設定 Anthropic

在 Claude Console 中，前往 [Workload identity](https://platform.claude.com/settings/workload-identity-federation) 頁面（`platform.claude.com/settings/workload-identity-federation`），點擊 **Connect workload**，然後選擇 **GitHub Actions** 圖塊。精靈會引導您完成註冊簽發者、建立服務帳戶以及建立聯合規則。

精靈會為您建立這些資源。無論您是在精靈中輸入這些值，還是將它們傳送至 [Admin API](https://platform.claude.com/docs/zh-TW/manage-claude/wif-admin-api)，請使用下列值：

**聯合簽發者：** GitHub 公開發布其 OIDC 探索文件與 JWKS，因此請使用探索模式（discovery mode）。當 GitHub 輪替金鑰時，Anthropic 會自動重新整理金鑰。

```json
{
  "name": "github-actions",
  "issuer_url": "https://token.actions.githubusercontent.com",
  "jwks": { "type": "discovery" }
}
```

**聯合規則：** 僅比對您打算信任的工作流程執行。請參閱[限制哪些工作流程可以進行驗證](https://platform.claude.com/docs/zh-TW/manage-claude/wif-providers/github-actions#restrict-which-workflows-can-authenticate)以了解如何安全地限定這些宣告的範圍。

```json
{
  "name": "gha-main",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "repo:your-org/your-repo:ref:refs/heads/main",
    "audience": "https://api.anthropic.com",
    "claims": {
      "repository_owner": "your-org"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

請在工作負載允許的範圍內盡可能具體。僅當規則必須比對來自同一儲存庫的多種事件類型時，才將 `subject_prefix` 放寬為 `repo:your-org/your-repo:*`（並搭配 `claims.ref` 限制條件），因為 `sub` 的尾段在 `ref:...`、`environment:...` 與 `pull_request` 事件之間會有所不同。

## 取得並使用權杖

在作業上設定聯合環境變數，然後照常呼叫 SDK。`Anthropic()` (typescript: `new Anthropic()`; csharp: `new AnthropicClient()`; go: `anthropic.NewClient()`; java: `AnthropicOkHttpClient.fromEnv()`; php: `new Client()`; ruby: `Anthropic::Client.new`) 會讀取 `ANTHROPIC_IDENTITY_TOKEN_FILE`，在第一次請求時交換 JWT，並在存取權杖到期前自動重新整理。

<CodeGroup>
  ```yaml Workflow
  name: Call Claude
  on: push

  permissions:
    id-token: write
    contents: read

  jobs:
    call-claude:
      runs-on: ubuntu-latest
      env:
        ANTHROPIC_FEDERATION_RULE_ID: fdrl_...
        ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
        ANTHROPIC_SERVICE_ACCOUNT_ID: svac_...
        ANTHROPIC_WORKSPACE_ID: wrkspc_...  # required when the rule covers multiple workspaces
        ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt
      steps:
        - uses: actions/checkout@v5
        - name: Fetch GitHub OIDC token
          run: |
            curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
              "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
              | jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
        - name: Run your script
          run: |
            pip install anthropic
            python your_script.py
  ```

  ```bash cURL
  JWT=$(cat /tmp/gha-jwt)

  RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
    -H "content-type: application/json" \
    --data @- <<JSON
  {
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": "$JWT",
    "federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
    "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
    "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
    "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
  }
  JSON
  )

  ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

  curl https://api.anthropic.com/v1/messages \
    -H "authorization: Bearer $ACCESS_TOKEN" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Hello, Claude"}]
    }' | jq -r '.content[] | select(.type == "text") | .text'
  ```

  ```python Python
  import anthropic

  # 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  # ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE
  # （皆取自工作環境）。
  client = anthropic.Anthropic()

  message = client.messages.create(
      model="claude-opus-5-5",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Hello, Claude"}],
  )
  print(next(block.text for block in message.content if block.type == "text"))
  ```

  ```typescript TypeScript
  import Anthropic from "@anthropic-ai/sdk";

  // 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  // ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE
  // （皆取自工作環境）。
  const client = new Anthropic();

  const message = await client.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Hello, Claude" }]
  });
  for (const block of message.content) {
    if (block.type === "text") {
      console.log(block.text);
    }
  }
  ```

  ```go Go
  // 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  // ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE
  // （皆取自工作環境）。
  client := anthropic.NewClient()

  message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
  	Model:     anthropic.ModelClaudeOpus5_5,
  	MaxTokens: 1024,
  	Messages: []anthropic.MessageParam{
  		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
  	},
  })
  if err != nil {
  	panic(err)
  }
  for _, block := range message.Content {
  	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  		fmt.Println(textBlock.Text)
  		break
  	}
  }
  ```

  ```java Java
  AnthropicClient client = AnthropicOkHttpClient.fromEnv();

  var message = client.messages().create(MessageCreateParams.builder()
          .model(Model.CLAUDE_OPUS_5_5)
          .maxTokens(1024)
          .addUserMessage("Hello, Claude")
          .build());

  IO.println(message.content());
  ```

  ```csharp C#
  // 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  // ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE，
  // 來源為工作（job）環境。
  using var client = new AnthropicClient();

  var message = await client.Messages.Create(new()
  {
      Model = Model.ClaudeOpus5_5,
      MaxTokens = 1024,
      Messages = [new() { Role = Role.User, Content = "Hello, Claude" }],
  });
  foreach (var block in message.Content)
  {
      if (block.Value is TextBlock textBlock)
      {
          Console.WriteLine(textBlock.Text);
      }
  }
  ```

  ```bash CLI
  # 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  # ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE
  # （皆取自工作環境）。
  ant messages create \
    --model claude-opus-5-5 \
    --max-tokens 1024 \
    --message '{role: user, content: "Hello, Claude"}'
  ```

  ```php PHP
  use Anthropic\Client;

  // 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  // ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE
  // （皆取自作業環境）。
  $client = new Client();

  $message = $client->messages->create(
      model: 'claude-opus-5-5',
      maxTokens: 1024,
      messages: [['role' => 'user', 'content' => 'Hello, Claude']],
  );
  $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
  echo $textBlock->text, PHP_EOL;
  ```

  ```ruby Ruby
  require "anthropic"

  # 讀取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
  # ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 及 ANTHROPIC_IDENTITY_TOKEN_FILE
  # （皆取自工作環境）。
  client = Anthropic::Client.new

  message = client.messages.create(
    model: "claude-opus-5-5",
    max_tokens: 1024,
    messages: [{role: "user", content: "Hello, Claude"}]
  )
  puts message.content.find { it.type == :text }.text
  ```
</CodeGroup>

每個由 GitHub 簽發的身分權杖大約在簽發後五分鐘到期。權杖請求端點（`ACTIONS_ID_TOKEN_REQUEST_URL`）在整個作業期間都保持有效，因此您可以在任何時間點取得新的權杖。SDK 會在首次使用時交換權杖，並快取所產生的 Anthropic 存取權杖。對於執行時間超過 Anthropic 權杖有效期的作業，SDK 會在每次重新整理時重新讀取 `ANTHROPIC_IDENTITY_TOKEN_FILE`，因此請定期重新執行取得步驟（或將其包裝在背景迴圈中）以保持檔案為最新狀態。或者，除了 Java 之外，您也可以向 SDK 傳入一個直接呼叫 `ACTIONS_ID_TOKEN_REQUEST_URL` 的權杖提供者，而非使用檔案路徑。

## 驗證設定

成功的交換會回傳以 `sk-ant-oat01-` 開頭的 `access_token`，以及以秒為單位的 `expires_in` 值。被拒絕的交換則會回傳不透明的 `401` `authentication_error`，並附帶固定訊息 `Authentication failed`，無論是哪項檢查失敗；在大多數情況下，拒絕原因會記錄在 Claude Console 中[驗證歷史記錄頁面](https://platform.claude.com/settings/workload-identity-federation?tab=history)的該次嘗試項目上，而[疑難排解失敗的交換](https://platform.claude.com/docs/zh-TW/manage-claude/wif-reference#troubleshoot-a-failed-exchange)會依序說明各項檢查。GitHub Actions 端最常見的原因是 `sub` 宣告格式不符（其結尾區段會因 `ref:...`、`environment:...` 和 `pull_request` 事件而有所不同）；歷史記錄項目會顯示原因 `match_subject_prefix`。

## 限制哪些工作流程可以進行驗證

<Warning>
  單獨使用 `repo:your-org/*` 作為 `subject_prefix` 會比對到您組織中的每一個儲存庫，而且若沒有 `ref` 限制條件，它也會比對到從 fork 觸發的 `pull_request` 執行。任何能夠對符合條件的儲存庫開啟 pull request 的人，都可能取得聯合的 Anthropic 權杖。
</Warning>

請將規則的 `match` 區塊鎖定在符合您使用情境的最小範圍：

* **固定至單一儲存庫：** 使用 `subject_prefix: "repo:your-org/your-repo:*"`，使組織中的其他儲存庫不會被比對到。
* **固定至受保護的分支：** 在 `claims` 下新增 `"ref": "refs/heads/main"`（或您的發行分支），使 pull request 執行與功能分支不會被比對到。
* **明確固定擁有者：** 在 `claims` 下新增 `"repository_owner": "your-org"`，作為針對 `sub` 解析邊界情況的縱深防禦檢查。
* **固定至部署環境：** 對於部署作業，比對 `subject_prefix: "repo:your-org/your-repo:environment:production"`，並在 GitHub 中以必要審查者來管控該環境。

## 後續步驟

* [Workload Identity Federation](https://platform.claude.com/docs/zh-TW/manage-claude/workload-identity-federation)：完整的設定逐步說明、環境變數與憑證優先順序。
* [驗證](https://platform.claude.com/docs/zh-TW/manage-claude/authentication)：聯合與 API 金鑰的比較。
