# 在 GitHub Actions 中使用 WIF

使用短期身份令牌而非长期 API 密钥，将 GitHub Actions 工作流认证到 Claude API。

---

每个 GitHub Actions 工作流运行都可以从 GitHub 托管的签发方 `https://token.actions.githubusercontent.com` 请求一个签名的身份令牌。通过 Workload Identity Federation，你的工作流将该令牌交换为短期的 Anthropic 访问令牌，这样你的 CI 任务无需在仓库中存储 `ANTHROPIC_API_KEY` 密钥即可调用 Claude API。

令牌的 `sub` 声明编码了仓库和触发上下文。对于推送到分支的操作，其格式为 `repo:<owner>/<repo>:ref:refs/heads/<branch>`。拉取请求运行使用 `repo:<owner>/<repo>:pull_request`，环境门控的部署使用 `repo:<owner>/<repo>:environment:<name>`。你的联合规则匹配此声明（以及其他声明，如 `repository_owner` 和 `ref`）来决定允许哪些工作流运行进行认证。

## 前置条件

- 熟悉 [WIF 概念](/docs/en/manage-claude/workload-identity-federation#concepts)：服务账号、联合签发方和联合规则。
- 一个可以编辑工作流文件并授予 `id-token: write` 权限的 GitHub 仓库。
- 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。
- 你的 Anthropic 组织 ID。你可以在 Claude Console 的 **Settings -> Organization** 下找到它。

## 配置你的工作流

GitHub 仅向明确请求它的作业签发身份令牌。在工作流或作业级别添加 `id-token: write` 权限：

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

在作业内部，运行器暴露两个环境变量：`ACTIONS_ID_TOKEN_REQUEST_URL` 和 `ACTIONS_ID_TOKEN_REQUEST_TOKEN`。使用请求令牌作为 Bearer 凭证调用请求 URL，并将你选择的受众作为查询参数，然后将返回的 JSON Web Token (JWT) 写入文件：

```yaml nocheck
- 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 nocheck
- 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 主体声明参考](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims)了解完整的 `sub` 格式列表。

## 配置 Anthropic

按照[设置指南](/docs/en/manage-claude/workload-identity-federation#set-up-federation)在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 GitHub Actions 特定的值。

**联合签发方：** GitHub 公开发布了其 OIDC 发现文档和 JWKS，因此使用发现模式。Anthropic 会在 GitHub 轮换密钥时自动刷新密钥。

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

**联合规则：** 仅匹配你打算信任的工作流运行。有关如何安全地限定这些声明的范围，请参阅[限制哪些工作流可以认证](#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()` 读取 `ANTHROPIC_IDENTITY_TOKEN_FILE`，在首次请求时交换 JWT，并在过期前自动刷新访问令牌。

<CodeGroup>

```yaml Workflow nocheck
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 nocheck
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-sonnet-4-6",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Hello, Claude"}]
  }' | jq -r '.content[0].text'
```

```python Python nocheck
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-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(message.content[0].text)
```

```typescript TypeScript nocheck
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-sonnet-4-6",
  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 nocheck hidelines={1..10,-1}
package main

import (
	"context"
	"fmt"

	"github.com/anthropics/anthropic-sdk-go"
)

func main() {
	// 从作业环境中读取 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.ModelClaudeSonnet4_6,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
		},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(message.Content[0].Text)
}
```

```java Java nocheck hidelines={1..6,-1}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

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

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

```csharp C# nocheck hidelines={1..3}
using Anthropic.Models.Messages;
using Anthropic.Oidc;

var result = AnthropicCredentials.Resolve()
    ?? throw new InvalidOperationException("No federation credentials found in environment");
using var client = new AnthropicOidcClient(result);

var message = await client.Messages.Create(new()
{
    Model = Model.ClaudeSonnet4_6,
    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 nocheck
# 从作业环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
ant messages create \
  --model claude-sonnet-4-6 \
  --max-tokens 1024 \
  --message '{role: user, content: "Hello, Claude"}'
```

```php PHP nocheck hidelines={1..3}
<?php
require 'vendor/autoload.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-sonnet-4-6',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
);
echo $message->content[0]->text, PHP_EOL;
```

```ruby Ruby nocheck
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-sonnet-4-6",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}]
)
puts message.content.first.text
```

</CodeGroup>

每个 GitHub 签发的身份令牌大约在签发五分钟后过期。令牌请求端点（`ACTIONS_ID_TOKEN_REQUEST_URL`）在整个作业期间保持有效，因此你可以在任何时候获取新令牌。SDK 在首次使用时交换令牌并缓存得到的 Anthropic 访问令牌。对于运行时间超过 Anthropic 令牌生命周期的作业，SDK 在每次刷新时重新读取 `ANTHROPIC_IDENTITY_TOKEN_FILE`，因此请定期重新运行获取步骤（或将其包装在后台循环中）以保持文件最新。或者，向 SDK 传递一个令牌提供者回调，直接调用 `ACTIONS_ID_TOKEN_REQUEST_URL` 而不是使用文件路径。

## 验证设置

成功交换会返回一个以 `sk-ant-oat01-` 开头的 `access_token` 和一个以秒为单位的 `expires_in` 值。如果出现 `400 invalid_grant`，请参阅[排查交换失败](/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange)；最常见的 GitHub Actions 侧原因是 `sub` 声明格式不匹配（其末尾段在 `ref:...`、`environment:...` 和 `pull_request` 事件之间有所不同）。

## 限制哪些工作流可以认证

<Warning>
仅使用 `repo:your-org/*` 的 `subject_prefix` 会匹配你组织中的每个仓库，且没有 `ref` 约束时还会匹配从 fork 触发的 `pull_request` 运行。任何能向匹配仓库提交拉取请求的人都可以获取联合 Anthropic 令牌。
</Warning>

将规则的 `match` 块锁定到适合你用例的最窄范围：

- **固定到单个仓库：** 使用 `subject_prefix: "repo:your-org/your-repo:*"`，这样组织中的其他仓库不会匹配。
- **固定到受保护的分支：** 在 `claims` 下添加 `"ref": "refs/heads/main"`（或你的发布分支），这样拉取请求运行和功能分支不会匹配。
- **显式固定所有者：** 在 `claims` 下添加 `"repository_owner": "your-org"`，作为防御 `sub` 解析边缘情况的深度防御检查。
- **固定到部署环境：** 对于部署作业，匹配 `subject_prefix: "repo:your-org/your-repo:environment:production"`，并在 GitHub 中使用必需的审查者来门控该环境。

## 后续步骤

- [Workload Identity Federation](/docs/en/manage-claude/workload-identity-federation)：完整的设置指南、环境变量和凭证优先级。
- [认证](/docs/en/manage-claude/authentication)：联合与 API 密钥的比较。
