GitHub Actions の azure/login で AADSTS700016(Application not found)が出る場合、その原因はほぼ Federated Credential の subject 不一致です。App Registration も Client ID も Tenant ID も正しいのに落ちるのは、OIDC 経路の登録が足りていないから。エラー文言に騙されて App Registration を疑い続けると半日溶けます。
ローカルの az login --service-principal は通るのに、同じ Client ID を渡した CI だけが死ぬ。この非対称こそが切り分けの入口でした。
症状の確認
以下に当てはまるならこの記事の対象です。
- ローカルでは
az login --service-principalで問題なくログインできる - GitHub Actions の
azure/login@v2でだけAADSTS700016: Application with identifier '<app-id>' was not found in the directory '<tenant-id>'が出る - App Registration は Portal に確かに存在している
- Client ID も Tenant ID もコピペで確認済み
半日の内訳はこうでした。最初の2時間は Client ID と Tenant ID の再コピペ、次の2時間は Secret の期限と SP の存在確認、その後に Portal の Federated credentials 画面を開いて、登録が refs/heads/main の1件しかないことに気づいた。原因に到達するまで、エラーメッセージの言葉どおりに App Registration ばかり疑っていたわけです。
700016 と 70021 の出し分け
ここは正確に押さえてください。subject 不一致で本来返るのは AADSTS70021(No matching federated identity record found)です。AADSTS700016 はアプリ/SP そのものが見つからない系のコードです。
にもかかわらず 700016 が出るケースがあります。よくある原因は次の3つ。
- Tenant ID が別テナントを指している:そのテナントに当該アプリが存在しないので、正しく 700016
- Enterprise Applications 側の Service Principal が消えている:App Registration だけ残っていても 700016
azure/loginに渡した Client ID が Object ID などの別 ID:ディレクトリ内で解決できず 700016
つまり実務上は「700016 が出たら ID / テナント / SP を、70021 が出たら subject と audience を疑う」の二段構えが正しい判断です。ログのコードを取り違えたまま片側だけ疑い続けるのが、最も時間を溶かすパターンでした。
なぜローカルは動いてCIで死ぬのか
同じ App Registration でも、認証手段ごとに別経路として扱われるからです。
- ローカル:Client Secret 認証。subject の照合は無く、Secret が合えば通る
- GitHub Actions:OIDC + Federated Credential。Client ID / Tenant ID に加えて subject と audience の完全一致が要る
ローカルで Secret が通ることは、OIDC 経路が生きている証明にはならない。ここを混同すると、正しい認証情報を延々と再コピペするループに入ります。
実際に送られた subject を取り出す
GitHub Actions の通常ログに生の subject は出ません。 claim を見るには自分で取り出す必要があります。方法は2つ。
ひとつはリポジトリの Settings → Secrets and variables → Actions で ACTIONS_STEP_DEBUG を true にし、デバッグログを出す方法。もうひとつは、ID トークンを取得してデコードするステップを一時的に挟む方法です。
permissions: id-token: write contents: read jobs: debug: runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: script: | const t = await core.getIDToken('api://AzureADTokenExchange') const c = JSON.parse( Buffer.from(t.split('.')[1], 'base64').toString() ) core.info(`sub: ${c.sub}`) core.info(`aud: ${c.aud}`) core.info(`iss: ${c.iss}`)
出力された sub / aud / iss を、Portal の登録値と1文字ずつ突き合わせます。この3行が出た瞬間に原因が確定します。
Federated Credential に不足している subject を追加する
subject はワークフローのトリガーによって変わります。
| トリガー | subject |
|---|---|
main への push |
repo:org/repo:ref:refs/heads/main |
| Pull Request | repo:org/repo:pull_request |
| Tag push | repo:org/repo:ref:refs/tags/v1.0.0 |
| Environment 指定 | repo:org/repo:environment:production |
PR トリガーなのに refs/heads/main しか登録されていなかったのが真因でした。注意点として、PR でも environment: を指定したジョブは subject が environment:production 側になります。pull_request を登録しただけでは通りません。
Portal なら App Registration → Certificates & secrets → Federated credentials → Add credential。CLI ならこうです。
az ad app federated-credential create \
--id <app-id> \
--parameters '{
"name": "gh-pull-request",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:org/repo:pull_request",
"audiences": ["api://AzureADTokenExchange"]
}'
audiences は api://AzureADTokenExchange が正です。ここを https:// 付きにしたり空にしたりする事故も頻出で、subject が合っていても 70021 になります。既存登録の確認は az ad app federated-credential list --id <app-id>。
ワークフロー側の permissions: id-token: write も必須です。抜けると subject 自体が送られません。
それでも直らないときの勘違い3種
- App ID = Object ID:Application (Client) ID、App Registration の Object ID、Service Principal の Object ID は全部別物。Portal のコピーボタンは隣接していて取り違えやすい
- App Registration があれば SP もある:Enterprise Applications 側の Service Principal だけ消えていると 700016 になる。
az ad sp show --id <client-id>で確認 - Tenant は1つだけ:個人テナントと組織テナントに両方所属し、
az loginが意図と違う方を選んでいることがある。az account show --query "{tenantId:tenantId, user:user.name}"で実行主体を確認
他サービスでも同じ構造
| 環境 | subject の形式(例) | 事故りやすい点 |
|---|---|---|
| GitHub Actions | repo:org/repo:ref:refs/heads/main |
branch / PR / environment ごとの登録漏れ |
| Azure DevOps | sc://org/project/connection-name |
Service Connection 再作成で subject が変わる |
| Terraform Cloud | organization:my-org:project:my-project:workspace:my-ws:run_phase:plan |
plan と apply で subject が別。片方だけだと apply で死ぬ |
| GitLab CI | project_path:group/proj:ref_type:branch:ref:main |
プロジェクトパス変更で subject が変わる |
AIに聞くときのプロンプトテンプレ
ハマっている最中、Claude にも他の AI にも聞きました。返ってきたのは「Client ID / Tenant ID を確認」「SP の存在を確認」「Secret の期限を確認」。全部正しいけれど、subject 不一致には辿り着かない。Web 上の解説記事の多くが ID と Secret の確認までしか書いておらず、AI もその平均値を返すからです。
事実を揃えて渡せば的中率は跳ね上がります。次の型を埋めて投げてください。
Azure OIDC 認証が GitHub Actions で失敗します。 - エラーコード: AADSTS70021(本文全文: ...) - 送信された claim: sub=repo:org/repo:pull_request / aud=api://AzureADTokenExchange - Portal の登録値: subject=repo:org/repo:ref:refs/heads/main / audiences=api://AzureADTokenExchange - トリガー: pull_request / environment 指定なし - ローカルの Secret 認証: 成功 この差分から原因と修正コマンドを示してください。
まとめ
- 700016 は ID / テナント / SP 系、70021 は subject・audience 系。コードで判断を分ける
- ローカルの Secret 認証が通っても、OIDC 経路の保証にはならない
- Federated Credential は トリガーごとに登録。environment 指定時は subject が切り替わる
audiencesはapi://AzureADTokenExchange、ワークフローにはid-token: write- subject は推測せず、ID トークンをデコードして実物を見る
よくある質問
AADSTS700016 と AADSTS70021 の違いは?
700016 は「そのディレクトリにアプリが見つからない」、70021 は「一致する Federated Credential が無い」です。前者は Tenant ID の取り違え・SP の削除・Client ID に別 ID を渡したケースで、後者は subject や audience の不一致で出ます。まずログのコードを確定させてから疑う対象を選んでください。
実際に送信された subject はどこで見られる?
通常のワークフローログには出ません。ACTIONS_STEP_DEBUG を有効にするか、actions/github-script で core.getIDToken() を呼び、JWT のペイロードをデコードして sub / aud を出力します。確認後はそのステップを削除してください。
Pull Requestトリガーでも認証を通すには?
Federated Credential に repo:org/repo:pull_request を追加します。ただしそのジョブが environment を指定している場合、subject は repo:org/repo:environment:<name> になるため、そちらの登録が必要です。