Invalid Date

Next.js App RouterでFacebookログインを実装する

本記事では、Next.jsのApp Routerを使用したアプリケーションに、next-auth (Auth.js) を利用してFacebookログイン機能を実装する手順を解説します。また、実装中に遭遇する可能性のある一般的な問題とその解決策についても触れます。

導入

ユーザー認証は多くのWebアプリケーションにとって不可欠な機能です。ソーシャルログインは、ユーザーが新しいアカウントを作成する手間を省き、既存のソーシャルメディアアカウントで簡単にサインインできるようにするため、ユーザーエクスペリエンスを向上させます。ここでは、Facebookログインを例に、その実装方法を詳細に見ていきます。

前提条件

実装ステップ

1. next-auth と Prisma Adapter のインストール

まず、認証ライブラリである next-auth と、Prismaと連携するためのアダプターをインストールします。

npm install next-auth @next-auth/prisma-adapter

2. Facebookアプリの作成と設定

Facebookログインを実装するには、Facebook開発者サイトで新しいアプリを作成し、必要な情報を設定する必要があります。

  1. Facebook for Developersにアクセス: https://developers.facebook.com/ にアクセスし、Facebookアカウントでログインします。
  2. 新しいアプリを作成: 「マイアプリ」から「アプリを作成」を選択し、アプリの種類(例: 「コンシューマー」)を選択します。アプリの表示名と連絡先メールアドレスを入力してアプリを作成します。
  3. App ID と App Secret の確認: アプリのダッシュボードの「設定」→「ベーシック」で、「アプリID」と「アプリシークレット」を確認します。これらは後で環境変数として使用します。
  4. Facebookログインの設定: 左側のナビゲーションメニューから「製品」→「製品を追加」を選択し、「Facebookログイン」を設定します。 「Facebookログイン」→「設定」で、「有効なOAuthリダイレクトURI」に以下のURLを追加します。
    • 開発環境: http://localhost:3000/api/auth/callback/facebook
    • 本番環境: https://your-domain.com/api/auth/callback/facebook (デプロイ後に確定したURL)
  5. アプリの公開: アプリを「ライブモード」に切り替えることで、テストユーザー以外もログインできるようになります。ただし、プライバシーポリシーのURLなど、追加の設定が必要になる場合があります。

3. 環境変数の設定

プロジェクトのルートディレクトリに .env.local ファイルを作成し、以下の環境変数を追加します。

# NextAuth.js
NEXTAUTH_URL=http://localhost:3000 # 本番環境ではデプロイ後のURLに変更
NEXTAUTH_SECRET=YOUR_NEXTAUTH_SECRET # openssl rand -base64 32 で生成した秘密鍵

# Facebook OAuth
FACEBOOK_ID=YOUR_FACEBOOK_APP_ID
FACEBOOK_SECRET=YOUR_FACEBOOK_APP_SECRET

NEXTAUTH_SECRET は、openssl rand -base64 32 コマンドで生成できます。

4. Prismaスキーマの更新

next-auth がユーザー、アカウント、セッション、検証トークンを管理するために必要なモデルを prisma/schema.prisma に追加します。

// prisma/schema.prisma

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  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 {
  // 既存のUserモデルのフィールド
  id            String    @id @default(cuid())
  email         String    @unique
  name          String
  password      String? // next-authでパスワードを使用しない場合、Optionalにする
  role          Role      @default(USER)
  favoriteJobs  Job[]     @relation("UserFavoriteJobs")
  forumPosts    ForumPost[]
  jobs          Job[]     @relation("UserJobs")
  comments      Comment[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  // next-authで追加するフィールド
  emailVerified DateTime?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

スキーマを更新したら、Prismaマイグレーションを実行してデータベースを更新します。

npx prisma migrate dev --name add_nextauth_models

もし既存のデータがある場合、name フィールドが必須で password がOptionalになることでエラーが発生する可能性があります。その場合は、npx prisma migrate reset でデータベースをリセットするか、マイグレーションファイルを調整してデフォルト値を設定してください。

5. next-auth の設定ファイル (src/app/api/auth/[...nextauth]/route.ts) の作成

App Routerでは、app/api/auth/[...nextauth]/route.ts に認証ロジックを記述します。

// src/app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import FacebookProvider from 'next-auth/providers/facebook';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import prisma from '@/lib/prisma'; // Prismaクライアントのインポート

const handler = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    FacebookProvider({
      clientId: process.env.FACEBOOK_ID as string,
      clientSecret: process.env.FACEBOOK_SECRET as string,
    }),
  ],
  secret: process.env.NEXTAUTH_SECRET,
  session: {
    strategy: 'jwt', // JWTセッションを使用
  },
  callbacks: {
    async session({ session, token, user }) {
      // セッションにユーザーIDとロール、名前、メールアドレスを追加
      if (token) {
        session.user.id = token.sub;
      }
      if (session.user.id) {
        const dbUser = await prisma.user.findUnique({
          where: { id: session.user.id },
          select: { role: true, name: true, email: true },
        });
        if (dbUser) {
          session.user.role = dbUser.role;
          session.user.name = dbUser.name;
          session.user.email = dbUser.email;
        }
      }
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.sub = user.id;
      }
      return token;
    },
  },
  pages: {
    signIn: '/login',
  },
});

