Next.js での認証実装方法
認証を理解することは、アプリケーションのデータを保護するために重要です。このページでは、React と Next.js の機能を使用して認証を実装する方法を説明します。
開始前に、プロセスを3つの概念に分解すると理解しやすくなります:
- 認証 (Authentication):ユーザーが自分自身であることを確認します。ユーザー名とパスワードなど、ユーザーが持っている情報で身元を証明する必要があります。
- セッション管理 (Session Management):リクエスト間でユーザーの認証状態を追跡します。
- 認可 (Authorization):ユーザーがアクセスできるルートやデータを決定します。
この図は、React と Next.js の機能を使用した認証フローを示しています:

このページの例では、教育目的で基本的なユーザー名とパスワード認証について説明します。カスタム認証ソリューションを実装することも可能ですが、セキュリティを強化し簡素化するために、認証ライブラリの使用を推奨します。これらは認証、セッション管理、認可のための組み込みソリューションに加え、ソーシャルログイン、多要素認証、ロールベースのアクセス制御などの追加機能を提供します。利用可能なライブラリのリストは認証ライブラリ (Auth Libraries)セクションで確認できます。
認証 (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>
)
}
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>
)
}
export async function signup(formData: FormData) {}
export async function signup(formData) {}
2. サーバー上でのフォームフィールドの検証
Server Actionを使用して、サーバー上でフォームフィールドを検証します。認証プロバイダーがフォーム検証を提供していない場合は、ZodやYupなどのスキーマ検証ライブラリを使用できます。
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
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(),
})
認証プロバイダーの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を呼び出す...
}
import { SignupFormSchema } from '@/app/lib/definitions'
export async function signup(state, 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>
)
}
'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
キーのみが利用可能です。- データを変更する前に、ユーザーがその操作を実行する権限があることを常に確認する必要があります。認証と認可 (Authentication and Authorization)を参照してください。
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. ユーザーのリダイレクト
}
export async function signup(state, 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つのタイプがあります:
- ステートレス: セッションデータ(またはトークン)がブラウザのクッキーに保存されます。クッキーは各リクエストとともに送信され、サーバーでセッションを検証できます。この方法はシンプルですが、正しく実装されていない場合は安全性が低くなる可能性があります。
- データベース: セッションデータがデータベースに保存され、ユーザーのブラウザには暗号化されたセッションIDのみが送信されます。この方法はより安全ですが、複雑でサーバーリソースを多く使用する可能性があります。
知っておくと良いこと: どちらの方法、または両方を使用できますが、iron-sessionやJoseなどのセッション管理ライブラリの使用をお勧めします。
ステートレスセッション
ステートレスセッションを作成および管理するには、いくつかの手順に従う必要があります:
- セッションの署名に使用する秘密鍵を生成し、環境変数として保存します。
- セッション管理ライブラリを使用してセッションデータを暗号化/復号化するロジックを作成します。
- Next.jsの
cookies
APIを使用してクッキーを管理します。
上記に加えて、ユーザーがアプリケーションに戻ったときにセッションを更新(またはリフレッシュ)する機能と、ユーザーがログアウトしたときにセッションを削除する機能を追加することを検討してください。
知っておくと良いこと: 認証ライブラリにセッション管理が含まれているか確認してください。
1. 秘密鍵の生成
セッションに署名するための秘密鍵を生成する方法はいくつかあります。例えば、ターミナルでopenssl
コマンドを使用できます:
openssl rand -base64 32
このコマンドは、環境変数ファイルに保存できる32文字のランダムな文字列を生成します:
SESSION_SECRET=your_secret_key
次に、この鍵をセッション管理ロジックで参照できます:
const secretKey = process.env.SESSION_SECRET
2. セッションの暗号化と復号化
次に、好みのセッション管理ライブラリを使用してセッションを暗号化および復号化できます。前の例の続きとして、Jose(Edge 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('セッションの検証に失敗しました')
}
}
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session) {
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: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId) {
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')
}
import { createSession } from '@/app/lib/session'
export async function signup(state, 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'
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)(
await cookies()
).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')
}
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')
}
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
await deleteSession()
redirect('/login')
}
データベースセッション
データベースセッションを作成・管理するには、以下の手順に従います:
- セッションとデータを保存するデータベーステーブルを作成(または認証ライブラリが処理するか確認)
- セッションの挿入、更新、削除機能を実装
- セッション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: '/',
})
}
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id) {
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つのタイプがあります:
- 楽観的チェック: クッキーに保存されたセッションデータを使用して、ユーザーがルートにアクセスしたりアクションを実行したりする権限があるかを確認。UI要素の表示/非表示や、権限やロールに基づくユーザーリダイレクトなど、迅速な操作に有用
- セキュアチェック: データベースに保存されたセッションデータを使用して、ユーザーがルートにアクセスしたりアクションを実行したりする権限があるかを確認。機密データやアクションへのアクセスが必要な操作で使用
どちらの場合も以下を推奨します:
- 認可ロジックを一元化するデータアクセスレイヤーの作成
- 必要なデータのみを返すデータ転送オブジェクト(DTO)の使用
- オプションでミドルウェアを使用した楽観的チェック
ミドルウェアを使用した楽観的チェック (オプション)
ミドルウェアを使用して、権限に基づきユーザーをリダイレクトする場合があります:
- 楽観的チェックを実行するため。ミドルウェアはすべてのルートで実行されるため、リダイレクトロジックを一元化し、未承認ユーザーを事前にフィルタリングする良い方法
- ユーザー間でデータを共有する静的ルート(有料コンテンツなど)を保護するため
ただし、ミドルウェアはプリフェッチされたルートを含むすべてのルートで実行されるため、パフォーマンス問題を防ぐため、クッキーからのセッション読み取り(楽観的チェック)のみを行い、データベースチェックは避けることが重要です。
例:
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$).*)'],
}
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()
}
// ミドルウェアが実行されないルート
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 }
})
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()
関数を呼び出せます:
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
}
})
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
}
})
ヒント:
データ転送オブジェクト (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,
}
}
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer) {
return true
}
function canSeePhoneNumber(viewer, team) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug) {
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')
}
}
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session.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 (
// ...
)
}
export default async function Layout({ children }) {
const user = await getUser();
return (
// ...
)
}
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// セッションからユーザーIDを取得し、データを取得
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// セッションからユーザーIDを取得し、データを取得
})
知っておくと良いこと:
- 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
}
// 認可されたユーザーのためにアクションを続行
}
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction() {
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 })
}
// 認可されたユーザーのために続行
}
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互換のライブラリとリソースを紹介します:
認証ライブラリ
セッション管理ライブラリ
さらに学ぶ
認証とセキュリティについてさらに学ぶには、以下のリソースを確認してください: