メインコンテンツまでスキップ

認証

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

始める前に、以下の3つの概念にプロセスを分解するとよいでしょう:

  1. 認証:ユーザーが主張する人物であることを確認することを指します。これは、ユーザーがユーザー名やパスワードなどを使用して自分の身元を証明することを要求します。
  2. セッション管理:リクエスト間でユーザーの認証状態を追跡します。
  3. 認可:ユーザーがどのルートやデータにアクセスできるかを決定します。

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

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

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

認証

サインアップとログイン機能

ReactのServer ActionsuseFormStateを使用して、ユーザーのクレデンシャルを取得し、フォームフィールドを検証し、認証プロバイダーのAPIまたはデータベースを呼び出すために<form>要素を使用できます。

Server Actionsは常にサーバーで実行されるため、認証ロジックを処理するための安全な環境を提供します。

サインアップ/ログイン機能を実装する手順は以下の通りです:

1. ユーザークレデンシャルを取得

ユーザーのクレデンシャルを取得するには、Server Actionが送信時に呼び出されるフォームを作成します。たとえば、ユーザーの名前、メールアドレス、およびパスワードを受け取るサインアップフォームがあります:

app/ui/signup-form.tsx
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>
)
}
app/actions/auth.tsx
export async function signup(formData: FormData) {}

2. サーバーでフォームフィールドを検証

サーバー上でフォームフィールドを検証するために、Server Actionを使用します。オーセンティケーションプロバイダーがフォームの検証を提供しない場合は、ZodYupのようなスキーマ検証ライブラリを使用することができます。

Zodを例に取ると、適切なエラーメッセージを含むフォームスキーマを定義できます:

app/lib/definitions.ts
import { z } from 'zod'

export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})

export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined

認証プロバイダーのAPIまたはデータベースへの不要な呼び出しを避けるために、定義されたスキーマに一致しないフォームフィールドがある場合、Server Actionで早期にreturnすることができます。

app/actions/auth.ts
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,
}
}

// プロバイダーまたはdbを呼び出してユーザーを作成します...
}

戻って<SignupForm />で、ReactのuseFormStateフックを使用して、フォーム送信中に検証エラーを表示できます:

app/ui/signup-form.tsx
'use client'

import { useFormState, useFormStatus } from 'react-dom'
import { signup } from '@/app/actions/auth'

export function SignupForm() {
const [state, action] = useFormState(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>
)}
<SubmitButton />
</form>
)
}

function SubmitButton() {
const { pending } = useFormStatus()

return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}

Good to know:

  • これらの例では、Next.js App RouterにバンドルされているReactのuseFormStateフックを使用しています。React 19を使用している場合は、代わりにuseActionStateを使用してください。詳細についてはReactのドキュメントを参照してください。
  • React 19では、useFormStatusは返されるオブジェクトにデータ、メソッド、アクションなどの追加キーを含んでいます。React 19を使用していない場合は、pendingキーのみを使用できます。
  • React 19では、useActionStateにも返される状態にpendingキーが含まれます。
  • データを変更する前に、ユーザーがそのアクションを実行する許可を持っていることを常に確認する必要があります。認証と認可を参照してください。

3. ユーザーを作成またはユーザー資格情報を確認

フォームフィールドを検証した後、認証プロバイダーのAPIまたはデータベースを呼び出して新しいユーザーアカウントを作成するか、ユーザーが存在するかどうかを確認することができます。

前の例から続けます:

app/actions/auth.tsx
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. ユーザーをリダイレクトする
}

ユーザーアカウントの作成に成功したり、ユーザー資格情報を確認した後、ユーザーの認証状態を管理するためにセッションを作成することができます。セッション管理方法によっては、セッションはcookieまたはデータベース、またはその両方に保存されることがあります。セッション管理セクションに続いてさらに学びましょう。

