Next.js API Routes認証エラーの解決方法|初心者向け完全ガイド

React / Next.js

Next.js API Routes認証エラーの解決方法|初心者向け完全ガイド

はじめに

Next.jsでAPI Routesを使用するときに、認証関連のエラーに困っていませんか?このエラーは初心者が非常に遭遇しやすい問題です。本記事では、Next.js API Routesの認証エラーの原因から解決方法まで、段階的に解説します。

Next.js API Routes認証エラーとは

Next.js API Routesは、フロントエンドとバックエンドを同じプロジェクト内で管理できる便利な機能です。しかし、認証機能を実装する際に、トークンの検証、セッション管理、CORS設定などで様々なエラーが発生します。

原因の説明

1. JWTトークンが正しく検証されていない

最も一般的な原因は、API Routesでトークンの検証ロジックが正しく実装されていないことです。クライアントから送信されたJWTトークンが、サーバー側で適切に検証されていないと、認証エラーが発生します。

2. リクエストヘッダーにトークンが含まれていない

クライアント側でAuthorizationヘッダーにトークンを含めていない、または形式が正しくない場合、サーバー側でトークンを取得できず、認証に失敗します。

3. CORS設定の不備

異なるオリジンからのリクエストに対してCORS設定がされていないと、ブラウザがリクエストをブロックし、認証プロセス全体が失敗します。

4. ミドルウェアの欠落

API Routesで認証チェックを行うミドルウェアが実装されていないか、正しく機能していない場合、保護されるべきエンドポイントが保護されていない状態になります。

5. 環境変数の設定ミス

JWT署名用の秘密鍵(SECRET_KEY)が.env.localに設定されていなかったり、ファイル名が間違っていたりすると、トークン検証に失敗します。

解決手順

ステップ1: 環境変数の確認と設定

まず、プロジェクトのルートディレクトリに.env.localファイルを作成し、必要な環境変数を設定します。

NEXT_PUBLIC_API_URL=http://localhost:3000
JWT_SECRET=your-secret-key-here-min-32-characters-long

重要なポイント:JWT_SECRETは十分に長く、ランダムな文字列にしてください。本番環境では、最低32文字以上が推奨されます。

ステップ2: 認証ミドルウェアの実装

API Routesで使用する認証ミドルウェアを実装します。これにより、すべてのリクエストで統一的にトークンを検証できます。

ステップ3: ログインエンドポイントの実装

ユーザー認証後にJWTトークンを発行するエンドポイントを実装します。

ステップ4: 保護されたエンドポイントの実装

ミドルウェアを使用して、トークン検証が必要なエンドポイントを実装します。

ステップ5: クライアント側のトークン管理

クライアント側でトークンを保存し、すべてのAPI呼び出しに含める実装を行います。

コード例

認証ミドルウェアの実装例

// lib/auth.ts
import jwt from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';

interface DecodedToken {
  userId: string;
  email: string;
  iat: number;
}

export function verifyToken(token: string): DecodedToken | null {
  try {
    const secret = process.env.JWT_SECRET;
    if (!secret) {
      console.error('JWT_SECRET is not defined');
      return null;
    }
    
    const decoded = jwt.verify(token, secret) as DecodedToken;
    return decoded;
  } catch (error) {
    console.error('Token verification failed:', error);
    return null;
  }
}

export function getTokenFromRequest(req: NextApiRequest): string | null {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return null;
  }
  
  // "Bearer TOKEN" 形式から TOKEN を抽出
  const parts = authHeader.split(' ');
  if (parts.length !== 2 || parts[0] !== 'Bearer') {
    return null;
  }
  
  return parts[1];
}

export function withAuth(
  handler: (req: NextApiRequest, res: NextApiResponse, user: DecodedToken) => Promise
) {
  return async (req: NextApiRequest, res: NextApiResponse) => {
    const token = getTokenFromRequest(req);
    
    if (!token) {
      return res.status(401).json({ error: 'Missing or invalid authorization header' });
    }
    
    const user = verifyToken(token);
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid or expired token' });
    }
    
    return handler(req, res, user);
  };
}

ログインエンドポイントの実装例

// pages/api/auth/login.ts
import { NextApiRequest, NextApiResponse } from 'next';
import jwt from 'jsonwebtoken';

interface LoginRequest {
  email: string;
  password: string;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const { email, password } = req.body as LoginRequest;

    // バリデーション
    if (!email || !password) {
      return res.status(400).json({ error: 'Email and password are required' });
    }

    // ここでデータベースからユーザーを取得し、パスワードを検証
    // 例:const user = await db.users.findOne({ email });
    // 実装例として、簡単なデモ用検証を行います
    if (email !== 'user@example.com' || password !== 'password123') {
      return res.status(401).json({ error: 'Invalid email or password' });
    }

    // JWTトークンを生成
    const secret = process.env.JWT_SECRET;
    if (!secret) {
      return res.status(500).json({ error: 'Server configuration error' });
    }

    const token = jwt.sign(
      {
        userId: '123',
        email: email,
      },
      secret,
      { expiresIn: '24h' }
    );

    // トークンをHTTP-only cookieに設定することも推奨
    res.setHeader('Set-Cookie', `token=${token}; Path=/; HttpOnly; Max-Age=86400`);

    return res.status(200).json({
      message: 'Login successful',
      token: token,
    });
  } catch (error) {
    console.error('Login error:', error);
    return res.status(500).json({ error: 'Internal server error' });
  }
}

保護されたエンドポイントの実装例

// pages/api/user/profile.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { withAuth } from '@/lib/auth';

