認証 (Authentication)

Next.js で認証を実装するには、以下の3つの基本概念を理解する必要があります:

  • 認証 (Authentication) - ユーザーが自分自身であることを確認します。ユーザー名とパスワードなど、ユーザーが持っている情報で本人確認を行います。
  • セッション管理 (Session Management) - ユーザーの状態(例: ログイン状態)を複数のリクエストにわたって追跡します。
  • 認可 (Authorization) - ユーザーがアプリケーションのどの部分にアクセスできるかを決定します。

このページでは、Next.js の機能を使用して一般的な認証、認可、セッション管理パターンを実装する方法を紹介します。アプリケーションのニーズに基づいて最適なソリューションを選択できるようになります。

認証 (Authentication)

認証はユーザーの身元を確認するプロセスです。ユーザーがユーザー名とパスワードでログインする場合や、Google などのサービスを介してログインする場合に行われます。ユーザーが本当に自分自身であることを確認することで、ユーザーデータとアプリケーションを不正アクセスや詐欺行為から保護します。

認証戦略

現代のウェブアプリケーションでは、以下の認証戦略が一般的に使用されます:

  1. OAuth/OpenID Connect (OIDC): ユーザーの認証情報を共有せずにサードパーティアクセスを可能にします。ソーシャルメディアログインやシングルサインオン (SSO) ソリューションに最適です。OpenID Connect でアイデンティティ層を追加します。
  2. 認証情報ベースのログイン (Email + Password): ユーザーがメールアドレスとパスワードでログインする、ウェブアプリケーションの標準的な選択肢です。実装が簡単で馴染み深いですが、フィッシングなどの脅威に対する堅牢なセキュリティ対策が必要です。
  3. パスワードレス/トークンベース認証: メールのマジックリンクやSMSのワンタイムコードを使用して、パスワードなしで安全にアクセスします。利便性とセキュリティ強化が特徴で、パスワード疲労を軽減します。ユーザーのメールや電話の可用性に依存するという制限があります。
  4. パスキー/WebAuthn: サイトごとに固有の暗号認証情報を使用し、フィッシングに対する高いセキュリティを提供します。安全ですが新しい技術であり、実装が難しい場合があります。

認証戦略の選択は、アプリケーションの特定の要件、ユーザーインターフェースの考慮事項、セキュリティ目標に沿って行うべきです。

認証の実装

このセクションでは、基本的なメールとパスワードによる認証をウェブアプリケーションに追加するプロセスを説明します。この方法は基本的なセキュリティレベルを提供しますが、一般的なセキュリティ脅威に対する保護を強化するために、OAuth やパスワードレスログインなどのより高度なオプションを検討する価値があります。以下に説明する認証フローは次のとおりです:

  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 { 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: '問題が発生しました。' })
    }
  }
}

このコードでは、signIn メソッドが保存されたユーザーデータに対して認証情報をチェックします。 認証プロバイダーが認証情報を処理した後、2つの可能な結果があります:

  • 認証成功: ログインが成功したことを意味します。保護されたルートへのアクセスやユーザー情報の取得など、さらなるアクションを開始できます。
  • 認証失敗: 認証情報が間違っているか、エラーが発生した場合、関数は認証失敗を示す対応するエラーメッセージを返します。

Next.js プロジェクトでより効率的な認証設定を行う場合、特に複数のログイン方法を提供する場合は、包括的な 認証ソリューション の使用を検討してください。

認可 (Authorization)

ユーザーが認証されたら、特定のルートへのアクセスを許可されているか、サーバーアクションでデータを変更したり、ルートハンドラーを呼び出したりする操作を実行できるかを確認する必要があります。

ミドルウェアでルートを保護

Next.js の ミドルウェア は、ウェブサイトのさまざまな部分に誰がアクセスできるかを制御するのに役立ちます。ユーザーダッシュボードなどのエリアを保護しながら、マーケティングページなどの他のページを公開するために重要です。すべてのルートにミドルウェアを適用し、公開アクセスのために除外を指定することをお勧めします。

Next.js で認証用のミドルウェアを実装する方法は次のとおりです:

ミドルウェアの設定:

  • プロジェクトのルートディレクトリに middleware.ts または .js ファイルを作成します。
  • 認証トークンのチェックなど、ユーザーアクセスを認可するロジックを含めます。

保護されたルートの定義:

  • すべてのルートで認可が必要なわけではありません。ミドルウェアの matcher オプションを使用して、認可チェックを必要としないルートを指定します。

ミドルウェアのロジック:

  • ユーザーが認証されているかどうかを確認するロジックを記述します。ルート認可のためにユーザーロールや権限をチェックします。

不正アクセスの処理:

  • 適切に、未認証ユーザーをログインページまたはエラーページにリダイレクトします。

ミドルウェアファイルの例:

import type { NextRequest } from 'next/server'

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

  if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) {
    return Response.redirect(new URL('/dashboard', request.url))
  }

  if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) {
    return Response.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
export function middleware(request) {
  const currentUser = request.cookies.get('currentUser')?.value

  if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) {
    return Response.redirect(new URL('/dashboard', request.url))
  }

  if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) {
    return Response.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

この例では、リクエストパイプラインの早い段階でリダイレクトを処理するために Response.redirect を使用しており、効率的でアクセス制御を一元化しています。

認証が成功した後、ユーザーのロールに基づいてナビゲーションを管理することが重要です。たとえば、管理者ユーザーは管理者ダッシュボードにリダイレクトされ、通常のユーザーは別のページに送られます。これは、ロール固有のエクスペリエンスや、必要に応じてユーザーにプロファイルの入力を促すような条件付きナビゲーションにとって重要です。

認可を設定する際には、アプリがデータにアクセスまたは変更する場所で主要なセキュリティチェックが行われるようにすることが重要です。ミドルウェアは初期検証に役立ちますが、データを保護する唯一の防衛線であってはなりません。セキュリティチェックの大部分はデータアクセス層 (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: 'ユーザーは認証されていません',
    })
    return
  }

  // ユーザーが'admin'ロールを持っているか確認
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: '不正なアクセス: ユーザーに管理者権限がありません',
    })
    return
  }

  // 認証済みユーザーのためのルート処理を続行
  // ... APIルートの実装
}
export default async function handler(req, res) {
  const session = await getSession(req)

  // ユーザーが認証されているか確認
  if (!session) {
    res.status(401).json({
      error: 'ユーザーは認証されていません',
    })
    return
  }

  // ユーザーが'admin'ロールを持っているか確認
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: '不正なアクセス: ユーザーに管理者権限がありません',
    })
    return
  }

  // 認証済みユーザーのためのルート処理を続行
  // ... APIルートの実装
}

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

ベストプラクティス

  • セキュアなセッション管理: 不正アクセスやデータ漏洩を防ぐため、セッションデータのセキュリティを優先します。暗号化と安全なストレージプラクティスを使用してください。
  • 動的なロール管理: 権限とロールの変更に柔軟に対応できるように、ユーザーロールの柔軟なシステムを使用し、ハードコードされたロールを避けます。
  • セキュリティファーストアプローチ: 認可ロジックのすべての側面でセキュリティを優先し、ユーザーデータを保護しアプリケーションの整合性を維持します。これには徹底的なテストと潜在的なセキュリティ脆弱性の考慮が含まれます。

セッション管理

セッション管理とは、ユーザーのアプリケーションとのやり取りを時間をかけて追跡・管理し、アプリケーションのさまざまな部分で認証状態が維持されるようにすることです。

これにより、繰り返しのログインが不要になり、セキュリティとユーザーの利便性の両方が向上します。セッション管理には主に2つの方法があります:クッキーベースのセッションとデータベースセッションです。

クッキーベースのセッション

🎥 視聴: Next.jsでのクッキーベースのセッションと認証について詳しく学ぶ → YouTube (11分).

クッキーベースのセッションは、暗号化されたセッション情報をブラウザのクッキーに直接保存することでユーザーデータを管理します。ユーザーログイン時に、この暗号化データがクッキーに保存されます。それ以降のサーバーリクエストにはこのクッキーが含まれ、繰り返しのサーバークエリが必要なくなり、クライアントサイドの効率が向上します。

ただし、この方法ではクッキーがクライアントサイドのセキュリティリスクの影響を受けやすいため、機密データを保護するために慎重な暗号化が必要です。クッキー内のセッションデータを暗号化することで、クッキーが盗まれても中身が読み取られないようにします。

また、個々のクッキーはサイズに制限があります(通常約4KB)が、クッキーチャンキングなどの技術を使用して、大きなセッションデータを複数のクッキーに分割することでこの制限を克服できます。

Next.jsプロジェクトでクッキーを設定する例は次のようになります:

サーバーでクッキーを設定:

import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'

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: 'クッキーの設定に成功しました!' })
}
import { serialize } from 'cookie'

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: 'クッキーの設定に成功しました!' })
}

データベースセッション

データベースセッション管理では、セッションデータをサーバー上に保存し、ユーザーのブラウザにはセッションIDのみを受け取ります。このIDはサーバーサイドに保存されているセッションデータを参照しますが、データ自体は含まれません。この方法は、機密性の高いセッションデータをクライアントサイド環境から遠ざけることでセキュリティを強化し、クライアントサイド攻撃による露出リスクを減らします。データベースセッションはまた、より大きなデータストレージニーズに対応できるため、スケーラビリティも向上します。

ただし、このアプローチにはトレードオフがあります。ユーザー操作ごとにデータベースルックアップが必要になるため、パフォーマンスオーバーヘッドが増加する可能性があります。セッションデータのキャッシュなどの戦略でこれを軽減できます。また、データベースに依存するということは、セッション管理がデータベースのパフォーマンスと可用性と同じくらい信頼性があることを意味します。

以下はNext.jsアプリケーションでデータベースセッションを実装する簡略化された例です:

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

import db from '../../lib/db'
import { 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: '内部サーバーエラー' })
  }
}
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: '内部サーバーエラー' })
  }
}

Next.jsにおけるセッション管理の選択

Next.jsでクッキーベースのセッションとデータベースセッションのどちらを選択するかは、アプリケーションの要件によって異なります。クッキーベースのセッションはシンプルでサーバー負荷が低く、小規模なアプリケーションに適していますが、セキュリティ面では劣る場合があります。データベースセッションはより複雑ですが、セキュリティとスケーラビリティに優れており、大規模でデータセンシティブなアプリケーションに理想的です。

NextAuth.jsなどの認証ソリューションを使用すると、クッキーまたはデータベースストレージを利用した効率的なセッション管理が可能になります。この自動化により開発プロセスは簡素化されますが、選択したソリューションが使用するセッション管理方法を理解することが重要です。アプリケーションのセキュリティとパフォーマンス要件に適合していることを確認してください。

どの方法を選択する場合でも、セッション管理戦略ではセキュリティを最優先にしてください。クッキーベースのセッションでは、セッションデータを保護するためにセキュアでHTTP-onlyのクッキーを使用することが重要です。データベースセッションでは、定期的なバックアップとセッションデータの安全な取り扱いが不可欠です。どちらのアプローチでも、不正アクセスを防ぎ、アプリケーションのパフォーマンスと信頼性を維持するために、セッションの有効期限とクリーンアップメカニズムを実装することが重要です。

以下はNext.jsと互換性のある認証ソリューションです。Next.jsアプリケーションでこれらを設定する方法については、以下のクイックスタートガイドを参照してください:

さらに学ぶ

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