はじめに/ガイド/認証

Next.js での認証実装方法

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

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

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

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

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

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

認証 (Authentication)

サインアップとログイン機能

ユーザーの認証情報を取得し、フォームフィールドを検証して認証プロバイダーのAPIやデータベースを呼び出すために、<form> 要素と React の Server Actions、そして useActionState を使用できます。

Server Actions は常にサーバー上で実行されるため、認証ロジックを処理する安全な環境を提供します。

以下はサインアップ/ログイン機能を実装する手順です:

1. ユーザー認証情報の取得

ユーザー認証情報を取得するには、送信時に Server Action を呼び出すフォームを作成します。例えば、ユーザーの名前、メールアドレス、パスワードを受け取るサインアップフォーム:

import { signup } from '@/app/actions/auth'

export function SignupForm() {
  return (
    <form action={signup}>
      <div>
        <label htmlFor="name">名前</label>
        <input id="name" name="name" placeholder="名前" />
      </div>
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input id="email" name="email" type="email" placeholder="メールアドレス" />
      </div>
      <div>
        <label htmlFor="password">パスワード</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">サインアップ</button>
    </form>
  )
}

2. サーバー上でのフォームフィールドの検証

Server Action を使用して、サーバー上でフォームフィールドを検証します。認証プロバイダーがフォーム検証を提供していない場合、ZodYup などのスキーマ検証ライブラリを使用できます。

Zod を例にすると、適切なエラーメッセージを含むフォームスキーマを定義できます:

import { z } from 'zod'

export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: '名前は2文字以上である必要があります。' })
    .trim(),
  email: z.string().email({ message: '有効なメールアドレスを入力してください。' }).trim(),
  password: z
    .string()
    .min(8, { message: '8文字以上である必要があります' })
    .regex(/[a-zA-Z]/, { message: '少なくとも1文字のアルファベットを含めてください。' })
    .regex(/[0-9]/, { message: '少なくとも1文字の数字を含めてください。' })
    .regex(/[^a-zA-Z0-9]/, {
      message: '少なくとも1文字の特殊文字を含めてください。',
    })
    .trim(),
})

export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined

認証プロバイダーのAPIやデータベースへの不要な呼び出しを防ぐため、フォームフィールドが定義されたスキーマに一致しない場合、Server Action で早期に return できます。

import { SignupFormSchema, FormState } from '@/app/lib/definitions'

export async function signup(state: FormState, formData: FormData) {
  // フォームフィールドの検証
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })

  // 無効なフォームフィールドがある場合は早期リターン
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // ユーザー作成のためにプロバイダーやDBを呼び出す...
}

<SignupForm /> に戻り、React の useActionState フックを使用して、フォーム送信中に検証エラーを表示できます:

'use client'

import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'

export default function SignupForm() {
  const [state, action, pending] = useActionState(signup, undefined)

  return (
    <form action={action}>
      <div>
        <label htmlFor="name">名前</label>
        <input id="name" name="name" placeholder="名前" />
      </div>
      {state?.errors?.name && <p>{state.errors.name}</p>}

      <div>
        <label htmlFor="email">メールアドレス</label>
        <input id="email" name="email" placeholder="メールアドレス" />
      </div>
      {state?.errors?.email && <p>{state.errors.email}</p>}

      <div>
        <label htmlFor="password">パスワード</label>
        <input id="password" name="password" type="password" />
      </div>
      {state?.errors?.password && (
        <div>
          <p>パスワードは以下の要件を満たす必要があります:</p>
          <ul>
            {state.errors.password.map((error) => (
              <li key={error}>- {error}</li>
            ))}
          </ul>
        </div>
      )}
      <button disabled={pending} type="submit">
        サインアップ
      </button>
    </form>
  )
}

知っておくと便利:

  • React 19 では、useFormStatus が返すオブジェクトに data、method、action などの追加キーが含まれます。React 19 を使用していない場合、pending キーのみが利用可能です。
  • データを変更する前に、ユーザーがその操作を実行する権限を持っていることを常に確認してください。認証と認可を参照してください。

3. ユーザー作成または認証情報の確認

フォームフィールドの検証後、認証プロバイダーのAPIやデータベースを呼び出して新しいユーザーアカウントを作成するか、既存ユーザーを確認できます。

前の例からの続き:

export async function signup(state: FormState, formData: FormData) {
  // 1. フォームフィールドの検証
  // ...

  // 2. データベース挿入用データの準備
  const { name, email, password } = validatedFields.data
  // 例:パスワードをハッシュ化して保存
  const hashedPassword = await bcrypt.hash(password, 10)

  // 3. ユーザーをデータベースに挿入または認証ライブラリのAPIを呼び出し
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })

  const user = data[0]

  if (!user) {
    return {
      message: 'アカウント作成中にエラーが発生しました',
    }
  }

  // TODO:
  // 4. ユーザーセッションの作成
  // 5. ユーザーのリダイレクト
}

