はじめに
ぼくはずんだもん。Web 技術なんもわからん初老おじさんなのだ。
![]() |
![]() |
🟩ずんだもん: イタコ、聞いてほしいのだ。Static Web Apps (SWA) と Azure Functions の Linked Backend 構成で、Entra ID の特定セキュリティグループのメンバーだけ叩ける 管理画面を作ってたのだ。
🟦イタコ: App Role とグループ割当、数時間で終わる……はずのお話ですわね。(フラグ)
🟩ずんだもん: 実装してる最中、公式ドキュメントに書かれていない「裏仕様」 に何度も踏み抜かれたのだ......。😭
🟦イタコ: SWA + Functions、パッと見モダンで便利そうなんですのよ。ただ Linked Backend の中身を開けてみると 明文化されていない挙動が結構な割合を占めている 、というのが今回の発見ですわ。
🟩ずんだもん: 今回はその中から、「公式ドキュメントだけ読んでも気付けない仕様の罠」を5つ に厳選して紹介するのだ。
結論から言うと
「SWA→Functionsの裏でMSが強力な認証を組んでいるのに、それを知らずに独自の認証対策を足そうとして自爆する話」
5罠の本質はこれ1つに集約される。
前提知識: Linked Backend の認証モデル(ここ知らないと全部ハマる)
🟩ずんだもん: 罠の話の前に、正しい認証モデル を先に共有するのだ。これ知らないと、この先の罠がただの不幸話に見えてしまうのだ。
🟦イタコ: 「なぜそれが罠に見えるのか」の背景知識ですわね。
Linked Backend が裏でやっていること
🟩ずんだもん: SWA と Function App を紐付けるのに使う az staticwebapp backends link 、このコマンドが Function App 側の Easy Auth に azureStaticWebApps という identity provider を自動追加している のだ。
🟦イタコ: az webapp auth show で Function App の Easy Auth 設定を見ると、こうなっていますの。
{
"platform": { "enabled": true },
"identityProviders": {
"azureStaticWebApps": {
"enabled": true,
"registration": {
"clientId": "gentle-sea-01234567.8.azurestaticapps.net"
}
},
"azureActiveDirectory": {
"enabled": true,
"registration": { ... }
}
}
}
🟩ずんだもん: 注目は azureStaticWebApps provider の clientId に SWA インスタンスのホスト名 (<自動生成名>.azurestaticapps.net の形式) が入ってる点なのだ。これが「どの SWA 秘密鍵と対になる公開鍵で検証するか」を識別するキーなのだ。
🟦イタコ: フロー全体はこうなっていますわ。
[Browser]
↓ Entra ID で SWA にログイン
[SWA]
↓ simplified clientPrincipal を JSON で組み立て
↓ SWA インスタンス固有の秘密鍵で署名
↓ X-MS-CLIENT-PRINCIPAL ヘッダとして付与
[Function App の Easy Auth]
↓ azureStaticWebApps provider が SWA 公開鍵で署名検証
↓ OK なら Kestrel 層を通過、NG なら 401
[app code (run.ps1)]
🟩ずんだもん: ポイントは 暗号署名で SWA 経由を保証している こと。共有 secret 不要、Bearer token forward 不要、改ざん検知が自動 という美しい仕組みなのだ。
🟦イタコ: しかも Function App の直 URL を偽造ヘッダで叩いても、SWA の秘密鍵を持っていない限り署名検証に失敗して 401 で弾かれる 。
実測するとこうですわ:
curl "https://func-xxx.azurewebsites.net/api/items?q=foo" \
-H "X-MS-CLIENT-PRINCIPAL-NAME: attacker@example.com"
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="..." authorization_uri="..."
Kestrel 層で即 strip され、アプリコードには一切到達しない 。
🟩ずんだもん: この仕組みを知らないと、「Access Restriction で守らなきゃ!」「shared secret 入れなきゃ!」って焦って、要らない対策を積んで自分で踏み抜く ことになるのだ。実際、これから紹介する罠1 はその典型例なのだ。
🟦イタコ: 「自動構成されている」の4文字の裏に、これだけの仕組みが隠れてますのよ。知らないと触ってはいけない場所すら見分けられない、というわけですわ。
罠1: SWA → backend 間に「共有secretで守る」機構は存在しない
🔰 身近な例で言うと: 「店の入口に貼る張り紙」と「店から配達員に持たせる添え状」を取り違えるようなもの。入口の張り紙に『暗証番号入力必須』と書いたら、暗証番号を知らない普通の来客まで全員追い返された、という状態。
![]() |
![]() |
🟩ずんだもん: Linked Backend 認証モデルを知らずに「Function App の直 URL が偽造ヘッダで叩かれたら危ないから、SWA 経由だけ通す shared secret を仕込もう!」と思って、こういう構成を試したのだ。
// staticwebapp.config.json
{
"forwardingGateway": {
"requiredHeaders": {
"X-Azure-FDID": "<random-secret>"
}
}
}
🟦イタコ: 本番 SWA に入れた瞬間、全ルートが 403 化 しましたわ。/ も /.auth/login/aad も、何もかも。
🟩ずんだもん: え、なんで全部壊れるのだ!? ヘッダチェック追加しただけなのだ!?
🟦イタコ: forwardingGateway.requiredHeaders の方向を誤解 していましたの。
| 設定名 | 実際の向き | 用途 |
|---|---|---|
forwardingGateway.requiredHeaders |
受信側検証 (AFD → SWA) | Azure Front Door 経由のトラフィックだけ SWA が受け付ける用 |
| (存在しない) | SWA → backend 方向の注入 | そもそもそんな機構は無い |
つまり「SWA が受信するリクエストに X-Azure-FDID が無ければ 403」という設定ですの。ブラウザの通常リクエストにそんなヘッダ含まれているわけもなく、全員が弾かれますわ。
🟩ずんだもん: SWA には backend に任意のヘッダを注入する機構がそもそも存在しない のだ。az staticwebapp secrets も app settings も、backend への中継 secret を仕込むためには使えない のだ。
🟦イタコ: Function App 側の Access Restriction で X-Azure-FDID マッチルールを作っても、SWA 経由トラフィックに一致しないので、default Deny を投入した瞬間に本番が即死 ですわ。これも実測で確認済みですの。
🟩ずんだもん: そもそもこんな対策は必要ない のだ。前提知識で書いた通り、azureStaticWebApps provider の署名検証で偽造は完璧に防げている 。既存の防御を知らずに、壊れた上にさらに別の何かを足そうとして自爆する、が今回のパターンなのだ。
🟦イタコ: 教訓: Linked Backend で「直 URL 叩きが不安だから ネットワーク層で守ろう」と思った瞬間に、要らない対策で本番を壊す罠が待っている 。既に署名検証で守られているので、Access Restriction も forwardingGateway も触らないのが最適解 ですわ。
🟩ずんだもん: 「追加の防御を入れる前に、既存の防御が何をしているかを確認する」、これインフラ全般に通じる心得なのだ。
罠2: X-MS-CLIENT-PRINCIPAL と X-MS-CLIENT-PRINCIPAL-NAME の違い、そして欠落可能性
🔰 身近な例で言うと: 「お名前」欄と「お名前(カナ)」欄を取り違えて、カナ欄を漢字フィールドとして処理する、みたいなもの。しかも「カナ欄は空で送られてくる場合もある」 ので、片方だけ信頼してたら実機で爆発する罠もセット。
![]() |
![]() |
🟩ずんだもん: アプリコードで principal を取り出すところで遭遇する罠なのだ。
🟦イタコ: ヘッダ一覧を整理しますわ。
| ヘッダ | 中身 | デコード |
|---|---|---|
X-MS-CLIENT-PRINCIPAL |
clientPrincipal JSON の base64 | base64復号必要 |
X-MS-CLIENT-PRINCIPAL-NAME |
UPN (例: user@example.com) |
平文、復号不要 |
X-MS-CLIENT-PRINCIPAL-ID |
OID (GUID) | 平文 |
X-MS-CLIENT-PRINCIPAL-IDP |
プロバイダ名 (aad 等) |
平文 |
-NAME サフィックス付きは平文、サフィックス無しの無印は base64 JSON 。逆にすると爆発しますわ。
🟩ずんだもん: さらに厄介なのが、-NAME は環境によって欠落する ことなのだ。特定のアイデンティティプロバイダや状況で送られてこないケースがあるのだ。
🟦イタコ: だから実装は 「-NAME があればそれを採用、無ければ -PRINCIPAL を base64 復号して userDetails フィールドを取る」のフォールバック構造 が必須ですの。
# PowerShell (run.ps1) 抜粋イメージ
$userPrincipalName = $Request.Headers['X-MS-CLIENT-PRINCIPAL-NAME']
if (-not $userPrincipalName) {
$principalHeader = $Request.Headers['X-MS-CLIENT-PRINCIPAL']
if ($principalHeader) {
$decoded = [System.Text.Encoding]::UTF8.GetString(
[System.Convert]::FromBase64String($principalHeader))
$principal = $decoded | ConvertFrom-Json
$userPrincipalName = $principal.userDetails
}
}
🟩ずんだもん: -NAME だけに頼った実装は、開発環境では動いて本番で爆発する 典型パターンなのだ。
🟦イタコ: 漢字とカナ、別フィールドなのは人間も API も同じ。どちらも欠落しうる前提で設計するのが、フォームでも認証でも同じですわね。
🟩ずんだもん: 教訓: Microsoft のヘッダ命名は 似た名前で形式違い がよくある。さらに 「サーバー側で常に送ってくる」とは限らない 。サフィックスの有無と欠落可能性、両方疑う のだ。
罠3: backend に届く X-MS-CLIENT-PRINCIPAL は「省略版」
🔰 身近な例で言うと: 「受付に戸籍謄本を提出したのに、奥の部屋には『お客さんが来ました』としか伝わらない」状態。詳細な情報はフロント側で止まっていて、backend には要約だけが届く。
![]() |
![]() |
🟩ずんだもん: 認証自体は通るようになった後、App Role による認可をやろうとすると別の壁 があるのだ。
🟦イタコ: SWA が backend に注入する X-MS-CLIENT-PRINCIPAL を base64 復号すると、中身はこう:
{
"identityProvider": "aad",
"userId": "00000000-0000-0000-0000-000000000000",
"userDetails": "user@contoso.com",
"userRoles": ["authenticated", "anonymous"]
}
🟩ずんだもん: claims 配列が入っていない のだ。フロント側で /.auth/me を叩くと見える完全版(App Role 入りの roles claim 含む)は、backend には届かないのだ。
🟦イタコ: これは SWA Linked Backend の仕様。SWA が backend に注入する X-MS-CLIENT-PRINCIPAL は simplified 版で固定 。フロント側で見えるリッチな情報は中継ポイントで圧縮されてしまいますわ。
🟩ずんだもん: 対策は2通りあるのだ。
| 方式 | 特徴 |
|---|---|
A. rolesSource エンドポイント経由 |
staticwebapp.config.json で SWA に登録、SWA が認証直後に一度呼び出して結果を userRoles に昇格 |
| B. アプリコードで Graph を直接叩いて解決 | 各エンドポイント冒頭で transitiveMemberOf を自前で問い合わせ |
🟦イタコ: 今回のプロジェクトは B で、独自モジュール (例: Roles.psm1 の Get-UserRoles 関数) を実装し、各 Function の先頭で Graph を叩く設計を採用しましたの。
🟩ずんだもん: 教訓: フロントで見える情報 ≠ backend で見える情報 。/.auth/me のレスポンスだけで動作確認すると、backend 実装時に「claims どこ行った!?」って混乱するのだ。
🟦イタコ: SWA の便利な抽象化は、内部が見えにくいがゆえに境界線で足元を取られやすい 、という典型ですわ。
罠4: staticwebapp.config.json は環境変数展開をサポートしない
🔰 身近な例で言うと: 「『ただいまの時刻は
${time}です』と書いた電光掲示板が、時刻表示じゃなく${time}の文字列そのまま を表示し続ける」状態。テンプレート記法を使ったつもりが、ただのリテラル文字列として扱われてしまう。
![]() |
![]() |
🟩ずんだもん: SWA の設定ファイル staticwebapp.config.json で、secret や環境依存の値を外部から注入したいケースがあるのだ。例えばこういう風に書きたくなるのだ。
{
"forwardingGateway": {
"requiredHeaders": {
"X-Custom-Header": "${env:MY_SECRET}"
}
}
}
🟦イタコ: これ、そのまま「${env:MY_SECRET}」という文字列として扱われます わ。SWA 設定ファイルは 環境変数展開を一切サポートしていません の。
🟩ずんだもん: App Service や Functions の ARM template 風の参照構文 ({{...}}) も効かない。config ファイルに書いた文字はリテラルとしてそのまま扱われる のだ。
🟦イタコ: しかも staticwebapp.config.json は リポジトリに commit する前提 のファイル。Git 履歴に永遠に残りますから、ここに secret を直書きしたら一生の思い出ですわ。
🟩ずんだもん: 対策は デプロイスクリプト側でテンプレ処理 するしかないのだ。
# deploy-frontend.ps1 抜粋
$template = Get-Content "staticwebapp.config.template.json" -Raw
$config = $template -replace '\{\{SOME_VALUE\}\}', $env:SOME_VALUE
$config | Set-Content "dist/staticwebapp.config.json"
テンプレ (.template.json) を Git 管理、ビルド時に環境変数で置換、生成物 (.json) は .gitignore に入れる。
🟦イタコ: 教訓: SWA 設定ファイルは 静的テンプレートではなく、静的な最終形。動的な値を入れたいなら パイプラインで事前生成 する以外の道はありませんわ。
🟩ずんだもん: SWA の便利な config file で env 参照くらいできるだろう、と思って一歩進めると、そこは立ち入り禁止区域なのだ。
罠5: az staticwebapp backends link CLI が 400 Bad Request で通らない
🔰 身近な例で言うと: 「役所の公式窓口で書類を出したら『この書類は受け付けられません』と突き返され、しかも理由欄に謎の業務用コードしか書いてない。でも実は裏の郵送受付なら普通に同じ書類で通る」状態。公式の入口が壊れてるのに、裏ルートは普通に動いている。
![]() |
![]() |
🟩ずんだもん: これは復旧作業中に遭遇した、ツール側のバグ系の罠 なのだ。
🟦イタコ: 復旧作業というのは、何らかの理由で azureStaticWebApps provider が消えてしまった Function App に対して、az staticwebapp backends unlink → link をやり直して provider を再生成する操作ですわ。
🟩ずんだもん: 普通にドキュメント通りに CLI を叩くのだ:
az staticwebapp backends link \
--name <swa-name> \
--resource-group <rg> \
--backend-resource-id <function-app-resource-id> \
--region japaneast
🟦イタコ: これが 400 Bad Request で通りませんの。しかもエラー本文が ASP.NET Runtime Error の HTML ページ で、何が悪いのか全く分からない。
🟩ずんだもん: JSON すら返ってこないのだ。 HTML が返ってくるのだ。エラーが HTML で返ってくる時点でなんか怖いのだ。
🟦イタコ: --debug をつけても、--verbose でも、原因の手がかり無し。CLI 自体のバグ臭いですわね。
🟩ずんだもん: 迂回策は ARM REST API を直接叩く ことなのだ。これは通るのだ。
# アクセストークン取得
TOKEN=$(az account get-access-token --query accessToken -o tsv)
# ARM REST の PUT で linked backend を登録
curl -X PUT \
"https://management.azure.com/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Web/staticSites/<swa>/linkedBackends/<name>?api-version=2022-09-01" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"backendResourceId": "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Web/sites/<func>",
"region": "japaneast"
}
}'
🟦イタコ: CLI (az) の内部で API のバージョンやらリクエスト形式やらが合っていない、のような状況が推定されますわ。ARM REST を直接叩けば、同じ操作が問題なく通りますの。
🟩ずんだもん: 教訓: Azure CLI のエラーは、たまに理由を何も教えてくれない HTML で返してくる のだ。そういう時は ARM REST 直叩きという裏ルート に切り替えると通ることが多い。CLI はあくまで ARM REST のラッパーだから、ラッパー側が壊れていても本体は動いているケースがあるのだ。
🟦イタコ: CLI が沈黙する時は、一段下の層を見に行くとヒントがある。Azure に限らずどのクラウドでも同じですわね。
🟩ずんだもん: ちなみに azureStaticWebApps provider の構成は、この link 操作の副作用として Function App 側に暗黙的に追加される仕様 なので、Terraform や ARM テンプレートで直接書いて管理するのは困難 なのだ。
🟦イタコ: IaC で強制管理しようとすると、link 時の暗黙設定との間で drift が発生しますから、「link 操作自体を CI のプレイブック化する」「provider の存在確認を定期ジョブ化する」 の二段構えが現実解ですわ。
罠カテゴリ別サマリ
![]() |
![]() |
🟦イタコ: 5つの罠をカテゴリ別に整理しますと、こうなりますわ。
| カテゴリ | 罠番号 | 本質 |
|---|---|---|
| 認証モデルの誤解 | 1 | SWA → backend の secret 注入機構が「ある」と誤解する |
| ヘッダ仕様 | 2 | 似た名前で形式違い + 欠落可能性 |
| SWA Linked Backend 仕様 | 3 | simplified clientPrincipal のみ forward |
| 設定ファイルの制約 | 4 | env var 展開なし、直書きしかできない |
| ツール側の挙動 | 5 | CLI が謎の 400、ARM REST 直叩きで迂回 |
🟩ずんだもん: 共通しているのは、「公式ドキュメントを読んだだけでは気付けない」 ことなのだ。
🟦イタコ: Microsoft Learn に書かれていない部分ではあるのですが、「書いていないからこそ、みんなドキュメント通りに試して同じ罠を踏む」 構造になってますの。この記事が罠回避の一次情報として機能すれば、読者の数日分の時間が救われるかもしれませんわ。
本番投入前チェックリスト
🟩ずんだもん: 同じ構成で本番投入する人のためのチェックリストなのだ。
[ ] 1. Function App の Easy Auth で azureStaticWebApps provider が
自動追加されているか確認
az webapp auth show --name <func> --resource-group <rg> \
--query "properties.identityProviders.azureStaticWebApps"
→ null 以外であればOK
[ ] 2. staticwebapp.config.json に forwardingGateway.requiredHeaders を
不用意に入れていないか (入れると全ルート403化のリスク)
[ ] 3. アプリコードの principal 取得が
"X-MS-CLIENT-PRINCIPAL-NAME 優先 → 無ければ X-MS-CLIENT-PRINCIPAL を
base64復号してフォールバック" の実装になっているか
[ ] 4. App Role 解決ルート (rolesSource or Graph直叩き) が
実装されているか (claims は X-MS-CLIENT-PRINCIPAL に含まれない)
[ ] 5. staticwebapp.config.json に env 参照構文を直書きしていないか
(リテラルになるだけ。テンプレ処理で事前生成が必須)
[ ] 6. 偽造 curl smoke test で 401 が返ることを CI で確認しているか
curl <func-direct-url>/api/xxx \
-H "X-MS-CLIENT-PRINCIPAL-NAME: attacker@example.com"
→ 401 Unauthorized + WWW-Authenticate: Bearer
[ ] 7. provider 存在確認を定期ジョブ化 (null 検知で Slack 通知など)
※ link 時に暗黙設定されるため Terraform 管理外。
運用手順+監視で維持するのが現実解。
🟦イタコ: CI に7番の smoke test を組み込むだけでも、かなりの事故が未然に防げますわ。
まとめ
![]() |
![]() |
🟩ずんだもん: 今回の最大の学びは、az staticwebapp backends link が自動構成する azureStaticWebApps provider の存在 なのだ。これを知らないと、「要らない対策を足して自分で壊す」 ループにハマるのだ。
🟦イタコ: 公式ドキュメントでは "Authentication is automatically configured" と一行で済まされている裏方の仕組み ですから、普通にドキュメントを読んでも気付けませんわね。「自動構成」の4文字の裏に、これだけの仕組みが隠れている。モダンなクラウドサービスあるあるですの。
🟩ずんだもん: 逆に言えば、一度正しいモデルを理解すれば、今回の5罠の半分は自然に回避できる のだ。「あ、これ要らない対策だな」「あ、これはドキュメントに書いてないやつだな」って勘が働くようになるのだ。
サーバーレス全般に通じる話
🟦イタコ: そしてこれは マイクロソフト固有の話ではありません のよ。
AWS Lambda + API Gateway の Custom Authorizer、Cloudflare Workers の Zero Trust Access、GCP Cloud Functions の IAM バインディング……サーバーレス構成の認証・認可には各社独自の「暗黙の仕様」と「裏方機構」が必ず存在 しますわ。
🟩ずんだもん: AWS なら API Gateway のペイロードサイズ制限、Cold Start、VPC 統合時の ENI 制約……みんな独自ルール持ってるのだ。
🟦イタコ: でも真実ですわ。だからこそ、本番適用前に PoC を徹底的にやる ことが何より重要ですの。「公式ドキュメント通りに作ったから大丈夫」は通用しません。
🟩ずんだもん: ドキュメントに書いてないことのほうが、本番で刺さる のがサーバーレスの怖いところなのだ。🤢
学習負荷 vs 運用負荷という観点
🟦イタコ: もう一つ重要な観点を。
今回のような苦労は、伝統的な Web サーバー(IIS / Apache / Nginx)を自前で立てて運用するときの恒久的な運用負荷とは、まったく別の性質の苦労 ですのよ。
🟩ずんだもん: つまりサーバーレスの苦労は 「学習負荷」 なのだ。
一度仕組みを理解してしまえば、OS パッチ当て、ミドルウェアアップデート、CVE 対応、証明書更新、ディスク容量監視、スケールアウト設計……毎日・毎月・毎年繰り返される運用タスクはほぼ消える のだ。
🟦イタコ: 対して、伝統的な Web サーバー運用は 「恒久的な運用負荷」 。10年運用すれば10年分発生し続けますわ。人員交代のたびに知識移転コストも増える。
🟩ずんだもん: 一方、今回の5罠は 「最初に覚えておけばいいこと」。一度踏み抜けば次回は回避できる。学習コスパは実はむしろ良い のだ。
🟦イタコ: 「学習コストは高いが、一度払えば継続的な運用負荷が軽くなる」──これがサーバーレスを選ぶ本質的なメリットですわね。
「自動構成されている = 理解しなくていい、ではない」
便利な自動機構ほど、中身を知らないと触るべきでない場所が見分けられない 。今回の azureStaticWebApps provider はまさにそれですのよ。
春の遠足にて
![]() |
![]() |
🟩ずんだもん: というわけでセキュリティや認証がしっかりした Web アプリができたから、今度はリアルで 「公式ドキュメントに書いてない裏仕様」 で遊ぶのだ!
🟦イタコ: まだ罠を踏み足りませんの? 今度はどんな遊びですの。
🟩ずんだもん: 横断歩道の「白いところ」以外を踏んだら 403 Forbidden で弾かれる ゲームなのだ!
🟦イタコ: 小学生の定番ルールに HTTP ステータスコードを混ぜないでくださいまし(笑)。
リンク集
利用キャラクター
- ずんだもん:VOICEVOX:ずんだもん
本記事で使用しているキャラクター画像の著作権は、それぞれの権利者に帰属します。
非商用目的での利用に基づき掲載しています。


