はじめに/ガイド/認証

Next.jsでの認証実装方法

認証を理解することは、アプリケーションのデータを保護するために重要です。このページでは、React と Next.js の機能を使って認証を実装する方法をガイドします。

始める前に、プロセスを3つの概念に分解すると理解しやすくなります:

  1. 認証 (Authentication): ユーザーが自分自身であることを確認します。ユーザー名とパスワードなど、ユーザーが持っている情報を使って本人確認を行います。
  2. セッション管理 (Session Management): リクエストを跨いでユーザーの認証状態を追跡します。
  3. 認可 (Authorization): ユーザーがアクセスできるルートやデータを決定します。

この図は、React と Next.js の機能を使った認証フローを示しています:

React と Next.js の機能を使った認証フローの図

このページの例では、教育目的で基本的なユーザー名とパスワードによる認証を説明します。カスタム認証ソリューションを実装することも可能ですが、セキュリティを強化し簡素化するため、認証ライブラリの使用を推奨します。これらは認証、セッション管理、認可のための組み込みソリューションに加え、ソーシャルログイン、多要素認証、ロールベースのアクセス制御などの追加機能を提供します。認証ライブラリセクションで一覧を確認できます。

認証 (Authentication)

サインアップおよび/またはログインフォームを実装する手順は次のとおりです:

  1. ユーザーがフォームを通じて認証情報を送信
  2. フォームがAPIルートで処理されるリクエストを送信
  3. 検証に成功すると、ユーザーの認証が成功したことが示されます
  4. 検証に失敗した場合、エラーメッセージが表示されます

ユーザーが認証情報を入力できるログインフォームの例:

import { FormEvent } from 'react'
import { useRouter } from 'next/router'

export default function LoginPage() {
  const router = useRouter()

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')

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

    if (response.ok) {
      router.push('/profile')
    } else {
      // エラーハンドリング
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">ログイン</button>
    </form>
  )
}

上記のフォームには、ユーザーのメールアドレスとパスワードを取得する2つの入力フィールドがあります。送信時、APIルート(/api/auth/login)にPOSTリクエストを送信する関数がトリガーされます。

次に、APIルートで認証プロバイダーのAPIを呼び出して認証を処理できます:

import type { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })

    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: '無効な認証情報です' })
    } else {
      res.status(500).json({ error: '問題が発生しました' })
    }
  }
}

セッション管理

セッション管理により、ユーザーの認証状態がリクエスト間で維持されます。セッションやトークンの作成、保存、更新、削除が含まれます。

セッションには2つのタイプがあります:

  1. ステートレス: セッションデータ(またはトークン)がブラウザのクッキーに保存されます。クッキーは各リクエストとともに送信され、サーバーでセッションを検証できます。この方法はシンプルですが、正しく実装されていないと安全性が低くなる可能性があります。
  2. データベース: セッションデータがデータベースに保存され、ユーザーのブラウザには暗号化されたセッションIDのみが送信されます。この方法はより安全ですが、複雑でサーバーリソースを多く消費する可能性があります。

知っておくと良いこと: どちらの方法も、または両方を使用できますが、iron-sessionJoseなどのセッション管理ライブラリの使用をお勧めします。

ステートレスセッション

クッキーの設定と削除

API Routes を使用して、サーバー上でセッションをクッキーとして設定できます:

import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'
import { encrypt } from '@/app/lib/session'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)

  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 1週間
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: 'Successfully set cookie!' })
}

データベースセッション

データベースセッションを作成・管理するには、以下の手順に従います:

  1. セッションとデータを保存するためのテーブルをデータベースに作成(または認証ライブラリがこれを処理しているか確認)
  2. セッションの挿入、更新、削除機能を実装
  3. セッションIDを暗号化してユーザーのブラウザに保存し、データベースとクッキーを同期させる(これはオプションですが、ミドルウェアでの楽観的な認証チェックに推奨)

サーバー上でのセッション作成:

import db from '../../lib/db'
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })

    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: 'Internal Server Error' })
  }
}

認可

ユーザーが認証されセッションが作成されたら、アプリケーション内でユーザーがアクセス・実行できる内容を制御する認可を実装できます。

認可チェックには主に2つのタイプがあります:

  1. 楽観的チェック: クッキーに保存されたセッションデータを使用して、ユーザーがルートにアクセスしたりアクションを実行したりする権限があるかどうかを確認します。これらのチェックは、UI要素の表示/非表示や、権限やロールに基づくユーザーのリダイレクトなど、迅速な操作に有用です。
  2. 安全なチェック: データベースに保存されたセッションデータを使用して、ユーザーがルートにアクセスしたりアクションを実行したりする権限があるかどうかを確認します。これらのチェックはより安全で、機密データへのアクセスやアクションを必要とする操作に使用されます。

どちらの場合も、以下を推奨します:

ミドルウェアによる楽観的チェック(オプション)

ミドルウェアを使用して、権限に基づいてユーザーをリダイレクトしたい場合があります:

  • 楽観的チェックを実行するため。ミドルウェアはすべてのルートで実行されるため、リダイレクトロジックを一元化し、未承認ユーザーを事前にフィルタリングする良い方法です。
  • ユーザー間でデータを共有する静的ルート(例: ペイウォールの背後にあるコンテンツ)を保護するため。

ただし、ミドルウェアはプリフェッチされたルートを含むすべてのルートで実行されるため、パフォーマンスの問題を防ぐために、クッキーからのセッション読み取り(楽観的チェック)のみを行い、データベースチェックは避けることが重要です。

例:

import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'

// 1. 保護されたルートと公開ルートを指定
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']

export default async function middleware(req: NextRequest) {
  // 2. 現在のルートが保護されているか公開されているかを確認
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)

  // 3. クッキーからセッションを復号化
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  // 4. ユーザーが認証されていない場合、/loginにリダイレクト
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  // 5. ユーザーが認証されている場合、/dashboardにリダイレクト
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }

  return NextResponse.next()
}

// ミドルウェアを実行しないルート
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

ミドルウェアは初期チェックに有用ですが、データ保護の唯一の防衛線として使用すべきではありません。セキュリティチェックの大部分は、データソースにできるだけ近い場所で実行する必要があります。詳細についてはデータアクセスレイヤーを参照してください。

ヒント:

  • ミドルウェアでは、req.cookies.get('session').value を使用してクッキーを読み取ることもできます。
  • ミドルウェアはEdge Runtimeを使用します。認証ライブラリとセッション管理ライブラリが互換性があるか確認してください。
  • ミドルウェアで実行するルートを指定するために matcher プロパティを使用できます。ただし、認証のためには、ミドルウェアがすべてのルートで実行されることを推奨します。

データアクセス層 (DAL) の作成

APIルートの保護

Next.jsのAPIルートは、サーバーサイドロジックとデータ管理を処理するために不可欠です。これらのルートを保護し、認証されたユーザーのみが特定の機能にアクセスできるようにすることが重要です。これには通常、ユーザーの認証状態とロールベースの権限の確認が含まれます。

APIルートを保護する例:

import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getSession(req)

  // ユーザーが認証されているか確認
  if (!session) {
    res.status(401).json({
      error: 'User is not authenticated',
    })
    return
  }

  // ユーザーが 'admin' ロールを持っているか確認
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Unauthorized access: User does not have admin privileges.',
    })
    return
  }

  // 認可されたユーザーのためにルートを続行
  // ... APIルートの実装
}

この例は、認証と認可の2段階のセキュリティチェックを持つAPIルートを示しています。最初にアクティブなセッションをチェックし、次にログインしているユーザーが 'admin' かどうかを確認します。このアプローチにより、認証され認可されたユーザーのみが安全にアクセスでき、リクエスト処理の堅牢なセキュリティが維持されます。

リソース

Next.jsでの認証について学んだので、安全な認証とセッション管理を実装するのに役立つNext.js互換のライブラリとリソースを紹介します:

認証ライブラリ

セッション管理ライブラリ

さらに学ぶ

認証とセキュリティについてさらに学ぶには、以下のリソースを参照してください: