# 在 Microsoft Azure 中使用 WIF

将 Azure 托管标识和 Entra Workload Identity 与 Claude API 联合，使你的 Azure 工作负载无需静态 API 密钥即可调用 Claude。

---

Azure 工作负载通过出示 Microsoft Entra ID 签发的 JSON Web Token (JWT) 来认证到 Claude API，然后将其交换为短期的 Anthropic 访问令牌。获取 Entra 签发的令牌有两种常见方式：

- **托管标识（VM、App Service、Functions、Container Apps）：** 工作负载调用 Azure 实例元数据服务 (IMDS) 地址 `http://169.254.169.254/metadata/identity/oauth2/token`，并接收其分配标识的 JWT。
- **Entra Workload Identity（AKS Pod）：** Kubernetes 将服务账号令牌（由 AKS 集群的 OIDC 签发方签名）投影到 Pod 中，路径在 `AZURE_FEDERATED_TOKEN_FILE` 中。工作负载在 Entra 处交换该令牌以获取 Entra 签发的访问令牌。

在两种情况下，你向 Anthropic 出示的 Entra 签发令牌都携带租户特定的 Entra 签发方（下方的[配置 Anthropic](#configure-anthropic) 步骤显示了要注册的确确 URL）以及托管标识的对象 ID（在 `sub` 和 `oid` 声明中）。你只需向 Anthropic 注册该签发方一次，编写一个匹配预期声明的联合规则，你的工作负载就会在运行时将其 Entra 令牌交换为 `sk-ant-oat01-...` 访问令牌。

<Tip>
AKS Pod 也可以跳过 Entra 交换，直接向 Anthropic 出示 Kubernetes 投影的服务账号令牌。该方式注册你的 AKS 集群的 OIDC 签发方而非 Entra 租户。有关该流程，请参阅 [Kubernetes](/docs/en/manage-claude/wif-providers/kubernetes)。
</Tip>

## 前置条件

- 熟悉 [WIF 概念](/docs/en/manage-claude/workload-identity-federation#concepts)：服务账号、联合签发方和联合规则。
- 一个具有分配托管标识权限（或在 AKS 上配置 Entra Workload Identity）的 Azure 订阅。
- 你的 Microsoft Entra 租户 ID。在 Azure 门户中 **Microsoft Entra ID -> Overview -> Tenant ID** 下找到它。
- 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。

## 配置 Azure

设置 Azure 将为其签发令牌的标识。选择与你的工作负载运行位置匹配的方式。

<Tabs>
<Tab title="VM、App Service、Functions、Container Apps">

在你的 Azure 资源上启用系统分配或用户分配的托管标识。在 Azure 门户中打开该资源，转到 **Identity**，然后开启 **System assigned**（或附加一个用户分配的标识）。

标识创建后，记下其 **Object (principal) ID**。此 GUID 在签发的令牌中同时作为 `sub` 和 `oid` 声明出现，你的 Anthropic 联合规则将匹配它。你可以在资源的 **Identity** 页面上找到它，或者对于用户分配的标识，在 **Microsoft Entra ID -> Enterprise applications** 下找到。

不需要进一步的 Azure 侧配置。标识附加后，Azure 实例元数据服务可从资源内部的 `169.254.169.254` 访问。

</Tab>
<Tab title="Entra Workload Identity (AKS)">

Entra Workload Identity 将 Kubernetes 服务账号与 Entra 应用联合，以便 Pod 可以将其集群签发的服务账号令牌交换为 Entra 签发的访问令牌。

1. 在你的 AKS 集群上启用 OIDC 签发方（`az aks update --enable-oidc-issuer --enable-workload-identity ...`）。
2. 部署 `azure-workload-identity` 变更 Webhook。
3. 创建一个用户分配的托管标识和一个联合凭据，该凭据信任集群的 OIDC 签发方用于你的 Kubernetes 服务账号。
4. 在你的 Pod 规范上添加标签 `azure.workload.identity/use: "true"`，并将 `serviceAccountName` 设置为联合的服务账号。

Webhook 将 `AZURE_FEDERATED_TOKEN_FILE`、`AZURE_CLIENT_ID` 和 `AZURE_TENANT_ID` 注入到 Pod 中。`AZURE_FEDERATED_TOKEN_FILE` 处的文件包含 Kubernetes 投影的服务账号令牌，由 AKS 集群的 OIDC 签发方签名。

</Tab>
</Tabs>

### 令牌声明

托管标识的 Entra 签发令牌携带以下声明：

```json
{
  "iss": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
  "sub": "9f8e7d6c-1a2b-3c4d-5e6f-...",
  "aud": "https://api.anthropic.com",
  "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
  "tid": "<TENANT_ID>",
  "azp": "<CLIENT_ID>",
  "exp": 1775527120
}
```

`sub` 和 `oid` 是相同的（托管标识的对象 ID）。`azp` 是应用程序或客户端 ID。匹配 `oid` 以授权一个特定标识，或匹配 `azp` 以授权与应用程序注册关联的任何标识。`tid` 声明重复了你的租户 ID；匹配它是深度防御，因为签发方 URL 已经固定了租户。

## 配置 Anthropic

按照[设置指南](/docs/en/manage-claude/workload-identity-federation#set-up-federation)在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。在 Console 中，选择 **OIDC** 提供者选项并提供以下 Entra 特定的值。

**联合签发方：** Entra 在每个租户的签发方 URL 处发布了 OIDC 发现文档，因此使用发现模式。每个你联合的 Azure 租户都需要自己的签发方记录。

```json
{
  "name": "azure-prod-tenant",
  "issuer_url": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
  "jwks_source": "discovery"
}
```

<Note>
根据令牌版本的不同，`iss` 声明可能是 `https://sts.windows.net/<TENANT_ID>/`。解码你的托管标识令牌（下方的验证部分展示了如何操作）并注册它包含的 `iss` 值。两个 URL 共享相同的 JWKS，因此发现模式对两者都适用。
</Note>

**联合规则：** 匹配托管标识的对象 ID 和你的租户 ID。

```json
{
  "name": "azure-inference-worker",
  "issuer_id": "fdis_...",
  "match": {
    "audience": "https://api.anthropic.com",
    "claims": {
      "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
      "tid": "<TENANT_ID>"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}
```

## 获取并使用令牌

在运行时，你的工作负载获取其 Entra 令牌，在 `POST /v1/oauth/token` 处交换它，然后使用返回的 Bearer 令牌调用 Claude。当你提供令牌提供者可调用对象时，每个 Anthropic SDK 都会处理交换和刷新循环，如下例所示。cURL 标签展示了原始流程。

<CodeGroup>

```bash cURL nocheck
# 1. 从 IMDS 获取 Entra 签发的令牌（托管标识）。
#    对于使用 Entra Workload Identity 的 AKS，请改用
#    "在 AKS 上使用 Entra Workload Identity" 部分中的两跳交换。
ENTRA_TOKEN=$(curl -sS -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com" \
  | jq -r .access_token)

# 2. 将其交换为 Anthropic 访问令牌。
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": "$ENTRA_TOKEN",
  "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)

# 3. 使用 Bearer 令牌调用 Claude API。
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 from Azure"}]
  }' | jq -r '.content[0].text'
```

```python Python nocheck
import os

import anthropic
import requests
from anthropic import WorkloadIdentityCredentials

IMDS_URL = "http://169.254.169.254/metadata/identity/oauth2/token"


def fetch_entra_token() -> str:
    """从 Azure IMDS 获取托管标识令牌。"""
    response = requests.get(
        IMDS_URL,
        headers={"Metadata": "true"},
        params={"api-version": "2018-02-01", "resource": "https://api.anthropic.com"},
        timeout=5,
    )
    response.raise_for_status()
    return response.json()["access_token"]


client = anthropic.Anthropic(
    credentials=WorkloadIdentityCredentials(
        identity_token_provider=fetch_entra_token,
        federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
        organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
        service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
        workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
    ),
)

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello from Azure"}],
)
print(message.content[0].text)
```

```typescript TypeScript nocheck
import Anthropic from "@anthropic-ai/sdk";
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";

const IMDS_URL =
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com";

async function fetchEntraToken(): Promise<string> {
  const response = await fetch(IMDS_URL, {
    headers: { Metadata: "true" }
  });
  const body = (await response.json()) as { access_token: string };
  return body.access_token;
}

const client = new Anthropic({
  credentials: oidcFederationProvider({
    identityTokenProvider: fetchEntraToken,
    federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
    organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
    serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
    workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
    baseURL: "https://api.anthropic.com",
    fetch
  })
});

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

```go Go nocheck
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"

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

const imdsURL = "http://169.254.169.254/metadata/identity/oauth2/token" +
	"?api-version=2018-02-01&resource=https://api.anthropic.com"

// azureIMDSToken 从 Azure IMDS 获取托管标识令牌。
func azureIMDSToken(ctx context.Context) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, imdsURL, nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Metadata", "true")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("call IMDS: %w", err)
	}
	defer resp.Body.Close()
	var body struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return "", fmt.Errorf("decode IMDS response: %w", err)
	}
	return body.AccessToken, nil
}

func main() {
	client := anthropic.NewClient(
		option.WithFederationTokenProvider(azureIMDSToken, option.FederationOptions{
			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
		}),
	)

	message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeSonnet4_6,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Azure")),
		},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(message.Content[0].Text)
}
```

```java Java nocheck hidelines={1..12,-1}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.credentials.IdentityTokenProvider;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

void main() {
    HttpClient http = HttpClient.newHttpClient();
    HttpRequest metadataRequest = HttpRequest.newBuilder()
            .uri(URI.create("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com"))
            .header("Metadata", "true")
            .build();

    IdentityTokenProvider fetchEntraToken = () -> {
        try {
            var response = http.send(metadataRequest, HttpResponse.BodyHandlers.ofString());
            return new ObjectMapper().readTree(response.body()).get("access_token").asText();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    };

    AnthropicClient client = AnthropicOkHttpClient.builder()
            .federationTokenProvider(
                    fetchEntraToken,
                    System.getenv("ANTHROPIC_FEDERATION_RULE_ID"),
                    System.getenv("ANTHROPIC_ORGANIZATION_ID"),
                    System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
            .build();

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

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

```csharp C# nocheck hidelines={1..4}
using System.Text.Json;
using Anthropic.Models.Messages;
using Anthropic.Oidc;

var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
    FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
    OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
    ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
    IdentityTokenProvider = new EntraTokenProvider(),
});
using var client = new AnthropicOidcClient(credentials);

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

class EntraTokenProvider : IIdentityTokenProvider
{
    private const string IMDS_URL =
        "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com";

    private static readonly HttpClient httpClient = new()
    {
        DefaultRequestHeaders = { { "Metadata", "true" } },
    };

    public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
    {
        using var json = await JsonDocument.ParseAsync(
            await httpClient.GetStreamAsync(IMDS_URL, ct), default, ct);
        return json.RootElement.GetProperty("access_token").GetString()!;
    }
}
```

```php PHP nocheck hidelines={1..3}
<?php
require 'vendor/autoload.php';

use Anthropic\Client;
use Anthropic\Credentials\WorkloadIdentityCredentials;

const IMDS_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com';

function fetchEntraToken(): string
{
    $context = stream_context_create([
        'http' => ['header' => "Metadata: true\r\n"],
    ]);
    $body = json_decode(file_get_contents(IMDS_URL, false, $context), true);
    return $body['access_token'];
}

$credentials = new WorkloadIdentityCredentials(
    identityTokenProvider: fetchEntraToken(...),
    federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
    organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
    serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
    workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
);
$client = new Client(credentials: $credentials);

$message = $client->messages->create(
    model: 'claude-sonnet-4-6',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello from Azure']],
);
echo $message->content[0]->text, PHP_EOL;
```

```ruby Ruby nocheck
require "anthropic"
require "json"
require "net/http"

IMDS_URL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com"

def fetch_entra_token
  response = Net::HTTP.get(URI(IMDS_URL), {"Metadata" => "true"})
  JSON.parse(response).fetch("access_token")
end

credentials = Anthropic::WorkloadIdentityCredentials.new(
  identity_token_provider: -> { fetch_entra_token },
  federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
  organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
  service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
  workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"]
)
client = Anthropic::Client.new(credentials: credentials)

message = client.messages.create(
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello from Azure"}]
)
puts message.content.first.text
```

```bash CLI nocheck
# 将 Entra 签发的访问令牌写入 CLI 可读取的文件
ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp)
curl -sS -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://api.anthropic.com" \
  | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
export ANTHROPIC_IDENTITY_TOKEN_FILE

# ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID 和
# ANTHROPIC_SERVICE_ACCOUNT_ID 以及 ANTHROPIC_WORKSPACE_ID 从环境变量中读取。
ant messages create \
  --model claude-sonnet-4-6 \
  --max-tokens 1024 \
  --message '{role: user, content: "Hello from Azure"}'
```

</CodeGroup>

### 在 AKS 上使用 Entra Workload Identity

在 AKS 上，`AZURE_FEDERATED_TOKEN_FILE` 处的文件是由集群 OIDC 签发方签名的 Kubernetes 投影服务账号令牌，而不是 Entra 签发的令牌。要保持在本页描述的 Entra 中介路径上，请先在 `https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token` 处交换该令牌（联合 `client_credentials` 授权），然后将得到的 Entra 访问令牌作为身份令牌传递给 Anthropic SDK。

<CodeGroup>

```bash cURL nocheck
# 1. 将 Kubernetes 投影的令牌（在 $AZURE_FEDERATED_TOKEN_FILE 处）
#    交换为 Entra 签发的 JWT。
ENTRA_JWT=$(curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
  -d grant_type=client_credentials \
  -d "client_id=$AZURE_CLIENT_ID" \
  --data-urlencode "scope=https://api.anthropic.com/.default" \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  --data-urlencode "client_assertion@$AZURE_FEDERATED_TOKEN_FILE" \
  | jq -r .access_token)

# 2. 将 Entra JWT 交换为 Anthropic 访问令牌。
ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d @- <<JSON | jq -r .access_token
{
  "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
  "assertion": "$ENTRA_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
)

# 3. 调用 Claude API。
curl -sS 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 from Azure"}]
  }' | jq -r '.content[0].text'
```

```python Python nocheck
import os
from pathlib import Path
import httpx
import anthropic
from anthropic import WorkloadIdentityCredentials


def fetch_entra_token_via_federation() -> str:
    federated_token = Path(os.environ["AZURE_FEDERATED_TOKEN_FILE"]).read_text()
    response = httpx.post(
        f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}/oauth2/v2.0/token",
        data={
            "client_id": os.environ["AZURE_CLIENT_ID"],
            "grant_type": "client_credentials",
            "scope": "https://api.anthropic.com/.default",
            "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            "client_assertion": federated_token,
        },
    )
    response.raise_for_status()
    return response.json()["access_token"]


client = anthropic.Anthropic(
    credentials=WorkloadIdentityCredentials(
        identity_token_provider=fetch_entra_token_via_federation,
        federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
        organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
        service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
        workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
    ),
)

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello from Azure"}],
)
print(message.content[0].text)
```

```typescript TypeScript nocheck
import Anthropic from "@anthropic-ai/sdk";
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
import { readFile } from "node:fs/promises";

async function fetchEntraTokenViaFederation(): Promise<string> {
  const federatedToken = await readFile(process.env.AZURE_FEDERATED_TOKEN_FILE!, "utf8");
  const response = await fetch(
    `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
    {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: process.env.AZURE_CLIENT_ID!,
        grant_type: "client_credentials",
        scope: "https://api.anthropic.com/.default",
        client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        client_assertion: federatedToken
      })
    }
  );
  const body = (await response.json()) as { access_token: string };
  return body.access_token;
}

