1. HOME
  2. ブログ
  3. エンジニアリング
  4. 【Next.js】Auth.js v5で認証機能を実装

BLOG

ブログ

エンジニアリング

【Next.js】Auth.js v5で認証機能を実装

本章では、SNSアプリに認証機能を導入します。

Next.jsで認証機能を実装する際に人気のライブラリであるAuth.jsを使います。

チュートリアルの全体像

本チュートリアルを通じて学べる内容は以下のようになります。

  1. Next.jsの開発環境をセットアップ
  2. 画面デザインしながら基礎を学ぼう
  3. App Routerを理解して使ってみよう
  4. Next.js+Postgres+PrismaでDB設計
  5. サーバコンポーネントでデータ取得
  6. サーバアクションでデータ変更
  7. Vercel Blobで画像アップロード実装
  8. Suspenseでローディングをリッチに
  9. Zodを使ってバリデーション実装
  10. useActionStateでローディング実装
  11. Auth.js v5で認証機能導入
  12. 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を設計してクライアントから呼び出す方法を解説します。

  1. この記事へのコメントはありません。

  1. この記事へのトラックバックはありません。

関連記事