見出し画像

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 };


《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。
経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。
SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
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/