ClaudeのWorkload Identity Federationを使ってGitHub ActionsからClaudeを呼び出してみた

ClaudeのWorkload Identity Federationを使ってGitHub ActionsからClaudeを呼び出してみた

こんにちは、GW中にちょうど Claudeの Workload Identity Federation がリリースされたみたいなので、検証してみました。

選択できる Identity Provider にはAWSやGoogleCloudなどいろいろありましたが、今回は一番手軽そうなGitHub ActionsをIdentity Providerにして検証ました。
具体的には、リポジトリにPRが作成されるとWIFによって呼び出されたClaudeがそのPRのdiffをレビューするよくあるワークフローを実装してみました。

TL;DR

  • Claude Console でワークロードIDの発行者,サービスアカウント,ルールを設定し、GitHubActionsのワークフローのyamlファイルでOIDC連携するだけ
  • ANTHROPIC_API_KEY をリポジトリのSecretsに置かなくてよくなる
  • 仕組みは AWSの OIDC認証と同じような感じ

Workload Identity Federation(通称WIF)とは

Workload Identity Federation(通称WIF) はざっくりいうと、ClaudeのAPIキーをGitHub ActionsのSecretsに置いておかなくても Identity Provider が発行するOIDCトークンと短命なAnthropicアクセストークンを引き換えてClaude APIを呼び出せるようになる機能 です。

Identity ProviderにGitHub Actionsを選択した場合、こんな感じの順番で処理が行われます↓

GitHub Actions       GitHub OIDC         Anthropic              Claude API
   workflow           Provider         /v1/oauth/token         /v1/messages
      │                  │                  │                       │
      │── ①JWT要求 ─────▶│                  │                       │
      │◀── ②署名JWT ─────│                  │                       │
      │                                     │                       │
      │── ③JWTを提示 ──────────────────────▶│                       │
      │                   (JWKSで署名検証)   │                       │
      │                  (Federation Rule)  │                       │
      │◀── ④sk-ant-oat01-... ──────────────│                       │
      │                                     │                       │
      │── ⑤Authorization: Bearer ... ─────────────────────────────▶│
      │◀── ⑥レスポンス ──────────────────────────────────────────────│

セットアップ手順

以下の権限を持つ必要があります。

  • Anthropic Organization の Admin 権限
  • GitHub リポジトリの workflow を編集できる権限

1. Claude Console 側

1-1. ワークロードの発行者(issuer)を登録

Claude Console > 組織の設定 にアクセスし、
ワークロード > 発行者を登録 にアクセスします。

/img/2026-05-05/img_1_marked.png
  • 名前: 任意。github-actions などわかりやすい名前がよさそう
  • 発行者URL: https://token.actions.githubusercontent.com
  • JWKSソース: ディスカバリー(GitHubは公開discoveryを持っているのでそのままでOK)
  • CA証明書: 不要
  • シングルユーズトークン: トークンを再利用する具体的なケースがいまいち分からないが安全に倒すなら一旦ONにしておいた方が良さそう

登録後以下のように一覧に表示されます。

/img/2026-05-05/img_4.png

1-2. サービスアカウント を作成

サービスアカウント > サービスアカウントを作成 にアクセスします。

/img/2026-05-05/img_5_marked.png
  • 名前: 任意。わかりやすい名前がよさそう
  • 説明: 任意。わかりやすい名前がよさそう

登録後これも一覧に表示されます。後で使うのでsvac_... というIDを控えておきましょう。

/img/2026-05-05/img_7.png

1-3. ワークロードのルール を作成

ワークロード > 新しいルール にアクセスします。

/img/2026-05-05/img_8_masked.png
  • 名前: 任意。わかりやすい名前がよさそう
  • 発行者: 1-1で作った発行者を選択
  • 一致設定: これがめっちゃ重要です
    • 手軽に設定するなら パターンマッチ を選択
      • 件名パターン: 一致させる sub のパターンを設定します。GitHub Actionsが払い出すOIDCトークンのsubの形式はワークフローの種類によるので、その形式に合わせた最適な設定が必要です。
        • 具体的な値はAppendixに記載してます
        • 今回はPRに反応するワークフローを作るので repo:<owner>/<repo>:pull_request を指定
    • 想定オーディエンス: 不要
    • 追加のクレーム条件(任意): これは設定した方が良いです。今回はGitHubのPR作成がトリガーになりますが、fork先リポジトリのPRからのリクエストをブロックするために、repository_owner クレームに <owner> を指定しておく必要があります。
      • repository_owner: リポジトリのオーナー名、この例の場合は diggymo
      • repository: リポジトリ名、この例の場合は diggymo/cc-plugin
  • サービスアカウント: 1-2で作成したサービスアカウントを選択
  • すべてのワークスペースで有効にする: 必要に応じて(あまり詳しくない)
  • トークンの有効期限: デフォルトは10分ですが調整できます。短ければ短いほど侵害されるリスクが減るのでできるだけ短い値を設定しましょう。今回の例だと5分もあれば十分なので5分を設定しました。

