Quickstart: Firebase Auth

Firebase認証のクイックスタート

これは少し複雑なため、Firebase認証を動作させるために必要な手順をすべて詳細に説明したブログ記事 (opens in a new tab)を作成しました。

クイックスタートの概要については、以下をお読みください。

概要

ブラウザ拡張機能での認証は、ウェブ開発者にとって少し調整が必要な場合がありますが、ReactやNext.jsで直接Firebaseを使用するよりもそれほど難しくありません。

基本的に、トークンをCookieに保存して、バックグラウンドサービスワーカーで使用できるようにします。

セットアップ

最初に、いくつかの依存関係をインストールしましょう。Firebaseを使用し、PlasmoでCookieを操作するには、3つのパッケージが必要です。

npm install firebase @plasmohq/messaging @plasmohq/storage

または

yarn add firebase @plasmohq/messaging @plasmohq/storage

または

pnpm install firebase @plasmohq/messaging @plasmohq/storage

Firebaseの初期化

最初に、クライアントサイドFirebaseアプリのシングルトンインスタンスを作成できます。

// src/firebase/firebaseClient.ts
 
import { getApps, initializeApp } from "firebase/app"
import { GoogleAuthProvider, getAuth } from "firebase/auth"
import { getFirestore } from "firebase/firestore"
import { getStorage } from "firebase/storage"
 
const clientCredentials = {
  apiKey: process.env.PLASMO_PUBLIC_FIREBASE_PUBLIC_API_KEY,
  authDomain: process.env.PLASMO_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.PLASMO_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.PLASMO_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.PLASMO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.PLASMO_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.PLASMO_PUBLIC_FIREBASE_MEASUREMENT_ID
}
 
let firebase_app
 
// Check if firebase app is already initialized to avoid creating new app on hot-reloads
if (!getApps().length) {
  firebase_app = initializeApp(clientCredentials)
} else {
  firebase_app = getApps()[0]
}
 
export const storage = getStorage(firebase_app)
export const auth = getAuth(firebase_app)
export const db = getFirestore(firebase_app)
export const googleAuth = new GoogleAuthProvider()
 
export default firebase_app
👀

注意: Firebaseの設定を.envファイルに設定する必要があります。これはFirebaseコンソール (opens in a new tab)で見つけることができます。

PLASMO_PUBLIC_FIREBASE_CLIENT_ID=""
PLASMO_PUBLIC_FIREBASE_PUBLIC_API_KEY=""
PLASMO_PUBLIC_FIREBASE_AUTH_DOMAIN=""
PLASMO_PUBLIC_FIREBASE_PROJECT_ID=""
PLASMO_PUBLIC_FIREBASE_STORAGE_BUCKET=""
PLASMO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=""
PLASMO_PUBLIC_FIREBASE_APP_ID=""
PLASMO_PUBLIC_FIREBASE_MEASUREMENT_ID=""

トークンの処理

これで準備ができたので、ログインとログアウトの方法、およびタブ/ポップアップ/オプションページでユーザー情報に簡単にアクセスするための再利用可能なフックを作成できます。

// src/firebase/useFirebaseUser.tsx
 
import {
  type User,
  browserLocalPersistence,
  onAuthStateChanged,
  setPersistence
} from "firebase/auth"
import { useEffect, useState } from "react"
 
import { sendToBackground } from "@plasmohq/messaging"
 
import { auth } from "./firebaseClient"
 
setPersistence(auth, browserLocalPersistence)
 
export default function useFirebaseUser() {
  const [isLoading, setIsLoading] = useState(false)
  const [user, setUser] = useState<User>(null)
 
  const onLogout = async () => {
    setIsLoading(true)
    if (user) {
      await auth.signOut()
 
      await sendToBackground({
        name: "removeAuth",
        body: {}
      })
    }
  }
 
  const onLogin = () => {
    if (!user) return
 
    const uid = user.uid
 
    // Get current user auth token
    user.getIdToken(true).then(async (token) => {
      // Send token to background to save
      await sendToBackground({
        name: "saveAuth",
        body: {
          token,
          uid,
          refreshToken: user.refreshToken
        }
      })
    })
  }
 
  useEffect(() => {
    onAuthStateChanged(auth, (user) => {
      setIsLoading(false)
      setUser(user)
    })
  }, [])
 
  useEffect(() => {
    if (user) {
      onLogin()
    }
  }, [user])
 
  return {
    isLoading,
    user,
    onLogin,
    onLogout
  }
}

上記のコードスニペットでは、removeAuthsaveAuthという2つのバックグラウンドプロセスを呼び出していることに注目してください。これらを作成しましょう。

// background/messages/saveAuth.ts
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { Storage } from "@plasmohq/storage"
 
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
  try {
    const { token, uid, refreshToken } = req.body
    const storage = new Storage()
 
    await storage.set("firebaseToken", token)
    await storage.set("firebaseUid", uid)
    await storage.set("firebaseRefreshToken", refreshToken)
 
    res.send({
      status: "success"
    })
  } catch (err) {
    console.log("There was an error")
    console.error(err)
    res.send({ err })
  }
}
 