ユーザーアカウントの作成または認証情報の確認に成功したら、ユーザーの認証状態を管理するセッションを作成できます。セッション管理戦略に応じて、セッションはクッキーやデータベース、または両方に保存されます。セッション管理セクションに進んで詳細を確認してください。

ヒント:

  • 上記の例は教育的な目的で認証手順を分解しているため冗長です。独自の安全なソリューションを実装すると複雑になりやすい点が強調されています。認証ライブラリを使用してプロセスを簡素化することを検討してください。
  • ユーザー体験を向上させるため、登録フローの早い段階で重複するメールアドレスやユーザー名を確認することをお勧めします。例えば、ユーザーがユーザー名を入力中や入力フィールドからフォーカスが外れた時点で確認できます。これにより不要なフォーム送信を防ぎ、即時のフィードバックを提供できます。use-debounceなどのライブラリを使用してこれらの確認の頻度を管理できます。

セッション管理

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

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

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

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

ステートレスセッション

ステートレスセッションを作成・管理するには、いくつかの手順が必要です:

  1. セッションの署名に使用する秘密鍵を生成し、環境変数として保存
  2. セッション管理ライブラリを使用してセッションデータの暗号化/復号化ロジックを記述
  3. Next.jsのcookies APIを使用してクッキーを管理

上記に加えて、ユーザーがアプリケーションに戻ったときにセッションを更新(またはリフレッシュ)する機能と、ユーザーがログアウトしたときにセッションを削除する機能を追加することを検討してください。

知っておくと良いこと: 認証ライブラリにセッション管理機能が含まれているか確認してください。

1. 秘密鍵の生成

セッションの署名に使用する秘密鍵を生成する方法はいくつかあります。例えば、ターミナルでopensslコマンドを使用できます:

terminal
openssl rand -base64 32

このコマンドは、環境変数ファイルに保存できる32文字のランダムな文字列を生成します:

.env
SESSION_SECRET=your_secret_key

この鍵はセッション管理ロジックで参照できます:

app/lib/session.js
const secretKey = process.env.SESSION_SECRET

2. セッションの暗号化と復号化

次に、選択したセッション管理ライブラリを使用してセッションを暗号化および復号化できます。前の例の続きとして、JoseEdge Runtimeと互換性あり)とReactのserver-onlyパッケージを使用して、セッション管理ロジックがサーバーでのみ実行されるようにします。

import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'

const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)

export async function encrypt(payload: SessionPayload) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('7d')
    .sign(encodedKey)
}

export async function decrypt(session: string | undefined = '') {
  try {
    const { payload } = await jwtVerify(session, encodedKey, {
      algorithms: ['HS256'],
    })
    return payload
  } catch (error) {
    console.log('セッションの検証に失敗しました')
  }
}

ヒント:

  • ペイロードには、ユーザーID、ロールなど、後続のリクエストで使用される最小限の一意のユーザーデータを含める必要があります。電話番号、メールアドレス、クレジットカード情報などの個人を特定できる情報や、パスワードなどの機密データを含めるべきではありません。

3. クッキーの設定(推奨オプション)

セッションをクッキーに保存するには、Next.jsのcookies APIを使用します。クッキーはサーバーで設定し、推奨オプションを含める必要があります:

  • HttpOnly: クライアントサイドJavaScriptがクッキーにアクセスできないようにします
  • Secure: クッキーを送信するためにhttpsを使用します
  • SameSite: クロスサイトリクエストでクッキーを送信できるかどうかを指定します
  • Max-AgeまたはExpires: 一定期間後にクッキーを削除します
  • Path: クッキーのURLパスを定義します

各オプションの詳細については、MDNを参照してください。

import 'server-only'
import { cookies } from 'next/headers'

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  const session = await encrypt({ userId, expiresAt })
  const cookieStore = await cookies()

  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

サーバーアクションでcreateSession()関数を呼び出し、redirect() APIを使用してユーザーを適切なページにリダイレクトできます:

import { createSession } from '@/app/lib/session'

export async function signup(state: FormState, formData: FormData) {
  // 前の手順:
  // 1. フォームフィールドの検証
  // 2. データベース挿入用データの準備
  // 3. ユーザーをデータベースに挿入またはライブラリAPIを呼び出し

  // 現在の手順:
  // 4. ユーザーセッションの作成
  await createSession(user.id)
  // 5. ユーザーのリダイレクト
  redirect('/profile')
}

