はじめに/ガイド/認証

Next.jsでの認証実装方法

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

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

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

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

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

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

認証 (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>
  )
}
import { FormEvent } from 'react'
import { useRouter } from 'next/router'

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

  async function handleSubmit(event) {
    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: '問題が発生しました。' })
    }
  }
}
import { signIn } from '@/auth'

export default async function handler(req, res) {
  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!' })
}
import { serialize } from 'cookie'
import { encrypt } from '@/app/lib/session'

export default function handler(req, res) {
  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' })
  }
}
import db from '../../lib/db'

export default async function handler(req, res) {
  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$).*)'],
}
import { 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) {
  // 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)

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

  // 6. ユーザーが認証されている場合、/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ルートの実装
}
export default async function handler(req, res) {
  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互換のライブラリとリソースを紹介します:

認証ライブラリ

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

さらに学ぶ

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