const client = new Anthropic({
  credentials: oidcFederationProvider({
    identityTokenProvider: fetchEntraTokenViaFederation,
    federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
    organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
    serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
    workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
    baseURL: "https://api.anthropic.com",
    fetch
  })
});

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

```go Go nocheck
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"

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

func fetchEntraTokenViaFederation(ctx context.Context) (string, error) {
	federatedToken, err := os.ReadFile(os.Getenv("AZURE_FEDERATED_TOKEN_FILE"))
	if err != nil {
		return "", err
	}
	form := url.Values{
		"client_id":             {os.Getenv("AZURE_CLIENT_ID")},
		"grant_type":            {"client_credentials"},
		"scope":                 {"https://api.anthropic.com/.default"},
		"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
		"client_assertion":      {strings.TrimSpace(string(federatedToken))},
	}
	tokenURL := "https://login.microsoftonline.com/" + os.Getenv("AZURE_TENANT_ID") + "/oauth2/v2.0/token"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return "", err
	}
	req.Header.Set("content-type", "application/x-www-form-urlencoded")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	var body struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return "", err
	}
	return body.AccessToken, nil
}

func main() {
	client := anthropic.NewClient(
		option.WithFederationTokenProvider(option.IdentityTokenFunc(fetchEntraTokenViaFederation), option.FederationOptions{
			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
		}),
	)
	message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeSonnet4_6,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Azure")),
		},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(message.Content[0].Text)
}
```

```java Java nocheck hidelines={1..18,-1}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.credentials.IdentityTokenProvider;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;

void main() {
    IdentityTokenProvider fetchEntraTokenViaFederation = () -> {
        try {
            var form = Map.of(
                            "client_id", System.getenv("AZURE_CLIENT_ID"),
                            "grant_type", "client_credentials",
                            "scope", "https://api.anthropic.com/.default",
                            "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
                            "client_assertion", Files.readString(Path.of(System.getenv("AZURE_FEDERATED_TOKEN_FILE"))))
                    .entrySet().stream()
                    .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), UTF_8))
                    .collect(Collectors.joining("&"));
            var request = HttpRequest.newBuilder(URI.create(
                            "https://login.microsoftonline.com/" + System.getenv("AZURE_TENANT_ID") + "/oauth2/v2.0/token"))
                    .header("content-type", "application/x-www-form-urlencoded")
                    .POST(HttpRequest.BodyPublishers.ofString(form))
                    .build();
            var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
            return new ObjectMapper().readTree(response.body()).get("access_token").asText();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    };

    AnthropicClient client = AnthropicOkHttpClient.builder()
            .federationTokenProvider(
                    fetchEntraTokenViaFederation,
                    System.getenv("ANTHROPIC_FEDERATION_RULE_ID"),
                    System.getenv("ANTHROPIC_ORGANIZATION_ID"),
                    System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
            .build();

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

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

```csharp C# nocheck hidelines={1..4}
using System.Text.Json;
using Anthropic.Models.Messages;
using Anthropic.Oidc;