export default handler
// background/messages/removeAuth.ts
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { Storage } from "@plasmohq/storage"
 
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
  try {
    const storage = new Storage()
 
    await storage.set("firebaseToken", null)
    await storage.set("firebaseUid", null)
 
    res.send({
      status: "success"
    })
  } catch (err) {
    console.log("There was an error")
    console.error(err)
    res.send({ err })
  }
}
 
export default handler

バックグラウンドSWの使用例

素晴らしい、これで更新トークン、IDトークン、UIDをCookieに保存できました。このようにして、他のバックグラウンドワーカーで使用できます。

// 現在のサインインユーザーからコレクション /users/{uid} のユーザーデータを取得するバックグラウンドSWの例
 
import type { PlasmoMessaging } from "@plasmohq/messaging"
import { Storage } from "@plasmohq/storage"
 
import refreshFirebaseToken from "~util/refreshFirebaseToken"
 
const fetchUserData = async (
  token,
  uid,
  refreshToken,
  storage,
  retry = true
) => {
  const response = await fetch(
    "https://firestore.googleapis.com/v1/projects/<firebase_project_id>/databases/(default)/documents/users/" +
      uid,
    {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json"
      }
    }
  )
 
  const responseData = await response.json()
 
  if (responseData?.error?.code === 401 && retry) {
    const refreshData = await refreshFirebaseToken(refreshToken)
    await storage.set("firebaseToken", refreshData.id_token)
    await storage.set("firebaseRefreshToken", refreshData.refresh_token)
    await storage.set("firebaseUid", refreshData.user_id)
 
    // Retry request after refreshing token
    return fetchUserData(
      refreshData.id_token,
      uid,
      refreshData.refresh_token,
      storage,
      false
    )
  }
 
  return responseData
}
 
const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
  try {
    const storage = new Storage()
 
    const token = await storage.get("firebaseToken")
    const uid = await storage.get("firebaseUid")
    const refreshToken = await storage.get("firebaseRefreshToken")
 
    const userData = await fetchUserData(token, uid, refreshToken, storage)
 
    res.send({
      status: "success",
      data: userData
    })
  } catch (err) {
    console.log("There was an error")
    console.error(err)
    res.send({ err })
  }
}
 
export default handler

上記の例では、応答が失敗した場合にトークンを更新して再試行しています。Firebaseトークンを更新するためのユーティリティメソッドを次に示します。

type RefreshTokenResponse = {
  expires_in: string
  token_type: string
  refresh_token: string
  id_token: string
  user_id: string
  project_id: string
}
 
export default async function refreshFirebaseToken(
  refreshToken: string
): Promise<RefreshTokenResponse> {
  const response = await fetch(
    "https://securetoken.googleapis.com/v1/token?key=<firebase_api_key>",
    {
      method: "POST",
      body: JSON.stringify({
        grant_type: "refresh_token",
        refresh_token: refreshToken
      })
    }
  )
 
  const {
    expires_in,
    token_type,
    refresh_token,
    id_token,
    user_id,
    project_id
  } = await response.json()
 
  return {
    expires_in,
    token_type,
    refresh_token,
    id_token,
    user_id,
    project_id
  }
}

Reactフックの使用例

これはReactコンポーネント(CSUIまたはタブ/拡張機能ページのいずれか)でも使用できます。

TailwindCSSを使用したoptions.tsxのログインページ設定例を次に示します。

// src/options.tsx
import AuthForm from "~components/AuthForm"
 
import "./style.css"
 
import useFirebaseUser from "~firebase/useFirebaseUser"
 
export default function Options() {
  const { user, isLoading, onLogin } = useFirebaseUser()
 
  return (
    <div className="min-h-screen bg-black p-4 md:p-10">
      <div className="text-white flex flex-col space-y-10 items-center justify-center">
        {!user && <AuthForm />}
        {user && <div>You're signed in! Woo</div>}
      </div>
    </div>
  )
}
// src/components/AuthForm.tsx
 
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword
} from "firebase/auth"
import React, { useState } from "react"
 
import { auth } from "~firebase/firebaseClient"
import useFirebaseUser from "~firebase/useFirebaseUser"
 
