レビュー済み·難易度: 初級·更新: 2026-04-18

Next.js で権限不足(forbidden)時の専用ページを表示する

認証済みで操作は許可されているが特定リソースへのアクセス権限がない場合に forbidden ページを表示し、unauthorized との使い分けを示す例。

nextjsauthenticationrouting

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の Server Component と認証フローの基本を理解していること

概要

Next.js 15 の forbidden() 関数を使い、認証済みユーザーが自分に権限のないリソースにアクセスした場合に HTTP 403 相当の専用ページを表示する。unauthorized() との使い分け(401 vs 403 のセマンティクス)と、forbidden.tsx によるカスタム UI の実装を示す。

インストール

# 追加インストールは不要

実装

forbidden() を呼び出す Server Component

// app/admin/page.tsx
import { forbidden } from "next/navigation";
import { getCurrentUser } from "@/lib/auth";

export default async function AdminPage() {
  const user = await getCurrentUser();

  // 未ログインは unauthorized(401)
  if (!user) {
    // redirect to login or call unauthorized()
    return null;
  }

  // ログイン済みだが admin ロールがない場合は forbidden(403)
  if (user.role !== "admin") {
    forbidden();
  }

  return (
    <div>
      <h1>管理者ページ</h1>
      {/* ...管理コンテンツ */}
    </div>
  );
}

forbidden.tsx でカスタム UI を定義する

forbidden.tsxapp/ 直下またはセグメント内に置くと、forbidden() が呼ばれたときにそのコンポーネントが表示される。

// app/forbidden.tsx

export default function ForbiddenPage() {
  return (
    <div className="flex min-h-[60vh] flex-col items-center justify-center gap-4 text-center">
      <p className="text-6xl font-bold text-gray-300">403</p>
      <h1 className="text-xl font-semibold text-gray-800">
        このページへのアクセス権限がありません
      </h1>
      <p className="text-sm text-gray-500">
        アクセスするには管理者権限が必要です。
      </p>
      <a
        href="/"
        className="mt-2 inline-block rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
      >
        トップページへ戻る
      </a>
    </div>
  );
}

リソース単位での権限チェック

// app/projects/[id]/page.tsx
import { forbidden } from "next/navigation";
import { getCurrentUser, getProject } from "@/lib/auth";

type Props = {
  params: Promise<{ id: string }>;
};

export default async function ProjectPage({ params }: Props) {
  const { id } = await params;
  const [user, project] = await Promise.all([
    getCurrentUser(),
    getProject(id),
  ]);

  if (!user) {
    // 未ログインは認証ページへ
    return null;
  }

  // プロジェクトメンバーでなければ forbidden
  if (!project.memberIds.includes(user.id)) {
    forbidden();
  }

  return (
    <div>
      <h1>{project.name}</h1>
    </div>
  );
}

unauthorized との使い分け

状況関数HTTP ステータス
未ログイン・セッション切れunauthorized()401
ログイン済みだが権限不足forbidden()403
// unauthorized.tsx(参考)
// app/unauthorized.tsx
export default function UnauthorizedPage() {
  return (
    <div className="flex min-h-[60vh] flex-col items-center justify-center gap-4 text-center">
      <p className="text-6xl font-bold text-gray-300">401</p>
      <h1 className="text-xl font-semibold text-gray-800">
        ログインが必要です
      </h1>
      <a
        href="/login"
        className="mt-2 inline-block rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
      >
        ログインページへ
      </a>
    </div>
  );
}

ポイント

  • forbidden() は Next.js 15 で追加された関数で、呼び出すと即座にレンダリングを中断し forbidden.tsx の UI を表示する。throw 不要でコードが簡潔になる
  • forbidden.tsxnot-found.tsx と同じ仕組みで、ファイルを置いたセグメントとその配下に適用される。app/forbidden.tsx はアプリ全体のデフォルト
  • 「未ログイン(401)」と「権限不足(403)」は異なる HTTP セマンティクスを持つ。未ログインは unauthorized() + unauthorized.tsx、権限不足は forbidden() + forbidden.tsx と使い分けることで、ユーザーに適切な案内を出せる
  • 権限チェックはリソース取得と並行して行える(Promise.all)。取得結果に基づく判定なので、存在しないリソースには notFound() を使い、存在するが見せられないリソースには forbidden() を使う
  • forbidden() は Server Component でのみ動作する。Client Component から呼ぶ場合は API Route や Server Action 経由でステータスを返してリダイレクトする

注意点

nextjs-unauthorized-page は認証済みでロール不足の 401/403 対応。nextjs-protected-layout は layout 単位の一括保護。これは Next.js 15 の forbidden() 関数を使いリソース単位の 403 アクセス制御に特化。unauthorized との HTTP セマンティクスの違いも示す。

関連サンプル