🔒

ホワイトリストを使ったいい感じの認証をFirebase & Nuxt3で作ってみた

2023/11/19に公開

Firebaseの無料枠だけで特定の人だけがアクセスできるFirestoreを使いたいなと思うことありますよね。
そこでこの記事ではホワイトリスト内の人だけがアクセスできるようにFirestoreの設定・アプリ側での実装をした話を紹介します。

ちなみにこの記事はKaijo Physics Club Advent Calendar 2024の19日目の記事です。昨日の記事は@basalteDynamixelの位置制御で速度を操作する方法でした。Dynamixelって響きがいいですよね。

認証の方法

Firestore内にホワイトリストを設置し、それをFirebase セキュリティルールから確認することで、Firestore・Cloud Storage側で特定の人のみアクセスできるように制限します。

Firestoreの中身

/database/(default)/documents/
├─ moderators
│  └─ (UID)
│     ├─ isAdmin: boolean
│     └─ ...
└─ ...

無料枠の中だけで収めたいのでデフォルトのデータベースの中にmoderatorsコレクションを追加しその中にアクセス可能なユーザーのUIDごとにドキュメントを保存・ユーザーの情報(管理者権限があるかどうか(isAdmin)など)を保存しています。

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

Firestore

rules_version="2";

service cloud.firestore {
  match /databases/{database}/documents {
  	function isModerator(){
      return(
      	(request.auth != null) &&
        exists(/databases/$(database)/documents/moderators/$(request.auth.uid))
      );
    }

    function isAdmin(){
      return (
      	isModerator() &&
        get(/databases/$(database)/documents/moderators/$(request.auth.uid)).data.isAdmin == true
      );
    }

    match /{document=**} {
      allow read: if isModerator();
      allow write: if isAdmin();
    }
  }
}

Firestoreのセキュリティールールではexists関数を用いてドキュメントが存在するかの確認・get関数を用いてドキュメントの取得を行うことができます。これを用いて/moderators内(ホワイトリスト内)に登録されているかどうかの確認・管理者権限があるかの確認を行なっています。
また、セキュリティールールではfunctionを用いて関数を登録することができ、今後セキュリティルールが複雑化したときのために関数化しています。

Cloud Storage

rules_version="2";

service firebase.storage {
  match /b/{bucket}/o {    
    match /{allPaths=**} {
    	function isModerator(){
      	return (
        	request.auth != null &&
          firestore.exists(/databases/(default)/documents/moderators/$(request.auth.uid))
        );
      }
      
      function isAdmin(){
      	return(
        	isModerator() &&
          firestore.get(/databases/(default)/documents/moderators/$(request.auth.uid)).data.isAdmin == true
        );
      }
    
      allow read: if isModerator();
      allow write: if isAdmin();
    }
  }
}

Cloud StorageのセキュリティールールもFirestoreと同じような感じで設定できますが、Firestore内のドキュメントの確認の関数が先ほどはexistsだったものがfirestore.existsgetだったものがfirestore.getというふうになります。

アプリで運用できるようにする

Firebase側でアクセスできないようにしていますが、アプリの画面が開かれてデータが取得できず何も表示できていないままだと味気ないですよね。なのでログインしたけどアクセスする権限がない人には管理者にアクセス権を求めるられる画面を表示するようにしてみました。

Nuxt Middlewareでアクセス権限がない場合に遷移させる

Nuxt Middlewareを用いることでページ遷移時の認証の確認などを行うことができます。
自らにアクセス権限があるかないかの確認は自分自身がホワイトリストに登録されているか取得することで行なっています。

~/middleware/moderator.ts
import { useAuth } from "~/hooks/useAuth"

export default defineNuxtRouteMiddleware(async (to) => {
  const { observeLogin, auth } = useAuth()
  const isLoggedIn = await new Promise<boolean>((resolve) => observeLogin(() => {
    resolve(true)
  }, () => {
    resolve(false)
  }))

  if( !isLoggedIn || !auth.currentUser ){ // auth.currentUserがnullではないことを明示的にするために!auth.currentUserも入れてます
    return navigateTo({
      path: "/login",
      query: {
        redirect: to.fullPath
      }
    })
  }

  const { uid } = auth.currentUser

  try {
    const isModerator = await checkIsModerator(uid) // ~/utils/checkIsModerator.tsから自動インポート
    if( isModerator ){
      return
    }else{
      return navigateTo({
        path: "/not-moderator"
      })
    }
  }catch(e){
    return navigateTo({
      path: "/login",
      query: {
        reason: "error-occurred"
      }
    })
  }
})
~/hooks/useAuth.ts
~/hooks/useAuth.ts
import { createAuthRepository } from "~/infra/firebase/authRepository"

export function useAuth(){
  const authRepository = createAuthRepository()

  const auth = authRepository.auth
  const login = authRepository.login
  
  function logout(){
    authRepository.logout()
    const router = useRouter()
    router.push("/login")
  }
  
  const observeLogin = authRepository.observeLogin

  return { auth, login, logout, observeLogin }
}
~/infra/authRepository.ts
~/infra/authRepository.ts
import { getAuth, onAuthStateChanged, type User, GoogleAuthProvider, signInWithPopup, type AuthError, signOut } from "firebase/auth"

const provider = new GoogleAuthProvider();

export function createAuthRepository(){
  checkFirebaseAppCreated()
  const auth = getAuth();

  const observeLogin = (
    loginCallback?: (user: User) => void,
    notLoginCallback?: () => void
  ) => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        if (loginCallback) loginCallback(user)
      } else {
        if (notLoginCallback) notLoginCallback()
      }
    });
  }

  const logout = async () => {
    await signOut(auth)
      .then(() => {
        const router = useRouter()
        router.push("/")
      })
      .catch((error) => {
        throw new Error(error)
      })
  }

  const login = async () => {
    await signInWithPopup(auth, provider)
      .catch((error: AuthError) => {
        throw new Error(error.message)
      });
  }

  return { observeLogin, auth, login, logout }
}

遷移先ページでUIDを登録してもらえるようにする


↑こんな感じでUIDを表示・コピーできる画面を作って管理者にUIDを送信できるようにします。

終わりに

以上がFirebaseの無料枠でホワイトリストを使ったいい感じの認証フローを作成する方法の紹介でした!
明日からもアドベントカレンダーは続くのでぜひお楽しみに!いい年末を〜!

Discussion