export default function AuthForm() {
  const [showLogin, setShowLogin] = useState(true)
  const [email, setEmail] = useState("")
  const [confirmPassword, setConfirmPassword] = useState("")
  const [password, setPassword] = useState("")
  const { isLoading, onLogin } = useFirebaseUser()
 
  const signIn = async (e: any) => {
    if (!email || !password)
      return console.log("Please enter email and password")
 
    e.preventDefault()
    try {
      await signInWithEmailAndPassword(auth, email, password)
    } catch (error: any) {
      console.log(error.message)
    } finally {
      setEmail("")
      setPassword("")
      setConfirmPassword("")
      onLogin()
    }
  }
 
  const signUp = async (e: any) => {
    try {
      if (!email || !password || !confirmPassword)
        return console.log("Please enter email and password")
 
      if (password !== confirmPassword)
        return console.log("Passwords do not match")
 
      e.preventDefault()
 
      const user = await createUserWithEmailAndPassword(auth, email, password)
      onLogin()
    } catch (error: any) {
      console.log(error.message)
    } finally {
      setEmail("")
      setPassword("")
      setConfirmPassword("")
    }
  }
 
  return (
    <div className="flex items-center justify-center w-full p-4 overflow-x-hidden rounded-xl overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
      <div className="w-full max-w-2xl max-h-full">
        <div className="p-10 bg-white rounded-lg shadow">
          <div className="flex flex-row items-center justify-center">
            {!isLoading && (
              <span className="text-black font-bold text-3xl text-center">
                {showLogin ? "Login" : "Sign Up"}
              </span>
            )}
            {isLoading && (
              <span className="text-black font-bold text-3xl text-center">
                Loading...
              </span>
            )}
          </div>
 
          <div className="p-4 rounded-xl bg-white text-black">
            {!showLogin && !isLoading && (
              <form className="space-y-4 md:space-y-6" onSubmit={signUp}>
                <div>
                  <label
                    htmlFor="email"
                    className="block mb-2 text-sm font-medium text-gray-900">
                    Your email
                  </label>
                  <input
                    type="email"
                    name="email"
                    id="email"
                    onChange={(e) => setEmail(e.target.value)}
                    value={email}
                    className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
                    placeholder="name@company.com"
                    required
                  />
                </div>
                <div>
                  <label
                    htmlFor="password"
                    className="block mb-2 text-sm font-medium text-gray-900">
                    Password
                  </label>
                  <input
                    type="password"
                    name="password"
                    id="password"
                    onChange={(e) => setPassword(e.target.value)}
                    value={password}
                    placeholder="••••••••"
                    className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
                    required
                  />
                </div>
                <div>
                  <label
                    htmlFor="confirm-password"
                    className="block mb-2 text-sm font-medium text-gray-900">
                    Confirm password
                  </label>
                  <input
                    type="password"
                    name="confirm-password"
                    id="confirm-password"
                    onChange={(e) => setConfirmPassword(e.target.value)}
                    value={confirmPassword}
                    placeholder="••••••••"
                    className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
                    required
                  />
                </div>
                <div className="flex items-start">
                  <div className="flex items-center h-5">
                    <input
                      id="terms"
                      aria-describedby="terms"
                      type="checkbox"
                      className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300"
                      required
                    />
                  </div>
                  <div className="ml-3 text-sm">
                    <label htmlFor="terms" className="font-light text-gray-500">
                      I accept the{" "}
                      <a
                        className="font-medium text-primary-600 hover:underline"
                        href="#">
                        Terms and Conditions
                      </a>
                    </label>
                  </div>
                </div>
                <button
                  type="submit"
                  className="w-full text-black bg-gray-300 hover:bg-primary-dark focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
                  Create an account
                </button>
                <p className="text-sm font-light text-gray-500">
                  Already have an account?{" "}
                  <button
                    onClick={() => setShowLogin(true)}
                    className="bg-transparent font-medium text-primary-600 hover:underline">
                    Login here
                  </button>
                </p>
              </form>
            )}
            {showLogin && !isLoading && (
              <form className="space-y-4 md:space-y-6" onSubmit={signIn}>
                <div>
                  <label
                    htmlFor="email"
                    className="block mb-2 text-sm font-medium text-gray-900">
                    Your email
                  </label>
                  <input
                    type="email"
                    name="email"
                    id="email"
                    onChange={(e) => setEmail(e.target.value)}
                    value={email}
                    className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
                    placeholder="name@company.com"
                    required
                  />
                </div>
                <div>
                  <label
                    htmlFor="password"
                    className="block mb-2 text-sm font-medium text-gray-900">
                    Password
                  </label>
                  <input
                    type="password"
                    name="password"
                    id="password"
                    onChange={(e) => setPassword(e.target.value)}
                    value={password}
                    placeholder="••••••••"
                    className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
                    required
                  />
                </div>
                <div className="flex items-center justify-between">
                  <div className="flex items-start">
                    <div className="flex items-center h-5">
                      <input
                        id="remember"
                        aria-describedby="remember"
                        type="checkbox"
                        className="w-4 h-4 border border-gray-300 rounded accent-primary bg-gray-50 focus:ring-3 focus:ring-primary"
                      />
                    </div>
                    <div className="ml-3 text-sm">
                      <label htmlFor="remember" className="text-gray-500 ">
                        Remember me
                      </label>
                    </div>
                  </div>
                  <a
                    href="#"
                    className="text-sm font-medium text-primary-600 hover:underline">
                    Forgot password?
                  </a>
                </div>
                <button
                  type="submit"
                  className="w-full text-black bg-gray-300 hover:bg-primary-dark focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center">
                  Sign in
                </button>
                <p className="text-sm font-light text-gray-500">
                  Don’t have an account yet?{" "}
                  <button
                    onClick={() => setShowLogin(false)}
                    className="bg-transparent font-medium text-primary-600 hover:underline">
                    Sign up
                  </button>
                </p>
              </form>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}

ログアウト方法は次のようになります。

const logout = async (e: any) => {
    e.preventDefault()
    try {
      await onLogout()
    } catch (error: any) {
      console.log(error.message)
    }
  }