🍣

Next.js(App Router)にNextAuth.jsを導入しGoogle認証とX(Twitter)ログインを実現する

2024/02/24に公開

はじめに

Next.js(App Router)NextAuth.jsを導入して、X(Twitter)と Google のログインを実装します。

今回、Firebase AuthではなくNextAuth.jsを利用しようとしたのは、いくつか理由がありますがZennの開発者であるcatnoseさんのポストが一因でFirebase Authの導入を諦めました🫠
※ 調べてみるとFirebase Auth単体でもログインは遅いらしいです。

NextAuth.jsの公式ドキュメントを読んでいると簡単に導入できるように見えますが、実際にはいくつかの注意点がありますので、備忘録として書いておきます。

※ 主にApp RouterとX(Twitter)ログインの仕様のせいでハマりました。

作るもの

作るものは至ってシンプルです。ログインページとトップページのみです。
ヘッダーには最初にログインボタンが表示されており、ログイン後には、ヘッダーにログインユーザーの情報とログアウトボタンを表示します。

作成する画面

ログイン前の TOP ページ

ログイン前のTOPページ

ログインページ

ログインページ

ログイン後の TOP ページ

ログイン後のTOPページ

手順

npx create-next-app@latestコマンドで作成した Next.js(App Router)のプロジェクト(すべてデフォルト設定)にNextAuth.jsを導入します。

npm install next-auth

JWT のシークレットを生成

opensslコマンドを使用して JWT のシークレットを生成します。

openssl rand -base64 32

.env.local の作成

zsh
touch .env.local

Twitter のクライアント ID とシークレットを取得

次のページを参考にして、Twitter のクライアント ID とシークレットを取得します。
Callback URIにはhttp://127.0.0.1:3000/api/auth/callback/twitterを設定します。

Twitter API の Key や Secret の取得・確認手順※2023 年 10 月最新 │Programming ZERO

Google のクライアント ID とシークレットを取得

次のページを参考にして、Google のクライアント ID とシークレットを取得します。
承認済みのリダイレクト URIにはhttp://127.0.0.1:3000/api/auth/callback/googleを設定します。

NextAuth.js で Next.js13 に Google 認証機能を実装

.env.local の設定

NEXTAUTH_SECRETにはopensslコマンドで生成した JWT のシークレットを設定します。

.env.local
NEXTAUTH_SECRET=YOUR_JWT_SECRET
NEXTAUTH_URL=http://127.0.0.1:3000
TWITTER_CLIENT_ID=YOUR_TWITTER_CLIENT_ID
TWITTER_CLIENT_SECRET=YOUR_TWITTER_CLIENT_SECRET
GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET

next.config.mjs の設定

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    // X(Twitter)とGoogleのプロフィール画像を表示するために追加
    domains: ["pbs.twimg.com", "lh3.googleusercontent.com"],
  },
};

export default nextConfig;

ヘッダーの作成

ヘッダーにはログインユーザーの情報を表示しますが、親コンポーネントからログインユーザーの情報を受け取るためにsessionを引数に取ります。
sessionnext-auth/reactuseSessionでも取得できますが、ページの初期描画時にuseSessionを使用すると、useSessionの初期化が完了するまでユーザー情報が描画されない(ログインボタンがチラついてしまう)ため、あえてサーバーコンポーネントからsessionを引数に取ることにします。

src/app/_components/header.tsx
"use client";

import Image from "next/image";
import Link from "next/link";
import { type Session } from "next-auth";
import { signOut } from "next-auth/react";

const Header = ({ session }: { session: Session | null }) => {
  return (
    <header className="flex items-center justify-between bg-white p-4 shadow-md">
      <div className="flex items-center">
        <Link href="/" className="text-4xl font-bold">
          あなたのイケてるサービス
        </Link>
      </div>
      <ul className="flex items-center space-x-4">
        {session ? (
          <>
            <li>
              <Image
                src={session.user?.image ?? ""}
                alt={session.user?.name ?? ""}
                width={40}
                height={40}
                className="rounded-full"
              />
            </li>
            <li>
              <button
                onClick={() => signOut()}
                className="rounded-lg bg-blue-500 px-4 py-[7px] text-white hover:bg-gray-600"
              >
                ログアウト
              </button>
            </li>
          </>
        ) : (
          <li>
            <Link href="/login">
              <button className="rounded-lg bg-blue-500 px-4 py-[7px] text-white hover:bg-gray-600">
                ログイン
              </button>
            </Link>
          </li>
        )}
      </ul>
    </header>
  );
};

