認証

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. フォームがサーバーアクションを呼び出します。
  3. 検証が成功すると、プロセスが完了し、ユーザーの認証成功を示します。
  4. 検証が失敗した場合、エラーメッセージが表示されます。

ユーザーが認証情報を入力できるログインフォームを考えてみましょう:

import { authenticate } from '@/app/lib/actions'

export default function Page() {
  return (
    <form action={authenticate}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">ログイン</button>
    </form>
  )
}
import { authenticate } from '@/app/lib/actions'

export default function Page() {
  return (
    <form action={authenticate}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">ログイン</button>
    </form>
  )
}

上記のフォームには、ユーザーのメールアドレスとパスワードを取得するための2つの入力フィールドがあります。送信時には、authenticate サーバーアクションが呼び出されます。

次に、サーバーアクションで認証プロバイダーのAPIを呼び出して認証を処理できます:

'use server'

import { signIn } from '@/auth'

export async function authenticate(_currentState: unknown, formData: FormData) {
  try {
    await signIn('credentials', formData)
  } catch (error) {
    if (error) {
      switch (error.type) {
        case 'CredentialsSignin':
          return '無効な認証情報です。'
        default:
          return '問題が発生しました。'
      }
    }
    throw error
  }
}
'use server'

import { signIn } from '@/auth'

export async function authenticate(_currentState, formData) {
  try {
    await signIn('credentials', formData)
  } catch (error) {
    if (error) {
      switch (error.type) {
        case 'CredentialsSignin':
          return '無効な認証情報です。'
        default:
          return '問題が発生しました。'
      }
    }
    throw error
  }
}

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

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

最後に、login-form.tsx コンポーネントで、React の useFormState を使用してサーバーアクションを呼び出し、フォームエラーを処理できます。また、useFormStatus を使用してフォームの保留状態を処理できます:

'use client'

import { authenticate } from '@/app/lib/actions'
import { useFormState, useFormStatus } from 'react-dom'

export default function Page() {
  const [errorMessage, dispatch] = useFormState(authenticate, undefined)

  return (
    <form action={dispatch}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <div>{errorMessage && <p>{errorMessage}</p>}</div>
      <LoginButton />
    </form>
  )
}

function LoginButton() {
  const { pending } = useFormStatus()

  const handleClick = (event) => {
    if (pending) {
      event.preventDefault()
    }
  }

  return (
    <button aria-disabled={pending} type="submit" onClick={handleClick}>
      ログイン
    </button>
  )
}
'use client'

import { authenticate } from '@/app/lib/actions'
import { useFormState, useFormStatus } from 'react-dom'

export default function Page() {
  const [errorMessage, dispatch] = useFormState(authenticate, undefined)

  return (
    <form action={dispatch}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <div>{errorMessage && <p>{errorMessage}</p>}</div>
      <LoginButton />
    </form>
  )
}

function LoginButton() {
  const { pending } = useFormStatus()

  const handleClick = (event) => {
    if (pending) {
      event.preventDefault()
    }
  }

  return (
    <button aria-disabled={pending} type="submit" onClick={handleClick}>
      ログイン
    </button>
  )
}

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 を使用しており、効率的でアクセス制御を一元化しています。

特定のリダイレクトニーズに対しては、redirect 関数をサーバーコンポーネント、ルートハンドラー、サーバーアクションで使用して、より細かい制御を提供できます。これは、ロールベースのナビゲーションやコンテキストに応じたシナリオに役立ちます。

import { redirect } from 'next/navigation'

export default function Page() {
  // リダイレクトが必要かどうかを判断するロジック
  const accessDenied = true
  if (accessDenied) {
    redirect('/login')
  }

  // 他のルートとロジックを定義
}
import { redirect } from 'next/navigation'

export default function Page() {
  // リダイレクトが必要かどうかを判断するロジック
  const accessDenied = true
  if (accessDenied) {
    redirect('/login')
  }

  // 他のルートとロジックを定義
}

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

認可を設定する際には、アプリがデータにアクセスまたは変更する場所で主要なセキュリティチェックが行われるようにすることが重要です。ミドルウェアは初期検証に役立ちますが、データを保護する唯一の防衛線であってはなりません。セキュリティチェックの大部分はデータアクセス層 (DAL) で実行する必要があります。

このアプローチは、このセキュリティブログで強調されているように、すべてのデータアクセスを専用のDAL(データアクセス層)に統合することを推奨しています。この戦略により、一貫したデータアクセスが確保され、認可バグが最小限に抑えられ、メンテナンスが簡素化されます。包括的なセキュリティを確保するためには、以下の主要な領域を考慮してください:

  • サーバーアクション: 特に機密性の高い操作に対して、サーバーサイドプロセスにセキュリティチェックを実装
  • ルートハンドラー: 認可されたユーザーのみがアクセスできるように、セキュリティ対策で受信リクエストを管理
  • データアクセス層 (DAL): データベースと直接対話し、データトランザクションの検証と認可に不可欠。データが最も重要な相互作用ポイント(アクセスまたは変更)で保護されるように、DAL内で重要なチェックを実行することが重要

DALの保護に関する詳細なガイド、コードスニペットの例、高度なセキュリティプラクティスについては、セキュリティガイドのデータアクセス層セクションを参照してください。

サーバーアクションの保護

サーバーアクションは、公開APIエンドポイントと同様のセキュリティ考慮事項で扱うことが重要です。各アクションに対するユーザー認可の検証が不可欠です。管理者ユーザーのみに特定のアクションを制限するなど、サーバーアクション内でユーザー権限を確認するチェックを実装してください。

以下の例では、アクションを続行する前にユーザーのロールを確認しています:

