【Next.js】NextAuthでシンプルなユーザー認証機能を実装する|簡単なToDoアプリの作成

Next.jsでWebアプリ開発の基本となるユーザー認証機能の実装方法を解説します。認証ライブラリNextAuthとPrismaを使い、メールアドレスとパスワードでの新規登録・ログイン機能を構築します。パスワードはbcryptで安全に暗号化し、ログイン状態に応じて表示を切り替える実践的な方法まで学べます。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • node: 22.14.0
  • react: 19.0.0
  • next: 15.3.2
  • tailwindcss: 4
  • Prisma: 6.8.2
  • NextAuth: 5.8.0
  • @auth/prisma-adapter: 2.9.1
  • @prisma/client: 6.8.2
  • bcrypt: 6.0.0
  • @types/bcrypt: 5.0.2

サンプルコード

/package.json

/package.json
1     "lint": "next lint"
2   },
3   "dependencies": {
4+    "@auth/prisma-adapter": "^2.9.1",
5+    "@next-auth/prisma-adapter": "^1.0.7",
6     "@prisma/client": "^6.8.2",
7+    "bcrypt": "^6.0.0",
8     "next": "15.3.2",
9+    "next-auth": "^4.24.11",
10     "prisma": "^6.8.2",
11     "react": "^19.0.0",
12     "react-dom": "^19.0.0"
13   "devDependencies": {
14     "@eslint/eslintrc": "^3",
15     "@tailwindcss/postcss": "^4",
16+    "@types/bcrypt": "^5.0.2",
17     "@types/node": "^20",
18     "@types/react": "^19",
19     "@types/react-dom": "^19",
20

/prisma/migrations/20250518034446_add_user_model/migration.sql

/prisma/migrations/20250518034446_add_user_model/migration.sql
1+-- CreateTable
2+CREATE TABLE `users` (
3+    `id` INTEGER NOT NULL AUTO_INCREMENT,
4+    `name` VARCHAR(191) NOT NULL,
5+    `email` VARCHAR(191) NULL,
6+    `password` VARCHAR(191) NOT NULL,
7+    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
8+    `updatedAt` DATETIME(3) NOT NULL,
9+
10+    UNIQUE INDEX `users_name_key`(`name`),
11+    UNIQUE INDEX `users_email_key`(`email`),
12+    PRIMARY KEY (`id`)
13+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
14

/prisma/schema.prisma

/prisma/schema.prisma
1   text  String
2 
3   @@map("todos")
4+}
5+
6+model User {
7+  id        Int      @id @default(autoincrement())
8+  name      String   @unique
9+  email     String?  @unique
10+  password  String
11+  createdAt DateTime @default(now())
12+  updatedAt DateTime @updatedAt
13+
14+  @@map("users")
15 }
16

/src/app/api/auth/[...nextauth]/route.tsx

/src/app/api/auth/[...nextauth]/route.tsx
1+import NextAuth, { NextAuthOptions } from "next-auth"
2+import CredentialsProvider from "next-auth/providers/credentials"
3+import { PrismaAdapter } from "@auth/prisma-adapter"
4+import { prisma } from "@lib/prisma"
5+import bcrypt from "bcrypt"
6+
7+export const authOptions: NextAuthOptions = {
8+  adapter: PrismaAdapter(prisma),
9+  providers: [
10+    CredentialsProvider({
11+      name: "Credentials",
12+      credentials: {
13+        email: { label: "Email", type: "text" },
14+        password: { label: "Password", type: "password" }
15+      },
16+      async authorize(credentials) {
17+        if (!credentials) return null
18+
19+        const user = await prisma.user.findUnique({
20+          where: { email: credentials.email }
21+        })
22+        if (!user) return null
23+
24+        const isValid = await bcrypt.compare(credentials.password, user.password)
25+        if (!isValid) return null
26+
27+        return {
28+          ...user,
29+          id: String(user.id),
30+        }
31+      }
32+    })
33+  ],
34+  session: { strategy: "jwt" },
35+  pages: {
36+    signIn: "/auth/signin",
37+  },
38+}
39+
40+const handler = NextAuth(authOptions)
41+
42+export { handler as GET, handler as POST }
43

/src/app/api/auth/register/route.tsx

/src/app/api/auth/register/route.tsx
1+import { NextResponse } from "next/server"
2+import { PrismaClient } from "@prisma/client"
3+import bcrypt from "bcrypt"
4+
5+const prisma = new PrismaClient()
6+
7+export async function POST(req: Request) {
8+  const { email, password, name } = await req.json()
9+
10+  const existingUser = await prisma.user.findUnique({ where: { email } })
11+  if (existingUser) {
12+    return NextResponse.json({ error: "既に存在するユーザーです" }, { status: 400 })
13+  }
14+
15+  const hashedPassword = await bcrypt.hash(password, 10)
16+
17+  const user = await prisma.user.create({
18+    data: {
19+      email,
20+      name,
21+      password: hashedPassword,
22+    },
23+  })
24+
25+  return NextResponse.json({ user })
26+}
27

/src/app/auth/register/page.tsx

/src/app/auth/register/page.tsx
1+"use client"
2+
3+import { useState } from "react"
4+import { useRouter } from "next/navigation"
5+
6+export default function RegisterPage() {
7+  const [email, setEmail] = useState("")
8+  const [password, setPassword] = useState("")
9+  const [name, setName] = useState("")
10+  const router = useRouter()
11+
12+  const handleRegister = async (e: React.FormEvent) => {
13+    e.preventDefault()
14+    const res = await fetch("/api/auth/register", {
15+      method: "POST",
16+      headers: { "Content-Type": "application/json" },
17+      body: JSON.stringify({ email, password, name }),
18+    })
19+
20+    if (res.ok) {
21+      router.push("/auth/signin")
22+    } else {
23+      const { error } = await res.json()
24+      alert(error)
25+    }
26+  }
27+
28+  return (
29+    <div className="flex items-center justify-center min-h-screen bg-gray-100">
30+      <form
31+        onSubmit={handleRegister}
32+        className="bg-white p-8 rounded shadow-md w-full max-w-md"
33+      >
34+        <h2 className="text-2xl font-bold mb-6 text-center">ユーザー登録</h2>
35+
36+        <div className="mb-4">
37+          <label className="block text-gray-700 mb-2">名前</label>
38+          <input
39+            type="text"
40+            className="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
41+            value={name}
42+            onChange={(e) => setName(e.target.value)}
43+            placeholder="山田 太郎"
44+          />
45+        </div>
46+
47+        <div className="mb-4">
48+          <label className="block text-gray-700 mb-2">メールアドレス</label>
49+          <input
50+            type="email"
51+            className="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
52+            value={email}
53+            onChange={(e) => setEmail(e.target.value)}
54+            placeholder="example@example.com"
55+          />
56+        </div>
57+
58+        <div className="mb-6">
59+          <label className="block text-gray-700 mb-2">パスワード</label>
60+          <input
61+            type="password"
62+            className="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
63+            value={password}
64+            onChange={(e) => setPassword(e.target.value)}
65+            placeholder="パスワード"
66+          />
67+        </div>
68+
69+        <button
70+          type="submit"
71+          className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition"
72+        >
73+          登録
74+        </button>
75+      </form>
76+    </div>
77+  )
78+}
79

/src/app/auth/signin/page.tsx

/src/app/auth/signin/page.tsx
1+"use client"
2+
3+import { signIn } from "next-auth/react"
4+import { useState } from "react"
5+import { useRouter } from "next/navigation"
6+
7+export default function SignInPage() {
8+  const [email, setEmail] = useState("")
9+  const [password, setPassword] = useState("")
10+  const router = useRouter()
11+
12+  const handleSubmit = async (e: React.FormEvent) => {
13+    e.preventDefault()
14+    const res = await signIn("credentials", {
15+      email,
16+      password,
17+      redirect: false,
18+    })
19+
20+    if (res?.ok) {
21+      router.push("/")
22+    } else {
23+      alert("ログイン失敗")
24+    }
25+  }
26+
27+  return (
28+    <div className="flex items-center justify-center min-h-screen bg-gray-100">
29+      <form
30+        onSubmit={handleSubmit}
31+        className="bg-white p-8 rounded shadow-md w-full max-w-md"
32+      >
33+        <h2 className="text-2xl font-bold mb-6 text-center">サインイン</h2>
34+
35+        <div className="mb-4">
36+          <label className="block text-gray-700 mb-2">メールアドレス</label>
37+          <input
38+            type="email"
39+            placeholder="example@example.com"
40+            value={email}
41+            onChange={(e) => setEmail(e.target.value)}
42+            className="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
43+          />
44+        </div>
45+
46+        <div className="mb-6">
47+          <label className="block text-gray-700 mb-2">パスワード</label>
48+          <input
49+            type="password"
50+            placeholder="パスワード"
51+            value={password}
52+            onChange={(e) => setPassword(e.target.value)}
53+            className="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-400"
54+          />
55+        </div>
56+
57+        <button
58+          type="submit"
59+          className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 transition"
60+        >
61+          ログイン
62+        </button>
63+      </form>
64+    </div>
65+  )
66+}
67

/src/app/components/Header.tsx

/src/app/components/Header.tsx
1 
2 import { useState } from 'react'
3 import Link from 'next/link'
4+import { useSession, signOut } from 'next-auth/react'
5 
6 export default function Header() {
7     const [menuOpen, setMenuOpen] = useState(false)
8+    const { data: session, status } = useSession()
9 
10     return (
11         <header className="bg-white shadow-md">
12 
13                 {/* メニュー(PC用) */}
14                 <nav className="hidden md:flex items-center space-x-4 md:space-x-6">
15-                    <Link href="/">
16-                        <span className="cursor-pointer px-3 py-1 hover:text-blue-600">
17-                            Menu1
18-                        </span>
19-                    </Link>
20-                    <Link href="/">
21-                        <span className="cursor-pointer px-3 py-1 hover:text-blue-600">
22-                            Menu2
23-                        </span>
24-                    </Link>
25-                    <Link href="/">
26-                        <span className="cursor-pointer px-3 py-1 hover:text-blue-600">
27-                            Menu3
28-                        </span>
29-                    </Link>
30+                    {status === 'loading' ? (
31+                        <span className="ml-4">Loading...</span>
32+                    ) : session ? (
33+                        <Link
34+                            href="#"
35+                            onClick={(e) => {
36+                                e.preventDefault()
37+                                signOut({ callbackUrl: '/' })
38+                            }}
39+                            className="hover:underline cursor-pointer"
40+                        >
41+                            ログアウト
42+                        </Link>
43+                    ) : (
44+                        <>
45+                            <Link href="/auth/register" className="hover:underline cursor-pointer">
46+                                登録
47+                            </Link>
48+                            <Link href="/auth/signin" className="hover:underline cursor-pointer">
49+                                サインイン
50+                            </Link>
51+                        </>
52+                    )}
53                 </nav>
54             </div>
55 
56             {/* メニュー(スマホ用) */}
57             {menuOpen && (
58                 <nav className="md:hidden px-4 pb-4 space-y-2 border-t border-gray-300 text-gray-700 font-medium">
59-                    <Link href="/">
60-                        <span className="block py-2 hover:text-blue-600 cursor-pointer">
61-                            Menu1
62-                        </span>
63-                    </Link>
64-                    <Link href="/">
65-                        <span className="block py-2 hover:text-blue-600 cursor-pointer">
66-                            Menu2
67-                        </span>
68-                    </Link>
69-                    <Link href="/">
70-                        <span className="block py-2 hover:text-blue-600 cursor-pointer">
71-                            Menu3
72-                        </span>
73-                    </Link>
74+                    {status === 'loading' ? (
75+                        <span className="block py-2">Loading...</span>
76+                    ) : session ? (
77+                        <Link
78+                            href="#"
79+                            onClick={(e) => {
80+                                e.preventDefault()
81+                                signOut({ callbackUrl: '/' })
82+                                setMenuOpen(false)
83+                            }}
84+                            className="block py-2 hover:underline cursor-pointer"
85+                            tabIndex={0}
86+                        >
87+                            ログアウト
88+                        </Link>
89+                    ) : (
90+                        <>
91+                            <Link
92+                                href="/auth/register"
93+                                onClick={() => setMenuOpen(false)}
94+                                className="block py-2 hover:underline cursor-pointer"
95+                            >
96+                                登録
97+                            </Link>
98+                            <Link
99+                                href="/auth/signin"
100+                                onClick={() => setMenuOpen(false)}
101+                                className="block py-2 hover:underline cursor-pointer"
102+                            >
103+                                サインイン
104+                            </Link>
105+                        </>
106+                    )}
107                 </nav>
108             )}
109         </header>
110

/src/app/components/SessionWrapper.tsx

/src/app/components/SessionWrapper.tsx
1+"use client"
2+
3+import { SessionProvider } from "next-auth/react"
4+
5+export function SessionWrapper({ children }: { children: React.ReactNode }) {
6+  return <SessionProvider>{children}</SessionProvider>
7+}
8

/src/app/layout.tsx

/src/app/layout.tsx
1 import type { Metadata } from "next";
2 import { Geist, Geist_Mono } from "next/font/google";
3 import "./globals.css";
4+import { SessionWrapper } from "./components/SessionWrapper";
5 import Header from '@/app/components/Header'
6 import Footer from '@/app/components/Footer'
7 
8       <body
9         className={`${geistSans.variable} ${geistMono.variable} antialiased`}
10       >
11-        <Header />
12+        <SessionWrapper>
13+          <Header />
14           {children}
15-        <Footer />
16+          <Footer />
17+        </SessionWrapper>
18       </body>
19     </html>
20   );
21

コード解説

変更点: 認証機能に必要なライブラリの追加

/package.json
1   "dependencies": {
2+    "@auth/prisma-adapter": "^2.9.1",
3+    "@next-auth/prisma-adapter": "^1.0.7",
4     "@prisma/client": "^6.8.2",
5+    "bcrypt": "^6.0.0",
6     "next": "15.3.2",
7+    "next-auth": "^4.24.11",
8     "prisma": "^6.8.2",
9     "react": "^19.0.0",
10   "devDependencies": {
11+    "@types/bcrypt": "^5.0.2",

ユーザー認証機能を実装するために必要なライブラリをインストールしています。

  • next-auth: Next.jsで認証機能を簡単に追加できるライブラリです。ログイン、ログアウト、セッション管理などを担当します。
  • @auth/prisma-adapter: NextAuthがデータベース(Prisma経由)と連携するためのアダプターです。ユーザー情報などをDBに保存・管理するために使用します。
  • bcrypt: パスワードを安全に暗号化(ハッシュ化)するためのライブラリです。ユーザーのパスワードをそのままデータベースに保存するのは危険なため、このライブラリで変換してから保存します。
  • @types/bcrypt: bcryptをTypeScriptで安全に使うための型定義ファイルです。

変更点: データベースにユーザー情報を保存するテーブルを追加

/prisma/migrations/20250518034446_add_user_model/migration.sql
1+-- CreateTable
2+CREATE TABLE `users` (
3+    `id` INTEGER NOT NULL AUTO_INCREMENT,
4+    `name` VARCHAR(191) NOT NULL,
5+    `email` VARCHAR(191) NULL,
6+    `password` VARCHAR(191) NOT NULL,
7+    `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
8+    `updatedAt` DATETIME(3) NOT NULL,
9+
10+    UNIQUE INDEX `users_name_key`(`name`),
11+    UNIQUE INDEX `users_email_key`(`email`),
12+    PRIMARY KEY (`id`)
13+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

データベースに users という名前のテーブルを作成するためのSQL文です。このテーブルに、登録されたユーザーの情報(ID、名前、メールアドレス、暗号化されたパスワードなど)が保存されます。Prismaが schema.prisma ファイルの定義をもとに自動生成したものです。

変更点: PrismaのスキーマにUserモデルを定義

/prisma/schema.prisma
1+}
2+
3+model User {
4+  id        Int      @id @default(autoincrement())
5+  name      String   @unique
6+  email     String?  @unique
7+  password  String
8+  createdAt DateTime @default(now())
9+  updatedAt DateTime @updatedAt
10+
11+  @@map("users")
12 }

Prismaがデータベースの構造を理解するための定義ファイル(スキーマ)に、Userモデルを追加しています。これは、アプリケーションのコード内で「ユーザー」というデータを扱うための設計図のようなものです。idnameemailpasswordなどのカラム(フィールド)を定義しており、これが実際のデータベースのusersテーブルに対応します。

変更点: NextAuthの認証設定を追加

/src/app/api/auth/[...nextauth]/route.tsx
1+import NextAuth, { NextAuthOptions } from "next-auth"
2+import CredentialsProvider from "next-auth/providers/credentials"
3+import { PrismaAdapter } from "@auth/prisma-adapter"
4+import { prisma } from "@lib/prisma"
5+import bcrypt from "bcrypt"
6+
7+export const authOptions: NextAuthOptions = {
8+  adapter: PrismaAdapter(prisma),
9+  providers: [
10+    CredentialsProvider({
11+      name: "Credentials",
12+      credentials: {
13+        email: { label: "Email", type: "text" },
14+        password: { label: "Password", type: "password" }
15+      },
16+      async authorize(credentials) {
17+        if (!credentials) return null
18+
19+        const user = await prisma.user.findUnique({
20+          where: { email: credentials.email }
21+        })
22+        if (!user) return null
23+
24+        const isValid = await bcrypt.compare(credentials.password, user.password)
25+        if (!isValid) return null
26+
27+        return { ...user, id: String(user.id) }
28+      }
29+    })
30+  ],
31+  session: { strategy: "jwt" },
32+  pages: {
33+    signIn: "/auth/signin",
34+  },
35+}
36+
37+const handler = NextAuth(authOptions)
38+
39+export { handler as GET, handler as POST }

NextAuthの心臓部となる設定ファイルです。[...nextauth]というフォルダ構成は、NextAuthが認証に関するすべてのリクエスト(例: /api/auth/signin)を処理するための特別なルールです。

  • CredentialsProviderは、メールアドレスとパスワードによる認証方法(ログインフォーム)を使うことを指定しています。
  • authorize関数内でログイン処理のロジックを定義します。入力されたメールアドレスでデータベースからユーザーを探し、見つかった場合はbcrypt.compareで入力されたパスワードとデータベースに保存されている暗号化されたパスワードが一致するかを検証します。
  • 検証が成功すればユーザー情報を返し、ログイン成功となります。
  • PrismaAdapterを指定することで、NextAuthがユーザー情報をPrisma経由でデータベースに保存するようになります。
  • pages: { signIn: "/auth/signin" }は、ログインが必要なページにアクセスした際にリダイレクトさせるカスタムログインページのパスを指定しています。

変更点: ユーザー新規登録APIエンドポイントの作成

/src/app/api/auth/register/route.tsx
1+import { NextResponse } from "next/server"
2+import { PrismaClient } from "@prisma/client"
3+import bcrypt from "bcrypt"
4+
5+const prisma = new PrismaClient()
6+
7+export async function POST(req: Request) {
8+  const { email, password, name } = await req.json()
9+
10+  const existingUser = await prisma.user.findUnique({ where: { email } })
11+  if (existingUser) {
12+    return NextResponse.json({ error: "既に存在するユーザーです" }, { status: 400 })
13+  }
14+
15+  const hashedPassword = await bcrypt.hash(password, 10)
16+
17+  const user = await prisma.user.create({
18+    data: {
19+      email,
20+      name,
21+      password: hashedPassword,
22+    },
23+  })
24+
25+  return NextResponse.json({ user })
26+}

ユーザーの新規登録処理を行うためのAPIを作成しています。このファイルは/api/auth/registerというURLでアクセスできます。 処理の流れは以下の通りです。

  1. 登録フォームから送信された名前、メールアドレス、パスワードを受け取ります。
  2. 同じメールアドレスのユーザーが既に存在しないかデータベースで確認します。
  3. bcrypt.hashを使って、受け取ったパスワードを安全なハッシュ値(暗号化された文字列)に変換します。
  4. ハッシュ化されたパスワードを含むユーザー情報をデータベースに保存します。
  5. 登録が完了したら、作成されたユーザー情報を返します。

変更点: ユーザー新規登録ページの作成

/src/app/auth/register/page.tsx
1+"use client"
2+
3+import { useState } from "react"
4+import { useRouter } from "next/navigation"
5+
6+export default function RegisterPage() {
7+  const [email, setEmail] = useState("")
8+  const [password, setPassword] = useState("")
9+  const [name, setName] = useState("")
10+  const router = useRouter()
11+
12+  const handleRegister = async (e: React.FormEvent) => {
13+    e.preventDefault()
14+    const res = await fetch("/api/auth/register", {
15+      method: "POST",
16+      headers: { "Content-Type": "application/json" },
17+      body: JSON.stringify({ email, password, name }),
18+    })
19+
20+    if (res.ok) {
21+      router.push("/auth/signin")
22+    } else {
23+      const { error } = await res.json()
24+      alert(error)
25+    }
26+  }
27+
28+  return (
29+    // ... フォームのJSX ...
30+  )
31+}

ユーザーが情報を入力して新規登録するためのUI(ユーザーインターフェース)ページです。"use client"は、このコンポーネントがブラウザ上で動作することを示します。

  • ReactのuseStateフックを使って、ユーザーがフォームに入力した名前、メールアドレス、パスワードをコンポーネントの状態として管理します。
  • 「登録」ボタンがクリックされるとhandleRegister関数が実行されます。
  • fetchを使い、先ほど作成した/api/auth/register APIに対して、入力された情報をPOSTメソッドで送信します。
  • 登録が成功したら、router.pushでログインページ(/auth/signin)に画面遷移させます。

変更点: ログインページの作成

/src/app/auth/signin/page.tsx
1+"use client"
2+
3+import { signIn } from "next-auth/react"
4+import { useState } from "react"
5+import { useRouter } from "next/navigation"
6+
7+export default function SignInPage() {
8+  const [email, setEmail] = useState("")
9+  const [password, setPassword] = useState("")
10+  const router = useRouter()
11+
12+  const handleSubmit = async (e: React.FormEvent) => {
13+    e.preventDefault()
14+    const res = await signIn("credentials", {
15+      email,
16+      password,
17+      redirect: false,
18+    })
19+
20+    if (res?.ok) {
21+      router.push("/")
22+    } else {
23+      alert("ログイン失敗")
24+    }
25+  }
26+
27+  return (
28+    // ... フォームのJSX ...
29+  )
30+}

ユーザーがメールアドレスとパスワードを入力してログインするためのUIページです。

  • useStateで入力されたメールアドレスとパスワードを管理します。
  • 「ログイン」ボタンがクリックされるとhandleSubmit関数が実行されます。
  • NextAuthが提供するsignIn関数を呼び出します。第一引数の"credentials"は、NextAuthの設定で定義したメールアドレスとパスワードによる認証方法を使うことを指定しています。
  • redirect: falseを指定することで、ログイン試行後のページ遷移を手動で制御できます。
  • signIn関数は内部で/api/auth/[...nextauth]/route.tsxにリクエストを送り、認証処理を行います。
  • ログインが成功した場合(res.okがtrue)、router.pushでトップページ(/)に遷移させます。

変更点: ログイン状態に応じてヘッダーの表示を切り替え

/src/app/components/Header.tsx
1 import Link from 'next/link'
2+import { useSession, signOut } from 'next-auth/react'
3 
4 export default function Header() {
5-    const [menuOpen, setMenuOpen] = useState(false)
6+    const { data: session, status } = useSession()
7 
8     return (
9         <header className="bg-white shadow-md">
10                 <nav className="hidden md:flex items-center space-x-4 md:space-x-6">
11-+                    {status === 'loading' ? (
12-+                        <span className="ml-4">Loading...</span>
13-+                    ) : session ? (
14++                    {status === 'loading' ? ( <span className="ml-4">Loading...</span> ) : session ? (
15+                        <Link href="#" onClick={(e) => { e.preventDefault(); signOut({ callbackUrl: '/' }) }}>
16+                            ログアウト
17+                        </Link>
18+                    ) : (
19+                        <>
20+                            <Link href="/auth/register">登録</Link>
21+                            <Link href="/auth/signin">サインイン</Link>
22+                        </>
23+                    )}
24                 </nav>

Webサイトのヘッダー部分を、ユーザーのログイン状態に応じて動的に変更する実装です。

  • NextAuthが提供するuseSessionフックを使い、現在のセッション情報(ログイン状態)を取得します。
  • statusは現在の状態(loading, authenticated, unauthenticated)を示します。
  • sessionオブジェクトが存在すればログインしている状態、存在しなければ未ログインの状態と判断できます。
  • 三項演算子を使って条件分岐を行い、
    • ログイン中(sessionが存在する)の場合は「ログアウト」リンクを表示します。このリンクをクリックするとsignOut関数が実行され、ログアウト処理が行われます。
    • 未ログインの場合は「登録」と「サインイン」のリンクを表示します。

変更点: アプリ全体でセッション情報を共有するためのコンポーネント作成

/src/app/components/SessionWrapper.tsx
1+"use client"
2+
3+import { SessionProvider } from "next-auth/react"
4+
5+export function SessionWrapper({ children }: { children: React.ReactNode }) {
6+  return <SessionProvider>{children}</SessionProvider>
7+}

アプリケーション全体でログイン状態などのセッション情報を共有するための準備をしています。

  • NextAuthが提供するSessionProviderというコンポーネントを使います。
  • ReactのContextという仕組みを利用しており、このSessionProviderでラップされたコンポーネント(ここではchildren)は、どこからでもuseSessionフックを使ってセッション情報にアクセスできるようになります。
  • これをSessionWrapperという別のコンポーネントに切り出すことで、layout.tsxでの記述をすっきりとさせることができます。

変更点: アプリケーションのルートにSessionProviderを配置

/src/app/layout.tsx
1 import "./globals.css";
2+import { SessionWrapper } from "./components/SessionWrapper";
3 import Header from '@/app/components/Header'
4 
5 export default function RootLayout({
6   children,
7 }: Readonly<{
8   children: React.ReactNode;
9 }>) {
10   return (
11     <html lang="ja">
12       <body className={...}>
13-        <Header />
14-          {children}
15-        <Footer />
16+        <SessionWrapper>
17+          <Header />
18+          {children}
19+          <Footer />
20+        </SessionWrapper>
21       </body>
22     </html>
23   );
24 }

すべてのページの共通レイアウトを定義するlayout.tsxファイルで、先ほど作成したSessionWrapperコンポーネントを使用しています。アプリケーション全体(Headerchildrenなど)をSessionWrapperで囲むことで、どのページでもuseSessionフックが機能するようになり、ログイン状態の取得が可能になります。これにより、ヘッダーコンポーネントでの表示切り替えなどが正しく動作するようになります。

おわりに

今回はNextAuthとPrismaを利用して、Webアプリケーションに必須となるユーザー認証機能を実装しました。ユーザー登録APIではbcryptでパスワードを安全にハッシュ化してデータベースに保存し、NextAuthの認証設定でログイン時にそれを検証する一連の流れを学びました。さらに、useSessionフックでセッション情報を取得し、ログイン状態に応じてヘッダーの表示を動的に切り替える実践的なUIの作り方も解説しました。

関連コンテンツ

関連IT用語