Tips:

  • 上記の例は、教育目的で認証ステップを細かく説明しているため冗長ですが、独自の安全なソリューションを実装することがどれほど複雑になるかを示しています。認証ライブラリを使用してプロセスを簡単にすることをお勧めします。
  • ユーザーエクスペリエンスを向上させるため、重複するメールやユーザー名が登録フローの早い段階でないか確認したいかもしれません。たとえば、ユーザーがユーザー名を入力中または入力フィールドがフォーカスを失う際に。これにより不要なフォーム送信を防ぎ、ユーザーに即座のフィードバックを提供できます。use-debounceなどのライブラリを使用して、これらのチェックの頻度を管理できます。

セッション管理

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

セッションには2種類あります:

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

Good to know: いずれの方法も使用できますが、iron-sessionJoseのようなセッション管理ライブラリを使用することをお勧めします。

ステートレスセッション

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

  1. セッションを署名するために使用される秘密鍵を生成し、それを環境変数として保存します。
  2. セッション管理ライブラリを使用してセッションデータの暗号化/復号化のロジックを作成します。
  3. Next.jsのcookies APIを使用してcookieを管理します。

上記に加えて、ユーザーがアプリケーションに戻ってきたときにセッションを更新または更新する機能を追加し、ユーザーがログアウトしたときにセッションを削除することを検討してください。

Good to know: 認証ライブラリがセッション管理を含んでいるかどうか確認してください。

1. 秘密鍵の生成

セッションを署名するための秘密鍵を生成する方法はいくつかあります。たとえば、ターミナルでopensslコマンドを使用することができます:

terminal
openssl rand -base64 32

このコマンドは、環境変数ファイルで秘密鍵として使用できる、32文字のランダムな文字列を生成します:

.env
SESSION_SECRET=your_secret_key

その後、セッション管理ロジック内でこの鍵を参照できます:

app/lib/session.js
const secretKey = process.env.SESSION_SECRET

2. セッションの暗号化と復号化

次に、好みのセッション管理ライブラリを使用してセッションを暗号化および復号化できます。前の例から続けて、JoseEdge Runtimeと互換性があります)とReactのserver-onlyパッケージを使用して、セッション管理ロジックがサーバーでのみ実行されるようにする例を示します:

app/lib/session.ts
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('Failed to verify session')
}
}

Tips:

  • ペイロードには、今後のリクエストで使用される、最小限の一意のユーザーデータ(ユーザーのIDやロールなど)が含まれるべきであり、電話番号、メールアドレス、クレジットカード情報などの個人を特定できる情報や、パスワードなどの機密データを含んではいけません。

セッションをcookieに保存するために、Next.jsのcookies APIを使用します。cookieはサーバーで設定され、推奨されるオプションを含めるべきです:

  • HttpOnly:クライアント側のJavaScriptがcookieにアクセスするのを防ぎます。
  • Secure:HTTPSを使用してcookieを送信します。
  • SameSite:このcookieがクロスオリジンリクエストで送信可能かを指定します。
  • Max-Age or Expires:cookieを一定期間後に削除します。
  • Path:cookieのURLパスを定義します。

それぞれのオプションについて詳しくは、MDNを参照してください。

app/lib/session.ts
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: '/',
})
}

Server Actionに戻ったら、createSession()関数を呼び出し、Next.jsのredirect() APIを使用してユーザーを適切なページにリダイレクトします:

app/actions/auth.ts
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')
}

Tips:

  • cookieはサーバー上で設定されるべきです。クライアントサイドの改ざんを防ぐためです。
  • 🎥 視聴:Next.jsを使用したステートレスなセッションと認証についてもっと学ぶ → YouTube (11分)

セッションの更新(またはリフレッシュ)

セッションの有効期限を延長することもできます。これは、ユーザーが再びアプリケーションにアクセスしたときにログイン状態を維持するのに役立ちます。たとえば:

app/lib/session.ts
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: '/',
})
}

Tip: 認証ライブラリがリフレッシュトークンをサポートしているかどうか確認してください。これを使用してユーザーのセッションを延長できます。

セッションの削除

セッションを削除するには、cookieを削除します:

app/lib/session.ts
import 'server-only'
import { cookies } from 'next/headers'

export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}

それから、deleteSession()関数を再利用して、たとえばログアウト時に使用できます:

app/actions/auth.ts
import { deleteSession } from '@/app/lib/session'

