vibecodingでAWS×SPA×OAuth2/Cognitoを使用してセキュリティ情報サイトを構築してみた

AWS
スポンサーリンク

本記事は、本リポジトリをベースにした「セキュリティ情報ブログSPA」を、AWS上で本番運用できる形に仕上げるまでのプロセス記録です。動作/開発環境、モチベーション、設計・実装、そしてハマり所まで、手元で再現できる具体性を重視してまとめました。


  1. サイトの作成
    1. 動作環境(インフラ構成)
  2. 開発環境(プロセスとツール)
    1. 進め方の全体像
    2. WSL 上で CodexCLI を動かす(例)
  3. モチベーション
  4. やったこと(ハイライト)
    1. 要件(抜粋)
    2. 設計書(抜粋)
    3. 躓きポイント(と対策)
  5. OAuth 2.0 + OpenID Connect の仕様と本構成でのトークン取り扱い
    1. 採用フロー
    2. 3種類のトークン
    3. 本構成でブラウザに返るトークンと保存
    4. このリポジトリでの一時方針(APIに送るトークン)
    5. 守るべき優先度(機密度)
    6. トークン検証の要点
    7. JWTの文字列表現とJWS/JWEの違い(本構成)
      1. JWEを使うべきケースと注意点(SPA観点)
  6. なぜSPAはクライアントシークレットを安全に保管できないのか/Cookie方式との比較
    1. SPA(Public + PKCE)で秘密を持てない理由
    2. Cookie(BFF/サーバ)方式との違い
  7. Permissions(Scopes) と Claims の詳細
    1. 用語整理
    2. 本構成でのScopes(権限)
    3. ID Token と Access Token の主なクレーム
    4. 本プロジェクトでのClaims活用
    5. ベストプラクティス
  8. ネットワーク設計(Public / Private / HA / VPC)
    1. 方針
    2. 構成要素(例)
    3. 通信経路
  9. EC2/ECSを使わずにLambdaを採用するメリットと向いている構成
    1. なぜLambdaか(EC2/ECSとの比較)
    2. このサイト構成とLambdaの相性
    3. 向いているユースケース
    4. 向いていない(ECS/EC2を検討)
    5. コスト・パフォーマンスの勘所
    6. Lambdaの考慮点(対策)
  10. SPAベストプラクティス適合性評価
    1. 合致している点
    2. 要改善(今後の方針)
    3. 推奨アクション(短期)
    4. 推奨アクション(中期)
  11. 一般的なSPA(純CSR)との違いと本構成のメリット/デメリット
    1. 一般的なSPA(純CSR)の流れ
    2. 本構成の主な違い
    3. メリット(本構成)
    4. デメリット/トレードオフ
    5. こう使い分ける
  12. 付録:運用コマンド例
    1. フロントのビルドと反映
    2. 必要なフロント環境変数(例)
  13. おわりに

サイトの作成

