言語を切り替える
テーマを切り替える

Next.js API 認証とセキュリティ:JWT からレート制限まで完全実践ガイド

午前3時、私のスマホが震えました。クラウドプロバイダーからの請求アラートでした。金額は7800ドル。

目をこすりながら見直しました。先月の請求額は120ドルだったはず。詳細を見ると、API 呼び出し回数が1800万回を超えていました。私の個人プロジェクトで、普段は1日数百回程度なのに。

原因は、私が何の保護もかけていなかった API エンドポイントを特定したあるボットが、3日間ぶっ通しでアクセスし続けたことでした。その瞬間、私は「API セキュリティはオプションではない」という言葉の重みを痛感しました。

正直なところ、多くの人(以前の私も含め)が Next.js プロジェクトを立ち上げる際、ページの見た目やインタラクションの滑らかさばかりを気にかけ、API のセキュリティはバックエンド担当の仕事だと思いがちです。しかし、Next.js の API Routes は本質的にあなたのバックエンドそのものです。あなたが守らなければ、誰も守ってくれません。

この記事では、私が数年かけて踏んできた地雷と、調査したソリューションを体系的に整理しました。JWT 認証から CORS 設定、レート制限から入力検証まで、単なる理論ではなく、プロジェクトですぐに使える実践的なコードとして紹介します。

なぜ API セキュリティが重要なのか

API セキュリティに対する一般的な脅威

10.0
CVSS スコア

昨年12月、React 公式から重大なセキュリティ勧告が出されました。CVE-2025-55182、CVSS スコアは満点の 10.0 です。
これは、攻撃者が特定の HTTP リクエストを作成するだけで、あなたのサーバー上で任意のコードを実行できることを意味します。React Server Components を使用していて更新を怠っていれば、実質的に丸裸の状態です。

これだけではありません。今年3月には認可バイパス脆弱性 (CVE-2025-29927) も発見されました。スコアは 9.1。攻撃者はリクエストヘッダーを偽装するだけでミドルウェアの認証を回避できました。

日常的な脅威も無視できません:

悪質なクローラーと DDoS 攻撃:レート制限のない API は一瞬でダウンさせられます。私の知人が運営するサイトのログイン API は、1秒間に3000回のパスワード総当たり攻撃(ブルートフォース)を受け、サーバーがダウンしました。

データ漏洩:権限管理が不十分で、ユーザーAがユーザーBの注文情報を見れてしまう。これが発覚すれば、ブランドの評判は地に落ちます。

インジェクション攻撃:SQL インジェクション、XSS、コマンドインジェクション…古臭く聞こえるかもしれませんが、今でも多くのプロジェクトが餌食になっています。「React が自動エスケープしてくれるから大丈夫」と思っていても、API 側で検証していなければ無意味です。

Next.js API Routes の特徴とリスク

Next.js の API Routes は従来のバックエンドとは少し異なります。

サーバーレスファースト:Vercel にデプロイする場合、各 API リクエストは独立したサーバーレス関数として実行されます。自動スケーリングは便利ですが、ステートレスであるため、従来のメモリ内セッションは使えず、JWT や DB セッションが必要です。

フロントエンドとの混在:コードが同じリポジトリにあるため、環境変数が誤ってクライアントに漏洩しやすいです。DATABASE_URL.env に入れたつもりが、バンドルに含まれて GitHub で公開されてしまったケースを見たことがあります。

エッジコンピューティングの制限:Edge Runtime を使用する場合、一部の Node.js API が使用できず、暗号化ライブラリや DB 接続方法を選び直す必要があります。

API 認証の実戦

認証方式の選び方

現実的な問題として、JWT と Session、どちらを使うべきでしょうか?

JWT が適しているケース

  • 複数のサーバー(サーバーレス、エッジノード)にデプロイする場合
  • 異なるドメイン間で認証が必要な場合(app.com と api.com)
  • セッション管理をできるだけシンプルにしたい場合

JWT の本質は、ユーザー情報をトークンにエンコードし、サーバー側で状態を持たないことです。水平スケーリングが非常に容易です。

Session が適しているケース

  • サーバー側で能動的に制御したい場合(特定ユーザーの強制ログアウト、権限のリアルタイム変更)
  • セキュリティ要件が極めて高く、クライアントに一切ユーザー情報を持たせたくない場合
  • 既に Redis や DB があり、セッション管理が負担にならない場合

