0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js(App Router)× NextAuth × Firebase Authenticationの認証を実装する

Last updated at Posted at 2025-01-25

はじめに

この記事での達成事項

  • NextAuthとFirebase Authenticationを使って、Login、Logoutを実装
  • ログイン状態による画面遷移制御の実装
  • ユーザIDをSessionに格納し、クライアントコンポーネントとサーバーコンポーネントのどちらもから、Session内のユーザIDを取り出せるように実装

前提

  • Next.jsの環境設定ができている
  • Firebaseのプロジェクトが作成できている
  • この記事は、npx create-next-app@latestを実行した直後の状態から開始しています

各ライブラリのバージョン

package.json
{
  "name": "next-auth_firebase-auth",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "firebase": "^11.2.0",
    "firebase-admin": "^13.0.2",
    "next": "15.1.5",
    "next-auth": "^4.24.11",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

最終成果物

↓↓↓以下、手順↓↓↓

NextAuth、Firebaseのライブラリをインストール

[1] NextAuthをインストール

npm install next-auth

[2] Firebase SDKのインストール

npm install firebase

[3] Firebase Admin SDKのインスール

npm install firebase-admin

※Next.jsは、クライアントサイドとサーバーサイドがあるので、Firebase Admin SDKも必要となる

Firebaseライブラリの設定

[1] .envへ環境変数の設定

.env
# Firebase SDK
# ※「プロジェクトの設定」→「全般」から取得
NEXT_PUBLIC_FIREBASE_API_KEY=<apiKey>
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=<authDomain>
NEXT_PUBLIC_FIREBASE_PROJECT_ID=<projectId>
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=<storageBucket>
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=<messagingSenderId>
NEXT_PUBLIC_FIREBASE_APP_ID=<appId>

# Firebase Admin SDK
# ※「プロジェクトの設定」→「サービスアカウント」→「新しい秘密鍵を作成」から取得
FIREBASE_CLIENT_EMAIL=<client_email>
FIREBASE_PRIVATE_KEY=<private_key>
NEXTAUTH_SECRET=//`openssl rand -base64 32`コマンドで作成したものを入れます

[2] Firebase SDK

src/lib/firebase/firebase.ts
import { initializeApp } from "firebase/app";
import type { FirebaseOptions } from "firebase/app";

const firebaseConfig: FirebaseOptions = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

export const firebaseApp = initializeApp(firebaseConfig);

[3] Firebase Admin SDK

src/lib/firebase/firebase-auth.ts
import * as admin from "firebase-admin";
import type { ServiceAccount } from "firebase-admin";

const cert: ServiceAccount = {
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  clientEmail: process.env.NEXT_PUBLIC_FIREBASE_CLIENT_EMAIL,
  privateKey: process.env.NEXT_PUBLIC_FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
};

const firebaseAdmin =
  admin.apps[0] ??
  admin.initializeApp({
    credential: admin.credential.cert(cert),
  });

export const firebaseAdminAuth = firebaseAdmin.auth();

参考

Next Authの設定

[1] SessionProviderをルートパスのLayout.tsxに記載

src/app/layout.tsx
"use client"

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

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <SessionProvider>      
          {children}
        </SessionProvider>   
      </body>
    </html>
  ); 
}

[2] NextAuthとFirebase Authの連携のコア部分の実装

lib/auth.ts
import type { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { firebaseAdminAuth } from './firebase/firebase-admin'

const fetchNewIdToken = async (refreshToken: string) => {
  const res = await fetch(
    `https://securetoken.googleapis.com/v1/token?key=${process.env.NEXT_PUBLIC_FIREBASE_API_KEY}`,
    {
      method: 'POST',
      body: JSON.stringify({
        grant_type: 'refresh_token',
        refreshToken,
      }),
    },
  )

  const { id_token } = await res.json()

  return id_token
}

export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      credentials: {},
      // @ts-ignore:理由を書く
      authorize: async ({ idToken, refreshToken }) => {
        if (idToken && refreshToken) {
          try {
            const decoded = await firebaseAdminAuth.verifyIdToken(idToken) // 2

            const user = {
              id: decoded.user_id,
              uid: decoded.uid,
              name: decoded.name || '',
              email: decoded.email || '',
              image: decoded.picture || '',
              emailVerified: decoded.email_verified || false,
              idToken,
              refreshToken,
              tokenExpiryTime: decoded.exp || 0,
            }

            return user
          } catch (err) {
            console.error(err)
          }
        }
        return null
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.uid = user.id
        token.name = user.name ?? ''
        token.emailVerified = !!user.emailVerified
        token.idToken = user.idToken
        token.refreshToken = user.refreshToken
        token.image = user.image ?? ''
        token.tokenExpiryTime = user.tokenExpiryTime
      }

      const currentTime = Math.floor(Date.now() / 1000)
      const tokenExpiryTime = token.tokenExpiryTime as number
      const isExpired = currentTime > tokenExpiryTime - 300 // 5分前には更新するようにする

      if (isExpired) {
        try {
          const newIdToken = await fetchNewIdToken(token.refreshToken as string)
          token.idToken = newIdToken
        } catch (error) {
          console.error('Error refreshing token:', error)
        }
      }

      return token
    },
    async session({ session, token }) {
      // sessionにFirebase Authenticationで取得した情報を追加。
      session.user.emailVerified = token.emailVerified
      session.user.uid = token.uid
      session.user.name = token.name
      session.user.image = token.image || ''
      session.user.email = token.email || ''
      session.user.emailVerified = token.emailVerified

      return session
    },
  },
  session: {
    strategy: 'jwt',
    maxAge: 90 * 24 * 60 * 60, // 90 days
  },
  pages: {
    signIn: '/',
  },
  secret: process.env.NEXTAUTH_SECRET,
}

