TOOLS BOX/ガイド/認証・権限チェックの失敗分岐
Concept

認証・権限チェックの失敗分岐

未認証・権限不足・フォーム認証失敗・想定外エラーの 4 種を区別し、redirect() / notFound() / return { formError } / throw の使い分けを認証文脈で整理するガイド。

nextjsauthenticationauthorizationredirectnotFounderrormiddlewareserver-component

どういう場面で使うか

  • ·未認証ユーザーが保護ページにアクセスしたとき → redirect('/login')
  • ·権限のないリソースへのアクセスでリソースの存在を隠したいとき → notFound()
  • ·権限不足をユーザーに明示したいとき → redirect('/forbidden') または専用ページ
  • ·フォーム送信での認証失敗(パスワード不一致など)→ return { formError }
  • ·DB 障害など想定外のエラー → throw して error.tsx にキャッチさせる

注意点 / Pitfalls

  • ·未認証と権限不足を同じ redirect('/login') にまとめると、ログイン済みユーザーがログイン画面に飛ばされて混乱する
  • ·権限不足に 403 を返すと、そのリソースが存在することが分かってしまう。存在を隠したい場合は notFound() が適切
  • ·フォーム送信の認証失敗を throw するとページ全体がエラー画面になる。return { formError } で返す
  • ·Middleware でのチェックと Server Component でのチェックを二重に書くと保守コストが増える。責務を分離する

補足

失敗の種類ごとに「ユーザーは回復できるか」「存在を隠すべきか」「エラーをユーザーに見せるか」で分岐を決める。認証方式(どこで判定するか)は nextjs-auth-strategies を参照。

失敗の種類と対応手段

認証・権限チェックの失敗は 4 種類に分けて考えます。

失敗の種類対応手段理由
未認証(ログインしていない)redirect('/login')ログインすれば解決できる
権限不足・存在を隠したいnotFound()リソースの存在自体を知らせない
権限不足・明示したいredirect('/forbidden') または専用ページ「見られないこと」をユーザーに伝える
フォーム送信での認証失敗return { formError }フォームに回復手段を提供する
DB 障害・想定外エラーthrow(伝播)error.tsx にキャッチさせる

未認証 → redirect()

セッションがない場合はログインページへ転送します。ログインすれば目的のページに到達できるため、redirect() が自然です。

// Server Component: セッション確認
import { redirect } from "next/navigation";
import { cookies } from "next/headers";

export default async function DashboardPage() {
  const cookieStore = await cookies();
  if (!cookieStore.get("session_id")) {
    redirect("/login"); // ログインすれば解決できる
  }
  return <Dashboard />;
}

ログイン後に元のページへ戻れるようにするには、redirect 先に next パラメータを付けるパターンがあります。

redirect(`/login?next=${encodeURIComponent(request.nextUrl.pathname)}`);

権限不足 — notFound() で存在を隠す

別ユーザーのリソースに不正アクセスされた場合、403 Forbidden を返すとリソースが存在することが分かります。セキュリティ上、存在自体を隠したい場合は notFound() が適切です。

// 他人の投稿にアクセスしようとした場合
export default async function PostPage({ params }: { params: { id: string } }) {
  const session = await getSession();
  const post = await db.post.findUnique({ where: { id: params.id } });

  if (!post) {
    notFound(); // 存在しない
  }

  if (post.authorId !== session?.userId) {
    notFound(); // 存在するが見せない(403 より 404 が安全)
  }

  return <PostDetail post={post} />;
}

notFound() を使う場面:

  • 他人のプライベートリソース(DM・下書き・注文履歴など)
  • 存在確認を許可したくない API エンドポイント

notFound() を使わない場面:

  • 管理者権限が必要なページ(権限不足であることをユーザーに伝えるべき)
  • RBAC で明示的なエラーメッセージが必要な場面

権限不足 — 明示する場合

管理者専用ページなど、「ログインはしているが権限がない」ことをユーザーに伝えたい場合は専用ページへ転送します。

import { redirect } from "next/navigation";

export default async function AdminPage() {
  const session = await getSession();

  if (!session) {
    redirect("/login"); // 未認証
  }

  if (session.role !== "admin") {
    redirect("/forbidden"); // 権限不足(403 相当)
  }

  return <AdminDashboard />;
}
// app/forbidden/page.tsx
export default function ForbiddenPage() {
  return (
    <div>
      <h1>アクセス権限がありません</h1>
      <p>このページにアクセスする権限がありません。</p>
    </div>
  );
}

フォーム送信での認証失敗 → return

ログインフォームでパスワードが違う場合など、フォームにエラーメッセージを返す場合は return { formError } を使います。redirect()throw を使うとフォームの入力値が失われたり、ページ全体がエラー画面になります。

"use server";
export async function loginAction(
  _prev: { formError?: string } | null,
  formData: FormData
) {
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  // バリデーション
  if (!email || !password) {
    return { formError: "メールアドレスとパスワードを入力してください" };
  }

  // 認証失敗(期待される失敗)
  const user = await authenticate(email, password);
  if (!user) {
    return { formError: "メールアドレスまたはパスワードが正しくありません" };
  }

  // 成功 → リダイレクト(try/catch の外)
  await createSession(user.id);
  redirect("/dashboard");
}

Middleware / Layout / Server Component での責務分離

場所向いているチェック制約
Middleware全ルートの一括保護(未認証ガード)Edge Runtime のみ・DB 直接参照不可
Layoutルートグループ単位の保護一度キャッシュされると再実行されないケースあり
Server Componentリソース単位の細かい権限チェックページごとに実装が必要

重複実装は避け、責務を決めて 1 箇所で管理することを推奨します。Middleware で未認証をはじき、個別の権限チェックは Server Component で行うパターンが一般的です。

判断フロー

アクセス失敗
├── セッションがない(未認証)
│   └── redirect('/login')
├── セッションはあるが権限がない
│   ├── 存在を隠したい → notFound()
│   └── 権限不足を伝えたい → redirect('/forbidden') または forbidden()
├── フォーム送信での認証失敗
│   └── return { formError } → useActionState でフォームに表示
└── DB 障害・想定外エラー
    └── throw → error.tsx がキャッチ

認証方式(Middleware / Layout / Server Component どこで判定するか)→ Next.js 認証方式比較

失敗の種類と扱い方の基本 → エラーハンドリングの考え方

Session 取得関数の型設計と責務分離 → 型安全な Session 管理パターン

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例