next-authでGoogleログイン機能を実装するまでの一連の流れ
2023/12/28

next-authでGoogleログイン機能を実装するまでの一連の流れ

7 mins to read

結構工程が多くて毎回忘れるのでまとめておきます。

next-authをインストールする

pnpm add next-auth

GCPからGoogle側の設定をする

GCPの管理画面のトップページから、APIとサービス->認証情報へ移動。

認証情報を作成からOAuthクライアントIDを選択する。

アプリケーションの種類を尋ねられるのでウェブアプリケーションを選択する。

承認済みのJavaScript生成元承認済みのリダイレクトURIを以下の様に設定する。

  • 承認済みのJavaScript生成元 ... ローカルホストのURL
  • 承認済みのリダイレクトURI ... ローカルホストのURL/api/auth/callback/google

例えばローカルホストのURLがhttp://localhost:3000の場合、

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

をそれぞれ設定。設定完了するとIDとSECRETが表示されるのでひかえて保存しておきます。

SECRETを発行

以下のコマンドをターミナルから実行する

openssl rand -base64 32

OpenSSLを使用して32バイトのランダムなデータを生成し、それをBase64エンコードされたものが文字列として出力されるので、こちらもひかえておく。

.envを編集する

.envファイルがない場合は作業ディレクトリ内に作成して、以下のように設定する

NEXTAUTH_URL=ローカルホストのURL
SECRET=先ほどOpenSSLで生成したSECRET
GOOGLE_CLIENT_ID=先ほどGCPでひかえたID
GOOGLE_CLIENT_SECRET=先ほどGCPでひかえたSECRET

next-authのオプションを設定する

lib->auth.tsを作成し、以下のように編集する

lib/auth.ts
// @ts-nocheck
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  session: { strategy: "jwt" },
  secret: process.env.SECRET,
};

prisma + データベースを使いたい場合

まずはprismaアダプタをインストールしてください

pnpm add @next-auth/prisma-adapter

オプションファイルを以下の様に書き換えてください。

lib/auth.ts
// @ts-nocheck
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/lib/prisma";

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  secret: process.env.SECRET,
  adapter: PrismaAdapter(prisma as any),
};

@/lib/prismaの内容は以下のようになっています。

lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = (globalThis as unknown) as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

その他データベース側の設定が必要になります。
PlanetScaleなどのデータベースの管理画面からDATABASE_URLを取得して.envに記述してください。

そして以下のようにschema.prismaを編集してマイグレーションを実行すればOKです。

generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
  relationMode = "prisma"
}

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

ターミナルからprisma db pushを実行して、データベースに反映させてください。

APIルート作成

pages->api->auth->[...nextauth].tsを以下の様に設定

pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth";

import { authOptions } from "@/lib/auth";

export default NextAuth(authOptions);

LayoutルートをProviderで囲う

まずはNextAuthProviderコンポーネントを作成します

providers/next-auth-provider.tsx
"use client";

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

type Props = {
  children?: React.ReactNode;
};

export const NextAuthProvider = (props: Props) => {
  return <SessionProvider>{props.children}</SessionProvider>;
};

これで全体を囲みます

app/layout.tsx
import { NextAuthProvider } from "@/providers/next-auth-provider";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja" suppressHydrationWarning>
      <body suppressHydrationWarning>
        <NextAuthProvider>
          <div>
            {children}
          </div>
        </NextAuthProvider>
      </body>
    </html>
  );
}

これでnext-authを使う準備は完了です🥳

実践: ログインボタンとログアウトボタンを作ってみる

"use client";

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

export const LoginButton = () => {
  return (
    <button onClick={() => signIn("google")}>
      ログイン
    </button>
  );
};

export const LogoutButton = () => {
  return (
    <button onClick={() => signOut()}>
      ログアウト
    </button>
  );
};

実践2: ログインしている場合、ユーザーのステータスを表示するコンポーネントを作ってみる

"use client";

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

export const LoginStatus = () => {
  const { data: session, status } = useSession();
  if (status === "authenticated") {
    return (
      <div className="flex items-center gap-2">
        {session?.user?.image ? (
          <img
            src={session?.user?.image}
            decoding="async"
            className="h-[30px] w-[30px] rounded-full object-cover"
          />
        ) : null}
        <div className="flex flex-col">
          <span className="text-sm">{session?.user?.name}.</span>
          <span className="text-xs">{session?.user?.email}</span>
        </div>
      </div>
    );
  }
  return null;
};

おまけ: 型の拡張

例えばユーザーごとにアカウント名を設定できるようにしたい場合は、userテーブルにaccountNameを追加するようにprismaのスキーマを修正してから、以下のようにオプションファイルを変更します。

lib/auth.ts
// @ts-nocheck
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/lib/prisma";

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  secret: process.env.SECRET,
  adapter: PrismaAdapter(prisma as any),
  callbacks: {
    session: ({ session, token, user }) => {
      session.user.accountName = user.accountName;
      return session;
    },
  },
};

すると型のエラーが発生するので以下のように型の拡張を行います。

types/next-auth.d.ts
import NextAuth from "next-auth";

declare module "next-auth" {
  /**
   * Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
   */
  interface Session {
    user: {
      accountName: string;
    };
  }
}

以上です✨