Firebaseで認証機能(Authentication)で会員情報データベース(Cloud Firestore)機能をNext.js アプリに導入するメモ
ゴール
全体感の共有
技術スタック
- Next.js:UI実装
- Firebase Authentication:ログイン認証
- Firebase Cloud Firestore:データベース
- React Hook Form:フォーム作成ライブラリ
- Zod:バリデーションライブラリ
コード
Firebase プロジェクトの作成
Firebase プロジェクトの全体感
いくつかあるFirebaseのサービスのうち以下を使います。
- Firebase
- Authentication: 認証(アカウント作成やログインなど)
- Firestore: データベース
- Cloud Storage: ファイル保管(画像など)
ログイン時のメール送信機能を実装したい場合
例えばSendgridというサービスを動かしたい場合
FirebaseのCloud Functionを用いて連携させるかもしれません。
FirebaseのAuthenticationの準備
FirebaseのAuthenticationの機能を利用するためにはFirebaseでユーザアカウント登録を行う必要があります。
Googleアナリティクスは使わないのでOFFにする。
設定して「プロジェクトを作成」ボタンをクリック。
プロジェクトの準備完了
ログイン方法はメールとパスワードを選択
詳細は
メールとパスワードだけ有効にした。
参考
- firebaseのプロジェクトは開発用と本番用があるといいらしい。
prefixで識別できるといいかも
Next.js から Firebaseのサービスに接続するための認証情報を作成
Webの場合これ
アプリの登録を行うためニックネームの設定
Firebaseに接続するための情報が表示される
Firebase SDKについて
Firebaseとアプリを連携するにはSDKと呼ばれるツールを使います。SDKには二種類あります。
Firebase SDKの種類
- Firebase Admin SDK
- Firebase JavaScript SDK
それぞれ Firebaseの機能を利用するための異なるSDK(Software Development Kit)であり、それぞれ異なる目的と使用状況に対応しています。
名前 | アクセス元 | 権限 |
---|---|---|
Firebase JavaScript SDK | クライアントサイド(ブラウザ) | 限定的 |
Firebase Admin SDK | サーバーサイド(Next.jsのgetStaticProps内) | 無制限 |
JavaScript SDKとAdmin SDKの主な違いは、その使用環境(クライアントサイドとサーバーサイド)と、それに伴うアクセス制御とセキュリティの仕組みです。
Next.jsはクライアント処理とサーバー処理の二つが混在するので両方のSDKを初期化する必要があります。
Firebase Admin SDK
Firebase Admin SDKは、主にサーバーサイドのアプリケーションで利用されます。これにより、開発者はFirebaseの機能をサーバーから直接操作することが可能となります。
主な用途
- Firebase Realtime DatabaseやFirestoreへの完全な読み書きアクセス
- Firebase Authによるユーザ管理(ユーザの作成、削除、認証情報の更新など)
- サーバサイドでのCloud Storageの操作
Admin SDKはセキュリティルールをバイパスするため、その使用は信頼できる環境(たとえば、クラウドのサーバーサイドアプリケーションや、管理者のみがアクセス可能な内部ツールなど)で行われるべきです。
Firebase JavaScript SDK
一方、Firebase JavaScript SDKはクライアントサイドのアプリケーション(WebブラウザやJavaScriptが動作する任意の環境)で利用されます。これにより、開発者はユーザーインターフェースからFirebaseの機能を直接利用することができます。主な機能は次のようなものがあります
主な用途
- Firestoreへの読み書きアクセス(ただし、Firebaseのセキュリティルールに従う必要があります)
- Firebase Authによるユーザ認証(ログイン、ログアウト、パスワードリセットなど)
- クライアントサイドでのCloud Storageへのアップロードとダウンロード
Next.js から Firebaseのサービスに接続する
Firebase JavaScript SDK をインストール
# Firebase JavaScript SDK
yarn add firebase
# Firebase Admin SDK
yarn add firebase-admin -D
.env に認証情報を登録
認証情報はプロジェクト設定画面下部のコード部分に記載されています。
該当箇所
REACT_APP_FIREBASE_APIKEY=""
REACT_APP_FIREBASE_AUTH_DOMAIN=""
REACT_APP_FIREBASE_PROJECT_ID=""
REACT_APP_FIREBASE_STORAGE_BUCKET=""
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=""
REACT_APP_FIREBASE_MESSAGING_APP_ID=""
Firebase JavaScript SDKの接続
firebase/client.ts を作成し、以下の内容を記述します。
import { getApps, initializeApp } from 'firebase/app'
import { getAuth } from 'firebase/auth'
import { getFirestore } from 'firebase/firestore'
import { getStorage } from 'firebase/storage'
const firebaseConfig = {
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,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
}
if (!getApps()?.length) initializeApp(firebaseConfig)
export const db = getFirestore()
export const storage = getStorage()
export const auth = getAuth()
Firebase Admin SDKの接続
認証情報の取得
Firebase コンソールに入って
新しい秘密鍵の生成
をクリック
認証情報を環境変数に記述
FIREBASE_ADMIN_KEY=JSONの内容を一行にして設置
VScode
使っている場合は、 cmd + shft + P
で join line
コマンドを実行すれば一行にして設置できます。
接続する処理
import { cert, initializeApp } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
initializeApp({
credential: cert(JSON.parse(process.env.FIREBASE_ADMIN_KEY as string))
})
export const adminDB = getFirestore()
すでに初期されている場合、再度初期化してエラーが起きるリスクを防止するために、すでに、アプリが存在していれば、初期化しない処理を入れます。
import { cert, getApps, initializeApp } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
if (getApps()?.length === 0) {
initializeApp({
credential: cert(JSON.parse(process.env.FIREBASE_ADMIN_KEY as string))
})
}
export const adminDB = getFirestore()
認証コンテクストの作成
ログイン状態や、ログインユーザーの情報をコンテクストにしてアプリケーション全体に適応させる処理を書きます。
コンテクストをどこに書くか?
グローバルな状態管理を入れておくような認識で、features/store/フォルダをsrc配下に作ります。
import { onAuthStateChanged } from 'firebase/auth'
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { auth } from '@/lib/firebase/client'
type ContextType = {
isLoggedIn: boolean
isLoading: boolean
userName: string
}
const AuthContext = createContext<ContextType>({
isLoggedIn: false,
isLoading: false,
userName: ''
})
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [isLoading, setIsLoading] = useState(true)
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [userName, setUserName] = useState('')
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setIsLoggedIn(!!user)
setIsLoading(false)
setUserName(user?.displayName || '')
})
return () => unsubscribe()
}, [])
return <AuthContext.Provider value={{ isLoggedIn, isLoading, userName }}>{children}</AuthContext.Provider>
}
export const useAuth = () => useContext(AuthContext)
上記にてAuthProvider
というコンポーネントを作成しています。
コンポーネント内ではコンテクストに中身をセットした上でプロバイダーで供給を行なっています。
参考
コンテクストのデータを受け取る処理
最後の行に追加してください。
export const useAuth = () => useContext(AuthContext)
認証コンテクスト(プロバイダー)を使用する処理
コンテクストを使える状態にするために作成したプロバイダーコンポーネントを _app.tsx
に設置します。
import '../styles/global.scss'
import { Rubik } from '@next/font/google'
import type { AppProps } from 'next/app'
import { Layout } from '@/components/Layout'
import { AuthProvider } from '@/features/stores/context/auth'
const rubik = Rubik({
subsets: ['latin']
})
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<div>
<style jsx global>{`
html,
body {
font-family: ${rubik.style.fontFamily}, 'Noto Sans JP', 游ゴシック体, 'Yu Gothic', YuGothic,
'ヒラギノ角ゴ ProN W3', 'Hiragino Kaku Gothic ProN', メイリオ, Meiryo, 'Hiragino Sans', sans-serif;
}
`}</style>
<AuthProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AuthProvider>
</div>
)
}
これによりアプリ内のあらゆるコンポーネントにコンテクストのデータを供給できるようになります。
Firestoreにセキュリティルールを設定する
Cloud Firestore データベースを作成する
ルールを設定する
今回は以下の二つのリクエストについて許可される必要があります。
- ログインしたユーザーのデータをFirestoreから読み込む
- ユーザーデータが存在しない場合、Firestoreにユーザーデータを作成する
before
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
after
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
Firestore セキュリティルールは許可制なので、現状はログイン中のユーザーが自分のユーザーデータを読み書きする以外、一切のFirestore通信が認められていない状態です。
この具体的なルールの説明は以下の通りです
rules_version = '2';
使用するセキュリティルールのバージョンを指定しています。'2' は現時点で最新のバージョンです。
service cloud.firestore { ... }
ブロック
Firestore に対するルールを定義するブロックです。
match /databases/{database}/documents { ... }
ブロック
Firestore データベース内の全てのドキュメントに対するルールを定義します。ここで {database} は任意のデータベース ID を指します。
match /users/{uid} { ... }
ブロック
/users/{uid} パス(つまり、users コレクション内の任意のドキュメント)に対するルールを定義します。ここで {uid} は任意のユーザー ID を指します。
allow read, write: if request.auth != null && request.auth.uid == uid;
読み取りと書き込みの操作を許可する条件を定義しています。この条件は、「リクエストが認証済み(request.auth != null)で、かつ、リクエストを行ったユーザーの ID がドキュメントの ID(uid)と一致する場合」です。
つまり、各ユーザーは自分自身のユーザー情報を読み取る(read)ことと書き込む(write)ことが許可されており、他のユーザーの情報にはアクセスできない、というルールを定義しています。
参考
Firebase Storageの作成
画像の保存先となるStorageを作成します。
Firestoreのリージョンをすでに決めているので、Storageも同じリージョンで作成されるそうです。
ルールの管理
ここでルールを設定できます。
サインアップの機能の実装
メールアドレスとパスワードを使った認証方法で実装します。
Firebase Authenticationでメールアドレスとパスワードを使った認証を行う場合、createUserWithEmailAndPassword
を使います。
...
/**
* Creates a new user account associated with the specified email address and password.
*
* @remarks
* On successful creation of the user account, this user will also be signed in to your application.
*
* User account creation can fail if the account already exists or the password is invalid.
*
* Note: The email address acts as a unique identifier for the user and enables an email-based
* password reset. This function will create a new user account and set the initial user password.
*
* @param auth - The {@link Auth} instance.
* @param email - The user's email address.
* @param password - The user's chosen password.
*
* @public
*/
export declare function createUserWithEmailAndPassword(auth: Auth, email: string, password: string): Promise<UserCredential>;
...
コメント部分(ドキュメンテーション)の内容を解説します
- メールアドレスとパスワードを指定して新規ユーザーアカウントを作成します
- 新規アカウントの作成が成功すると、同時にそのユーザーがアプリケーションにサインインします
- 以下の場合、ユーザーアカウントの作成は失敗します。
- アカウントが既に存在する
- パスワードが無効な場合
- メールアドレスはユーザーの一意の識別子として機能し、メールベースのパスワードリセットを可能にします
- この関数は新しいユーザーアカウントを作成し、初期ユーザーパスワードを設定します
関数の引数
- auth: Auth インスタンス。これは Firebase の Authentication サービスのインスタンスです。
- email: ユーザーのメールアドレス。
- password: ユーザーが選んだパスワード。
関数の戻り値
- Promise<UserCredential> を返します。これは非同期操作(新規ユーザーアカウントの作成)の結果を表します。成功すると、新しく作成されたユーザーアカウントの情報を含む UserCredential オブジェクトを解決します。失敗すると、エラー情報を拒否します。
返り値のUserCredentialは、userとproviderIdとoperationTypeを持っています。
...
/**
* A structure containing a {@link User}, the {@link OperationType}, and the provider ID.
*
* @remarks
* `operationType` could be {@link OperationType}.SIGN_IN for a sign-in operation,
* {@link OperationType}.LINK for a linking operation and {@link OperationType}.REAUTHENTICATE for
* a reauthentication operation.
*
* @public
*/
export declare interface UserCredential {
/**
* The user authenticated by this credential.
*/
user: User;
/**
* The provider which was used to authenticate the user.
*/
providerId: string | null;
/**
* The type of operation which was used to authenticate the user (such as sign-in or link).
*/
operationType: (typeof OperationType)[keyof typeof OperationType];
}
...
このインターフェースは UserCredential と呼ばれ、Firebase Authenticationのような認証システムで使用されるユーザーの認証情報を表しています。
このインターフェースには3つのプロパティが含まれています
- user: 認証されたユーザーを表すUser型のオブジェクトです
- providerId: ユーザーを認証するために使用されたプロバイダー(Google、Facebookなど)のIDを表す文字列です。null にもなり得ます
- operationType: ユーザー認証の種類を表す文字列です。OperationType の列挙型(
"link" | "reauthenticate" | "signIn"
)のいずれかであり、どの種類の認証操作を行ったかを示します
@publicというコメントは、このインターフェースがパブリックAPIの一部であることを示しています。そのため、このインターフェースは、他の開発者がライブラリまたはフレームワークを利用する際に使用できるという意味らしい。
createUserWithEmailAndPasswordを使ってサインインの機能を作成
前述の通り、createUserWithEmailAndPassword
は返り値がPromise
オブジェクトなのでasync
関数の中でawait
して書きます。
注意
updateProfileを使用してFirebaseのユーザープロファイルにdisplayNameを設定すると、その情報はFirebase Authのバックエンドサーバーに保存されます。しかし、この情報は直接Firebase Authの管理画面(Firebase Console)からは見ることができません。
Firebase Consoleのユーザー情報セクションでは、ユーザーのメールアドレス、パスワード、プロバイダデータ(ソーシャルログイン情報など)、作成日と最終ログイン日時などが表示されますが、displayNameやphotoURLといった詳細なユーザープロファイル情報は表示されません。
これらの情報にアクセスするには、コードを通じてFirebase Auth APIを利用する必要があります。以下のような形でアクセスすることができます:
ログインの機能の実装
これもサインアップと似ています。
Firebase認証とfirestoreの連携
サインアップ機能とフォームの作成
Firebase Authenticationに保存できた。
Firestore(ユーザー情報入れるところ)にもユーザーIDを連携して格納できた。
ログイン機能とフォームの作成
フォームの作成に関して
フォームの作成やバリデーションの実装はReact Hook Form
とZod
を用いました。
公式がわかりやすかったので、ぜひみてみてください。
他には
現状の問題点
新規ユーザー作成時にFirestoreのセキュリティルールがパスしないはずです。なぜなら、Cloud Firestore のメソッドaddDoc
の操作が認証プロセス完了前に行われているからと考えられます。
それに対処するためには、新規ユーザーが認証され、メール確認メールが送信された後にのみ、ユーザー情報をFirestoreに保存するようにすることです。
方法は < AuthProvider />
内のuseEffectに、ログイン状態を監視し、変化があったら発動するようにする。ログインしていた場合、ユーザーコレクションからユーザーデータを参照し、ユーザーデータを取得して格納する。そうでないなら、新規作成して格納する。このとき、ユーザーIDは取得できるはずなのでuid
このデータは格納するようにしましょう。
ここができれば、あとは、会員情報を作成、更新、削除するページや機能くらいでしょう。ここまで作れれば、あとは作りやすいと思います。
参考