Authentication
認証を理解することは、アプリケーションのデータを保護するために重要です。このページでは、認証を実装するために使用するReactとNext.jsの機能について説明します。
始める前に、プロセスを3つの概念に分解すると役立ちます:
- 認証: ユーザーが自分が主張する人物であるかどうかを確認します。ユーザーは、ユーザー名やパスワードなど、何かを持っているもので自分の身元を証明する必要があります。
- セッション管理: リクエスト間でユーザーの認証状態を追跡します。
- 認可: ユーザーがアクセスできるルートとデータを決定します。
この図は、ReactとNext.jsの機能を使用した認証フローを示しています:
このページの例では、教育目的で基本的なユーザー名とパスワードの認証を説明します。カスタム認証ソリューションを実装することもできますが、セキュリティとシンプルさを向上させるために、認証ライブラリを使用することをお勧めします。これらは、認証、セッション管理、認可のための組み込みソリューションを提供し、ソーシャルログイン、多要素認証、役割ベースのアクセス制御などの追加機能も提供します。Auth Librariesセクションでリストを見つけることができます。
認証
サインアップとログイン機能
Reactの<form>
要素とReactのServer ActionsおよびuseActionState
を使用して、ユーザーの資格情報をキャプチャし、フォームフィールドを検証し、認証プロバイダーのAPIまたはデータベースを呼び出すことができます。
Server Actionsは常にサーバーで実行されるため、認証ロジックを処理するための安全な環境を提供します。
サインアップ/ログイン機能を実装する手順は次のとおりです:
1. ユーザーの資格情報をキャプチャする
ユーザーの資格情報をキャプチャするには、送信時にServer Actionを呼び出すフォームを作成します。たとえば、ユーザーの名前、メールアドレス、パスワードを受け入れるサインアップフォームです:
- TypeScript
- JavaScript
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>
)
}
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>
)
}
- TypeScript
- JavaScript
export async function signup(formData: FormData) {}
export async function signup(formData) {}
2. サーバーでフォームフィールドを検証する
Server Actionを使用して、サーバーでフォームフィールドを検証します。認証プロバイダーがフォーム検証を提供していない場合は、ZodやYupなどのスキーマ検証ライブラリを使用できます。
Zodを例にとると、適切なエラーメッセージを含むフォームスキーマを定義できます:
- TypeScript
- JavaScript
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
することができます。
- TypeScript
- JavaScript
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'),
})
// フォームフィールドが無効な場合は早期にreturnする
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// プロバイダーまたはデータベースを呼び出してユーザーを作成する...
}
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'),
})
// フォームフィールドが無効な場合は早期にreturnする
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// プロバイダーまたはデータベースを呼び出してユーザーを作成する...
}
<SignupForm />
に戻り、ReactのuseActionState
フックを使用して、フォームが送信されている間に検証エラーを表示できます:
- TypeScript
- JavaScript
highlight={7,15,21,27-36}
'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>
)
}
highlight={7,15,21,27-36}
'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>
)
}
Good to know:
- React 19では、
useFormStatus
は返されるオブジェクトにdata、method、actionなどの追加のキーを含みます。React 19を使用していない場合、pending
キーのみが利用可能です。- データを変更する前に、ユーザーがそのアクションを実行する権限があることを常に確認する必要があります。Authentication and Authorizationを参照してください。
3. ユーザーを作成するか、ユーザーの資格情報を確認する
フォームフィールドを検証した後、認証プロバイダーのAPIまたはデータベースを呼び出して、新しいユーザーアカウントを作成するか、ユーザーが存在するかどうかを確認できます。
前の例から続けて:
- TypeScript
- JavaScript
export async function signup(state: FormState, formData: FormData) {
// 1. フォームフィールドを検証する
// ...
// 2. データベースへの挿入のためのデータを準備する
const { name, email, password } = validatedFields.data
// 例:ユーザーのパスワードを保存する前にハッシュ化する
const hashedPassword = await bcrypt.hash(password, 10)
// 3. データベースにユーザーを挿入するか、Auth Libraryの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. データベースにユーザーを挿入するか、Auth Libraryの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. ユーザーをリダイレクトする
}
ユーザーアカウントの作成またはユーザー資格情報の確認が成功した後、ユーザーの認証状態を管理するためにセッションを作成できます。セッション管理戦略に応じて、セッションはcookieまたはデータベース、またはその両方に保存できます。セッション管理セクションに進んで詳細を学びましょう。
Tips:
- 上記の例は教育目的で認証ステップを分解しているため冗長です。独自の安全なソリューションを実装することはすぐに複雑になる可能性があることを強調しています。プロセスを簡素化するためにAuth Libraryを使用することを検討してください。
- ユーザーエクスペリエンスを向上させるために、登録フローの早い段階で重複するメールアドレスやユーザー名を確認することを検討してください。たとえば、ユーザーがユーザー名を入力しているときや入力フィールドがフォーカスを失ったときです。これにより、不要なフォーム送信を防ぎ、ユーザーに即時のフィードバックを提供できます。use-debounceなどのライブラリを使用してリクエストをデバウンスし、これらのチェックの頻度を管理できます。
セッション管理
セッション管理は、ユーザーの認証された状態をリクエスト間で保持することを保証します。セッションまたはトークンの作成、保存、更新、削除を含みます。
セッションには2つのタイプがあります:
- ステートレス: セッションデータ(またはトークン)はブラウザのcookieに保存されます。cookieは各リクエストと共に送信され、サーバーでセッションを検証できます。この方法はシンプルですが、正しく実装されないとセキュリティが低くなる可能性があります。
- データベース: セッションデータはデータベースに保存され、ユーザーのブラウザには暗号化されたセッションIDのみが送信されます。この方法はより安全ですが、複雑でサーバーリソースを多く使用する可能性があります。
Good to know: どちらの方法も使用できますが、または両方を使用できますが、iron-sessionやJoseなどのセッション管理ライブラリを使用することをお勧めします。
ステートレスセッション
ステートレスセッションを作成および管理するには、次の手順を実行する必要があります:
- セッションを署名するために使用される秘密鍵を生成し、環境変数として保存します。
- セッション管理ライブラリを使用してセッションデータを暗号化/復号化するロジックを作成します。
- Next.jsの
cookies
APIを使用してcookieを管理します。
上記に加えて、ユーザーがアプリケーションに戻ったときにセッションを更新(またはリフレッシュ)する機能を追加し、ユーザーがログアウトしたときにセッションを削除することを検討してください。
Good to know: auth libraryがセッション管理を含んでいるかどうかを確認してください。
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
パッケージを使用して、セッション管理ロジックがサーバーでのみ実行されるようにします。
- TypeScript
- JavaScript
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('セッションの検証に失敗しました')
}
}
Tips:
- ペイロードには、ユーザーのIDや役割など、後続のリクエストで使用される最小限の一意のユーザーデータを含める必要があります。電話番号、メールアドレス、クレジットカード情報などの個人を特定できる情報や、パスワードなどの機密データを含めるべきではありません。
3. Cookieの設定(推奨オプション)
セッションをcookieに保存するには、Next.jsのcookies
APIを使用します。cookieはサーバーで設定され、推奨オプションを含める必要があります:
- HttpOnly: クライアント側のJavaScriptがcookieにアクセスするのを防ぎます。
- Secure: httpsを使用してcookieを送信します。
- SameSite: cookieがクロスサイトリクエストと共に送信できるかどうかを指定します。
- Max-AgeまたはExpires: 一定期間後にcookieを削除します。
- Path: cookieのURLパスを定義します。
これらのオプションの詳細については、MDNを参照してください。
- TypeScript
- JavaScript
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: '/',
})
}
Server Actionに戻り、createSession()
関数を呼び出し、redirect()
APIを使用してユーザーを適切なページにリダイレクトできます:
- TypeScript
- JavaScript
import { createSession } from '@/app/lib/session'
export async function signup(state: FormState, formData: FormData) {
// 前のステップ:
// 1. フォームフィールドを検証する
// 2. データベースへの挿入のためのデータを準備する
// 3. データベースにユーザーを挿入するか、Auth LibraryのAPIを呼び出す
// 現在のステップ:
// 4. ユーザーセッションを作成する
await createSession(user.id)
// 5. ユーザーをリダイレクトする
redirect('/profile')
}
import { createSession } from '@/app/lib/session'
export async function signup(state, formData) {
// 前のステップ:
// 1. フォームフィールドを検証する
// 2. データベースへの挿入のためのデータを準備する
// 3. データベースにユーザーを挿入するか、Auth LibraryのAPIを呼び出す
// 現在のステップ:
// 4. ユーザーセッションを作成する
await createSession(user.id)
// 5. ユーザーをリダイレクトする
redirect('/profile')
}
Tips:
- Cookieはサーバーで設定する必要があります。これにより、クライアント側の改ざんを防ぎます。
- 🎥 視聴: Next.jsを使用したステートレスセッションと認証について詳しく学ぶ → YouTube (11分)。
セッションの更新(またはリフレッシュ)
セッションの有効期限を延長することもできます。これは、ユーザーがアプリケーションに再度アクセスした後にログイン状態を維持するのに役立ちます。たとえば:
- TypeScript
- JavaScript
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: '/',
})
}
Tip: 認証ライブラリがリフレッシュトークンをサポートしているかどうかを確認してください。これにより、ユーザーのセッションを延長できます。
セッションの削除
セッションを削除するには、cookieを削除します:
- TypeScript
- JavaScript
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()
関数を再利用できます。たとえば、ログアウト時に:
- TypeScript
- JavaScript
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
deleteSession()
redirect('/login')
}
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
deleteSession()
redirect('/login')
}
データベースセッション
データベースセッションを作成および管理するには、次の手順を実行する必要があります:
- セッションとデータを保存するためのテーブルをデータベースに作成します(またはAuth Libraryがこれを処理するかどうかを確認します)。
- セッションを挿入、更新、削除する機能を実装します
- セッションIDをユーザーのブラウザに保存する前に暗号化し、データベースとcookieが同期していることを確認します(これはオプションですが、Middlewareでの楽観的な認証チェックのために推奨されます)。
たとえば:
- TypeScript
- JavaScript
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. 楽観的な認証チェックのためにcookieにセッションを保存する
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. 楽観的な認証チェックのためにcookieにセッションを保存する
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
Tips:
- より高速なデータ取得のために、Vercel Redisのようなデータベースを使用することを検討してください。ただし、セッションデータをプライマリデータベースに保持し、データリクエストを組み合わせてクエリの数を減らすこともできます。
- より高度なユースケースのために、データベースセッションを使用することを選択することができます。たとえば、ユーザーが最後にログインした時間やアクティブなデバイスの数を追跡したり、ユーザーにすべてのデバイスからログアウトする機能を提供したりすることができます。
セッション管理を実装した後、アプリケーション内でユーザーがアクセスできる内容と実行できる操作を制御するための認可ロジックを追加する必要があります。認可セクションに進んで詳細を学びましょう。
認可
ユーザーが認証され、セッションが作成された後、アプリケーション内でユーザーがアクセスできる内容と実行できる操作を制御するために認可を実装できます。
認可チェックには2つの主要なタイプがあります:
- 楽観的: cookieに保存されたセッションデータを使用して、ユーザーがルートにアクセスする権限があるか、アクションを実行する権限があるかを確認します。これらのチェックは、UI要素の表示/非表示や、権限や役割に基づいてユーザーをリダイレクトするなどの迅速な操作に役立ちます。
- セキュア: データベースに保存されたセッションデータを使用して、ユーザーがルートにアクセスする権限があるか、アクションを実行する権限があるかを確認します。これらのチェックはより安全で、機密データへのアクセスやアクションを必要とする操作に使用されます。
どちらの場合も、次のことをお勧めします:
- 認可ロジックを集中化するためにデータアクセス層(DAL)を作成する
- 必要なデータのみを返すためにデータ転送オブジェクト(DTO)を使用する
- オプションでMiddlewareを使用して楽観的なチェックを実行する。
Middlewareを使用した楽観的なチェック(オプション)
Middlewareを使用して、権限に基づいてユーザーをリダイレクトする場合があります:
- 楽観的なチェックを実行するため。Middlewareはすべてのルートで実行されるため、リダイレクトロジックを集中化し、許可されていないユーザーを事前にフィルタリングするのに適しています。
- ユーザー間でデータを共有する静的ルートを保護するため(例:ペイウォールの背後にあるコンテンツ)。
ただし、Middlewareはすべてのルートで実行され、prefetchedルートも含まれるため、パフォーマンスの問題を防ぐために、cookieからセッションを読み取るだけにする(楽観的なチェック)し、データベースチェックを避けることが重要です。
たとえば:
- TypeScript
- JavaScript
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. 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()
}
// Middlewareが実行されないルート
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. cookieからセッションを復号化する
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は初期チェックに役立ちますが、データを保護するための唯一の防御手段として使用すべきではありません。セキュリティチェックの大部分は、データソースにできるだけ近い場所で実行する必要があります。詳細については、データアクセス層(DAL)を参照してください。
Tips:
- Middlewareでは、
req.cookies.get('session').value
を使用してcookieを読み取ることもできます。- MiddlewareはEdge Runtimeを使用します。認証ライブラリとセッション管理ライブラリが互換性があるかどうかを確認してください。
- Middlewareの
matcher
プロパティを使用して、Middlewareが実行されるルートを指定できます。ただし、認証のためには、Middlewareがすべてのルートで実行されることをお勧めします。
データアクセス層(DAL)の作成
データリクエストと認可ロジックを集中化するためにDALを作成することをお勧めします。
DALには、アプリケーションと対話する際にユーザーのセッションを検証する関数が含まれている必要があります。少なくとも、関数はセッションが有効かどうかを確認し、ユーザー情報をリダイレクトまたは返す必要があります。
たとえば、DAL用の別のファイルを作成し、verifySession()
関数を含めます。その後、ReactのcacheAPIを使用して、Reactのレンダリングパス中に関数の戻り値をメモ化します:
- TypeScript
- JavaScript
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()
関数を呼び出すことができます:
- TypeScript
- JavaScript
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('ユーザーの取得に失敗しました')
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('ユーザーの取得に失敗しました')
return null
}
})
Tip:
- DALはリクエスト時にフェッチされるデータを保護するために使用できます。ただし、ユーザー間でデータを共有する静的ルートの場合、データはビルド時にフェッチされ、リクエスト時にはフェッチされません。Middlewareを使用して静的ルートを保護します。
- セキュアなチェックのために、データベースとセッションIDを比較してセッションが有効かどうかを確認できます。Reactのcache関数を使用して、レンダリングパス中にデータベースへの不要な重複リクエストを回避します。
- 関連するデータリクエストをJavaScriptクラスに統合し、メソッドを実行する前に
verifySession()
を実行することを検討することもできます。
データ転送オブジェクト(DTO)の使用
データを取得する際には、アプリケーションで使用される必要なデータのみを返し、全体のオブジェクトを返さないことをお勧めします。たとえば、ユーザーデータをフェッチする場合、ユーザーのIDと名前のみを返し、パスワードや電話番号などを含む全体のユーザーオブジェクトを返さないかもしれません。
ただし、返されるデータ構造を制御できない場合や、チームで作業している場合で、クライアントに全体のオブジェクトが渡されるのを避けたい場合は、クライアントに公開するフィールドを指定するなどの戦略を使用できます。
- TypeScript
- JavaScript
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を使用することで、すべてのデータリクエストが安全で一貫性があることを保証し、アプリケーションがスケールするにつれて、メンテナンス、監査、デバッグが容易になります。
Good to know:
- DTOを定義する方法はいくつかあります。
toJSON()
を使用する方法、上記の例のように個別の関数を使用する方法、またはJSクラスを使用する方法です。これらはJavaScriptのパターンであり、ReactやNext.jsの機能ではないため、アプリケーションに最適なパターンを見つけるために調査することをお勧めします。- Security in Next.js articleでセキュリティのベストプラクティスについて詳しく学びましょう。
Server Components
Server Componentsでの認証チェックは、役割ベースのアクセスに役立ちます。たとえば、ユーザーの役割に基づいてコンポーネントを条件付きでレンダリングする場合:
- TypeScript
- JavaScript
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'、および許可されていない役割をチェックします。このパターンは、各ユーザーが自分の役割に適したコンポーネントと対話することを保証します。
レイアウトと認証チェック
Partial Renderingのため、Layoutsでチェックを行う際には注意が必要です。これらはナビゲーション時に再レンダリングされないため、ユーザーセッションはすべてのルート変更でチェックされません。
代わりに、データソースや条件付きでレンダリングされるコンポーネントに近い場所でチェックを行うべきです。
たとえば、ナビゲーションでユーザー画像を表示する共有レイアウトを考えてみましょう。レイアウトで認証チェックを行う代わりに、レイアウトでユーザーデータ(getUser()
)をフェッチし、DALで認証チェックを行うべきです。
これにより、アプリケーション内でgetUser()
が呼び出される場所で認証チェックが実行され、開発者がデータにアクセスする権限があることを確認するのを忘れることを防ぎます。
- TypeScript
- JavaScript
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 (
// ...
)
}
- TypeScript
- JavaScript
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を取得し、データをフェッチする
})
Good to know:
- SPAで一般的なパターンは、ユーザーが許可されていない場合にレイアウトまたはトップレベルコンポーネントで
return null
することです。このパターンは推奨されません。Next.jsアプリケーションには複数のエントリーポイントがあり、ネストされたルートセグメントやServer Actionsへのアクセスを防ぐことはできません。
Server Actions
Server Actionsを公開APIエンドポイントと同じセキュリティ考慮事項で扱い、ユーザーがミューテーションを実行する権限があるかどうかを確認します。
以下の例では、アクションを進める前にユーザーの役割を確認します:
- TypeScript
- JavaScript
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction(formData: FormData) {
const session = await verifySession()
const userRole = session?.user?.role
// ユーザーがアクションを実行する権限がない場合は早期にreturnする
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
// ユーザーがアクションを実行する権限がない場合は早期にreturnする
if (userRole !== 'admin') {
return null
}
// 許可されたユーザーのためにアクションを進める
}
Route Handlers
Route Handlersを公開APIエンドポイントと同じセキュリティ考慮事項で扱い、ユーザーがRoute Handlerにアクセスする権限があるかどうかを確認します。
たとえば:
- TypeScript
- JavaScript
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段階のセキュリティチェックを備えたRoute Handlerを示しています。最初にアクティブなセッションを確認し、次にログインしているユーザーが'admin'であるかどうかを確認します。
コンテキストプロバイダー
認証のためのコンテキストプロバイダーの使用は、インターリーブのおかげで機能します。ただし、Reactのcontext
はServer Componentsではサポートされていないため、Client Componentsにのみ適用されます。
これは機能しますが、子Server Componentsは最初にサーバーでレンダリングされ、コンテキストプロバイダーのセッションデータにアクセスできません:
- TypeScript
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互換のライブラリとリソースを使用して、安全な認証とセッション管理を実装するのに役立ててください:
認証ライブラリ
セッション管理ライブラリ
さらなる学習
認証とセキュリティについて学び続けるために、次のリソースをチェックしてください: