Firebaseエミュレーターを使ってFirebase Authenticationでの認証をローカル環境で実装してみた
はじめに
こんにちは、SHIFTの開発部門に所属している Katayama です。
前回の記事「Firebaseプロジェクトを新規作成してCloud FunctionsとCloud Firestoreのローカル開発環境を整備してみた」では Cloud Functions をローカル環境で実行し動作確認を行う、という事をやってみた。
今回は同じエミュレーターを利用して、Firebase Authentication での認証をローカル環境で行い開発をやってみようと思う。
※Firebaseのエミュレーターについては、Firebase Local Emulator Suiteを参照。
※Firebase Authenticationはアプリケーションに必須といってもいい認証機能を実装するためのサービスで、詳細については公式ドキュメントを参照。
Firebase に Web アプリを追加する
まずは、Firebase のプロジェクトに Web アプリを追加する。Firebase のダッシュボード上で、「アプリを追加」から、 「ウェブ」を選択する。
「ウェブアプリに Firebase を追加」の Step①、Step② を設定する。
今回は Vite + Vue3 + Vuetify3 のアプリに Firebase を追加するので、npm で firebase を依存に追加する方法を取る。
画面に表示されている firebaseConfig の内容を自身のアプリの方に記載し、initializeApp()で初期化すれば準備は完了になる(本来的には firebase の設定などは別のファイルに切り出すべきだが、今回は検証なのでコンポーネントに全て記載している)。
<script setup>
import { onMounted } from "vue";
import { RouterLink } from "vue-router";
import firebase from "firebase/compat/app";
import * as firebaseui from "firebaseui";
import "firebaseui/dist/firebaseui.css";
import { initializeApp } from "firebase/app";
import {
getAuth,
connectAuthEmulator,
onAuthStateChanged,
signOut,
} from "firebase/auth";
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
connectAuthEmulator(auth, "http://localhost:9099");
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) console.log("login", user);
else console.log("not login");
});
const logout = async () => {
try {
await signOut(auth);
console.log("Sign-out successful.");
} catch (e) {
console.log(e);
}
};
onMounted(() => {
const ui =
firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(auth);
ui.start("#firebaseui-auth-container", {
signInOptions: [
{
provider: firebase.auth.GoogleAuthProvider.PROVIDER_ID,
scopes: ["email"],
customParameters: { prompt: "select_account" },
},
],
callbacks: {
// eslint-disable-next-line no-unused-vars
async signInSuccessWithAuthResult(authResult, redirectUrl) {
console.log(authResult);
return false;
},
},
});
});
onUnmounted(() => {
unsubscribe();
});
</script>
<template>
<v-app>
<v-app-bar density="compact">
...
<v-btn @click="logout">logout</v-btn>
...
</v-app-bar>
<v-main>
...
<div id="firebaseui-auth-container"></div>
...
</v-main>
</v-app>
</template>
...
※このアプリを追加しないと、ステップ 2: SDK をインストールして Firebase を初期化するに書かれている firebase の initializeApp()が実行できず、以下のようなエラーになってしまう(以下は const auth = getAuth();のように app を渡さなかった時に出たエラー)。
ローカル環境の Firebase Authentication で認証する
上記でアプリ側の準備は OK なので、後は firebase emulators:start コマンドでエミュレータを起動し、Authentication で認証できるか?を試してみようと思う(以下の画像には Cloud Functions などもエミュレータで起動しているが、そちらについてはFirebaseプロジェクトを新規作成してCloud FunctionsとCloud Firestoreのローカル開発環境を整備してみたで取り上げている)。
Web アプリを起動して firebaseUI で Google でログインを行ってみると、以下のように認証ができ、Authentication にユーザが追加されている事が確認できる。
※本番環境の場合には、以下の2点の設定が必要になる。
Sign-in methodの設定
これはどの認証方法を利用できるようにするか?の設定で、Firebaseのダッシュボードの以下から設定できる
Settings > 承認済みドメイン の設定 ローカル環境から本番環境のFirebase Authenticationにつないで開発をしたい場合には、以下の承認済みドメインを設定する必要がある。
localhostはデフォルトで設定済みだが、上記の動画のようにIPでWebアプリを開いている場合にはそのIPの設定も必要。
まとめとして
今回は Firebase のエミュレーターを利用して Firebase Authentication での認証をローカル環境で行えるようにし、開発をしてみた。
Authentication エミュレータと本番環境の違いに書かれているような違いはあるが、開発をする上で問題になるケースはそこまでないのではと思われる。
本番環境に開発時の検証用アカウントが作成されてしまうと、お掃除が大変だったりするがローカルのエミュレーターであればそういったゴミデータが作成されないので良いのではないかと思う。
おまけとして、よくある認証後にアプリのアカウント情報がなければ登録し、Firestore にアカウント情報を生成する、という処理をやってみた。
また、ローカル環境から本番の Firebase に接続する場合と、エミュレータに接続する場合をうまく分ける方法についても検討してみた。
おまけ
「認証後にアカウントがなければ Firestore にアカウント情報を登録する」をローカル環境でエミュレータを利用して検証する
実装としては以下。
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import md5 from 'crypto-js/md5';
import firebase from 'firebase/compat/app';
import * as firebaseui from 'firebaseui';
import 'firebaseui/dist/firebaseui.css';
import { doc, setDoc } from 'firebase/firestore';
import { auth, db } from '@/firebase';
import { converter } from '@/firebase/store';
import useUserStore from '@/stores/user';
import HelloWorld from '@/components/HelloWorld.vue';
const router = useRouter();
const userStore = useUserStore();
const { user } = storeToRefs(userStore);
const isLogined = computed(() => !!user.value.uid);
onMounted(() => {
const ui =
firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(auth);
if (!user.value.uid) {
ui.start('#firebaseui-auth-container', {
...
});
}
});
const step = ref(1);
const form = ref(null);
const firstName = ref('');
const lastName = ref('');
const userRegister = async () => {
const { valid } = await form.value.validate();
if (valid) {
await setDoc(doc(db, 'users', user.value.uid).withConverter(converter), {
id: user.value.uid,
email: user.value.email,
firstName: firstName.value,
lastName: lastName.value,
logoUri: `https://www.gravatar.com/avatar/${md5(user.value.email)}`
});
step.value = 2;
setTimeout(() => {
router.push({ name: 'home', params: {} });
}, 2500);
}
};
const currentTitle = computed(() => {
switch (step.value) {
case 1:
return '新規登録';
default:
return 'アカウント作成中';
}
});
</script>
<template>
<v-container>
...
<v-row v-if="!isLogined" class="pt-2">
<v-col>
<div id="firebaseui-auth-container"></div>
</v-col>
</v-row>
<v-row v-else class="justify-center pt-2">
<v-col cols="12" md="6">
<v-card elevation="1">
<v-card-title>
{{ currentTitle }}
</v-card-title>
<v-window v-model="step">
<v-window-item :value="1">
<v-form ref="form">
<v-container>
<v-row>
...
</v-row>
</v-container>
</v-form>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" @click="userRegister">
登録を完了する
<v-icon icon="mdi-chevron-right" end></v-icon>
</v-btn>
</v-card-actions>
</v-window-item>
<v-window-item :value="2">
<v-card-text class="text-center">
<v-progress-circular color="primary" indeterminate />
</v-card-text>
</v-window-item>
</v-window>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
import { initializeApp } from 'firebase/app';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
const firebaseConfig = {
...
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
connectAuthEmulator(auth, 'http://localhost:9099');
const db = getFirestore(app);
connectFirestoreEmulator(db, 'localhost', 8081);
export { auth, db };
上記の実装で、ローカル環境の Firebase エミュレーターでの動作確認をしてみると、以下の動画の通り意図通り users ドキュメントが作成できている事が確認でき、ローカル環境での検証が問題ない事が分かる。
とは言えローカルから本番の Firebase に接続して検証したい
上記では Firebase エミュレーターに接続していたが本番の Firebase に接続して開発をしたい、という場面をあるかもしれない。そのような場合には、Vite のモードを使ってコードに分岐を入れるのがいいだろう。
具体的には以下のようなコードにすれば、"--mode localdev"の時だけ、エミュレーターに接続し、それ以外の場合には本番の Firebase に接続するようにできる("localdev"にしているのは、"local"などはViteの予約語でエラーになるため)。
...
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
if (import.meta.env.MODE === 'localdev')
connectAuthEmulator(auth, 'http://localhost:9099');
const db = getFirestore(app);
if (import.meta.env.MODE === 'localdev')
connectFirestoreEmulator(db, 'localhost', 8081);
export { auth, db };
《この公式ブロガーの記事一覧》
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/