コンテンツにスキップ

Next.js で認証を実装する方法

認証を理解することは、アプリケーションのデータを保護するために不可欠です。このページでは、認証を実装するために使用する React および Next.js の機能について説明します。

開始する前に、プロセスを 3 つの概念に分解すると役立ちます

  1. 認証: ユーザーが本人であることを確認します。ユーザーは、ユーザー名とパスワードなどの何かで本人であることを証明する必要があります。
  2. セッション管理: リクエスト間でユーザーの認証状態を追跡します。
  3. 認可: ユーザーがアクセスできるルートとデータを決定します。

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

Diagram showing the authentication flow with React and Next.js features

このページにある例では、教育目的で基本的なユーザー名とパスワード認証を扱っています。カスタム認証ソリューションを実装することもできますが、セキュリティとシンプルさを向上させるために、認証ライブラリの使用をお勧めします。これらは、認証、セッション管理、認可の組み込みソリューション、およびソーシャルログイン、多要素認証、ロールベースのアクセス制御などの追加機能を提供します。リストは 認証ライブラリ セクションで見つけることができます。

認証

サインアップおよびログイン機能

サインアップまたはログイン機能には、<form> 要素と React の Server Actions および useActionState を使用して、ユーザー資格情報をキャプチャし、フォームフィールドを検証し、認証プロバイダーの API またはデータベースを呼び出すことができます。

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

サインアップ/ログイン機能を実装する手順は次のとおりです

1. ユーザー資格情報をキャプチャする

ユーザー資格情報をキャプチャするには、送信時に Server Action を呼び出すフォームを作成します。たとえば、ユーザーの名前、メールアドレス、パスワードを受け入れるサインアップフォーム

app/ui/signup-form.tsx
import { signup } from '@/app/actions/auth'
 
export function SignupForm() {
  return (
    <form action={signup}>
      <div>
        <label htmlFor="name">Name</label>
        <input id="name" name="name" placeholder="Name" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" placeholder="Email" />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">Sign Up</button>
    </form>
  )
}
app/actions/auth.ts
export async function signup(formData: FormData) {}

2. サーバーでフォームフィールドを検証する

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

例として Zod を使用すると、適切なエラーメッセージとともにフォームスキーマを定義できます

app/lib/definitions.ts
import * as z from 'zod'
 
export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { error: 'Name must be at least 2 characters long.' })
    .trim(),
  email: z.email({ error: 'Please enter a valid email.' }).trim(),
  password: z
    .string()
    .min(8, { error: 'Be at least 8 characters long' })
    .regex(/[a-zA-Z]/, { error: 'Contain at least one letter.' })
    .regex(/[0-9]/, { error: 'Contain at least one number.' })
    .regex(/[^a-zA-Z0-9]/, {
      error: 'Contain at least one special character.',
    })
    .trim(),
})
 
export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined

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

app/actions/auth.ts
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
 
export async function signup(state: FormState, formData: FormData) {
  // Validate form fields
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })
 
  // If any form fields are invalid, return early
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Call the provider or db to create a user...
}

<SignupForm /> に戻り、フォームが送信されている間に検証エラーを表示するために、React の useActionState フックを使用できます。

app/ui/signup-form.tsx
'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">Name</label>
        <input id="name" name="name" placeholder="Name" />
      </div>
      {state?.errors?.name && <p>{state.errors.name}</p>}
 
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" placeholder="Email" />
      </div>
      {state?.errors?.email && <p>{state.errors.email}</p>}
 
      <div>
        <label htmlFor="password">Password</label>
        <input id="password" name="password" type="password" />
      </div>
      {state?.errors?.password && (
        <div>
          <p>Password must:</p>
          <ul>
            {state.errors.password.map((error) => (
              <li key={error}>- {error}</li>
            ))}
          </ul>
        </div>
      )}
      <button disabled={pending} type="submit">
        Sign Up
      </button>
    </form>
  )
}

知っておくと良いこと

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

3. ユーザーを作成するか、ユーザー資格情報を確認する

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

前の例から続けます

app/actions/auth.tsx
export async function signup(state: FormState, formData: FormData) {
  // 1. Validate form fields
  // ...
 
  // 2. Prepare data for insertion into database
  const { name, email, password } = validatedFields.data
  // e.g. Hash the user's password before storing it
  const hashedPassword = await bcrypt.hash(password, 10)
 
  // 3. Insert the user into the database or call an Auth Library's API
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })
 
  const user = data[0]
 
  if (!user) {
    return {
      message: 'An error occurred while creating your account.',
    }
  }
 
  // TODO:
  // 4. Create user session
  // 5. Redirect user
}

ユーザーアカウントの作成またはユーザー資格情報の検証に成功した後、セッションを作成してユーザーの認証状態を管理できます。セッション管理戦略に応じて、セッションは Cookie またはデータベース、あるいはその両方に格納できます。詳細については、セッション管理セクションに進んでください。

ヒント

  • 上記の例は、教育目的で認証ステップを分解しているため冗長です。これにより、独自の安全なソリューションを実装することがすぐに複雑になる可能性があることがわかります。プロセスを簡素化するために、認証ライブラリの使用を検討してください。
  • ユーザーエクスペリエンスを向上させるために、登録フローの早い段階で重複するメールアドレスやユーザー名をチェックすることを検討してください。たとえば、ユーザーがユーザー名を入力したときや、入力フィールドがフォーカスを失ったときなどです。これにより、不要なフォーム送信を防ぎ、ユーザーに即座にフィードバックを提供できます。use-debounce のようなライブラリを使用してリクエストをデバウンスすることで、これらのチェックの頻度を管理できます。

セッション管理

セッション管理は、ユーザーの認証状態がリクエスト間で維持されることを保証します。これには、セッションまたはトークンの作成、保存、リフレッシュ、削除が含まれます。

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

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

補足: いずれかの方法、または両方を使用できますが、iron-session または Jose のようなセッション管理ライブラリの使用をお勧めします。

ステートレスセッション

ステートレスセッションを作成および管理するには、いくつかの手順を実行する必要があります

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

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

補足: 認証ライブラリにセッション管理が含まれているか確認してください。

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 パッケージを使用して、セッション管理ロジックがサーバーでのみ実行されるようにします。

app/lib/session.ts
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('Failed to verify session')
  }
}

ヒント:

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

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

  • HttpOnly: クライアントサイド JavaScript から Cookie へのアクセスを防ぎます。
  • Secure: https を使用して Cookie を送信します。
  • SameSite: Cookie がクロスサイトリクエストで送信できるかどうかを指定します。
  • Max-Age または Expires: 一定期間後に Cookie を削除します。
  • Path: Cookie の URL パスを定義します。

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

app/lib/session.ts
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: '/',
  })
}

Server Action に戻り、createSession() 関数を呼び出し、redirect() API を使用してユーザーを適切なページにリダイレクトできます。

app/actions/auth.ts
import { createSession } from '@/app/lib/session'
 
export async function signup(state: FormState, formData: FormData) {
  // Previous steps:
  // 1. Validate form fields
  // 2. Prepare data for insertion into database
  // 3. Insert the user into the database or call an Library API
 
  // Current steps:
  // 4. Create user session
  await createSession(user.id)
  // 5. Redirect user
  redirect('/profile')
}

ヒント:

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

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

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

app/lib/session.ts
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: '/',
  })
}

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

セッションの削除

セッションを削除するには、Cookie を削除できます

app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'
 
export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore.delete('session')
}

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

app/actions/auth.ts
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
 
export async function logout() {
  await deleteSession()
  redirect('/login')
}

データベースセッション

データベースセッションを作成および管理するには、次の手順に従う必要があります

  1. データベースにセッションとデータを保存するためのテーブルを作成します(または Auth Library がこれを処理するかどうかを確認します)。
  2. セッションの挿入、更新、削除の機能​​を実装します
  3. ユーザーのブラウザに格納する前にセッション ID を暗号化し、データベースと Cookie を同期させます(これはオプションですが、Proxy での楽観的認証チェックに推奨されます)。

例:

app/lib/session.ts
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. Create a session in the database
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Return the session ID
    .returning({ id: sessions.id })
 
  const sessionId = data[0].id
 
  // 2. Encrypt the session ID
  const session = await encrypt({ sessionId, expiresAt })
 
  // 3. Store the session in cookies for optimistic auth checks
  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

ヒント:

  • より高速なアクセスを実現するために、セッションの有効期間中にサーバーキャッシュを追加することを検討してください。また、プライマリデータベースにセッションデータを保持し、クエリの数を減らすためにデータリクエストを統合することもできます。
  • ログオンした最後の時刻、アクティブなデバイス数などを追跡するため、またはユーザーにすべてのデバイスからログアウトする機能を提供するためなど、より高度なユースケースではデータベースセッションの使用を選択する場合があります。

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

認可

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

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

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

どちらの場合も、次のことをお勧めします

プロキシによる楽観的チェック(オプション)

権限に基づいてユーザーをリダイレクトするために、Proxy を使用したい場合があります

  • 楽観的チェックを実行するため。Proxy はすべてのルートで実行されるため、リダイレクトロジックを集中化し、権限のないユーザーを事前フィルタリングするのに適しています。
  • (例:ペイウォールの裏にあるコンテンツのような)ユーザー間でデータを共有する静的ルートを保護するため。

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

例:

proxy.ts
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
 
// 1. Specify protected and public routes
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
 
export default async function proxy(req: NextRequest) {
  // 2. Check if the current route is protected or public
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)
 
  // 3. Decrypt the session from the cookie
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)
 
  // 4. Redirect to /login if the user is not authenticated
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 5. Redirect to /dashboard if the user is authenticated
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// Routes Proxy should not run on
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Proxy は初期チェックには役立ちますが、データを保護するための唯一の防御線であるべきではありません。ほとんどのセキュリティチェックは、データソースのできるだけ近くで実行されるべきです。詳細については、データアクセスレイヤーを参照してください。

ヒント:

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

データアクセスレイヤー(DAL)の作成

データリクエストと認可ロジックを集中化するために、DAL の作成をお勧めします。

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

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

app/lib/dal.ts
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 }
})

その後、データリクエスト、Server Actions、Route Handlers で verifySession() 関数を呼び出すことができます。

app/lib/dal.ts
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),
      // Explicitly return the columns you need rather than the whole user object
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })
 
    const user = data[0]
 
    return user
  } catch (error) {
    console.log('Failed to fetch user')
    return null
  }
})

ヒント:

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

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

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

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

app/lib/dto.ts
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),
    // Return specific columns here
  })
  const user = data[0]
 
  const currentUser = await getUser(user.id)
 
  // Or return only what's specific to the query here
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}

DAL でデータリクエストと認可ロジックを集中化し、DTO を使用することで、すべてのデータリクエストが安全で一貫性があることを保証し、アプリケーションがスケーリングするにつれて保守、監査、デバッグが容易になります。

知っておくと良いこと:

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

サーバーコンポーネント

ロールベースのアクセスに便利な Server Components での認証チェック。たとえば、ユーザーのロールに基づいてコンポーネントを条件付きでレンダリングするため

app/dashboard/page.tsx
import { verifySession } from '@/app/lib/dal'
 
export default async function Dashboard() {
  const session = await verifySession()
  const userRole = session?.user?.role // Assuming 'role' is part of the session object
 
  if (userRole === 'admin') {
    return <AdminDashboard />
  } else if (userRole === 'user') {
    return <UserDashboard />
  } else {
    redirect('/login')
  }
}

この例では、DAL の verifySession() 関数を使用して 'admin'、'user'、および権限のないロールを確認します。このパターンにより、各ユーザーは自分のロールに適したコンポーネントのみとやり取りすることが保証されます。

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

部分レンダリングのため、レイアウトでのチェックには注意してください。これらはナビゲーション時に再レンダリングされないため、ユーザーセッションがすべてのルート変更時にチェックされるわけではありません。

代わりに、データソースまたは条件付きでレンダリングされるコンポーネントの近くでチェックを実行する必要があります。

たとえば、ユーザーデータを取得し、ナビゲーションにユーザー画像を表示する共有レイアウトを検討してください。レイアウトで認証チェックを行う代わりに、レイアウトでユーザーデータ(getUser())を取得し、DAL で認証チェックを行う必要があります。

これにより、アプリケーション内で getUser() がどこで呼び出されても認証チェックが実行され、開発者がユーザーがデータにアクセスする権限があるかを確認し忘れるのを防ぐことができます。

app/layout.tsx
export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();
 
  return (
    // ...
  )
}
app/lib/dal.ts
export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null
 
  // Get user ID from session and fetch data
})

知っておくと良いこと

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

サーバーアクション

Server Actions を、公開 API エンドポイントと同様のセキュリティ上の考慮事項で扱い、ユーザーがミューテーションを実行する権限があるか確認してください。

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

app/lib/actions.ts
'use server'
import { verifySession } from '@/app/lib/dal'
 
export async function serverAction(formData: FormData) {
  const session = await verifySession()
  const userRole = session?.user?.role
 
  // Return early if user is not authorized to perform the action
  if (userRole !== 'admin') {
    return null
  }
 
  // Proceed with the action for authorized users
}

ルートハンドラ

Route Handlers を、公開 API エンドポイントと同様のセキュリティ上の考慮事項で扱い、ユーザーが Route Handler にアクセスする権限があるか確認してください。

例:

app/api/route.ts
import { verifySession } from '@/app/lib/dal'
 
export async function GET() {
  // User authentication and role verification
  const session = await verifySession()
 
  // Check if the user is authenticated
  if (!session) {
    // User is not authenticated
    return new Response(null, { status: 401 })
  }
 
  // Check if the user has the 'admin' role
  if (session.user.role !== 'admin') {
    // User is authenticated but does not have the right permissions
    return new Response(null, { status: 403 })
  }
 
  // Continue for authorized users
}

上記の例は、2 段階のセキュリティチェックを行う Route Handler を示しています。まずアクティブなセッションを確認し、次にログインしているユーザーが 'admin' であるかを確認します。

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

コンテキストプロバイダーを認証に使用することは、インターリービングにより機能しますが、React の context は Server Components ではサポートされていないため、Client Components にのみ適用できます。

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

app/layout.ts
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 (
    // ...
  );
}

セッションデータが Client Components で必要とされる場合(例:クライアントサイドデータ取得のため)、React の taintUniqueValue API を使用して、機密性の高いセッションデータがクライアントに公開されるのを防ぎます。

リソース

Next.js での認証について学習したところで、安全な認証とセッション管理の実装に役立つ Next.js 互換のライブラリとリソースを次に示します。

認証ライブラリ

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

さらに読む

認証とセキュリティについてさらに学習するために、次のリソースを確認してください。