export { handler as GET, handler as POST };

6. 型定義の拡張

next-auth のデフォルトの Session 型には、id, role, name などのカスタムフィールドが含まれていません。これらを拡張するために、プロジェクトのルートに next-auth.d.ts ファイルを作成します。

// next-auth.d.ts
import { DefaultSession, DefaultUser } from "next-auth";
import { DefaultJWT } from "next-auth/jwt";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      role: string; // Role enumの型に合わせる
      name: string;
      email: string;
    } & DefaultSession["user"];
  }

  interface User extends DefaultUser {
    id: string;
    role: string; // Role enumの型に合わせる
    name: string;
    email: string;
  }
}

declare module "next-auth/jwt" {
  interface JWT extends DefaultJWT {
    id: string;
    role: string; // Role enumの型に合わせる
    name: string;
    email: string;
  }
}

7. ログインページへのFacebookログインボタンの追加

ログインページ (src/app/[locale]/login/page.tsx) にFacebookログインボタンを追加し、next-auth/reactsignIn 関数を使用して認証フローを開始します。

// src/app/[locale]/login/page.tsx
'use client';

import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import { signIn } from 'next-auth/react'; // signInをインポート

// ... (既存のRegisterLinkコンポーネントとLoginPageのコード)

const LoginPage: React.FC<{ params: Promise<{ locale: string }> }> = ({ params }) => {
  // ... (既存のstateとuseEffect)

  const handleSubmit = async (e: React.FormEvent) => {
    // ... (既存のメール/パスワードログインロジック)
  };

  const handleFacebookLogin = () => {
    signIn('facebook', { callbackUrl: `/${locale}/` }); // Facebookログイン
  };

  return (
    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
      <h1 className="text-2xl font-bold mb-6 text-center">{t('login_title')}</h1>
      <form onSubmit={handleSubmit} className="space-y-4">
        {/* ... (メール/パスワード入力フィールドとログインボタン) */}
      </form>

      <div className="mt-4 text-center">
        <button
          onClick={handleFacebookLogin}
          disabled={!locale}
          className="w-full flex justify-center items-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-400"
        >
          <svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
            <path fillRule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33V22C18.343 21.128 22 16.991 22 12z" clipRule="evenodd" />
          </svg>
          {t('login_with_facebook')}
        </button>
      </div>

      {locale && <RegisterLink locale={locale} t={t} />}
    </div>
  );
};

export default LoginPage;

8. SessionProvider の設定

next-auth のセッションを使用するために、アプリケーション全体を SessionProvider でラップする必要があります。これは通常、ルートレイアウト (src/app/[locale]/layout.tsx) で行います。

// src/app/[locale]/layout.tsx
// ... (既存のインポート)
import { SessionProvider } from 'next-auth/react'; // 追加

export default async function RootLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}>) {
  // ... (既存のデータ取得ロジック)

  return (
    <html lang={locale} dir={dir(locale)} suppressHydrationWarning>
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} suppressHydrationWarning>
        <I18nProvider locale={locale} namespaces={['common']}>
          <SessionProvider> {/* 追加 */}
            <Header languages={languages} session={session} />
            <main className="container mx-auto px-6 py-8">{children}</main>
          </SessionProvider> {/* 追加 */}
        </I18nProvider>
      </body>
    </html>
  );
}

トラブルシューティング

client_id is required エラー

React Hook "useTranslation" cannot be called in an async function エラー

APIルートの型エラー (Type "{ params: { id: string; }; }" is not a valid type for the function's second argument.)

まとめ

本記事では、Next.js App RouterでFacebookログインを実装するための詳細な手順と、実装中に遭遇する可能性のある一般的なエラーの解決策を解説しました。next-auth を活用することで、OAuth認証フローを簡素化し、安全な認証システムを構築できます。

Facebookログインを実装することで、ユーザーは既存のFacebookアカウントで簡単にサインインできるようになり、アプリケーションの利便性が向上します。


← Previous Entry: Next Entry:
← Back to Blog List