動作環境(インフラ構成)

  • フロント(静的配信): S3 + CloudFront(Next.js output: export
  • 動的API: API Gateway + Lambda(DynamoDB をデータストアに想定)
  • 認証: Cognito User Pool + Hosted UI(OAuth2/OIDC: Authorization Code Flow)
  • ドメイン/証明書: Route 53 + ACM(us-east-1, CloudFront用)
  • セキュリティヘッダ/CSP: CloudFront Response Headers Policy(CSP, HSTS, CT-Options, Referrer-Policy, Permissions-Policy)
  • ルーティング補助: CloudFront Function(クリーンURL → index.html 付与)
  • ドメイン取得: お名前.comで取得し、初期費用を最小化(レジストラコストを抑制しつつ、Route53にネームサーバを委譲)
graph LR
  U[User Browser]
  CF[CloudFront / CSP HSTS]
  S3[S3 Hosting]
  API[API Gateway]
  L[Lambda]
  DB[(DynamoDB)]
  Cg[Cognito Hosted UI]
  R53[Route53]
  ACM[ACM us-east-1]

  U <--> CF
  CF --> S3
  U --> API
  API --> L
  L --> DB
  U --> Cg
  R53 --- CF
  ACM --- CF
  • CloudFront の要点

    • HTMLはキャッシュ無効(Managed-CachingDisabled)
    • Function で /path/path/index.html へ書き換え(/_next/* と拡張子付きは除外)
    • CSP の connect-src に Cognito/IdP/API を明示
    • エラーフォールバック(403/404 → index.html)は無効化(JS/CSSのMIME誤配信を防止)
  • Cognito User Pool Client

    • AllowedOAuthFlowsUserPoolClient=true, allowed_oauth_flows=["code"]
    • allowed_oauth_scopes=["openid","email","profile"]
    • supported_identity_providers=["COGNITO"]
    • callback_urls=["https://<domain>/auth/callback"], logout_urls=["https://<domain>"]
    • OAuth クライアント種別: Public + PKCE(SPAはクライアントシークレットを安全に保持できないため。PKCEでコード横取り攻撃への耐性を確保)

開発環境(プロセスとツール)

進め方の全体像

  • 要件/設計: Kiro で要件・設計書を先に固める
  • 実装(大枠): 設計反映の骨組みを作成
  • 実装(仕上げ): Cursor を用いたペアプロで差分修正・リファクタ
  • フェーズ2 改修: CodexCLI + VSCode(WSL上でCodexCLIを実行)で集中的に対応
flowchart TB
    Kiro["要件/設計"] --> Repo["Git Repository"]
    Repo --> Cursor["Cursor:差分実装/ペアプロ"]
    Repo --> Codex["Codex CLI + VSCode(WSL)"]
    Codex --> Repo
    Cursor --> Repo

WSL 上で CodexCLI を動かす(例)

  • 前提: Windows 11 + WSL2 + Ubuntu、VSCode Remote を利用
  • 手順(例)
    1. WSLでリポジトリをクローン
    2. Node/NPM, AWS CLI をセットアップ
    3. CodexCLI をインストールして実行
# 1) 基本セットアップ
sudo apt update && sudo apt install -y curl unzip
# Node (必要に応じてNVM)
# AWS CLI
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip && sudo ./aws/install

# 2) リポジトリ操作
git clone <this-repo>
cd security_spa

# 3) CodexCLI (例: npm経由のインストール)
npm i -g @codex/cli
codex --version

モチベーション

  • AWS上で SPA + OAuth2(Cognito)を用いた本番運用可能なセキュア構成をゼロから構築
  • 実プロダクトを通じて、セキュリティ設計・CSP・キャッシュ戦略・認証/認可を体系的に学ぶ
  • (追記)セキュリティ担当として、SPAの挙動をフロント/エッジ/バックエンドで実戦理解したかった
  • (追記)OAuth 2.0 / OpenID Connect のトークン(Authorization Code with PKCE、ID/Access/Refresh)仕様を、サイト実装と検証を通じて手を動かして把握したかった

やったこと(ハイライト)

要件(抜粋)

  • 記事一覧/詳細/検索が高速に動作する SPA
  • 認証(ログイン/ログアウト、プロフィール表示)
  • 管理者は記事の投稿/更新/削除が可能
  • 本番環境における安全なCSPキャッシュ無効化運用

設計書(抜粋)

  • インフラ: Terraform による S3/CloudFront/Cognito/API Gateway/Lambda/Route53/ACM
  • フロント: Next.js App Router, Tailwind、Amplify(Auth)
  • API: Lambda(DynamoDB想定)+ API Gateway(JWTオーソライザー)
  • 監視/運用: CloudFront 無効化、S3同期、E2E/Smoke テスト(Playwright)

躓きポイント(と対策)

  • CloudFront の SPA フォールバックにより、JS/CSSへ index.html が返却 → MIMEエラー
    • 対策: 403/404 フォールバックを削除し、URI リライトは Function に限定
  • Service Worker の旧資産キャッシュで更新反映が遅延
    • 対策: 本番は既定で SW 無効(環境変数でON可)。無効化後は CF 無効化で即時反映
  • CSP が厳しすぎて Cognito や API へ接続拒否
    • 対策: connect-srcexecute-api / cognito-idp / Hosted UI ドメインを追加
  • Cognito Hosted UI が invalid_request/unauthorized_client
    • 対策: User Pool Client の IDP/flows/scopes/Callback/Logout完全一致で再設定
  • API オーソライザーが ID トークン検証のため、Access トークンで 401
    • 対策: フロントの HTTP クライアントは ID トークン優先Authorization: Bearer
  • Route53 の Hosted Zone 不一致
    • 対策: domain_name の見直しと ACM(バージニア北部) の適用

OAuth 2.0 + OpenID Connect の仕様と本構成でのトークン取り扱い

採用フロー

  • 本構成は SPA のためクライアントは Public。クライアントシークレットを安全に保持できないため、Authorization Code + PKCE を採用。
  • 流れ(要約)
    1. ブラウザ → Cognito Hosted UI へリダイレクト(code_challenge 付き)
    2. 認証成功で authorization_code/auth/callback に付与
    3. ブラウザ側(Amplify)が code_verifier でトークンに交換(ID/Access/Refresh)

3種類のトークン

  • ID Token(JWT, OIDC)
    • 用途: 認証結果とプロフィール(sub, email, email_verified, cognito:groups など)をフロントに伝える
    • 注意: 本来は「ユーザー情報表示」向け。APIの認可ヘッダとしては原則非推奨
    • 検証: iss(発行者), aud(クライアントID), exp, iat, nonce(必要時)
  • Access Token(JWT)
    • 用途: APIアクセスのための権限トークン(scope, aud はリソースサーバに合わせる)
    • 原則: API への Authorization: Bearer は Access Token を使用
  • Refresh Token
    • 用途: 長寿命の再発行用シークレット
    • リスク: ブラウザ保管は漏えいリスクが高い。SPAでは「発行しない/短寿命/回数制限/メモリ保持」など運用でリスク低減

本構成でブラウザに返るトークンと保存

  • Hosted UI → /auth/callback 後、Amplify の fetchAuthSession() がトークンを取得しセッションに保持
  • 設定(例): access_token_validity = 60分, id_token_validity = 60分, refresh_token_validity = 30日
  • 保存: Amplify が Web Storage を利用(実運用では「メモリ優先」「localStorage回避」が望ましい。BFF+HttpOnly Cookie 方式がより安全)

このリポジトリでの一時方針(APIに送るトークン)

  • API Gateway の JWT オーソライザーが ID Token 検証に寄っていたため、フロントの HTTP クライアントは一時的に ID Token を優先 して送出
  • 望ましい最終形: API オーソライザーを Access Token 検証に切替え、フロントも Access Token を送出
    • 理由: ID Token は「認証の証跡」、Access Token は「認可トークン」。役割分離がベストプラクティス

守るべき優先度(機密度)

  • 高: Refresh Token > Access Token > ID Token: 低
  • 対策例
    • Refresh: できれば SPA には発行しない or 短寿命+回数制限。発行する場合はメモリ保持やローテーション
    • Access/ID: XSS対策(CSP堅牢化/依存ライブラリ健全化)、Storage の最小化、不要時クリア

トークン検証の要点

  • 署名: JWKs(/.well-known/jwks.json)で kid による鍵選択→署名検証
  • クレーム: iss(Cognito発行者URL), aud(クライアントID), exp(期限)
  • 権限/ロール: Cognito グループ(cognito:groups)を ID Token のクレームから抽出→フロント表示/ゲートに反映

JWTの文字列表現とJWS/JWEの違い(本構成)

  • 形式: header.payload.signature. で連結した文字列。
    • それぞれ Base64URL エンコード(+-, /_, パディング=なし)
  • 構成
    • header: 署名アルゴリズムや鍵ID
    • payload: クレーム(iss, sub, aud, exp, iat, email, email_verified, cognito:groups など)
    • signature: base64url(header) + "." + base64url(payload) に対する署名

例(概念図)

<base64url>{"alg":"RS256","typ":"JWT","kid":"abc123"}</base64url>.
<base64url>{
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
  "sub": "user-sub",
  "aud": "<app-client-id>",
  "exp": 1727000000,
  "iat": 1726996400,
  "email": "user@example.com",
  "email_verified": true,
  "cognito:groups": ["admin"]
}</base64url>.
<base64url>(RS256 signature)</base64url>
  • JWS(JSON Web Signature)
    • 署名付き(改ざん検知)。payloadは「平文」なので閲覧可能(秘匿ではない)。
    • 本サイトの ID Token / Access Token は Cognito により JWS(RS256)で発行。
    • 検証は Cognito の JWKs(/.well-known/jwks.json)で kid を用いて公開鍵選択→署名検証。
  • JWE(JSON Web Encryption)
    • 暗号化(秘匿)されたJWT。5セグメント構成(header.encryptedKey.iv.ciphertext.tag)。
    • 本サイトでは未使用(HTTPS + JWS で十分。秘匿が必要ならBFF/HttpOnly Cookieなど別方式を検討)。
  • Refresh Token について
    • CognitoのRefreshは一般に「不透明(opaque)」な長い文字列(JWTでない)。
    • 機密度が最も高い。SPAでは極力保持を避け、短寿命・回数制限・メモリ保持などでリスク低減が望ましい。

JWEを使うべきケースと注意点(SPA観点)

  • 使うべきケース
    • JWTペイロードに**秘匿すべきクレーム(PII/PHI/金融情報/機密属性)**をどうしても入れる要件がある
    • 法規制や内部規程でトークン内容の秘匿が明示的に要求される(TLSだけでは不十分と判断される)
    • 受信側が**サーバ(秘匿鍵を安全に保管可能)**であり、復号をサーバで完結できる
  • SPAでの注意点
    • JWEは復号鍵の秘匿が前提。ブラウザ(SPA)に復号用の秘密鍵を置くと漏えいするため本末転倒
    • よって、SPAが直接JWEを復号する設計は基本的に不適。JWEはBFF/サーバ間での秘匿用途に適合
  • 推奨の代替策
    • JWTに機密クレームを入れない(最小クレーム)。必要情報はAPI経由で都度取得
    • BFF + HttpOnly Cookie 方式でサーバがトークン管理し、ブラウザはCookieのみ(JS不可視)
    • リファレンストークン(オペークトークン)を用い、クライアントは識別子だけを持ち、内容はサーバ側ストアで管理
    • どうしても暗号化が必要なデータはアプリケーションペイロードを受信者公開鍵で暗号化(エンドツーエンド)し、トークンとは分離

なぜSPAはクライアントシークレットを安全に保管できないのか/Cookie方式との比較

SPA(Public + PKCE)で秘密を持てない理由

  • 実行環境がユーザー側(ブラウザ)
    • 配布されたJSバンドルは誰でも取得・解析可能(DevTools/拡張/ソースマップ)
    • localStorage/sessionStorage/IndexedDB はJSから可読で、XSS成立時に窃取されうる
    • .envはビルド時に埋め込まれ、配布物に含まれる(秘匿不可)
  • 結論: SPAはクライアントシークレットを保持できないため、OAuthクライアントはPublicとし、PKCEでコード横取りを緩和するのが前提

Cookie(BFF/サーバ)方式との違い

  • 概要

    • BFF/サーバ: サーバ側でトークンを取得・保管し、ブラウザへは HttpOnly + Secure + SameSite Cookie を付与。ブラウザJSからトークンは不可視
    • SPA(Public+PKCE): ブラウザJSがトークンを取得・保持し Authorization: Bearer でAPIへ送信
  • メリット/デメリット

    • BFF/サーバ(Cookie)
      • 長所: トークンがJSから不可視(XSS耐性向上)、HttpOnly/SameSiteでCSRF対策が取りやすい、トークンローテーション/失効制御がしやすい
      • 短所: サーバ実装/運用コスト(スケーリング、ステート管理、追加費用)
    • SPA(Public+PKCE)
      • 長所: サーバ不要でS3+CloudFrontの静的配信で完結、コスト最小・スケール容易、実装がシンプル
      • 短所: トークンがJS空間に存在しXSSリスク、Refresh Tokenの安全運用が難しい、CORS/CSPのチューニングが必須
  • 本プロジェクトの選択理由

    • 学習・検証フェーズでは、静的配信 + Cognito Hosted UI + PKCE が最小コストで迅速
    • 将来的により強固な防御が必要なら BFF/Server(Cookie)方式 へ段階的移行(Access Token検証・HttpOnly運用)
  • SPA方式での実務的ガード(推奨)

    • 厳格なCSP(script-src, connect-src 等)、依存ライブラリの健全性維持
    • トークンは可能な限りメモリ保持(永続ストレージ最小化)、短寿命化、Refreshは無効/短期/回数制限
    • API側はAccess Token検証へ移行し役割分離(ID=認証、Access=認可)

Permissions(Scopes) と Claims の詳細

用語整理

  • Permissions(権限): OAuth 2.0 では通常、APIが許可する操作を表現するためにscopeで表す(例: articles:read, articles:write)。
  • Claims(クレーム): JWT(ID/Access)に含まれる属性情報(iss, sub, aud, exp などの標準、emailcognito:groups 等の拡張)。

本構成でのScopes(権限)

  • 採用スコープ: openid, email, profile(OIDC標準。IDの取得を目的)
  • 将来拡張例:
    • 読み取り: articles:read
    • 投稿/更新: articles:write
    • 管理操作: admin:*
  • 運用ポイント: スコープは「最小権限」で付与。API Gateway 側でスコープ検証を組み込むか、Lambdaオーソライザーでscopeクレームを解釈して許可/拒否。

ID Token と Access Token の主なクレーム

  • 共通(標準): iss(発行者), aud(クライアントID), exp(有効期限), iat(発行時刻), sub(一意ユーザーID)
  • ID Token: ユーザー属性の表明(email, email_verified, name 等)、Cognito固有(cognito:groups
  • Access Token: 認可関連(scope, token_use: "access", client_id, username

例: ID Token のpayload(抜粋)

{
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
  "sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "aud": "<app-client-id>",
  "exp": 1727000000,
  "iat": 1726996400,
  "email": "user@example.com",
  "email_verified": true,
  "cognito:groups": ["admin"],
  "token_use": "id"
}

例: Access Token のpayload(抜粋)

{
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/<user-pool-id>",
  "sub": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "client_id": "<app-client-id>",
  "username": "user@example.com",
  "scope": "openid email profile articles:read",
  "exp": 1727000000,
  "iat": 1726996400,
  "token_use": "access"
}

本プロジェクトでのClaims活用

  • フロント: ID Token の cognito:groups から admin/user を導出し、ナビ表示/管理UI出し分け。
  • API: 現状はID Token検証に寄せているが、最終的にはAccess Token検証へ切替予定。必要に応じてscopeで操作別のガードを実装。

ベストプラクティス

  • ID Token は表示/識別用途。APIのAuthorizationにはAccess Tokenを使用。
  • スコープは細分化し、最小権限で発行。管理操作は専用スコープやグループで厳格に制御。
  • カスタムクレームに機密情報を載せない。必要な詳細はAPI呼び出しで都度取得。
  • 署名/JWK検証(kid選択), iss/aud/exp 検証を必ず行う。

ネットワーク設計(Public / Private / HA / VPC)

方針

  • 静的配信(S3/CloudFront)はVPC外(グローバル)で提供し、API経路はAPI Gatewayを正面に置く構成。
  • LambdaはPrivateサブネット(複数AZ)に配置し、DynamoDB/S3はVPCエンドポイント経由(可能な限りNAT経由を削減)。
  • 高可用性(HA)はマルチAZのサブネット設計とNAT GW冗長化で担保。

構成要素(例)

  • VPC: 10.0.0.0/16
  • AZ: ap-northeast-1a, ap-northeast-1c(例)
  • Public Subnets: 10.0.0.0/24 (1a), 10.0.1.0/24 (1c)
    • Internet Gateway(IGW)へデフォルトルート
    • NAT Gatewayを各AZに配置(NAT単一障害点を回避)
  • Private App Subnets: 10.0.10.0/24 (1a), 10.0.11.0/24 (1c)
    • Lambda ENI配置
    • 送信先は基本VPCエンドポイント、必要時のみ同AZのNAT GW経由
  • VPC Endpoints(推奨)
    • Gateway型: DynamoDB, S3
    • Interface型: CloudWatch Logs, Secrets Manager, STS など(必要に応じて)

通信経路

  • ブラウザ → CloudFront → S3(静的アセット)
  • ブラウザ → CloudFront → API Gateway → Lambda(VPC)→ DynamoDB(Gateway Endpoint)
  • Lambda の外向き通信は VPC Endpoint を優先し、外部のみ NAT GW を利用

EC2/ECSを使わずにLambdaを採用するメリットと向いている構成

なぜLambdaか(EC2/ECSとの比較)

  • 運用負荷最小: OS/ミドル/パッチ/オートスケールの管理不要。コードに専念できる
  • 自動スケーリング: リクエストに応じて自動拡縮。スパイク耐性が高い
  • 従量課金: 実行時間とリクエスト数に応じて課金。アイドルコストがほぼゼロ
  • 周辺統合が容易: API Gateway/Step Functions/S3/DynamoDB/EventBridge とシームレス連携
  • セキュリティ境界: コンテナ/OS面の責任共有が小さく、最小権限IAM設計に集中できる

EC2/ECS は常時稼働・長時間処理・特殊ミドル要件・独自ネットワーク制御が必要なときに選択。Lambda は短時間・イベント駆動・サージがあるワークロードに適合。

このサイト構成とLambdaの相性

  • 静的フロント(S3+CloudFront)× 短時間API: 記事取得/投稿などは短いI/O中心で、Lambdaと相性が良い
  • イベント駆動: 記事のインデクシング・分析・画像処理などをS3/イベントで非同期実行可能
  • マイクロサービス分割: 記事/カテゴリ/ユーザ/監視などドメインごとに小さな関数へ分離
  • 今回ECSを採用しなかった理由: フロントはS3+CloudFrontの静的配信、APIはAPI Gateway→Lambdaで完結し、常時稼働のWebサーバ/コンテナが不要だったため(Webサーバの維持管理コストを削減)

向いているユースケース

  • REST/GraphQL APIの短時間処理(~数百ms〜数秒)
  • Webhook/バッチ/スケジュール実行(EventBridge/CloudWatch Events)
  • S3/DynamoDBトリガでの非同期処理・サムネイル生成・エンリッチ
  • ワークフロー(Step Functions)での段階処理・リトライ・補償

向いていない(ECS/EC2を検討)

  • 長時間処理ストリーミング常駐が必要(Lambda最大15分)
  • 低レイテンシ厳格かつ超高RPSでコールドスタートの影響が許容できない
  • 特殊ミドルウェアネイティブ依存、大容量ディスク/永続プロセスが必要
  • 双方向通信の恒常接続(WebSocketはAPI Gatewayで可能だが運用制約がある)

コスト・パフォーマンスの勘所

  • 低〜中トラフィック: Lambdaが有利(アイドルコストゼロ)
  • 高トラフィック一定: ECS(Fargate)/EC2の常時稼働が単価で有利になる境界がある
  • プロファイルしてから選択: 実測の継続時間・同時実行・メモリで月額比較

Lambdaの考慮点(対策)

  • コールドスタート: Provisioned Concurrency、軽量ランタイム、バンドル最適化、VPCエンドポイントでENI遅延低減
  • タイムアウト/再試行: timeout適正化、Idempotency設計、DLQ/リトライポリシー
  • 観測性: CloudWatch Logs + Tracing(X-Ray/OTel)、メトリクス/アラート整備
  • ネットワーク: VPC内接続はENI生成に伴うレイテンシ増。必要なVPCEを作成しNAT経由を最小化

本プロジェクトは「静的SPA + 短時間API + スパイクあり」を想定しており、Lambdaの強み(自動スケール・従量課金・運用負荷軽減)を享受できる構成です。

SPAベストプラクティス適合性評価

合致している点

  • 配信構成: S3 + CloudFront(CDN)で静的アセットを配信
  • HTTPS強制とセキュリティヘッダ: HSTS, X-Content-Type-Options, Referrer-Policy などをCloudFrontで付与
  • CSP: script-src/style-src/connect-src をホワイトリスト運用(Cognito/IDP/APIのみ許可)
  • ルーティング: CloudFront Functionで/path/path/index.htmlの書き換え。403/404でのSPAフォールバックを無効化してMIME誤配信を防止
  • キャッシュ戦略: HTMLはno-cache, no-store, must-revalidateで即時反映。CloudFront無効化フローを運用
  • 認証フロー: Publicクライアント + Authorization Code with PKCE(Hosted UI)
  • トークン検証観点: iss/aud/exp検証前提、JWKsで署名検証(JWS)。機密クレームはトークンへ載せない方針
  • 秘密管理: クライアントにシークレットを置かない(Publicクライアント)。環境変数注入で動作
  • IaC/自動化: Terraformでインフラ定義、S3同期→CloudFront無効化の運用を確立
  • 品質担保: Playwrightのスモーク/E2E(モバイルメニュー配慮)

要改善(今後の方針)

  • API認可トークンの統一: 現状はAPIオーソライザー都合で一時的にID Tokenを送出。最終形はAccess Token送出に統一(役割分離)
  • スコープ設計: OIDC標準(openid email profile)に加え、将来的にarticles:read/writeなどリソース単位のscopeを導入し最小権限化
  • トークン保管: Amplify既定のWeb StorageはXSS時に窃取リスク。可能ならメモリ保持優先、長寿命Refreshの利用抑制
  • 静的アセットのキャッシュ: ハッシュ付き/_next/static/*長期キャッシュ+immutableが理想(HTMLはno-cacheのまま)。現状のヘッダ適用範囲を再確認
  • BFF検討: より厳密な防御が必要な場合はBFF+HttpOnly Cookieへ移行(JSからトークン不可視、CSRF対策同時適用)
  • API側ガード: scope/groupcognito:groups)を用いたルート単位の許可制御をLambdaで明文化
  • 監視/可観測性: X-Ray/OTel導入、アラート閾値(4xx/5xx, p95/p99)設定の強化

推奨アクション(短期)

  1. API GatewayのJWTオーソライザーをAccess Token検証へ切替 → フロントHTTPクライアントもAccess優先に変更
  2. /_next/static/*に長期キャッシュ(Cache-Control: public, max-age=31536000, immutable)を適用(HTMLはno-cache維持)
  3. トークンのメモリ保持(永続Storage最小化)と短寿命化を検討(必要に応じてRefresh無効/短期/回数制限)

推奨アクション(中期)

  • articles:*スコープの導入とAPI側のscope検証
  • BFF方式のPoC(Auth Code with PKCE→サーバ取得、ブラウザはHttpOnly Cookie)
  • 可観測性強化(トレーシング/メトリクス/ダッシュボード)

一般的なSPA(純CSR)との違いと本構成のメリット/デメリット

一般的なSPA(純CSR)の流れ

  • ブラウザがJSバンドルを取得 → JSがAPIへ直接アクセス → 返却JSONからDOM構築
  • ルーティングはフロント側、CDN/ストレージはシンプルに静的配信

本構成の主な違い

  • Next.js を静的エクスポート(output: export)で採用し、CloudFrontでCSP/セキュリティヘッダを一括付与
  • CloudFront FunctionでクリーンURL/index.htmlへリライトし、SPAの404/403フォールバックを無効化(MIME誤配信対策)
  • 認証は Cognito Hosted UI + Authorization Code with PKCE(Public) に統一
  • HTMLはno-cacheで即時反映、ハッシュ付き静的アセットは長期キャッシュ想定(将来最適化)
  • Service Workerは本番既定オフ(更新反映遅延の事故を回避)

メリット(本構成)

  • セキュリティ強化: 厳格CSP/HSTS/Permissions-Policy、JWS署名検証、PKCE採用、サーバシークレット不要
  • 運用容易: S3+CloudFront+Lambda+API Gatewayでサーバ不要。デプロイはS3同期とCF無効化で完結
  • 可搬性/コスト: 静的配信が中心で低コスト、Lambdaは従量でスパイクにも強い
  • ルーティング健全性: 404/403でindex.htmlを返さないため、JS/CSSのMIMEエラーを根本防止

デメリット/トレードオフ

  • SSR/SSGの制約: 完全CSR寄りのため、初期描画/SEOでSSRほど強くない(静的エクスポートできる範囲に限定)
  • キャッシュ運用の複雑さ: HTML no-cache、静的アセット長期キャッシュ、CF無効化の運用知識が必要
  • トークン管理はブラウザ側: BFFでないためXSS時のリスクは相対的に高い(CSP/メモリ保持/短寿命で軽減)
  • 高度なリアルタイムや長時間処理: Lambda/静的配信構成では設計上の工夫や別サービスの併用が必要

こう使い分ける

  • 本構成: 「静的SPA + 短時間API + コスト/運用最小化 + 学習/検証」に最適
  • さらにセキュア/高機能にするなら: BFF + HttpOnly Cookie、SSR/SSG混在、WebSocket/Streams対応サービスの併用を段階導入

付録:運用コマンド例

フロントのビルドと反映

npm run build
aws s3 sync out/ s3://<your-s3-bucket>/ --delete --no-cli-pager
aws cloudfront create-invalidation --distribution-id <CF_DIST_ID> --paths "/*" --no-cli-pager

必要なフロント環境変数(例)

NEXT_PUBLIC_APP_URL=https://<custom-domain>
NEXT_PUBLIC_COGNITO_USER_POOL_ID=
NEXT_PUBLIC_COGNITO_CLIENT_ID=
NEXT_PUBLIC_COGNITO_DOMAIN=
NEXT_PUBLIC_API_GATEWAY_URL=https://<api-id>.execute-api.ap-northeast-1.amazonaws.com/prod

おわりに

  • SPA×OAuth2×CSP×キャッシュ戦略を一通り組み合わせると、「表示は速いが安全」な構成が作れます。
  • 本記事の構成/手順は、静的SPAを本番運用する上での落とし穴(CSP/キャッシュ/フォールバック/Hosted UI設定)を回避する実践例として活用できます。

コメント

タイトルとURLをコピーしました