【設計】ECS Fargate + Next.js + FastAPI のアクセス制御設計 — SSR内部通信の認証をどう解決するか

この記事でわかること
- ALB が付与する x-amzn-oidc-data ヘッダーが、SSR の内部通信だけ届かない理由
- middleware / Server Component / CSR の3つで認可チェックを置いたときの差
- x-internal-api-key で SSR の内部通信を「信頼された経路」として扱う設計
AWS ECS Fargate + ALB(Cognito OIDC 認証)/ Next.js 16(App Router、standalone、basePath あり)/ FastAPI / 同一 VPC 内で Cloud Map 経由のサービス間通信
概要: AWS ALB + Cognito OIDC で認証されたNext.js + FastAPI構成において、SSR内部通信では認証ヘッダーが届かない問題をどう解決するか。Next.js middleware / Server Component / CSR の3パターンを比較検討し、最適な設計を選定した記録。
はじめに
AWS ECS Fargate上でNext.js(フロントエンド)とFastAPI(バックエンド)を動かし、ALB + Cognito + Entra IDでOIDC認証を行っている環境に、「許可されたメールアドレスのみアクセス可能」というアクセス制御を導入することになった。
一見シンプルな要件だが、SSR(Server Side Rendering)の内部通信がALBを経由しないため、認証ヘッダーが届かないという根本的な問題に直面した。
通信経路の整理
まず、リクエストがどの経路を通るかを理解する必要がある。
ブラウザ → ALB → バックエンド(CSR)
ブラウザからのAPI呼び出しはALBを経由する。ALBはCognito認証後に x-amzn-oidc-data ヘッダー(JWT)を自動付与するため、バックエンドはこのJWTからメールアドレスを取得できる。
Next.js SSR → バックエンド(内部通信)
Server Componentからのデータ取得はECS内部のCloud Map DNS経由で直接バックエンドに接続する。ALBを経由しないため、x-amzn-oidc-data ヘッダーは存在しない。
flowchart LR
B["ブラウザ"] -->|CSR| ALB["ALB<br>Cognito OIDC"]
ALB -->|ヘッダーあり| API["FastAPI"]
SSR["Next.js SSR"] -.->|Cloud Map<br>ヘッダーなし| API
この「SSR内部通信にはOIDCヘッダーがない」という事実が、設計の核心的な課題となる。
検討した3つのパターン
パターンA: Next.js middleware で認可チェック
middleware.tsで全リクエストをインターセプトし、x-amzn-oidc-data からメールを取得。バックエンドの /auth/check に内部通信で問い合わせる方式。
export const runtime = 'nodejs' を書けば Node.js API や api-server.ts を使えます(Next.js 16 でも既定は Edge のまま)。ただし下記の basePath の二重化や実装の複雑さは残るので、Server Component を選んだ判断自体は変わりません。問題点:
- middlewareは既定でEdge Runtimeで動作し、
api-server.ts(Node.js Runtime)を直接importできない - 内部通信用のfetchを別途実装する必要があり、ヘッダー転送やエラー処理が複雑化
- basePath(
/sm)の二重化問題が発生しやすい - 実際にstg環境で複数の問題が発生しデプロイ不可に
パターンB: CSR コンポーネントで認可チェック
layout.tsxに<Authenticator>クライアントコンポーネントを配置。useEffectでマウント後にブラウザからALB経由で/auth/checkを呼ぶ方式。
メリット: 実装が最もシンプルで、SSR内部通信の問題が完全に無関係。
デメリット: SSRのデータフェッチが認可チェック前に実行され、ローディング表示が一瞬入る。
パターンC: Server Component(layout.tsx)で認可チェック【採用】
ルートのlayout.tsxをasync関数にし、headers()でx-amzn-oidc-dataを取得してメールを抽出。api-server.ts経由でバックエンドに許可チェックする方式。
メリット: Node.js Runtimeで動作し、api-server.tsを直接importできる。Edge Runtime制約なし、basePath問題なし。未許可ユーザーにはページのレンダリング自体が実行されない。
比較表
| 評価軸 | A: middleware | B: CSR | C: Server Component |
|---|---|---|---|
| セキュリティ | 高 | 中 | 高 |
| UX | コンテンツ見えない | ローディング挟む | コンテンツ見えない |
| 実装の複雑さ | 高 | 低 | 低〜中 |
| SSR内部通信 | fetchを別途実装 | 無関係 | api-server.ts流用 |
| Edge Runtime制約 | あり | なし | なし |
採用した設計: x-internal-api-key パターン
パターンCを採用し、SSR内部通信には共有秘密鍵(x-internal-api-key)で「信頼された内部通信」と識別する設計とした。
バックエンドの認証ミドルウェアは、リクエストを上から順に判定する:
x-internal-api-keyがある → 信頼された内部通信、認証スキップx-amzn-oidc-dataがある → JWT検証 → メール抽出 → 許可リストチェック- どちらもない → 401
フロントエンドでは、layout.tsxでheaders()からOIDCヘッダーを取得し、api-server.ts経由でバックエンドに認可チェックを行う。
// (authenticated)/layout.tsx
export default async function AuthenticatedLayout({ children }) {
const { authorized, is_admin } = await checkAuth();
if (!authorized) redirect("/unauthorized");
return (
<AuthProvider isAdmin={is_admin ?? false}>
<Header />
<main>{children}</main>
</AuthProvider>
);
}Edge RuntimeとNode.js Runtimeの違い
Next.jsには2つのサーバーサイド実行環境がある:
- Edge Runtime: 軽量だが制約あり。
fs、net等のNode.js APIが使えない。middlewareはデフォルトでこちら - Node.js Runtime: フル機能。Server Components、API Routesはこちら
middlewareからはapi-server.tsを直接importできないが、layout.tsx(Server Component)からは同じNode.js Runtimeなのでそのまま使える。
Tips
- ALBの
x-amzn-oidc-dataはALB→ターゲット間でのみ付与される。SSR内部通信では使えない x-internal-api-keyはECS内部通信でのみ使用し、セキュリティグループで外部からのアクセスを遮断する前提- Server ComponentのルートグループでCSR/SSRの認証パスを分離すると見通しがよい
x-internal-api-keyの実体は Secrets Manager に置き、タスク定義のsecretsから注入する。環境変数に直書きするとタスク定義の履歴に残り続ける
まとめ
- SSR内部通信ではALBのOIDCヘッダーが届かないため、別の認証手段が必要
- Next.js middlewareはEdge Runtime制約があり、Server Componentでの認可チェックが最もシンプル
x-internal-api-keyパターンでSSR内部通信を信頼済みとして扱う設計が有効- layout.tsxでの認可チェックにより、未許可ユーザーにはレンダリング自体が実行されないセキュリティを実現
参考リンク
同じ ECS + FastAPI 構成での設計はこちらでも書いています。
更新履歴
- 目次を追加、参考リンクと関連記事へのリンクを追加、x-internal-api-key の保管場所について Tips に追記
- Next.js 15.5 で Node.js runtime の middleware が stable になった点を注記。通信経路を図解に変更


