ホワイトリストを使ったいい感じの認証をFirebase & Nuxt3で作ってみた
Firebaseの無料枠だけで特定の人だけがアクセスできるFirestoreを使いたいなと思うことありますよね。
そこでこの記事ではホワイトリスト内の人だけがアクセスできるようにFirestoreの設定・アプリ側での実装をした話を紹介します。
ちなみにこの記事はKaijo Physics Club Advent Calendar 2024の19日目の記事です。昨日の記事は@basalteのDynamixelの位置制御で速度を操作する方法でした。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.exists
、get
だったものがfirestore.get
というふうになります。
アプリで運用できるようにする
Firebase側でアクセスできないようにしていますが、アプリの画面が開かれてデータが取得できず何も表示できていないままだと味気ないですよね。なのでログインしたけどアクセスする権限がない人には管理者にアクセス権を求めるられる画面を表示するようにしてみました。
Nuxt Middlewareでアクセス権限がない場合に遷移させる
Nuxt Middlewareを用いることでページ遷移時の認証の確認などを行うことができます。
自らにアクセス権限があるかないかの確認は自分自身がホワイトリストに登録されているか取得することで行なっています。
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
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
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