Next.jsでAuth.js認証を実装する

投稿日:

更新日:

はじめに

Auth.js v4を使ってNext.jsでログイン機能を実装してみます。

下準備

必要なパッケージをインストールする

Next.jsをインストールします。

ターミナル
npx create-next-app@latest next-auth-demo

✔ Would you like to use the recommended Next.js defaults? › No, customize settings
✔ Would you like to use TypeScript? … Yes
✔ Which linter would you like to use? › ESLint
✔ Would you like to use React Compiler? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No

Auth.jsとreact-iconsをインストールします。

react-iconsはログインボタンにソーシャルアイコンを設定するためのものなので、任意で大丈夫です。

ターミナル
npm install next-auth react-icons

shadcn/uiを初期化します。

ターミナル
npx shadcn@latest init

簡易的なページ作成のためにshadcn/uiのコンポーネントを追加します。

ターミナル
npx shadcn@latest add button card avatar

.env.localを追加します。

ターミナル
touch .env.local

.env.localには以下のように記載しておきます。

実際にサイトを公開するときは、NEXTAUTH_URLにサイトのURLを設定してください。

.env.local
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=

NEXTAUTH_SECRETに設定するシークレットキーは、以下のコマンドを実行して生成した値を設定します。

NEXTAUTH_SECRETを途中で変更するとJWT複合エラーになるので、変えないこと。

ターミナル
openssl rand -base64 32

ページを作成する

src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

export const authOptions = {
  providers: [
    GitHub({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
    Google({
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    }),
  ],
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
src/app/login/page.tsx
import LoginButtons from "@/components/LoginButtons";

export default async function LoginPage({ searchParams }: { searchParams: Promise<{ callbackUrl?: string }> }) {
  const { callbackUrl } = await searchParams;

  return (
    <div className="max-w-sm space-y-4">
      <h1 className="text-xl font-bold">ログイン</h1>
      <LoginButtons callbackUrl={callbackUrl ?? "/"} />
    </div>
  );
}
src/app/mypage/page.tsx
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";

export default async function MyPage() {
  const session = await getServerSession(authOptions);

  if (!session) {
    redirect("/login?callbackUrl=%2Fmypage");
  }

  const user = session.user;

  return (
    <div className="space-y-4">
      <h1 className="text-xl font-bold">マイページ</h1>

      <Card>
        <CardHeader>
          <CardTitle>アカウント情報</CardTitle>
        </CardHeader>
        <CardContent className="flex items-center gap-4">
          <Avatar>
            <AvatarImage src={user?.image ?? ""} alt={user?.name ?? ""} />
            <AvatarFallback>{(user?.name ?? user?.email ?? "U").slice(0, 1).toUpperCase()}</AvatarFallback>
          </Avatar>

          <div className="space-y-1">
            <p className="font-medium">{user?.name ?? "(名前なし)"}</p>
            <p className="text-sm text-muted-foreground">{user?.email ?? "(メールなし)"}</p>
          </div>
        </CardContent>
      </Card>
    </div>
  );
}
src/app/layout.tsx
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import LogoutButton from "@/components/LogoutButton";
import { getServerSession } from "next-auth";
import Link from "next/link";
import "./globals.css";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const session = await getServerSession(authOptions);

  return (
    <html lang="ja">
      <body className="min-h-dvh">
        <header className="border-b">
          <div className="mx-auto flex max-w-4xl items-center justify-between gap-4 p-4">
            <Link href="/" className="font-bold">
              Auth Test
            </Link>

            <nav className="flex items-center gap-3 text-sm">
              <Link href="/" className="underline">
                Top
              </Link>
              {session ? (
                <>
                  <Link href="/mypage" className="underline">
                    MyPage
                  </Link>
                  <LogoutButton />
                </>
              ) : (
                <>
                  <Link href="/login" className="underline">
                    Login
                  </Link>
                  <Link href="/mypage" className="underline">
                    MyPage
                  </Link>
                </>
              )}
            </nav>
          </div>
        </header>

        <main className="mx-auto max-w-4xl p-6">{children}</main>
      </body>
    </html>
  );
}
src/app/page.tsx
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getServerSession } from "next-auth";
import Link from "next/link";

export default async function Home() {
  const session = await getServerSession(authOptions);

  return (
    <div className="space-y-4">
      <h1 className="text-xl font-bold">Top</h1>

      {session ? (
        <Link href="/mypage" className="underline">
          マイページへ
        </Link>
      ) : (
        <Link href="/login" className="underline">
          ログインへ
        </Link>
      )}
    </div>
  );
}
src/components/LoginButtons.tsx
"use client";

import { Button } from "@/components/ui/button";
import { signIn } from "next-auth/react";
import { FaGithub } from "react-icons/fa";
import { FcGoogle } from "react-icons/fc";

type Props = {
  callbackUrl: string;
};

export default function LoginButtons({ callbackUrl }: Props) {
  return (
    <div className="space-y-2">
      <Button variant={"outline"} className="w-full" onClick={() => signIn("github", { callbackUrl })}>
        <FaGithub />
        GitHubでログイン
      </Button>

      <Button variant={"outline"} className="w-full" onClick={() => signIn("google", { callbackUrl })}>
        <FcGoogle />
        Googleでログイン
      </Button>
    </div>
  );
}
src/components/LogoutButton.tsx
"use client";

import { signOut } from "next-auth/react";

export default function LogoutButton() {
  return (
    <button type="button" className="underline bg-transparent p-0 text-sm" onClick={() => signOut({ callbackUrl: "/" })}>
      ログアウト
    </button>
  );
}

各プロバイダの設定

GitHub

  1. GitHubのSettingsを開きます。
  2. メニューの一番下にあるDeveloper settingsを開きます。
  3. メニューのOAuth Appsを開きます。
  4. New OAuth Appを選択します。
  5. 以下を参考に各項目を設定してください。
    以下はローカルで作成する際の例ですが、公開するサイトの場合は実際のドメインに変更してください。

    Application name

    任意のアプリ名

    Homepage URL

    http://localhost:3000

    Authorization callback URL

    http://localhost:3000/api/auth/callback/github

  6. Register applicationを選択します。
  7. Generate a new client secretをクリックし、シークレットキーを発行します。

Client IDとClient secretsが表示されるので、コピーして.env.localに設定してください。

.env.local
GITHUB_ID=Ov23liBzquFYuEDCT7Za
GITHUB_SECRET=d3615cbaa51cf317ad2810f98f09cc4e13b56e85

Google

  1. Google Cloudにアクセスして、新しいプロジェクトを作成します。
    https://console.cloud.google.com/projectcreate
  2. ダッシュボードの「APIとサービス > 認証情報」を開きます。
  3. 「認証情報を作成 > OAuth クライアント ID」を選択します。
  4. 「アプリケーションの種類」を「ウェブ アプリケーション」にし、「名前」に任意の名前を設定します。
  5. 「承認済みの JavaScript 生成元」にはパスなしのURL、「承認済みのリダイレクト URI」にはAPIのURIを設定します。
    以下はローカルで作成する際の例ですが、公開するサイトの場合は実際のドメインに変更してください。

    承認済みの JavaScript 生成元

    http://localhost:3000

    承認済みのリダイレクト URI

    http://localhost:3000/api/auth/callback/google

  6. 「作成」を選択します。

クライアント IDとクライアント シークレットが表示されるので、コピーして.env.localに設定してください。

.env.local
GOOGLE_ID=123456789012-xxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx

実際に作成したサイト

この記事をシェアする