
【新機能】ClaudeのAPIキーなしで認証。Claude API Workload Identity Federation を実装してみた
こんにちは、せーのです。
本日、Anthropic(Claude API)がAPIキーを使わないキーレス認証として、Workload Identity Federation(WIF)を発表しました。
主なポイントは次のとおりです。
- APIキー管理の手間と漏洩リスクを減らせる
- AWS/GCP/Azure などの既存クラウドIDやOIDCトークンで直接認証できる
- CLIでもブラウザ経由の認証フローを使える
- 定期的なキーのローテーション作業を減らせる
- CI/CDパイプラインへ、より安全に組み込みやすくなる
WIFという概念自体は、AWSやGoogle Cloud、あるいはGitHub ActionsのOIDCを触っていれば馴染みがあると思います。今回の要点は、その仕組みがClaude APIの認証にも正式に載ったことです。
Workload Identity Federationとは(おさらい)
Workload Identity Federationは、ざっくり言うと「自分のワークロードが持っている短命な身分証明書を、相手サービス側の短命なアクセストークンに交換する仕組み」です。
今回の流れはこうです。
- ワークロードがIdPからOIDC JWTを受け取る
- そのJWTをAnthropicの
/v1/oauth/tokenに渡す - AnthropicがJWTの署名やクレームを検証する
- 問題なければ
sk-ant-oat01-...で始まる短命なアクセストークンを返す - そのアクセストークンでClaude APIを呼ぶ

違いを整理すると、APIキーは「Claude API用の長期的な秘密情報をあらかじめ作って置いておく」もの、Workload Identity Federationは「実行環境がその場で発行・取得した短命トークンを、Claude API用の短命トークンに交換する」もの、というイメージです。
どんな場面で使えるか
公式ドキュメントとリサーチを踏まえると、WIFはOIDC対応の実行基盤で広く使えます。代表例は次のとおりです。
- 主要クラウド上のアプリケーション(AWS / Google Cloud / Azure)
- Kubernetesクラスター上のワークロード(Podのprojected service-account token)
- GitHub ActionsのようなCI/CD実行環境
- Oktaなど、OIDC準拠のIdPを使うワークロード
要するに、OIDC JWTを安全に取得できる実行環境なら、Claude APIの認証をAPIキーからWIFに置き換えられる、という整理です。
やってみた
今回はその中でも再現しやすいGitHub Actionsを使って、実際に認証フローを通してみます。
ゴールはシンプルで、workflowからClaude APIを叩き、APIキーなしで期待どおりの本文が返ることを確認することです。実験では認証の切り分けのため、プロンプトは「決め打ちの1文をそのまま返す」スモークテストに寄せました(モデルの自由回答と混ぜない)。
Anthropic Workload Identity Federation smoke test OK.
(Python SDK版は別プロンプトで、同様に固定文を返させています。)
作るものは大きく3つです。
- Anthropic Service Account
- Anthropic Federation Issuer
- Anthropic Federation Rule
これに加えて、GitHub Actions側でOIDCトークンを取得し、Anthropicのアクセストークンに交換します。

Anthropic Consoleで設定する
まずAnthropic ConsoleでService Accountを作ります。
Service Accountは、ワークロードがClaude APIを呼ぶ時に「誰として振る舞うか」を表す非人間のアイデンティティです。APIキーそのものではなく、短命なアクセストークンを発行される対象、と考えるとわかりやすいです。


今回のConsole UIでは、後述のFederation Rule作成時にService AccountとWorkspaceを直接指定する形でした(別途Workspace Members画面で追加するステップは取っていません)。
次にFederation Issuerを作ります。今回はGitHub Actionsを試すので、OIDC issuerは以下です。
https://token.actions.githubusercontent.com
JWKS sourceは discovery にします。GitHub Actionsは公開されたOIDC discovery documentとJWKSを提供しているので、ここは素直にdiscoveryでOKです。