export default Header;

レイアウトファイルの編集

NextAuth.jsgetServerSessionを使用してサーバーサイドでsessionを取得し、ヘッダーに渡します。
これにより、ページの初期描画時にログインユーザーの情報が表示されます。

src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

import { getServerSession } from "next-auth/next";
import NextAuthProvider from "@/app/providers";

import Header from "@/app/_components/header";
import { nextAuthOptions } from "@/app/_utils/next-auth-options";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const session = await getServerSession(nextAuthOptions);

  return (
    <html lang="ja">
      <body className={inter.className}>
        <NextAuthProvider>
          <Header session={session} />
          {children}
        </NextAuthProvider>
      </body>
    </html>
  );
}

NextAuth.js のオプションを設定

X(Twitter)と Google のプロバイダーを設定します。

src/app/_utils/next-auth-options.ts
import TwitterProvider from "next-auth/providers/twitter";
import GoogleProvider from "next-auth/providers/google";

import type { NextAuthOptions } from "next-auth";

export const nextAuthOptions: NextAuthOptions = {
  debug: true,
  session: { strategy: "jwt" },
  providers: [
    TwitterProvider({
      clientId: process.env.TWITTER_CLIENT_ID!,
      clientSecret: process.env.TWITTER_CLIENT_SECRET!,
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    jwt: async ({ token, user, account, profile }) => {
      // 注意: トークンをログ出力してはダメです。
      console.log("in jwt", { user, token, account, profile });

      if (user) {
        token.user = user;
        const u = user as any;
        token.role = u.role;
      }
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    session: ({ session, token }) => {
      console.log("in session", { session, token });
      token.accessToken;
      return {
        ...session,
        user: {
          ...session.user,
          role: token.role,
        },
      };
    },
  },
};

【参考】
Next.js 13 App Router での Auth.js の使い方

API ルートの作成

src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";

import { nextAuthOptions } from "@/app/_utils/next-auth-options";

const handler = NextAuth(nextAuthOptions);

export { handler as GET, handler as POST };

ログインページの作成

src/app/login/page.tsx
"use client";

import React from "react";
import { useEffect } from "react";

import { redirect } from "next/navigation";
import { signIn } from "next-auth/react";
import { useSession } from "next-auth/react";

const LoginPage = () => {
  const { data: session, status } = useSession();
  useEffect(() => {
    // ログイン済みの場合はTOPページにリダイレクト
    if (status === "authenticated") {
      redirect("/");
    }
  }, [session, status]);

  const handleLogin = (provider: string) => async (event: React.MouseEvent) => {
    event.preventDefault();
    const result = await signIn(provider);

    // ログインに成功したらTOPページにリダイレクト
    if (result) {
      redirect("/");
    }
  };

  return (
    <div className="flex min-h-screen items-center justify-center bg-gray-100">
      <form className="w-full max-w-xs space-y-6 rounded bg-white p-8 shadow-md">
        <button
          onClick={handleLogin("google")}
          type="button"
          className="w-full bg-red-500 text-white rounded-lg px-4 py-2"
        >
          Googleでログイン
        </button>
        <button
          onClick={handleLogin("twitter")}
          type="button"
          className="w-full bg-blue-500 text-white rounded-lg px-4 py-2"
        >
          Twitterでログイン
        </button>
      </form>
    </div>
  );
};

export default LoginPage;

ページの背景色を白に

src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}

TOP ページの作成

src/app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>TOPページだよ</h1>
    </main>
  );
}

プロバイダーの設定

src/app/providers.tsx
"use client";

import { type ReactNode } from "react";

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

export default function NextAuthProvider({
  children,
}: {
  children: ReactNode;
}) {
  return <SessionProvider>{children}</SessionProvider>;
}

動作確認

npm run devで開発サーバーを起動し、http://localhost:3000ではなく、http://127.0.0.1:3000にアクセスします。

Google と X(Twitter)両方ともログインできるようになり、ログイン後にユーザーアイコンが右上に表示されるようになったら動作確認は終了です。

ログイン前の TOP ページ

ログイン前のTOPページ

ログインページ

ログインページ

ログイン後の TOP ページ

ログイン後のTOPページ

お疲れ様でした!!!

記事中で紹介できなかった参考資料

Next.js | NextAuth.js

NextAuth.js で Twitter ログインを実装したものの、現在は使用できなくなってしまった話

Discussion