【Next.js】Auth.js v5で認証機能を実装
本章では、SNSアプリに認証機能を導入します。
Next.jsで認証機能を実装する際に人気のライブラリであるAuth.jsを使います。
目次
チュートリアルの全体像
本チュートリアルを通じて学べる内容は以下のようになります。
- Next.jsの開発環境をセットアップ
- 画面デザインしながら基礎を学ぼう
- App Routerを理解して使ってみよう
- Next.js+Postgres+PrismaでDB設計
- サーバコンポーネントでデータ取得
- サーバアクションでデータ変更
- Vercel Blobで画像アップロード実装
- Suspenseでローディングをリッチに
- Zodを使ってバリデーション実装
- useActionStateでローディング実装
- Auth.js v5で認証機能導入
- Route HandlersでAPIを設計
動画で学びたい方はこちらから!
Auth.jsインストール
npm i next-auth@beta
環境変数設定
openssl rand -base64 32
出力されたキーを.envに貼り付けます。
.env
AUTH_SECRET=secret-key
Auth.js設定
auth.config.ts
import type { NextAuthConfig } from "next-auth";
const guestRoutes = ["/", "/login", "/register"];
export const authConfig = {
pages: {
signIn: "/login",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnMainPage = guestRoutes.every(
(guestRoute) => nextUrl.pathname !== guestRoute,
);
if (isOnMainPage) {
if (isLoggedIn) return true;
return false;
} else if (isLoggedIn) {
return Response.redirect(new URL("/dashboard", nextUrl));
}
return true;
},
},
providers: [],
} satisfies NextAuthConfig;
ミドルウェア
middleware.ts
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
export default NextAuth(authConfig).auth;
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$|.*\\.jpg$).*)"],
};
認証機能追加
auth.ts
import prisma from "@/lib/prisma";
import type { User } from "@prisma/client";
import bcrypt from "bcrypt";
import console from "console";
import NextAuth from "next-auth";
import credentials from "next-auth/providers/credentials";
import { z } from "zod";
import { authConfig } from "./auth.config";
async function getUser(email: string): Promise<User | undefined> {
try {
return prisma.user.findFirstOrThrow({ where: { email } });
} catch (error) {
console.error("Failed to fetch user:", error);
throw new Error("Failed to fetch user.");
}
}
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
credentials({
async authorize(credentials) {
const parsedCredentials = z
.object({
email: z.string().email(),
password: z.string().min(8),
})
.safeParse(credentials);
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) return user;
}
console.log("Invalid credentials");
return null;
},
}),
],
});
サーバアクション定義
lib/actions.ts
import { signIn } from "@/auth";
import prisma from "@/lib/prisma";
import { put } from "@vercel/blob";
import bcrypt from "bcrypt";
import { AuthError } from "next-auth";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
export async function authenticate(
state: string | undefined,
formData: FormData,
) {
try {
await signIn("credentials", formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return "メールアドレスまたはパスワードが正しくありません。";
default:
return "エラーが発生しました。";
}
}
throw error;
}
}
ログインフォーム
./components/pages/login/login-form.tsx
"use client";
import Loader from "@/components/loaders/loader";
import { authenticate } from "@/lib/actions";
import clsx from "clsx";
import Link from "next/link";
import { useActionState } from "react";
export default function LoginForm() {
const [error, action, isPending] = useActionState(authenticate, undefined);
return (
<form
action={action}
className={clsx("relative", { "opacity-20": isPending })}
>
{error && <p className="mb-4 text-xs text-red-500">{error}</p>}
<div>
<label className="block text-sm font-medium text-gray-700">
メールアドレス
</label>
<input
name="email"
type="email"
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required
/>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700">
パスワード
</label>
<input
type="password"
name="password"
className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required
/>
</div>
<div className="mt-8 flex items-center justify-end">
<Link
className="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
href="/register"
>
新規会員登録はこちら
</Link>
<button className="ml-4 inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900">
ログイン
</button>
</div>
{isPending && <Loader />}
</form>
);
}
ログアウト
import { signIn, signOut } from "@/auth";
export async function logout() {
await signOut();
redirect("/login");
}
./components/layouts/navigation-menu.tsx
import { logout } from "@/lib/actions";
<form action={logout}>
<button
type="submit"
className="block px-4 py-2 text-sm text-gray-700"
>
ログアウト 🐾
</button>
</form>
認証ユーザー取得
ダッシュボード
lib/apis.ts
import { auth } from "@/auth";
import prisma from "@/lib/prisma";
export async function fetchDashboard() {
const session = await auth();
if (!session?.user?.email) throw new Error("Invalid Request");
const email = session.user.email;
// 省略
}
プロフィール
lib/apis.ts
export async function fetchMe() {
const session = await auth();
if (!session?.user?.email) throw new Error("Invalid Request");
const email = session.user.email;
// 省略
}
まとめ
以上、Auth.jsを使ってNext.jsに認証を導入する方法についてでした!
次回は、APIを設計してクライアントから呼び出す方法を解説します。
この記事へのコメントはありません。