const handler = withAuth(async (req, res, user) => {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  // ここで認証済みユーザーの情報を返す
  return res.status(200).json({
    message: 'User profile',
    user: {
      userId: user.userId,
      email: user.email,
    },
  });
});

export default handler;

クライアント側でのAPI呼び出し例

// components/LoginForm.tsx
import { useState } from 'react';

export default function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');

    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        const data = await response.json();
        setError(data.error);
        return;
      }

      const data = await response.json();
      // トークンをローカルストレージに保存
      localStorage.setItem('token', data.token);
      // リダイレクトなど
      window.location.href = '/dashboard';
    } catch (err) {
      setError('An error occurred during login');
    }
  };

  return (
    
setEmail(e.target.value)} required /> setPassword(e.target.value)} required /> {error &&

{error}

}
); } // 保護されたエンドポイントへのAPI呼び出し export async function fetchUserProfile() { const token = localStorage.getItem('token'); if (!token) { throw new Error('No token found'); } const response = await fetch('/api/user/profile', { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { if (response.status === 401) { // トークン無効またはログイン必要 localStorage.removeItem('token'); window.location.href = '/login'; } throw new Error('Failed to fetch profile'); } return response.json(); }

CORS対応の例

// lib/cors.ts
import { NextApiRequest, NextApiResponse } from 'next';

export function enableCors(
  req: NextApiRequest,
  res: NextApiResponse,
  origin: string = '*'
) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader(
    'Access-Control-Allow-Methods',
    'GET, POST, PUT, DELETE, OPTIONS'
  );
  res.setHeader(
    'Access-Control-Allow-Headers',
    'Content-Type, Authorization'
  );
  res.setHeader('Access-Control-Allow-Credentials', 'true');

  // OPTIONSリクエストの処理
  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return true;
  }

  return false;
}

// pages/api/example.ts で使用
import { enableCors } from '@/lib/cors';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // CORSの有効化
  if (enableCors(req, res)) {
    return;
  }

  // 通常の処理
  res.status(200).json({ message: 'CORS enabled' });
}

よくある間違い

間違い1: トークンの形式を忘れる

❌ 間違った例:

// 間違い:Bearerプレフィックスを忘れている
headers: {
  'Authorization': token,
}

✅ 正しい例:

// 正しい:Bearer形式を使用
headers: {
  'Authorization': `Bearer ${token}`,
}

間違い2: 環境変数の設定忘れ

❌ 間違った例:

const secret = 'my-secret-key'; // ハードコード
const decoded = jwt.verify(token, secret);

✅ 正しい例:

const secret = process.env.JWT_SECRET;
if (!secret) {
  throw new Error('JWT_SECRET is not defined');
}
const decoded = jwt.verify(token, secret);

間違い3: トークン検証エラーのハンドリング不足

❌ 間違った例:

const decoded = jwt.verify(token, secret); // エラーハンドリングなし
return decoded;

✅ 正しい例:

try {
  const decoded = jwt.verify(token, secret);
  return decoded;
} catch (error) {
  console.error('Token verification failed:', error);
  return null;
}

間違い4: HTTPヘッダーのオプションを忘れる

❌ 間違った例:

const token = jwt.sign({ userId: '123' }, secret); // オプションなし

✅ 正しい例:

const token = jwt.sign(
  { userId: '123' },
  secret,
  { expiresIn: '24h' } // 有効期限を設定
);

間違い5: リクエストメソッドのチェック忘れ

❌ 間違った例:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  // メソッドチェックなしにPOST処理
  const data = req.body;
}

✅ 正しい例:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  const data = req.body;
}

デバッグのコツ

ブラウザの開発者ツールで確認する

Chrome DevToolsの「Network」タブで、APIリクエストのヘッダーとレスポンスを確認できます。特に「Authorization」ヘッダーが正しく送信されているかを確認しましょう。

コンソールログを活用する

API Routesの実装時に、トークン検証の各ステップでconsole.logを挿入し、どの段階で失敗しているかを追跡します。

JWTトークンをデコードする

jwt.ioなどのオンラインツールを使用して、生成されたトークンの内容を確認できます。

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

1. トークンをHTTP-only Cookieに保存する

JavaScriptからアクセス不可能なHTTP-only Cookieにトークンを保存することで、XSS攻撃を防げます。

2. 短い有効期限を設定する

アクセストークンの有効期限は短く(15分~1時間)設定し、リフレッシュトークンで更新する方式を使用します。

3. HTTPS通信を使用する

本番環境では必ずHTTPSを使用し、トークンが平文で送信されないようにします。

4. レート制限を実装する

ブルートフォース攻撃を防ぐため、ログインエンドポイントにレート制限を実装します。

まとめ

Next.js API Routesの認証エラーは、正しいトークン検証ロジック、適切なエラーハンドリング、セキュリティ対策を実装することで解決できます。このガイドで紹介した内容をまとめると:

  • 環境変数の設定:JWT_SECRETを.env.localに設定する
  • 認証ミドルウェア:再利用可能な認証チェック機能を実装する
  • トークン管理:クライアント側で正しくトークンを保存・送信する
  • エラーハンドリング:すべての認証エラーに対応する
  • セキュリティ:HTTP-only Cookie、短い有効期限、HTTPS通信を使用する

これらのポイントを実装することで、堅牢で安全なNext.js API Routesの認証システムを構築できます。初心者の方でも段階的に進めることで、確実に実装できるはずです。困ったときは、このガイドの該当セクションを再度確認してください。

Next.js APIの認証実装を成功させ、より安全で信頼性の高いアプリケーションを開発してください。

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