Closed12

Firebaseで認証機能(Authentication)で会員情報データベース(Cloud Firestore)機能をNext.js アプリに導入するメモ

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

Firebase プロジェクトの作成

Firebase プロジェクトの全体感

いくつかあるFirebaseのサービスのうち以下を使います。

  • Firebase
    • Authentication: 認証(アカウント作成やログインなど)
    • Firestore: データベース
    • Cloud Storage: ファイル保管(画像など)

ログイン時のメール送信機能を実装したい場合
例えばSendgridというサービスを動かしたい場合
FirebaseのCloud Functionを用いて連携させるかもしれません。

FirebaseのAuthenticationの準備

FirebaseのAuthenticationの機能を利用するためにはFirebaseでユーザアカウント登録を行う必要があります。

Googleアナリティクスは使わないのでOFFにする。

設定して「プロジェクトを作成」ボタンをクリック。

プロジェクトの準備完了

ログイン方法はメールとパスワードを選択

詳細は

https://support.google.com/firebase/answer/7000714

メールとパスワードだけ有効にした。

参考

prefixで識別できるといいかも

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

Next.js から Firebaseのサービスに接続するための認証情報を作成

Webの場合これ

アプリの登録を行うためニックネームの設定

Firebaseに接続するための情報が表示される

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

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へのアップロードとダウンロード
志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

Next.js から Firebaseのサービスに接続する

Firebase JavaScript SDK をインストール

ターミナル
# Firebase JavaScript SDK
yarn add firebase

# Firebase Admin SDK
yarn add firebase-admin -D

https://github.com/ryosuketter/personal-blog/commit/ed11a29f80eded7ad3784cf6680819395fc5affb

https://github.com/ryosuketter/personal-blog/commit/da83c76bacee624b5cac044c56caea08341c7b36

.env に認証情報を登録

認証情報はプロジェクト設定画面下部のコード部分に記載されています。

該当箇所

.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 を作成し、以下の内容を記述します。

src/lib/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 コンソールに入って

新しい秘密鍵の生成 をクリック

認証情報を環境変数に記述

.env.local
FIREBASE_ADMIN_KEY=JSONの内容を一行にして設置

VScode使っている場合は、 cmd + shft + Pjoin lineコマンドを実行すれば一行にして設置できます。

接続する処理

src/lib/firebase/server.ts

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()

すでに初期されている場合、再度初期化してエラーが起きるリスクを防止するために、すでに、アプリが存在していれば、初期化しない処理を入れます。

src/lib/firebase/server.ts

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()

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

認証コンテクストの作成

ログイン状態や、ログインユーザーの情報をコンテクストにしてアプリケーション全体に適応させる処理を書きます。

コンテクストをどこに書くか?

グローバルな状態管理を入れておくような認識で、features/store/フォルダをsrc配下に作ります。

src/features/stores/context/auth.tsx
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というコンポーネントを作成しています。

コンポーネント内ではコンテクストに中身をセットした上でプロバイダーで供給を行なっています。

参考

https://note.com/ryoppei/n/n2e3e7a66e758

コンテクストのデータを受け取る処理

最後の行に追加してください。

src/features/stores/context/auth.tsx
export const useAuth = () => useContext(AuthContext)
志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

認証コンテクスト(プロバイダー)を使用する処理

コンテクストを使える状態にするために作成したプロバイダーコンポーネントを _app.tsx に設置します。

pages/_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>
  )
}

これによりアプリ内のあらゆるコンポーネントにコンテクストのデータを供給できるようになります。

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

Firestoreにセキュリティルールを設定する

Cloud Firestore データベースを作成する

https://firebase.google.com/docs/firestore/quickstart?hl=ja

ルールを設定する

今回は以下の二つのリクエストについて許可される必要があります。

  • ログインしたユーザーのデータを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)ことが許可されており、他のユーザーの情報にはアクセスできない、というルールを定義しています。

参考

https://zenn.dev/nino_cast/books/43c539eb47caab/viewer/6f3c96

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

Firebase Storageの作成

画像の保存先となるStorageを作成します。

Firestoreのリージョンをすでに決めているので、Storageも同じリージョンで作成されるそうです。

ルールの管理

ここでルールを設定できます。

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

サインアップの機能の実装

メールアドレスとパスワードを使った認証方法で実装します。

Firebase Authenticationでメールアドレスとパスワードを使った認証を行う場合、createUserWithEmailAndPasswordを使います。

https://firebase.google.com/docs/auth/web/password-auth?hl=ja

node_modules/@firebase/auth/dist/auth-public.d.ts
...
/**
 * 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を持っています。

node_modules/@firebase/auth/dist/auth-public.d.ts
...
/**
 * 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して書きます。

https://github.com/ryosuketter/personal-blog/commit/bfbd5ba920e4c0bcd2f864f86c0ba2bdd452dffa

注意

updateProfileを使用してFirebaseのユーザープロファイルにdisplayNameを設定すると、その情報はFirebase Authのバックエンドサーバーに保存されます。しかし、この情報は直接Firebase Authの管理画面(Firebase Console)からは見ることができません。

Firebase Consoleのユーザー情報セクションでは、ユーザーのメールアドレス、パスワード、プロバイダデータ(ソーシャルログイン情報など)、作成日と最終ログイン日時などが表示されますが、displayNameやphotoURLといった詳細なユーザープロファイル情報は表示されません。

これらの情報にアクセスするには、コードを通じてFirebase Auth APIを利用する必要があります。以下のような形でアクセスすることができます:

ログインの機能の実装

これもサインアップと似ています。

https://github.com/ryosuketter/personal-blog/commit/bfbd5ba920e4c0bcd2f864f86c0ba2bdd452dffa

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

Firebase認証とfirestoreの連携

サインアップ機能とフォームの作成

Firebase Authenticationに保存できた。

Firestore(ユーザー情報入れるところ)にもユーザーIDを連携して格納できた。

ログイン機能とフォームの作成

フォームの作成に関して

フォームの作成やバリデーションの実装はReact Hook FormZodを用いました。

公式がわかりやすかったので、ぜひみてみてください。

https://react-hook-form.com/

https://zod.dev/

他には

https://youtu.be/f1fysEKNwQA

志水 亮介 (Ryosuke Shimizu)志水 亮介 (Ryosuke Shimizu)

現状の問題点

新規ユーザー作成時にFirestoreのセキュリティルールがパスしないはずです。なぜなら、Cloud Firestore のメソッドaddDocの操作が認証プロセス完了前に行われているからと考えられます。

それに対処するためには、新規ユーザーが認証され、メール確認メールが送信された後にのみ、ユーザー情報をFirestoreに保存するようにすることです。

方法は < AuthProvider />内のuseEffectに、ログイン状態を監視し、変化があったら発動するようにする。ログインしていた場合、ユーザーコレクションからユーザーデータを参照し、ユーザーデータを取得して格納する。そうでないなら、新規作成して格納する。このとき、ユーザーIDは取得できるはずなのでuidこのデータは格納するようにしましょう。

ここができれば、あとは、会員情報を作成、更新、削除するページや機能くらいでしょう。ここまで作れれば、あとは作りやすいと思います。

参考

https://zenn.dev/nino_cast/books/43c539eb47caab/viewer/90a2a8

このスクラップは2023/06/05にクローズされました