Menu

認証

アプリケーションのデータを保護するために、認証の理解は非常に重要です。このページでは、認証を実装するために使用する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
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: 'エラーが発生しました。' })
    }
  }
}
pages/api/auth/login.js
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: 'エラーが発生しました。' })
    }
  }
}

セッション管理

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

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

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

補足: どちらのメソッドも使用できますし、両方を組み合わせることもできますが、iron-sessionJoseなどのセッション管理ライブラリの使用を推奨します。

ステートレスセッション

クッキーの設定と削除

APIルートを使用して、サーバーでセッションをクッキーとして設定できます:

pages/api/login.ts
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: 'クッキーの設定に成功しました!' })
}
pages/api/login.js
import { serialize } from 'cookie'
import { encrypt } from '@/app/lib/session'
 
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: 'クッキーの設定に成功しました!' })
}

データベースセッション

データベースセッションを作成および管理するには、以下のステップを実行する必要があります:

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

サーバーでのセッション作成

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

認可

ユーザーが認証され、セッションが作成されると、ユーザーがアプリケーション内で何にアクセスし、何ができるかを制御する認可を実装できます。

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

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

どちらの場合も、以下を推奨します:

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

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

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

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

例えば:

middleware.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 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()
}
 
// Middlewareを実行しないルート
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
middleware.js
import { 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) {
  // 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)
 
  // 5. ユーザーが認証されていない場合は/loginにリダイレクト
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 6. ユーザーが認証済みの場合は/dashboardにリダイレクト
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// Middlewareを実行しないルート
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Middlewareは初期チェックに役立ちますが、データを保護する唯一の防衛線であってはなりません。セキュリティチェックの大部分は、データソースにできるだけ近い場所で実行される必要があります。詳細についてはデータアクセスレイヤーを参照してください。

ヒント

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

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

APIルートの保護

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

APIルートを保護する例:

pages/api/route.ts
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ルートの実装
}
pages/api/route.js
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'であるかを確認します。このアプローチにより、認証されかつ権限のあるユーザーに限定された安全なアクセスを保証し、リクエスト処理のための堅牢なセキュリティを維持します。

リソース

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

認証ライブラリ

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

さらなる学習

認証とセキュリティについて学び続けるために、以下のリソースをチェックしてください: