Okta 可以通过 OAuth 2.0 client_credentials 授权方式向服务应用(service application)颁发 OIDC 访问令牌,从而充当工作负载身份提供方。您的工作负载向 Okta 进行身份验证(通常使用 private_key_jwt,因此无需存储共享密钥),获得一个已签名的 “JSON Web Token”(JSON Web 令牌),即 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 以及向 Okta 进行身份验证的方式有很多,这些内容超出了本文档的范围。请确保您的配置和身份验证机制遵循贵公司的指导方针和安全实践。
前提条件
- 熟悉 WIF 概念:服务账户、联合颁发者和联合规则。
- 一个已启用 API Access Management 的 Okta 组织(自定义授权服务器需要此功能)。
- 有权在 Claude Console 中为您的 Anthropic 组织创建服务账户、联合颁发者和联合规则。
- 一个能够从 Okta 的
/v1/token端点请求令牌并能访问api.anthropic.com的工作负载。
配置 Okta
总体而言,您需要:
- 创建一个 Okta 服务应用。
- 为您的默认授权服务器(或新建一个自定义授权服务器)配置受众(audience)、作用域(scope)、访问策略,以及您希望用于匹配的任何自定义声明(claim)。
具体的导航路径取决于您的 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 中,前往 Workload identity 页面(platform.claude.com/settings/workload-identity-federation),点击 Connect workload,然后选择 Custom OIDC。向导将引导您完成注册颁发者、创建服务账户和创建联合规则的过程。
向导会为您创建这些资源。无论您是在向导中输入这些值,还是将它们发送到 Admin API,都请使用以下值:
联合颁发者: 使用您的 Okta 自定义授权服务器 URL 和发现模式。Anthropic 会读取 Okta 的 .well-known/openid-configuration 发现文档,并从其公布的 jwks_uri 获取 JWKS。
{
"name": "okta-prod",
"issuer_url": "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9",
"jwks": { "type": "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。
cURL
# 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-opus-5-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello, Claude"}]
}'
CLI
# 1. 从 Okta 请求访问令牌并将其写入临时文件。
export 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"
# 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-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello, Claude"}'
Python
import os
import httpx2
import anthropic
from anthropic import WorkloadIdentityCredentials
def fetch_okta_token() -> str:
response = httpx2.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",
# 构建 RFC 7523 client_assertion JWT,并使用您的 Okta 应用私钥签名
"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-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(next(block.text for block in message.content if block.type == "text"))
TypeScript
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",
// 构建 RFC 7523 client_assertion JWT,并使用您的 Okta 应用私钥签名
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-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude" }]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
}
C#
using Anthropic.Credentials;
// ...
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 AnthropicClient(new ClientOptions { Credentials = credentials });
var message = await client.Messages.Create(new()
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = [new UserMessageParam { 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",
// 构建 RFC 7523 client_assertion JWT,并使用您的 Okta 应用私钥签名
["client_assertion"] = BuildSignedClientAssertion(),
});
var response = await Http.PostAsync(
$"{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()!;
}
}
Go
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/anthropics/anthropic-sdk-go/v2"
"github.com/anthropics/anthropic-sdk-go/v2/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"},
// 构建 RFC 7523 client_assertion JWT,并使用您的 Okta 应用私钥签名
"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.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
},
})
if err != nil {
panic(err)
}
for _, block := range message.Content {
if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
fmt.Println(textBlock.Text)
break
}
}
}
Java
import com.anthropic.config.AuthenticationConfig;
import com.anthropic.config.AuthenticationType;
import com.anthropic.config.IdentityTokenConfig;
import com.anthropic.config.InMemoryProfileConfigProvider;
import com.anthropic.config.ProfileConfig;
// ...
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() throws Exception {
var form = Map.of(
"grant_type", "client_credentials",
"scope", "anthropic.access",
"client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
// 构建 RFC 7523 client_assertion JWT,并使用您的 Okta 应用私钥签名
"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();
// Java SDK 接受令牌本身或文件路径,而非函数;使用
// source("value") 时,path(...) 保存该令牌。SDK 会在每次刷新
// Anthropic 访问令牌时发送同一令牌,一旦该令牌
// 过期,或(若含 jti 声明)已被使用,刷新即会失败。对于长时间运行的
// 进程,请传入 source("file"),并在每次刷新前
// 将新令牌写入该文件。
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
String oktaToken = new ObjectMapper().readTree(response.body()).get("access_token").asText();
AnthropicClient client = AnthropicOkHttpClient.builder()
.configurationProvider(InMemoryProfileConfigProvider.of(ProfileConfig.builder()
.organizationId(System.getenv("ANTHROPIC_ORGANIZATION_ID"))
.workspaceId(System.getenv("ANTHROPIC_WORKSPACE_ID"))
.authentication(AuthenticationConfig.builder()
.type(AuthenticationType.OIDC_FEDERATION)
.federationRuleId(System.getenv("ANTHROPIC_FEDERATION_RULE_ID"))
.serviceAccountId(System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
.identityToken(IdentityTokenConfig.builder()
.source("value")
.path(oktaToken)
.build())
.build())
.build()))
.build();
var message = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024)
.addUserMessage("Hello, Claude")
.build());
IO.println(message.content());
}
PHP
use Anthropic\Client;
use Anthropic\Lib\Credentials\CredentialResult;
use Anthropic\Lib\Credentials\IdentityTokenProvider;
use Anthropic\Lib\Credentials\TokenCache;
use Anthropic\Lib\Credentials\WorkloadIdentityCredentials;
class OktaIdentityToken implements IdentityTokenProvider
{
public function getIdentityToken(): 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',
// 构建 RFC 7523 client_assertion JWT,并使用您的 Okta 应用私钥签名
'client_assertion' => buildSignedClientAssertion(),
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
return $body['access_token'];
}
}
$client = new Client(
credentials: new CredentialResult(
provider: new TokenCache(new WorkloadIdentityCredentials(
identityProvider: new OktaIdentityToken(),
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-opus-5-5',
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello, Claude']],
);
echo array_find($message->content, static fn ($block): bool => $block->type === 'text')->text, PHP_EOL;
Ruby
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",
# 构建符合 RFC 7523 的 client_assertion JWT,并使用您的 Okta 应用私钥签名
"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-opus-5-5",
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}]
)
puts message.content.find { it.type == :text }.text
除 Java 外(请参阅该示例中的注释),SDK 示例会将您的 Okta 令牌获取函数作为令牌提供程序传递给 identity_token_provider (typescript: identityTokenProvider; php: identityProvider; csharp: IdentityTokenProvider; go: option.WithFederationTokenProvider(); java: IdentityTokenConfig)。每当 Anthropic 访问令牌即将过期时,Claude SDK 都会调用它,因此您的 Okta 获取函数应在每次调用时返回新令牌,而不是无限期地缓存某个令牌。ant CLI 在每次交换时都会重新读取 ANTHROPIC_IDENTITY_TOKEN_FILE,因此对于长时间运行的 shell,请定时刷新该文件。
验证设置
交换成功后会返回一个以 sk-ant-oat01- 开头的 access_token,以及一个以秒为单位的 expires_in 值。如果交换失败并返回不透明的 401 authentication_error 响应(消息为 Authentication failed),请在 Claude Console 中查看身份验证历史记录页面以了解拒绝原因,并参阅排查失败的交换。Okta 端最常见的原因是 issuer_url 不匹配。issuer_url 必须包含 /oauth2/<auth-server-id> 路径,且 Okta 组织授权服务器不可用。
限定规则范围
警告
同一 Okta 授权服务器下的多个服务应用共享同一个颁发者。 省略
subject_prefix的规则会匹配该服务器上的每一个服务应用, 因此任何能够注册服务应用的团队都可能获得联合的 Anthropic 令牌。
将规则的 match 块锁定到符合您用例的最小范围:
- 固定精确的 Client ID: 将
subject_prefix设置为服务应用的完整 Client ID,末尾不带*。 - 固定受众: 匹配您在授权服务器上配置的
audience值,以便拒绝为其他受众签发的令牌。 - 基于自定义声明进行匹配: 若要实现更细粒度的范围限定,请在授权服务器的 Claims 选项卡中添加声明,并使用规则的
claims映射或 CELcondition对其进行匹配。 - 每个服务应用使用一条规则: 为每个服务应用创建单独的联合规则,而不是在多个应用之间共享一条规则。