export async function logout() {
deleteSession()
redirect('/login')
}

データベースセッション

データベースセッションを作成して管理するには、次の手順を実行する必要があります:

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

例として:

app/lib/session.ts
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: '/',
})
}

Tips:

  • より迅速なデータ取得のために、Vercel Redisのようなデータベースを使用することを検討してください。ただし、セッションデータをプライマリデータベースに保持し、データリクエストを組み合わせてクエリの数を減らすこともできます。
  • セッション管理は、ユーザーが最後にログインした時間やアクティブなデバイスの数を追跡したり、ユーザーにすべてのデバイスからログアウトさせる機能を提供する、より高度なユースケースに適用できます。

セッション管理を実装した後は、アプリケーション内でユーザーがアクセスして行動する内容を制御するために許可ロジックを追加する必要があります。認可セクションに続いてさらに学びましょう。

認可

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

認可チェックには主に2種類があります:

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

両方の場合において、以下の手法が推奨されます:

ミドルウェアを用いた楽観的チェック(オプション)

いくつかの場合にはミドルウェアを使用して権限に基づいてユーザーをリダイレクトすることを検討できます:

  • 楽観的チェックを実行するために、ミドルウェアはすべてのルートで実行されるため、リダイレクトロジックを集中化し、未承認のユーザーを事前にフィルタリングします。
  • ユーザー間でデータを共有する静的ルートを保護するため(たとえば、有料コンテンツ)。

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

例として:

middleware.ts
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()
}

// ミドルウェアが実行されないようにするルート
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

ミドルウェアは初回チェックに役立ちますが、データを保護する唯一の手段として使用すべきではありません。大部分のセキュリティチェックはデータソースに可能な限り近いところで実行するべきです。Data Access Layerをご参照ください。

Tips:

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

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

データリクエストと認可ロジックを集中化するDALを作成することをお勧めします。

DALには、アプリケーションとの対話中にユーザーのセッションを確認する関数が含まれているべきです。最低でも、関数はセッションが有効かどうかを確認し、その後リダイレクトまたは追加のリクエストを行うために必要なユーザー情報を返すべきです。

たとえば、DAL用の別のファイルを作成し、その中にverifySession()関数を含めます。その後、Reactのcache APIを使用して、Reactのレンダーパス中に関数の返り値をメモ化します:

app/lib/dal.ts
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()関数を呼び出すことができます:

app/lib/dal.ts
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はリクエスト時にデータを保護するために使用されます。ただし、ユーザー間でデータを共有する静的ルートの場合、データはリクエスト時ではなくビルド時にフェッチされます。ミドルウェアを使用して静的ルートを保護してください。
  • セキュアなチェックのために、セッションIDをデータベースと比較することでセッションが有効かどうか確認することができます。Reactのcache関数を使用して、レンダーパス中のデータベースへの重複したリクエストを避けましょう。
  • 関連するデータリクエストをJavaScriptクラスにまとめて、どのメソッドの前にverifySession()を実行するかを管理することができます。

データ転送オブジェクト(DTO)の使用

データを取得する際には、アプリケーションで使用される必要なデータだけを返し、全体のオブジェクトを返さないことが推奨されます。たとえば、ユーザーデータをフェッチしている場合、ユーザーのIDと名前のみに留め、パスワード、電話番号などを含む全ユーザーオブジェクトを返さないことをお勧めします。

しかし、返されるデータ構造を制御できない場合や、クライアントに渡される全てのオブジェクトを避けたいチームで作業している場合、クライアントに公開する安全なフィールドを指定するなどの戦略を使用することができます。

app/lib/dto.ts
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,
}
}

データリクエストと許可ロジックをDALに集中化し、DTOを使用することにより、すべてのデータリクエストがセキュアで一貫していることを確認でき、アプリケーションの拡張に伴ってメンテナンス、監査、およびデバッグが容易になります。