ヒント:

  • クッキーはクライアントサイドでの改ざんを防ぐためにサーバーで設定する必要があります
  • 🎥 視聴: Next.jsでのステートレスセッションと認証についてさらに学ぶ → YouTube (11分)

セッションの更新(またはリフレッシュ)

セッションの有効期限を延長することもできます。これは、ユーザーがアプリケーションに再度アクセスした後もログイン状態を維持するのに役立ちます。例:

import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export async function updateSession() {
  const session = (await cookies()).get('session')?.value
  const payload = await decrypt(session)

  if (!session || !payload) {
    return null
  }

  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: 'lax',
    path: '/',
  })
}

ヒント: 認証ライブラリがリフレッシュトークンをサポートしているか確認してください。リフレッシュトークンを使用してユーザーのセッションを延長できます。

セッションの削除

セッションを削除するには、クッキーを削除します:

import 'server-only'
import { cookies } from 'next/headers'

export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore.delete('session')
}

その後、deleteSession() 関数をアプリケーション内で再利用できます。例えば、ログアウト時に:

import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'

export async function logout() {
  await deleteSession()
  redirect('/login')
}

データベースセッション

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

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

例:

import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'

export async function createSession(id: number) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  // 1. データベースにセッションを作成
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // セッションIDを返す
    .returning({ id: sessions.id })

  const sessionId = data[0].id

  // 2. セッションIDを暗号化
  const session = await encrypt({ sessionId, expiresAt })

  // 3. 楽観的な認証チェックのためにクッキーにセッションを保存
  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

ヒント:

  • 高速なアクセスのため、セッションの存続期間中にサーバーキャッシュを追加することを検討できます。また、セッションデータをプライマリデータベースに保持し、データリクエストを結合してクエリ数を減らすこともできます。
  • 高度なユースケース(ユーザーの最終ログイン時間の追跡、アクティブなデバイス数の管理、全デバイスからのログアウト機能の提供など)には、データベースセッションを使用することを選択できます。

セッション管理を実装した後、アプリケーション内でユーザーがアクセス・実行できる内容を制御する認可ロジックを追加する必要があります。詳細については認可セクションに進んでください。

認可

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

認可チェックには主に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)の作成

データリクエストと認可ロジックを一元化するためにDALを作成することを推奨します。

DALには、ユーザーがアプリケーションとやり取りする際にユーザーのセッションを検証する関数を含める必要があります。少なくとも、この関数はセッションが有効かどうかを確認し、リダイレクトまたはさらなるリクエストに必要なユーザー情報を返す必要があります。

例えば、DAL用に別のファイルを作成し、verifySession() 関数を含めます。次に、Reactのcache APIを使用して、Reactのレンダーパス中に関数の戻り値をメモ化します:

import 'server-only'

import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  if (!session?.userId) {
    redirect('/login')
  }

  return { isAuth: true, userId: session.userId }
})

その後、データリクエスト、サーバーアクション、ルートハンドラーで verifySession() 関数を呼び出すことができます:

export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null

  try {
    const data = await db.query.users.findMany({
      where: eq(users.id, session.userId),
      // ユーザーオブジェクト全体ではなく、必要な列のみを明示的に返す
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })

    const user = data[0]

    return user
  } catch (error) {
    console.log('Failed to fetch user')
    return null
  }
})

ヒント:

  • DALは、リクエスト時にフェッチされるデータを保護するために使用できます。ただし、ユーザー間でデータを共有する静的ルートの場合、データはビルド時にフェッチされ、リクエスト時にはフェッチされません。静的ルートを保護するにはミドルウェアを使用してください。
  • 安全なチェックの場合、セッションIDをデータベースと比較してセッションが有効かどうかを確認できます。Reactのcache関数を使用して、レンダーパス中のデータベースへの不要な重複リクエストを回避します。
  • 関連するデータリクエストを、任意のメソッドの前に verifySession() を実行するJavaScriptクラスに統合することもできます。

データ転送オブジェクト (DTO) の使用

データを取得する際は、アプリケーションで使用する必要なデータのみを返し、オブジェクト全体を返さないことを推奨します。例えば、ユーザーデータを取得する場合、パスワードや電話番号などが含まれるユーザーオブジェクト全体ではなく、ユーザーIDと名前のみを返すことが考えられます。

ただし、返されるデータ構造を制御できない場合や、クライアントにオブジェクト全体が渡されるのを防ぎたいチームで作業している場合は、クライアントに公開しても安全なフィールドを指定するなどの戦略を使用できます。

import 'server-only'
import { getUser } from '@/app/lib/dal'

function canSeeUsername(viewer: User) {
  return true
}

