はじめに
この記事での達成事項
- 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のライブラリをインストール
npm install next-auth
npm install firebase
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に持たせたい情報を自分好みに変更しよう
参考