在 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 步骤显示了要注册的确确 URL)以及托管标识的对象 ID(在 suboid 声明中)。你只需向 Anthropic 注册该签发方一次,编写一个匹配预期声明的联合规则,你的工作负载就会在运行时将其 Entra 令牌交换为 sk-ant-oat01-... 访问令牌。

Tip

AKS Pod 也可以跳过 Entra 交换,直接向 Anthropic 出示 Kubernetes 投影的服务账号令牌。该方式注册你的 AKS 集群的 OIDC 签发方而非 Entra 租户。有关该流程,请参阅 Kubernetes

前置条件

  • 熟悉 WIF 概念:服务账号、联合签发方和联合规则。
  • 一个具有分配托管标识权限(或在 AKS 上配置 Entra Workload Identity)的 Azure 订阅。
  • 你的 Microsoft Entra 租户 ID。在 Azure 门户中 Microsoft Entra ID -> Overview -> Tenant ID 下找到它。
  • 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。

配置 Azure

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

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

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

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

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_FILEAZURE_CLIENT_IDAZURE_TENANT_ID 注入到 Pod 中。AZURE_FEDERATED_TOKEN_FILE 处的文件包含 Kubernetes 投影的服务账号令牌,由 AKS 集群的 OIDC 签发方签名。

令牌声明

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

{
  "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
}

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

配置 Anthropic

按照设置指南在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。在 Console 中,选择 OIDC 提供者选项并提供以下 Entra 特定的值。

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

{
  "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,因此发现模式对两者都适用。

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

{
  "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 标签展示了原始流程。

# 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'
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)
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);
  }
}
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)
}
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());
}
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
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;
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
# 将 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"}'

在 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。

# 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'
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)
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);
  }
}
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)
}
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());
}
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(
            {{CONTENT}}quot;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
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;
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
# 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"}'

或者,直接向 Anthropic 注册你的 AKS 集群的 OIDC 签发方,跳过 Entra 中转。有关该模式,请参阅 Kubernetes

验证设置

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

限定规则范围

Warning

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

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

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

后续步骤