function canSeePhoneNumber(viewer: User, team: string) {
  return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug: string) {
  const data = await db.query.users.findMany({
    where: eq(users.slug, slug),
    // ここで特定のカラムを返す
  })
  const user = data[0]

  const currentUser = await getUser(user.id)

  // またはクエリに固有のデータのみを返す
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}

データアクセス層 (DAL) でデータリクエストと認可ロジックを集中化し、DTOを使用することで、すべてのデータリクエストが安全で一貫性を持つようになり、アプリケーションのスケールに伴うメンテナンス、監査、デバッグが容易になります。

知っておくと良いこと:

  • DTOを定義する方法はいくつかあります。toJSON() を使用する方法、上記の例のような個別の関数、JSクラスなどです。これらはReactやNext.jsの機能ではなくJavaScriptのパターンなので、アプリケーションに最適なパターンを見つけるために調査を行うことを推奨します。
  • セキュリティのベストプラクティスについては Next.jsのセキュリティ記事 で詳しく学べます。

サーバーコンポーネント

サーバーコンポーネント での認証チェックは、ロールベースのアクセス制御に有用です。例えば、ユーザーのロールに基づいてコンポーネントを条件付きでレンダリングする場合:

import { verifySession } from '@/app/lib/dal'

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

  if (userRole === 'admin') {
    return <AdminDashboard />
  } else if (userRole === 'user') {
    return <UserDashboard />
  } else {
    redirect('/login')
  }
}

この例では、DALから verifySession() 関数を使用して 'admin'、'user'、および未認証のロールをチェックしています。このパターンにより、各ユーザーは自身のロールに適したコンポーネントのみと対話します。

レイアウトと認証チェック

部分レンダリング のため、レイアウト でチェックを行う際は注意が必要です。これらはナビゲーション時に再レンダリングされないため、ルート変更ごとにユーザーセッションがチェックされません。

代わりに、データソースに近い場所や条件付きでレンダリングされるコンポーネントでチェックを行うべきです。

例えば、ユーザーデータを取得しナビにユーザー画像を表示する共有レイアウトを考えます。レイアウトで認証チェックを行うのではなく、レイアウトでユーザーデータ (getUser()) を取得し、DALで認証チェックを行うべきです。

これにより、アプリケーション内で getUser() が呼び出される場所では常に認証チェックが行われ、開発者がデータへのアクセス権限をチェックするのを忘れるのを防ぎます。

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();

  return (
    // ...
  )
}

知っておくと良いこと:

  • SPAでは、ユーザーが認証されていない場合にレイアウトやトップレベルのコンポーネントで return null とするパターンが一般的です。このパターンは 推奨されません。Next.jsアプリケーションには複数のエントリーポイントがあり、ネストされたルートセグメントやサーバーアクションへのアクセスを防げないためです。

サーバーアクション

サーバーアクション は、公開APIエンドポイントと同じセキュリティ考慮事項で扱い、ユーザーが変更を実行できるかどうかを確認してください。

以下の例では、アクションを進める前にユーザーのロールをチェックしています:

'use server'
import { verifySession } from '@/app/lib/dal'

export async function serverAction(formData: FormData) {
  const session = await verifySession()
  const userRole = session?.user?.role

  // ユーザーがアクションを実行する権限がない場合は早期リターン
  if (userRole !== 'admin') {
    return null
  }

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

ルートハンドラー

ルートハンドラー は、公開APIエンドポイントと同じセキュリティ考慮事項で扱い、ユーザーがルートハンドラーにアクセスできるかどうかを確認してください。

例:

import { verifySession } from '@/app/lib/dal'

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

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

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

  // 認可されたユーザーのために続行
}

上記の例は、2段階のセキュリティチェックを持つルートハンドラーを示しています。最初にアクティブなセッションをチェックし、次にログインしているユーザーが 'admin' かどうかを確認します。

コンテキストプロバイダー

認証にコンテキストプロバイダーを使用するのは、インターリービング により機能します。ただし、Reactの context はサーバーコンポーネントではサポートされていないため、クライアントコンポーネントにのみ適用可能です。

これは機能しますが、子のサーバーコンポーネントは最初にサーバーでレンダリングされ、コンテキストプロバイダーのセッションデータにはアクセスできません:

import { ContextProvider } from 'auth-lib'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <ContextProvider>{children}</ContextProvider>
      </body>
    </html>
  )
}
'use client';

import { useSession } from "auth-lib";

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}
'use client';

import { useSession } from "auth-lib";

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}

クライアントコンポーネントでセッションデータが必要な場合(例えばクライアントサイドのデータフェッチング)、Reactの taintUniqueValue APIを使用して、機密性の高いセッションデータがクライアントに公開されないようにします。

リソース

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

認証ライブラリ

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

さらに学ぶ

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