[3] NextAuthのSessionとUserの型定義を行う

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

declare module "next-auth" {
    interface User extends DefaultUser{
        id: string;
        uid: string;
        name: string;
        email: string;
        image: string;
        emailVerified: boolean;
        idToken: string;
        refreshToken: string;
        tokenExpiryTime: number;
    }

    interface Session extends DefaultSession{
        user: {
            id: string;
            uid: string;
            name: string;
            email: string;
            image: string;
            emailVerified: boolean;
        } & DefaultSession['user']
    } 
}

declare module "next-auth/jwt" {
  interface JWT extends DefaultJWT{
    id: string;
    uid: string;
    name: string;
    email: string;
    image: string;
    emailVerified: boolean;
    idToken: string;
    refreshToken: string;
    tokenExpiryTime: number;
  }
}

[4] 以下のファイルを作成する

app/api/auth/[...nextauth]/route.ts
import { authOptions } from '@/lib/firebase/auth'
import NextAuth from 'next-auth'

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

参考

[1]

[2]

[3]

[4]

Login、Logoutの実装

以下のファイルを作成する

src/lib/firebase/firebase-auth.ts
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { firebaseAuth } from "./firebase";
import { signIn as signInWithNextAuth, signOut as signOutWithNextAuth } from "next-auth/react";

export function logInWithFirebaseAuth(){
    const provider = new GoogleAuthProvider();

    signInWithPopup(firebaseAuth, provider)
        .then(async ({user})=>{
            if(user){
                const refreshToken = user.refreshToken;
                const idToken = await user.getIdToken();
                await signInWithNextAuth("credentials", {
                    idToken,
                    refreshToken,
                    callbackUrl: `/protected-page`,    //ログイン後に遷移する画面の指定
                })
            }
        })
        .catch((error)=>{
            console.error("Error Sing In with Google", error)
        });
}

export function logOutWithFirebaseAuth(){
    firebaseAuth
    .signOut()
    .then(()=>{
        signOutWithNextAuth({callbackUrl: `/`});   //ログアウト後に遷移する画面の指
    })
    .catch((error)=>{
        console.error("Error Sign Out with Google", error)
    })
}

参考

ログインしてない人が見られるページの作成

[1] ログインしている人は別ページに飛ばすロジック

src/app/(public)/layout.tsx
import { authOptions } from "@/lib/auth"
import { getServerSession } from "next-auth"
import { redirect } from "next/navigation"
import { ReactElement } from "react"

const Layout = async ({children}:{children:ReactElement}) => {
  const session = await getServerSession(authOptions)

  if(session?.user) redirect(`/protected-page`)

  return <>{children}</>
  
}

export default Layout

[2] ログインページの作成

src/app/(public)/page.tsx
"use client"

import { logInWithFirebaseAuth } from "@/lib/firebase/firebase-auth"
import Link from "next/link"

const Page = () => {
  return (
    <div style={{display:"flex", flexDirection:"column", gap: 20}}>
        <div style={{fontSize:20, fontWeight:20}}>
            ログインページです
        </div>
        <Link href={`/protected-page`}>
          <button>
              "/protected-page"ページへのリンク(※ログインしてないためこのページに戻ってくるよ
          </button>
        </Link>
        <button onClick={logInWithFirebaseAuth} style={{height:"48px", padding: "8px"}}>
            Googleでログインする
        </button>
    </div>
  )
}

export default Page

ログインしている人が見られるページの作成

[1] ログインしてない人を弾くロジックの作成

src/app/(protected)/layout.tsx
import { authOptions } from "@/lib/auth"
import { getServerSession } from "next-auth"
import { redirect } from "next/navigation"
import { ReactElement } from "react"

const Layout = async ({children}:{children: ReactElement}) => {
    const session = await getServerSession(authOptions)

    if(!session?.user) redirect(`/`)

    return <>{children}</>
}

export default Layout

[2] ログインした人限定のぺーじをさくせ

src/app/(protected)/protected-page/page.tsx
"use client"

import { logOutWithFirebaseAuth } from "@/lib/firebase/firebase-auth"
import { useSession } from "next-auth/react";
import Link from "next/link"

const Page = () => {
  const {data:session} = useSession();

  return (
    <div style={{display:"flex", flexDirection:"column", gap: 20}}>  
        <div style={{fontSize: 20, fontWeight: 20}}>ログインした人限定のページ</div>
        <div>あなたの名前は{session?.user?.name}さんです</div>
        <div style={{display:"flex", flexDirection:"column"}}>
            <button>
                <Link href={`/`}>"/"ページへのリンク</Link>
            </button>
            (※ログインしているセッションが残っているためこのページに戻ってくるよ
        </div>
        <button onClick={logOutWithFirebaseAuth}>ログアウト</button>
    </div>
  )
}

export default Page

終わりに

ここまで見てくださりありがとうございます。

NextAuthを理解したいので、解説を加えたものをZennの方で投稿しようと思っています。

最後に「参考」で紹介している記事を元に実装したので、是非その方達の記事も参考にしてみてください。

次は、自分のアプリケーション好みに変えていこう

  • layout.tsxの認証ロジックは自分好みに変更しよう
  • NextAuthのコア部分で、sessionやtokenに持たせたい情報を自分好みに変更しよう

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?