var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
    FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
    OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
    ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
    IdentityTokenProvider = new EntraFederationTokenProvider(),
});
using var client = new AnthropicOidcClient(credentials);

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

class EntraFederationTokenProvider : IIdentityTokenProvider
{
    private static readonly HttpClient Http = new();

    public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
    {
        var federatedToken = await File.ReadAllTextAsync(
            Environment.GetEnvironmentVariable("AZURE_FEDERATED_TOKEN_FILE")!, ct);
        var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
        var form = new FormUrlEncodedContent(new Dictionary<string, string>
        {
            ["client_id"] = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")!,
            ["grant_type"] = "client_credentials",
            ["scope"] = "https://api.anthropic.com/.default",
            ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            ["client_assertion"] = federatedToken,
        });
        var response = await Http.PostAsync(
            $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token", form, ct);
        response.EnsureSuccessStatusCode();
        using var json = await JsonDocument.ParseAsync(
            await response.Content.ReadAsStreamAsync(ct), default, ct);
        return json.RootElement.GetProperty("access_token").GetString()!;
    }
}
```

```php PHP nocheck hidelines={1..3}
<?php
require 'vendor/autoload.php';

use Anthropic\Client;
use Anthropic\Credentials\WorkloadIdentityCredentials;

function fetchEntraTokenViaFederation(): string
{
    $ch = curl_init('https://login.microsoftonline.com/' . getenv('AZURE_TENANT_ID') . '/oauth2/v2.0/token');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => http_build_query([
            'client_id' => getenv('AZURE_CLIENT_ID'),
            'grant_type' => 'client_credentials',
            'scope' => 'https://api.anthropic.com/.default',
            'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
            'client_assertion' => file_get_contents(getenv('AZURE_FEDERATED_TOKEN_FILE')),
        ]),
    ]);
    $body = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $body['access_token'];
}

$client = new Client(
    credentials: new WorkloadIdentityCredentials(
        identityTokenProvider: fetchEntraTokenViaFederation(...),
        federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
        organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
        serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
        workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
    ),
);

