在 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(在 sub 和 oid 声明中)。你只需向 Anthropic 注册该签发方一次,编写一个匹配预期声明的联合规则,你的工作负载就会在运行时将其 Entra 令牌交换为 sk-ant-oat01-... 访问令牌。
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 在签发的令牌中同时作为 sub 和 oid 声明出现,你的 Anthropic 联合规则将匹配它。你可以在资源的 Identity 页面上找到它,或者对于用户分配的标识,在 Microsoft Entra ID -> Enterprise applications 下找到。
不需要进一步的 Azure 侧配置。标识附加后,Azure 实例元数据服务可从资源内部的 169.254.169.254 访问。
Entra Workload Identity 将 Kubernetes 服务账号与 Entra 应用联合,以便 Pod 可以将其集群签发的服务账号令牌交换为 Entra 签发的访问令牌。
- 在你的 AKS 集群上启用 OIDC 签发方(
az aks update --enable-oidc-issuer --enable-workload-identity ...)。 - 部署
azure-workload-identity变更 Webhook。 - 创建一个用户分配的托管标识和一个联合凭据,该凭据信任集群的 OIDC 签发方用于你的 Kubernetes 服务账号。
- 在你的 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 签发方签名。
令牌声明
托管标识的 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
}
sub 和 oid 是相同的(托管标识的对象 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"
}
根据令牌版本的不同,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.0 或 https://sts.windows.net/<TENANT_ID>/。
限定规则范围
oid 声明是托管标识的 GUID,没有稳定的前缀。带有 * 的 subject_prefix 可以匹配租户中的任意标识,因此任何持有托管标识的工作负载都可以获取联合 Anthropic 令牌。
将规则的 match 块锁定到适合你用例的最窄范围:
- 精确匹配
oid: 将claims.oid设置为托管标识的完整对象 ID,永远不要对 Azure 令牌使用subject_prefix。 - 固定
tid作为深度防御: 签发方 URL 已经固定了你的租户,但添加claims.tid可以在签发方记录被编辑时防止配置漂移。 - 固定受众: 将
audience设置为https://api.anthropic.com,这样为其他资源铸造的令牌会被拒绝。 - 为每个托管标识使用单独的规则: 为每个标识创建一个规则,而不是一个规则授权多个标识,这样你可以撤销单个工作负载的访问而不影响其他。
后续步骤
- 在 Workload Identity Federation 中查看完整的配置模型。
- 参阅提供者指南了解 AWS、Google Cloud、GitHub Actions 和 Kubernetes。
- 有关环境变量、配置文件和凭证优先级,请参阅 WIF 参考文档。