Good to know:

  • DTOを定義するにはいくつかの方法があり、toJSON()の使用、例にあるような個別の関数、またはJavaScriptクラスがあります。これらはJavaScriptのパターンであり、ReactまたはNext.jsの機能ではないため、アプリケーションに最適なパターンを見つけるために調査することをお勧めします。
  • セキュリティのベストプラクティスについてもっと知るには、Next.jsのセキュリティに関する記事を参照してください。

サーバーコンポーネント

Server Componentsでの認証チェックは役割ベースのアクセスに役立ちます。たとえば、ユーザーのロールに基づいてコンポーネントを条件付きでレンダリングする場合:

app/dashboard/page.tsx
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')
}
}

例では、DALからのverifySession()関数を使用して'admin'、'user'、および未認証のロールをチェックしています。このパターンにより、各ユーザーが自分の役割に適したコンポーネントとだけ対話することが保証されます。

レイアウトと認証チェック

Partial Renderingのため、レイアウトでチェックを実行する際には注意が必要です。これらはナビゲーションで再レンダリングされないため、ユーザーセッションはすべてのルート変更でチェックされません。

代わりに、データソースや条件付きでレンダリングされるコンポーネントの近くでチェックを行うべきです。

たとえば、ナビゲーションにユーザー画像を表示するユーザーデータをフェッチする共通のレイアウトを考慮します。レイアウト内で認証チェックを行うのではなく、レイアウトでユーザーデータ(getUser())をフェッチし、DALで認証チェックを行うべきです。

これにより、アプリケーション内でgetUser()が呼び出される場合は常に認証チェックが実行されることが保証され、開発者がデータにアクセスする許可があることを確認するのを忘れるのを防ぎます。

app/layout.tsx
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();

return (
// ...
)
}
app/lib/dal.ts
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を公開APIエンドポイントと同じセキュリティ考慮を払って扱い、ユーザーが実行を許可されているか確認します。

以下の例では、アクションが続行される前にユーザーのロールを確認します:

app/lib/actions.ts
'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
}

// 許可されたユーザーに対してアクションを続行
}

ルートハンドラー

ルートハンドラーを公開APIエンドポイントと同じセキュリティ考慮を払って扱い、ユーザーがルートハンドラーにアクセスする許可があるか確認します。

たとえば:

app/api/route.ts
import { verifySession } from '@/app/lib/dal'

export async function GET() {
// ユーザー認証とロールの確認
const session = await verifySession()

// ユーザーが認証されているかどうかチェック
if (!session) {
// ユーザーが認証されていません
return new Response(null, { status: 401 })
}

// ユーザーが管理者ロールを持っているかどうかチェック
if (session.user.role !== 'admin') {
// ユーザーが認証されていても、適切な権限を持っていない場合
return new Response(null, { status: 403 })
}

// 許可されたユーザーに対して処理を続行
}

上記の例では、認証中のユーザーの管理者権限を確認するための2層セキュリティチェックを含むルートハンドラーを示しています。これにより、ログインしているユーザーが'admin'であるかどうかを確認し、セキュリティが確保されます。

コンテキストプロバイダー

Next.jsでは、サーバーコンポーネントとクライアントコンポーネントを組み合わせて使用できますが、Reactのcontextはサーバーコンポーネントでサポートされていないため、クライアントコンポーネントにのみ適用されます。したがって、クライアントコンポーネントで認証データを利用する場合は、useSession()のようなフックを使って認証データをコンポーネント内で取得することができます。

認証用のコンテキストプロバイダーを利用することが可能です。ただし、子サーバーコンポーネントは最初にサーバーでレンダリングされ、コンテキストプロバイダーのセッションデータにアクセスできません:

app/layout.ts
import { ContextProvider } from 'auth-lib'

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ContextProvider>{children}</ContextProvider>
</body>
</html>
)
}
app/ui/profile.ts
"use client";

import { useSession } from "auth-lib";
import useSWR from 'swr'

export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)

return (
// ...
);
}

クライアントコンポーネントでセッションデータが必要な場合(たとえば、クライアントサイドでのデータフェッチ)、ReactのtaintUniqueValue APIを使用して、クライアントへ機密性のあるセッションデータが露出されないようにしてください。

リソース

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

認証ライブラリ

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

さらなる学習

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