$message = $client->messages->create(
    model: 'claude-sonnet-4-6',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello from Azure']],
);
echo $message->content[0]->text, PHP_EOL;
```

```ruby Ruby nocheck
require "anthropic"
require "json"
require "net/http"

def fetch_entra_token_via_federation
  tenant_id = ENV.fetch("AZURE_TENANT_ID")
  federated_token = File.read(ENV.fetch("AZURE_FEDERATED_TOKEN_FILE"))
  response = Net::HTTP.post_form(
    URI("https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token"),
    "client_id" => ENV.fetch("AZURE_CLIENT_ID"),
    "grant_type" => "client_credentials",
    "scope" => "https://api.anthropic.com/.default",
    "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    "client_assertion" => federated_token
  )
  JSON.parse(response.body).fetch("access_token")
end

client = Anthropic::Client.new(
  credentials: Anthropic::WorkloadIdentityCredentials.new(
    identity_token_provider: -> { fetch_entra_token_via_federation },
    federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
    organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
    service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"]
  )
)

message = client.messages.create(
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello from Azure"}]
)
puts message.content.first.text
```

```bash CLI nocheck
# 1. 将 Kubernetes 投影的令牌交换为 Entra 签发的访问令牌
#    并写入 CLI 可读取的临时文件。
ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp)
curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
  -d client_id="$AZURE_CLIENT_ID" \
  -d grant_type=client_credentials \
  --data-urlencode scope=https://api.anthropic.com/.default \
  -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
  --data-urlencode client_assertion@"$AZURE_FEDERATED_TOKEN_FILE" \
  | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
export ANTHROPIC_IDENTITY_TOKEN_FILE

# 2. 调用 Claude API。ANTHROPIC_FEDERATION_RULE_ID、
# ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID
# 从环境变量中读取。
ant messages create \
  --model claude-sonnet-4-6 \
  --max-tokens 1024 \
  --message '{role: user, content: "Hello from Azure"}'
```

</CodeGroup>

或者，直接向 Anthropic 注册你的 AKS 集群的 OIDC 签发方，跳过 Entra 中转。有关该模式，请参阅 [Kubernetes](/docs/en/manage-claude/wif-providers/kubernetes)。

## 验证设置

从你的 Azure 资源运行前面展示的 cURL 交换，确认 `POST /v1/oauth/token` 返回 `200`，其中包含以 `sk-ant-oat01-` 开头的 `access_token` 和以秒为单位的 `expires_in` 值。如果出现 `400 invalid_grant`，请参阅[排查交换失败](/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange)；最常见的 Azure 侧原因是注册的 `issuer_url` 与解码令牌中的 `iss` 声明不匹配。它们必须完全匹配。对于托管标识令牌，`iss` 值为 `https://login.microsoftonline.com/<TENANT_ID>/v2.0` 或 `https://sts.windows.net/<TENANT_ID>/`。

## 限定规则范围

<Warning>
  `oid` 声明是托管标识的 GUID，没有稳定的前缀。带有 `*` 的 `subject_prefix` 可以匹配租户中的任意标识，因此任何持有托管标识的工作负载都可以获取联合 Anthropic 令牌。
</Warning>

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

- **精确匹配 `oid`：** 将 `claims.oid` 设置为托管标识的完整对象 ID，永远不要对 Azure 令牌使用 `subject_prefix`。
- **固定 `tid` 作为深度防御：** 签发方 URL 已经固定了你的租户，但添加 `claims.tid` 可以在签发方记录被编辑时防止配置漂移。
- **固定受众：** 将 `audience` 设置为 `https://api.anthropic.com`，这样为其他资源铸造的令牌会被拒绝。
- **为每个托管标识使用单独的规则：** 为每个标识创建一个规则，而不是一个规则授权多个标识，这样你可以撤销单个工作负载的访问而不影响其他。

## 后续步骤

- 在 [Workload Identity Federation](/docs/en/manage-claude/workload-identity-federation) 中查看完整的配置模型。
- 参阅[提供者指南](/docs/en/manage-claude/workload-identity-federation#identity-providers)了解 AWS、Google Cloud、GitHub Actions 和 Kubernetes。
- 有关环境变量、配置文件和凭证优先级，请参阅 [WIF 参考文档](/docs/en/manage-claude/wif-reference)。
