コンテンツにスキップ

認証

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

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

  1. 認証 (Authentication): ユーザーが名乗っている人物であるかどうかを確認します。ユーザーは、ユーザー名やパスワードなど、持っているもので身元を証明する必要があります。
  2. セッション管理 (Session Management): リクエスト間でユーザーの認証状態を追跡します。
  3. 認可 (Authorization): ユーザーがどのルートやデータにアクセスできるかを決定します。

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

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

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

認証

サインアップまたはログインフォームを実装する手順は次のとおりです。

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

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

pages/login.tsx
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 {
      // Handle errors
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</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: 'Invalid credentials.' })
    } else {
      res.status(500).json({ error: 'Something went wrong.' })
    }
  }
}

セッション管理

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

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

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

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

ステートレスセッション

Cookieの設定と削除

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

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, // One week
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: 'Successfully set cookie!' })
}

データベースセッション

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

  1. セッションとデータを保存するためのテーブルをデータベースに作成します(または、認証ライブラリがこれを処理するかどうかを確認します)。
  2. セッションの挿入、更新、削除の機能を実装します。
  3. ユーザーのブラウザにセッションIDを保存する前に暗号化し、データベースとCookieが同期していることを確認します(これは任意ですが、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: 'Internal Server Error' })
  }
}

認可

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

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

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

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

ミドルウェアによる楽観的チェック (任意)

権限に基づいてユーザーをリダイレクトするためにミドルウェアを使用したい場合があるでしょう。

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

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

middleware.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 middleware(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 Middleware should not run on
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

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

ヒント:

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

データアクセス層 (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)
 
  // Check if the user is authenticated
  if (!session) {
    res.status(401).json({
      error: 'User is not authenticated',
    })
    return
  }
 
  // Check if the user has the 'admin' role
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Unauthorized access: User does not have admin privileges.',
    })
    return
  }
 
  // Proceed with the route for authorized users
  // ... implementation of the API Route
}

この例は、認証と認可の2段階のセキュリティチェックを備えたAPIルートを示しています。まずアクティブなセッションをチェックし、次にログインしたユーザーが「管理者」であるかどうかを検証します。このアプローチにより、認証済みおよび認可済みのユーザーにアクセスが限定され、リクエスト処理の堅牢なセキュリティが維持されます。

リソース

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

認証ライブラリ

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

さらに読む

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