Menu

Next.jsでの認証の実装方法

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

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

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

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

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

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

認証

サインアップおよび/またはログインフォームを実装するステップは以下の通りです。

  1. ユーザーはフォームを通じて認証情報を送信します。
  2. フォームはAPI ルートで処理されるリクエストを送信します。
  3. 検証が成功すると、プロセスが完了し、ユーザーの認証が成功したことが示されます。
  4. 検証が失敗した場合は、エラーメッセージが表示されます。

ユーザーが認証情報を入力できるログインフォームを考えてみてください。

pages/login.tsx
TypeScript
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="メールアドレス" required />
      <input type="password" name="password" placeholder="パスワード" required />
      <button type="submit">ログイン</button>
    </form>
  )
}

上記のフォームには、ユーザーのメールアドレスとパスワードをキャプチャするための2つの入力フィールドがあります。送信時に、API ルート(/api/auth/login)にPOSTリクエストを送信する関数をトリガーします。

その後、API ルートで認証プロバイダーのAPIを呼び出して認証を処理できます。

pages/api/auth/login.ts
TypeScript
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: '問題が発生しました。' })
    }
  }
}

セッション管理

セッション管理は、ユーザーの認証状態がリクエスト全体を通じて保持されることを保証します。セッションまたはトークンの作成、保存、更新、削除を伴います。

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

  1. ステートレス:セッションデータ(またはトークン)はブラウザーのCookieに保存されます。Cookieは各リクエストで送信され、セッションをサーバーで検証できるようにします。このメソッドはより単純ですが、正しく実装されていない場合、セキュリティが低くなる可能性があります。
  2. データベース:セッションデータはデータベースに保存され、ユーザーのブラウザーは暗号化されたセッションIDのみを受け取ります。このメソッドはより安全ですが、複雑になる可能性があり、より多くのサーバーリソースを使用できます。

補足: どちらの方法も使用できます、またはその両方を使用できますが、iron-sessionJoseなどのセッション管理ライブラリの使用をお勧めします。

ステートレスセッション

Cookieを設定および削除する

API Routesを使用してセッションをサーバー上のCookieとして設定できます。

pages/api/login.ts
TypeScript
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: 'Cookieの設定に成功しました。' })
}

データベースセッション

データベースセッションを作成して管理するには、以下のステップに従う必要があります。

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

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

pages/api/create-session.ts
TypeScript
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: '内部サーバーエラー' })
  }
}

認可

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

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

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

どちらの場合も、推奨事項は以下の通りです。

Proxyを使用した楽観的チェック(オプション)

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

  • 楽観的なチェックを実行する。Proxyはすべてのルートで実行されるため、リダイレクトロジックを一元化し、許可されていないユーザーを事前フィルタリングする良い方法です。
  • ユーザー間でデータを共有する静的ルート(ペイウォールの背後にあるコンテンツなど)を保護する。

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

例えば:

proxy.ts
TypeScript
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 proxy(req: NextRequest) {
  // 2. 現在のルートが保護されているか公開されているかをチェックする
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)
 
  // 3. Cookieからセッションを復号化する
  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()
}
 
// Proxyが実行されないルート
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Proxyは初期チェックに役立つ場合がありますが、データを保護するための唯一の防止線ではないはずです。セキュリティチェックの大多数は、データソースにできるだけ近い場所で実行する必要があります。詳細については、データアクセスレイヤーを参照してください。

ヒント

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

データアクセスレイヤー(DAL)を作成する

API Routesを保護する

Next.jsのAPI Routesはサーバー側ロジックとデータ管理を処理するために不可欠です。これらのルートを保護して、認可されたユーザーのみが特定の機能にアクセスできるようにすることが重要です。これは通常、ユーザーの認証状態とロールベースの権限を確認することを伴います。

API Routeを保護する例を示します。

pages/api/route.ts
TypeScript
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 Routeの実装
}

この例は、認証と認可の2段階のセキュリティチェックを持つAPI Routeを示しています。まずアクティブなセッションをチェックし、次にログインしたユーザーが「admin」であるかどうかを検証します。このアプローチは安全なアクセスを保証し、認可されたユーザーのみに制限し、リクエスト処理のための堅牢なセキュリティを維持します。

リソース

Next.jsで認証について学んだので、セキュアな認証とセッション管理を実装するのに役立つNext.js互換ライブラリとリソースは以下の通りです。

認証ライブラリ

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

さらに詳しく読む

認証とセキュリティについて学び続けるには、以下のリソースを確認してください。