next.js学習記録

Next.js×NextAuth認証機能を爆速で作る方法!コピペで即実装!

yuya_react

こんにちは、優也@月収100万を目指して月100時間勉強する男です。

Next.jsで認証機能を実装したいけど、複雑で時間がかかりそう…そんな悩みを解決します! この記事では、NextAuth.jsを使って最速で認証機能を実装する方法を解説します。

前提条件

  • Next.js 16のプロジェクトが作成済み
  • TypeScriptを使用
  • Vercel Postgresなどのデータベースが設定済み
  • usersテーブルにemail、passwordカラムが存在

実装手順

①NextAuthのインストール

まずは必要なパッケージをインストールします。

npm i next-auth@beta bcrypt zod
npm i -D @types/bcrypt

②環境変数の設定

.envファイルにAUTH_SECRETを追加します。\

AUTH_SECRET=your-secret-key-here

シークレットキーはこちらにアクセスすると自動生成できます。

③auth.config.tsを作成

プロジェクトルートにauth.config.tsを作成します。

import { NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";

export const authConfig = {
  pages: {
    signIn: "/login",
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnAuthenticatedPage = nextUrl.pathname.startsWith("/dashboard");

      if (isOnAuthenticatedPage) {
        if (isLoggedIn) return true;
        if (!isLoggedIn) {
          // falseを返すと,Signinページにリダイレクトされる
          return false;
        }
      } else if (isLoggedIn) {
        return Response.redirect(new URL("/dashboard", nextUrl));
      }

      return true;
    },
  },
  providers: [Credentials({})],
} satisfies NextAuthConfig;

このファイルでは以下を設定しています:

  • ログインページのパス(/login)
    • 認証が通っていない(authorizedのreturnがfalse)場合、このパスに戻ります。
  • 認証が必要なページ(/dashboard配下)へのアクセス制御(isOnAuthenticatedPage)
    • この配下へアクセスした場合、認証チェックが入ります。
  • ログイン済みユーザーが/loginにアクセスした場合、/dashboardへリダイレクト
    • ログイン済みのユーザーが再度ログインしなくて済むようUX向上

④proxy.tsを作成

重要①:srcフォルダを使っている場合は、srcフォルダ直下に配置してください。

重要②:next.js15以下の方は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$).*)"],
};

next.jsは、このproxyが全てのリクエストをインターセプトし、認証状態をチェックします。 matcherで静的ファイルやAPIルートは除外しています。

⑤auth.tsを作成

lib/auth.ts(またはsrc配下の場合はsrc/lib/auth.ts)を作成します。

import NextAuth from "next-auth";
import { authConfig } from "../auth.config";
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import bcrypt from "bcrypt";
import postgres from "postgres";

const sql = postgres(process.env.POSTGRES_URL!, { ssl: "require" });

async function getUser(email: string): Promise<any> {
  try {
    const user = await sql`
      SELECT * FROM users WHERE email = ${email}
    `;
    return user[0];
  } 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.email(), password: z.string().min(6) })
          .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;
        }
        return null;
      },
    }),
  ],
});

このファイルでは:

  • Zodでメール・パスワードのバリデーション
  • bcryptでパスワードの照合
  • データベースからユーザー情報を取得
  • signInsignOutauth関数をエクスポート

⑥Server Actionの作成

lib/action.tsを作成し、ログイン処理を実装します。

"use server";
import { signIn } from "./auth";
import { AuthError } from "next-auth";

export async function authenticate(
  prevState: string | undefined,
  formData: FormData,
) {
  try {
    const redirectTo = formData.get("redirectTo") as string;
    await signIn("credentials", {
      email: formData.get("email"),
      password: formData.get("password"),
      redirectTo: redirectTo || "/dashboard",
    });
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return "メールアドレスまたはパスワードが正しくありません";
        default:
          return "予期しないエラーが発生しました";
      }
    }
    throw error;
  }
}

⑦ログインフォームの作成

app/login/login-form.tsxを作成します。

"use client";
import { useActionState } from "react";
import { useSearchParams } from "next/navigation";
import { authenticate } from "../lib/action";

const LoginForm = () => {
  const searchParams = useSearchParams();
  const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
  const [errorMessage, formAction, isPending] = useActionState(
    authenticate,
    undefined,
  );
  
  return (
    <form action={formAction}>
      <h1 className="font-bold text-4xl">salon-reservations-system</h1>
      <div className="w-80">
        <div className="flex flex-col gap-2 mb-4">
          <label htmlFor="email" className="mb-2">
            email
          </label>
          <input
            name="email"
            type="text"
            placeholder="email"
            className="border-2 border-gray-300 rounded-md p-2"
          />

          <label htmlFor="password" className="mb-2">
            password
          </label>
          <input
            name="password"
            type="password"
            placeholder="password"
            className="border-2 border-gray-300 rounded-md p-2"
          />

          <input type="hidden" name="redirectTo" value={callbackUrl} />
        </div>

        {errorMessage && (
          <p className="text-sm text-red-500">{errorMessage}</p>
        )}
      </div>
      <button 
        className="bg-blue-500 text-white px-6 py-2 rounded-md hover:bg-blue-600"
        disabled={isPending}
      >
        {isPending ? "ログイン中..." : "Login"}
      </button>
    </form>
  );
};

export default LoginForm;

⑧Vercelへのデプロイ時の設定

Vercelにデプロイする際は、環境変数の設定を忘れずに!

  1. Vercelのダッシュボードでプロジェクトを選択
  2. Settings > Environment Variables
  3. AUTH_SECRETを追加(ローカルと同じ値)

まとめ

これでNextAuth.jsを使った認証機能の実装が完了です!

ポイント:

  • proxy.tsの配置場所に注意(srcフォルダ使用時)
  • 環境変数の設定を忘れずに
  • Server Actionsでフォーム送信を処理
  • useActionStateでローディング状態とエラーを管理

この実装パターンを使えば、10分程度で本格的な認証機能が実装できます。 ぜひプロジェクトに組み込んでみてください!

ABOUT ME
記事URLをコピーしました