在 Okta 中使用 WIF
通过 Workload Identity Federation 将 Okta 服务应用程序标识联合到 Claude API。
Okta 可以通过 OAuth 2.0 client_credentials 授权向服务应用程序签发 OIDC 访问令牌,充当工作负载标识提供者。你的工作负载认证到 Okta(通常使用 private_key_jwt,因此不存储共享密钥),接收签名的 JSON Web Token (JWT),然后将该 JWT 与 Anthropic 交换为短期访问令牌。
Okta 授权服务器的签发方 URL 格式为 https://<your-domain>.okta.com/oauth2/<auth-server-id>。如果你使用内置的默认服务器,路径为 /oauth2/default。
你必须使用 Okta 自定义授权服务器(包括 default 服务器)。直接由 Okta 组织授权服务器签发的令牌(路径中没有授权服务器 ID 的 /oauth2/v1/token 端点)无法被外部方验证,因为 Okta 不为其发布签名密钥。
配置和认证到 Okta 的方式有很多,超出了本文档的范围。请确保你的配置和认证机制遵循公司的指导和安全实践。
前置条件
- 熟悉 WIF 概念:服务账号、联合签发方和联合规则。
- 一个启用了 API Access Management 的 Okta 组织(自定义授权服务器所需)。
- 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。
- 一个能够从 Okta 的
/v1/token端点请求令牌并能访问api.anthropic.com的工作负载。
配置 Okta
在高层次上你需要:
- 创建一个 Okta 服务应用程序。
- 配置你的默认授权服务器(或创建新的自定义授权服务器),包括受众、范围、访问策略和你想要匹配的任何自定义声明。
具体导航取决于你的 Okta 组织配置和管理控制台版本。下面的编号步骤展示了一条常见路径:
- 创建服务应用集成。 在 Okta Admin Console 中,创建一个类型为 API Services(OIDC,机器对机器)的新应用集成。记下生成的 Client ID。
- 配置客户端认证。 对于无密钥设置,选择 Public key / Private key(
private_key_jwt)并注册你的工作负载的公共 JWK。或者,如果你的环境可以安全存储客户端密钥,也可以使用客户端密钥。对于以下示例,你可能需要禁用应用程序上的 DPoP 要求;请确保你的生产设置符合组织的安全要求。 - 设置受众。 在你的自定义授权服务器上,将受众设置为
https://api.anthropic.com,以便签发的访问令牌携带该aud声明。Anthropic 会根据此固定值验证aud。 - 授予范围。 在你的自定义授权服务器上,确保存在至少一个服务应用程序被允许请求的范围(例如
anthropic.access)。Okta 会拒绝不包含已授予范围的client_credentials请求。 - 创建访问策略。 在你的自定义授权服务器上,创建一个访问策略,其中至少包含一个允许你的服务应用程序请求你在第 4 步中授予的范围的规则。
- (可选)添加自定义声明。 如果你想匹配客户端 ID 之外的内容,请在授权服务器的 Claims 选项卡中向访问令牌添加声明。
对于使用 client_credentials 的服务应用程序,Okta 将签发的访问令牌的 sub 声明设置为应用程序的 Client ID,将 iss 设置为授权服务器的签发方 URL。
配置 Anthropic
按照设置指南在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 Okta 特定的值。
联合签发方: 使用你的 Okta 自定义授权服务器 URL 和发现模式。Anthropic 会读取 Okta 的 .well-known/openid-configuration 发现文档,并从其公布的 jwks_uri 获取 JWKS。
{
"name": "okta-prod",
"issuer_url": "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9",
"jwks_source": "discovery"
}
联合规则: 匹配 Okta 的 sub 声明,即服务应用程序的 Client ID。如果你在 Okta 中定义了自定义声明,可以改用 claims 映射或 CEL condition 来匹配它们。
{
"name": "okta-pipeline",
"issuer_id": "fdis_...",
"match": {
"subject_prefix": "0oa1b2c3d4e5f6g7h8i9",
"audience": "https://api.anthropic.com"
},
"target": { "type": "service_account", "service_account_id": "svac_..." },
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}
获取令牌并调用 Claude API
与平台原生提供者(AWS、Google Cloud、Kubernetes)不同,后者在工作负载的运行时中提供令牌(通过投影文件或本地元数据端点),Okta 不这样做。你的工作负载必须调用 Okta 的令牌端点获取 JWT,然后将该 JWT 作为身份令牌传递给 Anthropic SDK。
# 1. 从 Okta 请求访问令牌(client_credentials + private_key_jwt)。
OKTA_JWT=$(curl -sS "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9/v1/token" \
-d grant_type=client_credentials \
-d scope=anthropic.access \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data-urlencode client_assertion="$SIGNED_CLIENT_ASSERTION" \
| jq -r .access_token)
# 2. 将 Okta 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": "$OKTA_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 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'
import os
import httpx
import anthropic
from anthropic import WorkloadIdentityCredentials
def fetch_okta_token() -> str:
response = httpx.post(
f"{os.environ['OKTA_ISSUER']}/v1/token",
data={
"grant_type": "client_credentials",
"scope": "anthropic.access",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
# 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
"client_assertion": build_signed_client_assertion(),
},
)
response.raise_for_status()
return response.json()["access_token"]
client = anthropic.Anthropic(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_okta_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, Claude"}],
)
print(message.content[0].text)
import Anthropic from "@anthropic-ai/sdk";
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
async function fetchOktaToken(): Promise<string> {
const response = await fetch(`${process.env.OKTA_ISSUER}/v1/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "anthropic.access",
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
client_assertion: buildSignedClientAssertion()
})
});
const body = (await response.json()) as { access_token: string };
return body.access_token;
}
const client = new Anthropic({
credentials: oidcFederationProvider({
identityTokenProvider: fetchOktaToken,
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, Claude" }]
});
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 fetchOktaToken(ctx context.Context) (string, error) {
form := url.Values{
"grant_type": {"client_credentials"},
"scope": {"anthropic.access"},
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
// 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
"client_assertion": {buildSignedClientAssertion()},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
os.Getenv("OKTA_ISSUER")+"/v1/token", 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(fetchOktaToken), 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, Claude")),
},
})
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.util.Map;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
void main() {
IdentityTokenProvider fetchOktaToken = () -> {
try {
var form = Map.of(
"grant_type", "client_credentials",
"scope", "anthropic.access",
"client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
"client_assertion", buildSignedClientAssertion())
.entrySet().stream()
.map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), UTF_8))
.collect(Collectors.joining("&"));
var request = HttpRequest.newBuilder(URI.create(System.getenv("OKTA_ISSUER") + "/v1/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(
fetchOktaToken,
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, Claude")
.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 OktaTokenProvider(),
});
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, Claude" }],
});
foreach (var block in message.Content)
{
if (block.Value is TextBlock textBlock)
{
Console.WriteLine(textBlock.Text);
}
}
class OktaTokenProvider : IIdentityTokenProvider
{
private static readonly HttpClient Http = new();
public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
{
var form = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["scope"] = "anthropic.access",
["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
["client_assertion"] = BuildSignedClientAssertion(),
});
var response = await Http.PostAsync(
{{CONTENT}}quot;{Environment.GetEnvironmentVariable("OKTA_ISSUER")}/v1/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()!;
}
}
# 1. 从 Okta 请求访问令牌并写入临时文件。
ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp)
curl -sS "$OKTA_ISSUER/v1/token" \
-d grant_type=client_credentials \
-d scope=anthropic.access \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data-urlencode client_assertion="$SIGNED_CLIENT_ASSERTION" \
| jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
export ANTHROPIC_IDENTITY_TOKEN_FILE
# 2. 调用 Claude API。CLI 读取 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
require 'vendor/autoload.php';
use Anthropic\Client;
use Anthropic\Credentials\WorkloadIdentityCredentials;
function fetchOktaToken(): string
{
$ch = curl_init(getenv('OKTA_ISSUER') . '/v1/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'client_credentials',
'scope' => 'anthropic.access',
'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
// 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
'client_assertion' => buildSignedClientAssertion(),
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
return $body['access_token'];
}
$client = new Client(
credentials: new WorkloadIdentityCredentials(
identityTokenProvider: fetchOktaToken(...),
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, Claude']],
);
echo $message->content[0]->text, PHP_EOL;
require "anthropic"
require "json"
require "net/http"
def fetch_okta_token
uri = URI("#{ENV.fetch('OKTA_ISSUER')}/v1/token")
response = Net::HTTP.post_form(
uri,
"grant_type" => "client_credentials",
"scope" => "anthropic.access",
"client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
# 构建用你的 Okta 应用私钥签名的 RFC 7523 client_assertion JWT
"client_assertion" => build_signed_client_assertion
)
JSON.parse(response.body).fetch("access_token")
end
client = Anthropic::Client.new(
credentials: Anthropic::WorkloadIdentityCredentials.new(
identity_token_provider: -> { fetch_okta_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"]
)
)
message = client.messages.create(
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}]
)
puts message.content.first.text
每个 SDK 标签都展示了可调用模式:Anthropic SDK 会在 Anthropic 访问令牌接近过期时再次调用你的身份令牌提供者,因此你的 Okta 获取器应该每次调用都返回新令牌,而不是无限期缓存。ant CLI 在每次交换时重新读取 ANTHROPIC_IDENTITY_TOKEN_FILE,因此对于长时间运行的 Shell,请定时刷新该文件。
验证设置
成功交换会返回一个以 sk-ant-oat01- 开头的 access_token 和一个以秒为单位的 expires_in 值。如果出现 400 invalid_grant,请参阅排查交换失败;最常见的 Okta 侧原因是 issuer_url 不匹配(它必须包含 /oauth2/<auth-server-id> 路径;Okta 组织授权服务器不可用)。
限定规则范围
同一 Okta 授权服务器下的多个服务应用程序共享同一个签发方。省略 subject_prefix 的规则会匹配该服务器上的每个服务应用程序,因此任何能注册服务应用程序的团队都可以获取联合 Anthropic 令牌。
将规则的 match 块锁定到适合你用例的最窄范围:
- 固定精确的 Client ID: 将
subject_prefix设置为服务应用程序的完整 Client ID,不带末尾的*。 - 固定受众: 匹配你在授权服务器上配置的
audience值,这样为不同受众铸造的令牌会被拒绝。 - 匹配自定义声明: 为了更细粒度的范围限定,在授权服务器的 Claims 选项卡中添加声明,并使用规则的
claims映射或 CELcondition来匹配它们。 - 为每个服务应用程序使用一个规则: 为每个服务应用程序创建单独的联合规则,而不是跨应用程序共享一个规则。