'use server'

// ...

export async function serverAction() {
  const session = await getSession()
  const userRole = session?.user?.role

  // ユーザーがアクションを実行する権限があるか確認
  if (userRole !== 'admin') {
    throw new Error('不正なアクセス: ユーザーに管理者権限がありません')
  }

  // 認可されたユーザーのためのアクションを続行
  // ... アクションの実装
}
'use server'

// ...

export async function serverAction() {
  const session = await getSession()
  const userRole = session?.user?.role

  // ユーザーがアクションを実行する権限があるか確認
  if (userRole !== 'admin') {
    throw new Error('不正なアクセス: ユーザーに管理者権限がありません')
  }

  // 認可されたユーザーのためのアクションを続行
  // ... アクションの実装
}

ルートハンドラーの保護

Next.jsのルートハンドラーは、受信リクエストを管理する上で重要な役割を果たします。サーバーアクションと同様に、特定の機能にアクセスできるのが認可されたユーザーのみであることを保証するために保護する必要があります。これには通常、ユーザーの認証状態と権限の確認が含まれます。

以下はルートハンドラーを保護する例です:

export async function GET() {
  // ユーザー認証とロール確認
  const session = await getSession()

  // ユーザーが認証されているか確認
  if (!session) {
    return new Response(null, { status: 401 }) // ユーザーが認証されていません
  }

  // ユーザーが'admin'ロールを持っているか確認
  if (session.user.role !== 'admin') {
    return new Response(null, { status: 403 }) // ユーザーは認証済みですが適切な権限がありません
  }

  // 認可されたユーザーのためのデータ取得
}
export async function GET() {
  // ユーザー認証とロール確認
  const session = await getSession()

  // ユーザーが認証されているか確認
  if (!session) {
    return new Response(null, { status: 401 }) // ユーザーが認証されていません
  }

  // ユーザーが'admin'ロールを持っているか確認
  if (session.user.role !== 'admin') {
    return new Response(null, { status: 403 }) // ユーザーは認証済みですが適切な権限がありません
  }

  // 認可されたユーザーのためのデータ取得
}

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

サーバーコンポーネントを使用した認可

Next.jsのサーバーコンポーネントは、サーバーサイド実行用に設計されており、認可のような複雑なロジックを統合するための安全な環境を提供します。これらはバックエンドリソースへの直接アクセスを可能にし、データ集約型タスクのパフォーマンスを最適化し、機密操作のセキュリティを強化します。

サーバーコンポーネントでは、ユーザーのロールに基づいてUI要素を条件付きでレンダリングするのが一般的なプラクティスです。このアプローチは、ユーザーが認可されたコンテンツのみにアクセスできるようにすることで、ユーザーエクスペリエンスとセキュリティを向上させます。

例:

export default async function Dashboard() {
  const session = await getSession()
  const userRole = session?.user?.role // セッションオブジェクトに'role'が含まれていると仮定

  if (userRole === 'admin') {
    return <AdminDashboard /> // 管理者ユーザー向けコンポーネント
  } else if (userRole === 'user') {
    return <UserDashboard /> // 一般ユーザー向けコンポーネント
  } else {
    return <AccessDenied /> // 不正アクセス時に表示するコンポーネント
  }
}
export default function Dashboard() {
  const session = await getSession()
  const userRole = session?.user?.role // セッションオブジェクトに'role'が含まれていると仮定

  if (userRole === 'admin') {
    return <AdminDashboard /> // 管理者ユーザー向けコンポーネント
  } else if (userRole === 'user') {
    return <UserDashboard /> // 一般ユーザー向けコンポーネント
  } else {
    return <AccessDenied /> // 不正アクセス時に表示するコンポーネント
  }
}

この例では、Dashboardコンポーネントが'admin'、'user'、および不正アクセスロールに対して異なるUIをレンダリングします。このパターンにより、各ユーザーが自分のロールに適したコンポーネントのみと対話するようになり、セキュリティとユーザーエクスペリエンスの両方が向上します。

ベストプラクティス

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

セッション管理

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

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

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

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

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

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

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

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

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

'use server'

import { cookies } from 'next/headers'

export async function handleLogin(sessionData) {
  const encryptedSessionData = encrypt(sessionData) // セッションデータを暗号化
  cookies().set('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 1週間
    path: '/',
  })
  // クッキー設定後のリダイレクトまたはレスポンス処理
}
'use server'

import { cookies } from 'next/headers'

export async function handleLogin(sessionData) {
  const encryptedSessionData = encrypt(sessionData) // セッションデータを暗号化
  cookies().set('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 1週間
    path: '/',
  })
  // クッキー設定後のリダイレクトまたはレスポンス処理
}

サーバーコンポーネントでクッキーに保存されたセッションデータにアクセス:

import { cookies } from 'next/headers'

export async function getSessionData(req) {
  const encryptedSessionData = cookies().get('session')?.value
  return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null
}
import { cookies } from 'next/headers'

export async function getSessionData(req) {
  const encryptedSessionData = cookies().get('session')?.value
  return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null
}

データベースセッション

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

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

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

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

import db from './lib/db'

export async function createSession(user) {
  const sessionId = generateSessionId() // 一意のセッションIDを生成
  await db.insertSession({ sessionId, userId: user.id, createdAt: new Date() })
  return sessionId
}

ミドルウェアまたはサーバーサイドロジックでセッションを取得:

import { cookies } from 'next/headers'
import db from './lib/db'

export async function getSession() {
  const sessionId = cookies().get('sessionId')?.value
  return sessionId ? await db.findSession(sessionId) : null
}

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

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

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

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

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

さらに学ぶ

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