最後にFederation Ruleを作ります。ここが一番大事です。
まずGitHub Actions側でOIDC tokenのclaimsだけを確認し、その sub に合わせて狭めに作るのが安全です。下は workflow_dispatch を main ブランチから実行した時に、sub が repo:<OWNER>/<REPO>:ref:refs/heads/main だった場合の例です(Console上のフィールド名は subject_pattern 側に寄せて読み替えてください)。
subject_pattern: repo:<OWNER>/<REPO>:ref:refs/heads/main
audience: https://api.anthropic.com
claims:
repository_owner: <OWNER>
ref: refs/heads/main
ポイントは audience です。GitHub Actions向けの公式ドキュメントでは https://api.anthropic.com を使っています。api.anthropic.com ではありません。
ここは完全一致なので、プロトコル有無で別物になります。地味ですが、ここで躓きやすいです。
実際には先にGitHub ActionsのOIDC claimsをログに出し、その値にRuleを合わせるのが確実です。今回の事前確認用workflowでは、実行前〜実行中〜成功サマリ〜claimsログまで、画面で追いやすいように切っています。






Ruleを作ったら fdrl_...、Service Accountの svac_...、Organization IDを控えておきます。Rule IDはクライアントから明示して使います(Anthropic側が自動で「合うRule」を探してくれる、という挙動ではない点だけ注意です)。
やってみた:Repository VariablesとcURL版
Repository Variables
GitHub Actions側では、まずRepository Variablesに次の3つを入れます。
ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
ANTHROPIC_SERVICE_ACCOUNT_ID=svac_...
値そのものは「秘密鍵」ではないので、SecretsではなくVariablesで十分、という整理です(公開リポジトリでどこまで見せるかはチームのポリシーに従ってください)。
今回の検証では、gh variable list がPAT権限の都合で HTTP 403: Resource not accessible by personal access token になり、CLIでは一覧確認できませんでした。最終的にはGitHubのWeb UIで3件そろっていることを見て切り上げています。

cURL版workflow(全体)
まずはSDKに逃げず、cURLで「JWTの中身 → トークン交換 → Messages API」までをログで追える形にします。GitHub ActionsでOIDCトークンを取るには permissions: id-token: write が必須で、これがないとトークン取得用の環境変数が使えません。
モデルIDは実行時点の推奨に合わせて読み替えてください。実験時点では claude-sonnet-4-6 を使いました。
name: Claude API via WIF - curl
on:
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
call-claude:
runs-on: ubuntu-latest
env:
ANTHROPIC_FEDERATION_RULE_ID: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
ANTHROPIC_ORGANIZATION_ID: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
ANTHROPIC_SERVICE_ACCOUNT_ID: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
steps:
- uses: actions/checkout@v5
- name: Fetch GitHub Actions OIDC token
run: |
curl -sS \
-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
| jq -r .value > /tmp/gha-jwt
- name: Decode OIDC claims
run: |
jq -rR 'split(".")[1] | gsub("-"; "+") | gsub("_"; "/") | @base64d | fromjson' \
< /tmp/gha-jwt \
| jq '{iss, sub, aud, repository, repository_owner, ref, event_name, workflow, actor, exp, iat}'
- name: Exchange OIDC token for Anthropic access token
run: |
JWT=$(cat /tmp/gha-jwt)
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": "$JWT",
"federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
"organization_id": "$ANTHROPIC_ORGANIZATION_ID",
"service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID"
}
JSON
)
echo "$RESPONSE" | jq '{token_type, expires_in, scope, access_token_prefix: (.access_token | if startswith("sk-ant-oat01-") then "sk-ant-oat01-..." else "unexpected" end)}'
echo "ANTHROPIC_ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)" >> "$GITHUB_ENV"
- name: Call Claude API
run: |
curl -sS https://api.anthropic.com/v1/messages \
-H "authorization: Bearer $ANTHROPIC_ACCESS_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
--data @- <<'JSON' | jq -r '.content[0].text'
{
"model": "claude-sonnet-4-6",
"max_tokens": 32,
"messages": [
{
"role": "user",
"content": "Respond with exactly this sentence: Anthropic Workload Identity Federation smoke test OK."
}
]
}
JSON
トークン交換のログでは、アクセストークン全体をそのまま出さず、sk-ant-oat01-... に始まるかだけを確認できるようにしています。
実際に出たOIDC claims(JWT本体は載せない)
Decode OIDC claims の出力は、次のような形でした(2026-05-05時点・同一リポジトリでの実行例)。
iss: https://token.actions.githubusercontent.com
sub: repo:seino-tsuyoshi/claude-wif-hands-on:ref:refs/heads/main
aud: https://api.anthropic.com
repository: seino-tsuyoshi/claude-wif-hands-on
repository_owner: seino-tsuyoshi
ref: refs/heads/main
event_name: workflow_dispatch
workflow: Claude API via WIF - curl
actor: seino-tsuyoshi
ここで見るべきは sub です。Federation Rule側の subject_pattern(または同等のmatch条件)は、この文字列と整合させます。

トークン交換のレスポンス
/v1/oauth/token 側の結果は次のとおりでした。
token_type: Bearer
expires_in: 598 (有効時間)
scope: workspace:developer
access_token_prefix: sk-ant-oat01-...
expires_in が598秒なのは、Federation Ruleにて、発行するトークンは最長でも大体10分としていたことと、GitHub ActionsのOIDC JWT自体が短命であることの両方が効いているイメージです。公式ドキュメントの整理どおり、Ruleの設定だけが「そのまま秒数になる」わけではないので、CIではログで一度実測しておくのが安心です。

Messages APIの結果
APIキーではなく、交換したアクセストークンを Authorization: Bearer で渡して呼びます。今回のスモークテストでは、本文だけを抜き出して確認しました。
Anthropic Workload Identity Federation smoke test OK.


やってみた:Python SDK版
cURL版は「何が起きたか」を追うのに向いています。実アプリやバッチに落とすなら、SDK側にトークン交換と更新を寄せた方がコードが薄くなります。
scripts/ci_claude.py は次のような最小構成で十分でした(APIキーはコードにも環境にも置いていません)。
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=32,
messages=[
{
"role": "user",
"content": "Respond with exactly this sentence: Anthropic Python SDK WIF smoke test OK.",
}
],
)
print(message.content[0].text)
workflow側では、GitHub ActionsのOIDC JWTをファイルに書き、ANTHROPIC_IDENTITY_TOKEN_FILE を渡します。
name: Claude API via WIF - SDK
on:
workflow_dispatch:
permissions:
id-token: write
contents: read
jobs:
call-claude:
runs-on: ubuntu-latest
env:
ANTHROPIC_FEDERATION_RULE_ID: ${{ vars.ANTHROPIC_FEDERATION_RULE_ID }}
ANTHROPIC_ORGANIZATION_ID: ${{ vars.ANTHROPIC_ORGANIZATION_ID }}
ANTHROPIC_SERVICE_ACCOUNT_ID: ${{ vars.ANTHROPIC_SERVICE_ACCOUNT_ID }}
ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt
steps:
- uses: actions/checkout@v5
- name: Fetch GitHub Actions OIDC token
run: |
curl -sS \
-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
| jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
- name: Install dependencies
run: pip install anthropic
- name: Run Claude with SDK
run: python scripts/ci_claude.py
実行結果として、pip install のログに anthropic 0.98.1 が出ること、Run Claude with SDK の標準出力に Anthropic Python SDK WIF smoke test OK. が出ることを確認しました。
初回に古いモデルIDを指定していたときはDeprecated警告が出たので、実験リポジトリ側を claude-sonnet-4-6 に寄せたら警告は消えました。認証とは別軸ですが、CIログのノイズとしては地味に効きます。



SDKに寄せると、アプリ側は Anthropic() を引数なしで起動できるのが大きいです。コンテナや長めのジョブでは、JWTファイルの更新タイミングも含めて設計する余地はありますが、それはまた別の話として、まずは「APIキーを置かずに回る」体感を取りに行くのにちょうどよい塩梅でした。
ハマりどころ
audienceは完全一致
今回のポイントの一つは「audienceは正確に同じ文字列にしなくてはいけない」ということです。
なぜ https://api.anthropic.com というURLなのか
OIDCのJWTには aud(Audience) という項目があり、「このトークンは、誰向けに発行されたものか」 を表しています。別の言い方をすると、取り違え防止用の宛名です。
Anthropicはそのラベルとして、Claude APIのベースURLと同じ文字列を使う、と決めています。なので見た目がURLになります。いまブラウザで開くためのリンクを書いているわけではなく、「Claude API向けのトークンだよ」と示す決め打ちの名前がURL形式なだけ、と捉えると読みやすいです。
2か所に書くのは「同じ文章を二重にメモする」ことではない
GitHub側とAnthropic側で 同じ文字列 を使いますが、やっていることは別です。
| どこ | 何をしているか |
|---|---|
GitHub Actions(ACTIONS_ID_TOKEN_REQUEST_URL の audience=) |
トークンを発行するときに、「JWTの中の aud ラベルを この文字列にして」とGitHubに依頼する |
Anthropic Console(Federation Ruleの audience フィールド) |
トークン交換のときに、「渡されたJWTの aud ラベルが この文字列と完全一致するか」をAnthropicが確認する |
つまり 一方がラベルを付け、もう一方がそのラベルを検査している 関係です。付ける値と検査する値がズレると、まだ sub やリポジトリの話に進む前に不一致になります。
実際のJWTをデコードすると、aud にはだいたい次の1行だけが入っています(記事のログ例と同じです)。
aud: https://api.anthropic.com
GitHubの audience= も、AnthropicのRuleの audience も、この1行と同じ文字列になるように揃えるのが目的です。
指定する文字列(完全一致)
https://api.anthropic.com
https:// が付いているかどうかも含めて別物です。api.anthropic.com だけ(スキームなし)では足りません。
実際に間違えると、トークン交換は 400 invalid_grant になりやすいです。レスポンス本文だけだと原因が特定しづらいケースもあるので、Console側のAuthentication historyとセットで見るのが早い、というのが手応えでした。
今回の記事用の実行ログとしては、わざとaudienceを崩した失敗パターンまでは取り込んでいません(時間の都合)。再現するなら、OIDC取得の audience= を api.anthropic.com 側に寄せる、などが定番です。
subject(match条件)は広げすぎない
GitHub Actionsの sub は、実行イベントによって形が変わります。
例えばbranch参照なら次のような形です。
repo:your-org/your-repo:ref:refs/heads/main
pull requestなら末尾が変わります。
repo:your-org/your-repo:pull_request
ここで repo:your-org/* のように広く許可すると、意図しないリポジトリやイベントからトークンが取得できてしまう可能性があります。
まずは特定リポジトリ・特定ブランチに寄せ、repository_owner と ref でもう一段絞る、という作りが無難です。今回の事前確認workflowで sub をログに出してからRuleを作る、という順番は、そのまま運用にも持ち込みやすいです。
ANTHROPIC_API_KEY が残っているとFederationが使われない
移行時に地味に困りそうなのが、認証情報の優先順位です。
Anthropic SDKでは、ANTHROPIC_API_KEY や ANTHROPIC_AUTH_TOKEN がFederationより優先されます。つまり、既存のAPIキー環境変数が残っていると、せっかくWIFの環境変数を設定してもFederationが選ばれません。
さらに、公式ドキュメントでは空文字でもそのスロットを占有すると説明されています。
ANTHROPIC_API_KEY=""
これは「空だから無視してWIFにフォールバック」ではありません。きちんと unset ANTHROPIC_API_KEY する必要があります。
今回のworkflowではそもそも ANTHROPIC_API_KEY を触っていないので、優先順位の実測ログは別途です。移行フェーズでは、ログや ant auth status 相当の確認まで含めて一度通した方が安全そうです。
まとめ
Claude APIのWorkload Identity Federationを使うと、GitHub Actionsに長期APIキーを置かずにClaude APIを呼べます。
今回の実験で強く残ったのは次の4点です。
長期キーをSecretsに固定しない
OIDC JWTを起点に短命トークンへ交換するので、運用上の「常時有効な秘密」が減ります。漏洩時の影響範囲の考え方も、APIキー時代と少し違ってきます。
Ruleのmatchが設計の中心
Service Account、Issuer、Ruleの3点セットのうち、どこまで許すかはRuleに集約されます。audience の完全一致、sub との整合、必要なら repository_owner / ref まで落とす、が堅いです。
cURLで一度通すと、後工程が楽
SDKに行く前にログで追えると、トラブル時に「JWTが期待どおりか」「交換が通っているか」を切り分けやすいです。今回も、inspect用workflowを分けたのが効きました。
SDKは Anthropic() だけ、が強い
実装を薄くしたいなら、環境変数を揃えてSDKに寄せるのが一番シンプルでした。モデルIDのDeprecation警告みたいな別問題はありますが、認証まわりの記述量は明確に減ります。
移行時だけは、ANTHROPIC_API_KEY の消し忘れに注意です。環境変数が残っているとFederationより優先されるので、「設定した」で終わらず、実際にどの経路で認証されたかまで確認できると安心です。
静的APIキーをCIに置かなくてよくなるのは、運用面でかなり大きい変化だと思います。GitHub ActionsやKubernetesからClaude APIを叩いている方は、まず小さなリポジトリでスモークテストしてみると良いと思います。









