
AWS ECS Fargate上でNext.js(フロントエンド)とFastAPI(バックエンド)を動かし、ALB + Cognito + Entra IDでOIDC認証を行っている環境に、「許可されたメールアドレスのみアクセス可能」というアクセス制御を導入することになった。
一見シンプルな要件だが、SSR(Server Side Rendering)の内部通信がALBを経由しないため、認証ヘッダーが届かないという根本的な問題に直面した。
ALBが付与する x-amzn-oidc-data ヘッダーの特性と制約
SSR内部通信で認証情報が失われる理由
Next.js middleware / Server Component / CSR での認可チェックの比較
x-internal-api-key パターンによるSSR内部通信の認証設計
AWS ECS + ALB OIDC環境でNext.jsアプリを運用している方
SSRとCSRで認証の扱いが異なることに困っている方
アプリ層でのアクセス制御設計を検討している方
AWS ECS Fargate + ALB(Cognito OIDC認証)
Next.js 16(App Router, standalone mode, basePath: /sm)
FastAPI(Python)
同一VPC内でCloud Map経由のサービス間通信
まず、リクエストがどの経路を通るかを理解する必要がある。
ブラウザからのAPI呼び出しはALBを経由する。ALBはCognito認証後に x-amzn-oidc-data ヘッダー(JWT)を自動付与するため、バックエンドはこのJWTからメールアドレスを取得できる。
Server Componentからのデータ取得はECS内部のCloud Map DNS経由で直接バックエンドに接続する。ALBを経由しないため、x-amzn-oidc-data ヘッダーは存在しない。
# CSR: ALB経由 → ヘッダーあり
ブラウザ → ALB(x-amzn-oidc-data付与) → FastAPI
# SSR: 内部通信 → ヘッダーなし
Next.js SSR → Cloud Map → FastAPI
この「SSR内部通信にはOIDCヘッダーがない」という事実が、設計の核心的な課題となる。
middleware.tsで全リクエストをインターセプトし、x-amzn-oidc-data からメールを取得。バックエンドの /auth/check に内部通信で問い合わせる方式。
問題点:
middlewareはEdge Runtimeで動作し、api-server.ts(Node.js Runtime)を直接importできない
内部通信用のfetchを別途実装する必要があり、ヘッダー転送やエラー処理が複雑化
basePath(/sm)の二重化問題が発生しやすい
実際にstg環境で複数の問題が発生しデプロイ不可に
layout.tsxに<Authenticator>クライアントコンポーネントを配置。useEffectでマウント後にブラウザからALB経由で/auth/checkを呼ぶ方式。
メリット: 実装が最もシンプルで、SSR内部通信の問題が完全に無関係。
デメリット: SSRのデータフェッチが認可チェック前に実行され、ローディング表示が一瞬入る。
ルートの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制約 | あり | なし | なし |
パターン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>
);
}
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なのでそのまま使える。
ALBのx-amzn-oidc-dataはALB→ターゲット間でのみ付与される。SSR内部通信では使えない
x-internal-api-keyはECS内部通信でのみ使用し、セキュリティグループで外部からのアクセスを遮断する前提
Server ComponentのルートグループでCSR/SSRの認証パスを分離すると見通しがよい
SSR内部通信ではALBのOIDCヘッダーが届かないため、別の認証手段が必要
Next.js middlewareはEdge Runtime制約があり、Server Componentでの認可チェックが最もシンプル
x-internal-api-keyパターンでSSR内部通信を信頼済みとして扱う設計が有効
layout.tsxでの認可チェックにより、未許可ユーザーにはレンダリング自体が実行されないセキュリティを実現
この記事が役に立ったら共有しよう

Koki
フルスタックエンジニア / React, Next.js, TypeScript