【新機能】ClaudeのAPIキーなしで認証。Claude API Workload Identity Federation を実装してみた
ちょっと話題の記事

【新機能】ClaudeのAPIキーなしで認証。Claude API Workload Identity Federation を実装してみた

2026.05.05

こんにちは、せーのです。

本日、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は、ざっくり言うと「自分のワークロードが持っている短命な身分証明書を、相手サービス側の短命なアクセストークンに交換する仕組み」です。

今回の流れはこうです。

  1. ワークロードがIdPからOIDC JWTを受け取る
  2. そのJWTをAnthropicの /v1/oauth/token に渡す
  3. AnthropicがJWTの署名やクレームを検証する
  4. 問題なければ sk-ant-oat01-... で始まる短命なアクセストークンを返す
  5. そのアクセストークンでClaude APIを呼ぶ

claude-wif-architecture1.jpg

違いを整理すると、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のアクセストークンに交換します。

claude-wif-architecture2.jpg

Anthropic Consoleで設定する

まずAnthropic ConsoleでService Accountを作ります。

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

anthropic-cwn-service-account-form-window-cli_masked.png

anthropic-cwn-service-account-created-window-cli_masked.png

今回の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です。

anthropic-cwn-workload-id-page-window-cli_masked.png

anthropic-cwn-register-issuer-filled-window-cli_masked.png

anthropic-cwn-issuer-created-window-cli_masked.png

最後にFederation Ruleを作ります。ここが一番大事です。

まずGitHub Actions側でOIDC tokenのclaimsだけを確認し、その sub に合わせて狭めに作るのが安全です。下は workflow_dispatchmain ブランチから実行した時に、subrepo:<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ログまで、画面で追いやすいように切っています。

github-actions-inspect-oidc-before-run-window-cli_masked.png

github-actions-inspect-oidc-running-window-cli_masked.png

github-actions-inspect-oidc-success-summary-window-cli_masked.png

github-actions-inspect-oidc-claims-log-window-cli_masked.png

anthropic-cwn-rule-filled-window-cli_masked.png

anthropic-cwn-rule-created-window-cli_masked.png

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件そろっていることを見て切り上げています。

github-actions-repo-variables-created-window-cli_masked.png

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条件)は、この文字列と整合させます。

github-actions-curl-redacted-claims-log-window-cli.png

トークン交換のレスポンス

/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ではログで一度実測しておくのが安心です。

github-actions-curl-redacted-token-log-window-cli.png

Messages APIの結果

APIキーではなく、交換したアクセストークンを Authorization: Bearer で渡して呼びます。今回のスモークテストでは、本文だけを抜き出して確認しました。

Anthropic Workload Identity Federation smoke test OK.

github-actions-curl-redacted-api-log-window-cli.png

github-actions-curl-redacted-success-summary-window-cli.png

やってみた: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ログのノイズとしては地味に効きます。

github-actions-sdk-install-deps-window-cli.png

github-actions-sdk-active-model-log-window-cli.png

github-actions-sdk-active-model-success-summary-window-cli.png

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 ActionsACTIONS_ID_TOKEN_REQUEST_URLaudience= トークンを発行するときに、「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_ownerref でもう一段絞る、という作りが無難です。今回の事前確認workflowで sub をログに出してからRuleを作る、という順番は、そのまま運用にも持ち込みやすいです。

ANTHROPIC_API_KEY が残っているとFederationが使われない

移行時に地味に困りそうなのが、認証情報の優先順位です。

Anthropic SDKでは、ANTHROPIC_API_KEYANTHROPIC_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を叩いている方は、まず小さなリポジトリでスモークテストしてみると良いと思います。

参考資料


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事