登録後これも一覧に表示されます。後で使うのでfdlr_... というIDを控えておきましょう。

/img/2026-05-05/img_7.png

Claude Console側での設定は以上です。

2. GitHub Actions 側

2-1. ワークフローを設定する

ワークフローを設定します。
実際に動作したyamlファイルはこちらから参照できます。

https://github.com/diggymo/cc-plugin/blob/main/.github/workflows/claude-plugin-review.yml

name: Claude Plugin Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  id-token: write      # WIFの設定に必要です
  contents: read       # WIFの設定に必要です
  pull-requests: write # これはレビュー結果をPRにコメントするためだけの権限です。WIFには不要です

jobs:
  review:
    runs-on: ubuntu-latest
    # GitHub Actions の Variables に登録しておくと他ワークフローで再利用できて便利です
    env:
      # 1-3で作ったFederation RuleのID、fdrl_... という形式
      ANTHROPIC_FEDERATION_RULE_ID: fdrl_01QWMfa6kLk7z6kq4nhitjHV
      # Anthropic Organization ID、Claude Console の Organization settingsから確認できる
      ANTHROPIC_ORGANIZATION_ID: db2294e8-5d54-4d31-af31-171bc972fa81
      # 1-2で作ったService AccountのID、svac_... という形式
      ANTHROPIC_SERVICE_ACCOUNT_ID: svac_01Jo5cZmDB6bHiW8U8sxEGwi
      # ワークフロー内で一時的にJWTを保存するファイルパス
      ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt

      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      PR_NUMBER: ${{ github.event.pull_request.number }}
      MODEL: claude-sonnet-4-6
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0

      # GitHub OIDCトークンの取得とAnthropicアクセストークンへの交換
      - name: Fetch GitHub 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"

      # 取得したJWTをAnthropicのアクセストークンに交換
      - name: Exchange JWT 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
          )

          ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

          # 取得できているかprefix確認
          echo "Acquired access token (prefix): ${ACCESS_TOKEN:0:5}..."

          # 以降のステップでアクセスできるよう環境変数にセット
          echo "::add-mask::$ACCESS_TOKEN"
          echo "ANTHROPIC_ACCESS_TOKEN=$ACCESS_TOKEN" >> "$GITHUB_ENV"

      # `ANTHROPIC_ACCESS_TOKEN` を使ってClaude APIを呼び出すステップの例
      - name: Review with Claude
        run: |

          # もろもろ設定

          REQUEST_BODY=$(jq -n \
            --arg model "$MODEL" \
            --arg content "$USER_CONTENT" \
            '{
              model: $model,
              max_tokens: 4096,
              tools: [
                { type: "web_search_20260209", name: "web_search", max_uses: 3 },
                { type: "web_fetch_20260209", name: "web_fetch", max_uses: 5 }
              ],
              messages: [
                { role: "user", content: $content }
              ]
            }')

          RESPONSE=$(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" \
            -d "$REQUEST_BODY")

          # text タイプの content ブロックを連結
          echo "$RESPONSE" | jq -r '.content // [] | map(select(.type == "text") | .text) | join("\n")' \
            > /tmp/review.md

          echo "----- Claude review -----"
          cat /tmp/review.md
          echo "-------------------------"

      - name: Post review comment
        run: gh pr comment "$PR_NUMBER" --body-file /tmp/review.md

ANTHROPIC_ORGANIZATION_IDについては、組織の設定画面に記載されています。以下の例だとdb2294eから始まる文字列です

/img/2026-05-05/img_13_marked.png

GitHub Actions側での設定は以上です

3. 動作確認

PRを作ってワークフローを実行してみましたが、無事にClaudeが呼び出されてレビューコメントが投稿されました。

  • PRはこちらから確認できます。
  • 実行結果はこちらから確認できます。

検証するために雑に作ったPRなのでかなり指摘されてます🥺

うまくいかない時

Claude Consoleの ワークフローID > 認証イベントから、イベントログが閲覧できるのでうまくいかないときはまずここを確認して原因を調べた方が良いです。

/img/2026-05-05/img_15.png

私の場合は、最初設定した際にルールに設定したclaimのマッチングルールが間違っていたため、以下のようなエラーになっていました。はっきりとしたエラー内容が記載されてるので原因の特定は比較的簡単でした。

  "status": {
    "detail": "assertion does not satisfy rule match: subject does not satisfy subject_prefix: sub (hash=a9d1f427bf4f896c) does not match prefix \"repo:morimorikochan/claude-notify-plugin:pull_request\"",
    "outcome": "failure",
    "reason": "match_subject_prefix"
  },

うまくいった場合はこんな感じです。

/img/2026-05-05/img_19.png

JSONは以下のようになっていました。
event_data.oidc_token.claimsを見ると actor_id(PRを実行したユーザーのID) や event_name(トリガーとなったイベント名) なども入っているのでこれらを使ってより厳密なルールを設定することも可能そうです。

{
  "actor": {
    "audience": [
      "https://api.anthropic.com"
    ],
    "ip_address": "172.183.134.178",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:diggymo/cc-plugin:pull_request",
    "type": "federated_identity_actor",
    "user_agent": "curl/8.5.0"
  },
  "created_at": "2026-05-05T10:03:31.881038Z",
  "event_data": {
    "federation_rule_id": "fdrl_01QWMfa6kLk7z6kq4nhitjHV",
    "issuer_id": "fdis_01QAY7ynYzSouwvKwNYQzoPB",
    "oidc_token": {
      "claims": {
        "actor": "diggymo",
        "actor_id": "24623083",
        "aud": "https://api.anthropic.com",
        "base_ref": "main",
        "check_run_id": "74390810760",
        "event_name": "pull_request",
        "exp": 1777975711,
        "head_ref": "feat/pr-description",
        "iat": 1777975411,
        "iss": "https://token.actions.githubusercontent.com",
        "job_workflow_ref": "diggymo/cc-plugin/.github/workflows/claude-plugin-review.yml@refs/pull/3/merge",
        "job_workflow_sha": "173206af73c3af8dd880bfcdfc20c8e807b23b21",
        "jti": "4cf4801e-1e19-410b-9371-cbbc077c9c18",
        "nbf": 1777975111,
        "ref": "refs/pull/3/merge",
        "ref_protected": "false",
        "ref_type": "branch",
        "repository": "diggymo/cc-plugin",
        "repository_id": "1175104230",
        "repository_owner": "diggymo",
        "repository_owner_id": "24623083",
        "repository_visibility": "public",
        "run_attempt": "1",
        "run_id": "25370016512",
        "run_number": "3",
        "runner_environment": "github-hosted",
        "sha": "173206af73c3af8dd880bfcdfc20c8e807b23b21",
        "sub": "repo:diggymo/cc-plugin:pull_request",
        "workflow": "Claude Plugin Review",
        "workflow_ref": "diggymo/cc-plugin/.github/workflows/claude-plugin-review.yml@refs/pull/3/merge",
        "workflow_sha": "173206af73c3af8dd880bfcdfc20c8e807b23b21"
      },
      "jti": "4cf4801e-1e19-410b-9371-cbbc077c9c18"
    },
    "requested_service_account_id": "svac_01Jo5cZmDB6bHiW8U8sxEGwi"
  },
  "id": "activity_01XmEVAU2uPriE94ZMpbvg82",
  "organization_id": "org_01U4TwuxQRsocnVEpSRVjqjn",
  "request_id": "req_011CajEP1Wp2FLV5g7p1abjh",
  "resources": [
    {
      "id": "svac_01Jo5cZmDB6bHiW8U8sxEGwi",
      "type": "service_account"
    }
  ],
  "status": {
    "outcome": "success"
  },
  "type": "platform_federated_authentication"
}

GitHub Actions のOIDCトークンの sub に入る値

1-3でルールに設定する sub のパターンは、ワークフローのトリガーや設定によって変わります。
GitHub公式リファレンス によると、デフォルトの sub は以下の優先順位で決まります(上から評価され、最初にマッチしたものが採用されます)。

# 条件 sub のフォーマット
1 ジョブが environment を参照している repo:<owner>/<repo>:environment:<environment> repo:octo-org/octo-repo:environment:Production
2 pull_request イベントがトリガー(環境参照なし) repo:<owner>/<repo>:pull_request repo:octo-org/octo-repo:pull_request
3 ブランチへのpush等(上記以外) repo:<owner>/<repo>:ref:refs/heads/<branch> repo:octo-org/octo-repo:ref:refs/heads/main
4 タグへのpush(上記以外) repo:<owner>/<repo>:ref:refs/tags/<tag> repo:octo-org/octo-repo:ref:refs/tags/v1.0.0

補足

  • 値の中に : が含まれる場合(例: Production:V1 という environment 名)はURLエンコードされ Production%3AV1 となります
  • sub 以外のクレーム(repository_owner, repository, ref, event_name, actor など)は本記事のJSONログにある通りトークンに含まれているので、Federation Ruleの「追加のクレーム条件」で組み合わせて検証することで、sub だけでは表現できない細かい制約をかけられます

参考資料


See also