概要
Next.js 15 の unauthorized() 関数を使い、認証済みだが権限が不足しているユーザーに専用の unauthorized ページを表示する。not-found() との使い分け、unauthorized.tsx ファイルの配置、ロールベースのアクセス制御パターンを示す。
インストール
# 追加インストールは不要
実装
unauthorized.tsx(カスタム unauthorized ページ)
// app/unauthorized.tsx
import Link from "next/link";
export default function UnauthorizedPage() {
return (
<main className="flex min-h-screen flex-col items-center justify-center px-4 text-center">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-red-50">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-10 w-10 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
</svg>
</div>
<h1 className="mb-2 text-2xl font-bold text-gray-900">アクセス権限がありません</h1>
<p className="mb-8 max-w-sm text-sm text-gray-500">
このページを表示する権限がありません。管理者にお問い合わせいただくか、別のアカウントでサインインしてください。
</p>
<div className="flex gap-3">
<Link
href="/"
className="rounded-lg border border-gray-300 px-5 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
>
トップに戻る
</Link>
<Link
href="/login"
className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
別のアカウントでログイン
</Link>
</div>
</main>
);
}
ロールチェックして unauthorized を返すページ
// app/admin/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { unauthorized } from "next/navigation";
type UserRole = "admin" | "editor" | "viewer";
async function getCurrentUserRole(): Promise<UserRole | null> {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session_id");
if (!sessionId) return null;
// 実際のアプリでは DB からセッションとロールを取得する
// ここではサンプルのため固定値を返す
return "viewer";
}
export default async function AdminPage() {
const role = await getCurrentUserRole();
// 未認証 → ログインページへ redirect
if (!role) {
redirect("/login");
}
// 認証済みだが権限不足 → unauthorized ページを表示
if (role !== "admin") {
unauthorized();
}
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-4 text-2xl font-bold">管理者ダッシュボード</h1>
<p className="text-gray-600">管理者のみ表示されるページです。</p>
</main>
);
}
ルートグループ内での unauthorized.tsx 配置
app/
├── unauthorized.tsx # アプリ全体のデフォルト unauthorized ページ
├── (admin)/
│ ├── unauthorized.tsx # admin グループ専用(より詳細なメッセージ)
│ ├── layout.tsx # admin 共通レイアウト
│ └── dashboard/
│ └── page.tsx
└── (public)/
└── login/
└── page.tsx
not-found との使い分け
// app/posts/[id]/page.tsx — not-found と unauthorized の両方を使う例
import { cookies } from "next/headers";
import { notFound, redirect, unauthorized } from "next/navigation";
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const cookieStore = await cookies();
const sessionId = cookieStore.get("session_id");
if (!sessionId) {
redirect("/login");
}
// 実際のアプリでは DB から取得
const post = await fetchPost(id);
// 投稿が存在しない → 404
if (!post) {
notFound();
}
// 投稿は存在するが閲覧権限がない → 403 unauthorized
if (post.isPrivate && post.authorId !== getCurrentUserId()) {
unauthorized();
}
return <article>{/* ... */}</article>;
}
// ダミー関数(サンプル用)
async function fetchPost(id: string) {
return id === "1" ? { id: "1", title: "テスト", isPrivate: true, authorId: "user-2" } : null;
}
function getCurrentUserId() {
return "user-1";
}
ポイント
unauthorized()は Next.js 15 で追加された関数。notFound()と同様にコンポーネントのレンダリングを中断し、最も近いunauthorized.tsxを表示するnot-foundとunauthorizedの使い分け: リソース自体が存在しない(404)場合はnotFound()、リソースは存在するが権限がない(403)場合はunauthorized()を使う。セキュリティ上、権限不足の場合にもnotFound()を使って存在を隠すケースもあるunauthorized.tsxはルートグループ単位で配置でき、管理者エリアと一般エリアで異なるメッセージを表示できる- 未認証(Cookie なし)の場合は
redirect("/login")でログインページへ誘導する。unauthorized()は「ログイン済みだが権限が足りない」状態に使う getCurrentUserRoleのようなロール取得関数は Server Component 内で呼び出し、クライアントにロール情報を露出させない