私の選択基準:個人プロジェクトや小規模なアプリなら JWT(手軽だから)。企業レベルや細かい制御が必要なら Session

JWT 認証の完全実装

では、JWT を採用する場合の Next.js での実装方法を見ていきましょう。

ステップ1:トークンの生成と検証

まずライブラリをインストールします:

npm install jose

なぜ jsonwebtoken ではなく jose か? Edge Runtime をサポートしているからです。

lib/auth.ts:

import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(
  process.env.JWT_SECRET || 'your-secret-key-at-least-32-characters'
);

export async function createToken(payload: { userId: string }) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m') // 15分で期限切れ
    .sign(secret);
}

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch {
    return null;
  }
}

注意すべきは 15m という短い有効期限です。多くの人が7日や30日に設定しますが、トークンが漏れた場合のリスクが高まります。短期の Access Token + 長期の Refresh Token が正解です。

ステップ2:トークンの保存——localStorage は絶対ダメ

ここが最重要ポイントです。多くのチュートリアルが localStorage への保存を推奨していますが、XSS 攻撃を受ければ一発で盗まれます。

正解は HttpOnly Cookie です。

// app/api/login/route.ts
import { NextResponse } from 'next/server';
import { createToken } from '@/lib/auth';

export async function POST(request: Request) {
  // ユーザー検証ロジック...

  const token = await createToken({ userId: user.id });

  const response = NextResponse.json({ success: true });
  response.cookies.set('token', token, {
    httpOnly: true,    // JS からアクセス不可(XSS対策)
    secure: true,      // HTTPS 通信のみ
    sameSite: 'lax',   // CSRF対策
    maxAge: 900,       // 15分
  });

  return response;
}

ステップ3:ミドルウェアによる API 保護

Next.js の Middleware を使って、認証が必要な API を一括で保護します。

middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const payload = await verifyToken(token);
  if (!payload) {
    return NextResponse.json(
      { error: 'Invalid token' },
      { status: 401 }
    );
  }

  // 検証成功後、ユーザー情報をヘッダーに付与して API へ渡す
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-id', payload.userId as string);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: '/api/protected/:path*',
};

これで /api/protected/* 以下のすべての API が自動的に保護されます。

NextAuth.js (Auth.js) による高速統合

もし上記の実装(特にリフレッシュトークンやOAuth)を自分で書くのが大変だと感じるなら、NextAuth.js (現在は Auth.js) を使いましょう。

セキュリティのベストプラクティス(CSRF保護、セッション暗号化など)がデフォルトで組み込まれており、Google や GitHub ログインも数行で実装できます。

CORS 設定の詳細解説

CORS の本質と問題

CORS(Cross-Origin Resource Sharing)は多くの開発者を悩ませます。開発環境(localhost)では問題ないのに、本番にデプロイするとエラーが出る…。

ブラウザは、あるドメイン(app.example.com)のページから別のドメイン(api.example.com)の API を叩く際、「このリクエストを許可しますか?」と API 側に確認します。これが CORS です。

Next.js での3つの設定方法

1. next.config.js でのグローバル設定

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
        ],
      },
    ];
  },
};

2. Middleware での設定(動的制御向け)

Middleware を使えば、リクエストの送信元に応じて動的に許可を与えることができます。

3. API Route 内での設定

特定の API だけ公開したい場合などに使います。

CORS セキュリティのベストプラクティス

ワイルドカード * を乱用しない
Access-Control-Allow-Origin: * は「誰でも私の API を叩いていいよ」という意味です。公開データならいいですが、ユーザー情報や操作 API でこれをやると、誰でもあなたの API を利用したフィッシングサイトを作れてしまいます。

Preflight (OPTIONS) リクエストの処理
Authorization ヘッダーや Content-Type: application/json を含むリクエストは、ブラウザが自動的に OPTIONS メソッドで事前確認(Preflight)を送ります。API がこれを適切に処理(200 OK と許可ヘッダーを返す)しないと、CORS エラーになります。

API レート制限(Rate Limiting)

冒頭の話に戻りますが、もし私がレート制限を導入していれば、被害は数万回のアクセス拒否ログだけで済み、7800ドルも請求されることはなかったでしょう。

おすすめのソリューション:@upstash/ratelimit

サーバーレス環境(Vercel 等)では、メモリ内にカウントを保存する方法は機能しません(リクエストごとに新しいインスタンスが立ち上がる可能性があるため)。Redis などの外部ストアが必要です。

Upstash は Vercel と相性が良い Serverless Redis で、専用のライブラリがあります。

npm install @upstash/ratelimit @upstash/redis

実装例:

// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// 10秒間に10回まで
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
});

API での使用:

// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';

export async function GET(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  return NextResponse.json({ data: 'Success' });
}

入力検証と防御

「ユーザー入力を決して信頼するな」——これはセキュリティの鉄則です。フロントエンドでの検証は UX のためであり、セキュリティのためではありません(簡単にバイパスできるからです)。

Zod による型安全な検証

Zod は TypeScript と相性抜群のスキーマ検証ライブラリです。

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  age: z.number().int().min(18),
});

export async function POST(request: Request) {
  const body = await request.json();
  const result = userSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json(
      { error: 'Input validation failed', details: result.error.format() },
      { status: 400 }
    );
  }

  // result.data は型安全
  const { email } = result.data;
  // ...
}

これにより、SQL インジェクションのリスクを減らし(ORM と併用)、不正なデータ形式によるサーバーエラーを防ぐことができます。

まとめ:完全セキュリティチェックリスト

最後に、ご自身のプロジェクトを診断するためのチェックリストを提供します。

認証

  • トークンは HttpOnly Cookie に保存されているか(localStorage は不可)
  • Access Token の有効期限は短いか(例:15-30分)
  • Refresh Token の仕組みはあるか
  • JWT シークレットは十分長く、環境変数に保存されているか

CORS

  • 敏感な API に Access-Control-Allow-Origin: * を使っていないか
  • 許可するオリジンを明示的に指定しているか
  • OPTIONS リクエストを適切に処理しているか

レート制限

  • ログイン、登録などの重要エンドポイントに厳格なレート制限があるか
  • 認証済みユーザーと未認証ユーザーで制限を分けているか
  • 429 ステータスコードを返しているか

入力検証

  • すべてのユーザー入力をサーバー側で検証しているか
  • Zod などのライブラリを使用しているか
  • 文字列や配列の最大長を制限しているか(DoS 対策)

その他

  • npm audit を定期的に実行し、依存関係の脆弱性を修正しているか
  • 秘密鍵(API Keyなど)が NEXT_PUBLIC_ プレフィックス付きで公開されていないか

API セキュリティは地道な作業ですが、一度設定してしまえば、あとはテンプレートとして使い回せます。7800ドルの請求書が届く前に、今すぐ対策を始めましょう。

FAQ

Next.js API では JWT と Session どちらを使うべきですか?
シーンによります。サーバーレス環境やクロスドメイン認証が必要な場合はステートレスな JWT が適しており、スケーリングも容易です。一方、サーバー側での強制ログアウトなど高度な制御が必要な場合や、セキュリティ要件が極めて高い場合は Session が適しています。個人開発なら JWT が手軽でおすすめです。
なぜ JWT トークンを localStorage に保存してはいけないのですか?
localStorage は JavaScript から読み取ることができるため、もしサイトに XSS(クロスサイトスクリプティング)脆弱性があると、攻撃者にトークンを盗まれるリスクがあるからです。HttpOnly Cookie ならば JS からアクセスできないため、はるかに安全です。
開発環境では CORS エラーが出ないのに、本番環境で出るのはなぜですか?
開発環境ではフロントエンドと API が同じ `localhost:3000` 上にあるため「同源(Same-Origin)」とみなされます。本番環境でフロントエンドと API のドメインが異なる場合、ブラウザは CORS ポリシーを適用し、API 側の許可ヘッダーがないとリクエストをブロックします。
サーバーレス環境(Vercel 等)でレート制限を実装するには?
@upstash/ratelimit と Redis(Vercel KV や Upstash)の組み合わせが推奨されます。サーバーレス関数はリクエストごとにインスタンスが変わるためメモリ内でのカウントは機能しませんが、Redis を使うことで全インスタンス間でカウントを共有できます。
API へのブルートフォース攻撃(総当たり攻撃)を防ぐには?
多層的な防御が必要です:1) ログイン API に厳格なレート制限(例:IP ごとに毎分5回)をかける、2) Zod 等でパスワード強度を検証する、3) アカウントロック機能(連続失敗で一定時間ロック)を実装する、4) 必要に応じて CAPTCHA を導入する。

5 min read · 公開日: 2026年1月5日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事