Firebase Authentication+SvelteKitでSign in with Googleを実現する
前提
以下のようなアプリケーションを作りたい
- SvelteKit(フロントエンド) + Golang(バックエンド) + Cloudflare Pages(SvelteKitのデプロイ先)で構成予定
- FirebaseはFirebase Authenticationのみ(アカウント登録、ログインのみ)を使う予定
- その他の情報はGolang側、PostgreSQL?で管理する
- SvelteKitとGolangとではREST APIで通信する
- GolangからはSvelteKitから受け取るtokenをもとにFirebase Admin SDKからアカウント情報を引き出す?
筆者の知識
- Firebase何もわからん。手探り状態。
- SvelteKitやCloudflare Pagesは触ったことがあるが、そこまで深くはやってない。
このスクラップが取り扱うこと
- 前提を踏まえてFirebase Authenticationでユーザの登録、ログインを最低限扱えるまで
- 具体的には?
- Golang、PostgreSQLやFirebase Admin SDKは登場しない予定
- Cloudflare Pages固有の問題も出てこない(はず
- ロクインや登録ページからログイン、登録をさせて、ログインしたユーザでないと見られないページにリダイレクトさせるまで?
- 具体的には?
モチベーション
このscrapを書くにいたった動機
- Svelte + Firebaseを取り扱う記事が少ない
- Firebaseのセキュリティ的に注意すべき点が解説されていることがあまり多くなさそう?
- Firebase SDKのバージョンを意識しない記事が多い
- 現行バージョン9の情報に辿りつきにくい
日時指定検索を使え
- 現行バージョン9の情報に辿りつきにくい
ライブラリのバーションについて:
あまり関わりのなさそうな部分はカットした。
$ cat package.json
{
...
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "8.56.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
...
"firebase": "^10.7.2",
}
}
FirebaseのOptionを環境変数に書いておき、読み出したい!
そもそもFirebaseのOptionって何?
FirebaseのSDKをinitializeする際に与えるパラメータ。
具体的にはapikey
、appId
、projectId
とかのこと。
使うFirebaseの機能によって増減するらしいが、上の3つは必須項目らしい。
Firebaseのサイトでプロジェクトを作成すると出てくるので、そのまま使えばよさそう。
SvelteKitで環境変数を扱いたい!
幾つかバリエーションがあるっぽい?
-
private: SvelteKitにおけるserver(今の場合はCloudflare Pages)のみに共有したいときに使う
- clientであるbrowserには共有されない前提
-
public: SvelteKitにおけるclientにも共有される情報を入れる
-
static: buildの成果物であるコードに直書きされる
-
dynamic: staticに対して成果物には直書きされず、responseを返す際に都度埋め込まれる?
これらから$env/dynamic/private
、$env/dynamic/public
、$env/static/private
、$env/static/public
のバリエーションがあるっぽい
環境変数にセットできそうなのはいいけど、FirebaseのOptionってそもそもブラウザに晒していいものなの?
調べてみたら、こんな記事が出てきた
TODO: 後で読む
SvelteKit上でFirebase Authenticationを動かす
先駆者の記事を参考に:
だいたい上の記事を参考にした。
FirebaseでFirebase App named '[DEFAULT]' already exists with different options or config (app/duplicate-app).
と怒られた!
バージョンによって解決方法が違うらしい?ので微妙に詰まった。
initializeappみたいなやつを何度も動かすと怒られるらしい?
https://dabohaze.site/firebase-initialize-app-error/ によれば、以下のように初期化されたAppの数が0のとき(=getApps().length
が0であるとき?)のみinitializeAppを呼んでやればいいらしい:
import { initializeApp, getApps, type FirebaseOptions } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig: FirebaseOptions = {
apiKey: "hogehoge",
authDomain: "hogehoge",
projectId: "fugafuga",
appId: "foobar",
...
}
let app;
if (!getApps().length) {
app = initializeApp(firebaseConfig);
}
...
const auth = getAuth(app);
Firebase JS SDKくん、initializeAppの返り値を変数としてとっておかなくとも内部的に保持していそうな雰囲気が様々な記事(この記事もそうだけど)を読んでいて感じる。Black Box感があってややもにょる……。
SigninwithRedirect
が最近のブラウザだとうまく動かないらしい?
こいつに書いてありそう:
サードパーティ Cookie をブロックするブラウザでリダイレクト ログインを使用する場合のベスト プラクティスについて説明します。本番環境のすべてのブラウザで signInWithRedirect() を意図したとおりに機能させるには、ここに記載されているオプションのいずれかに従う必要があります。
Cloudflare Pagesを使っているのでこのページにおける解決策からいちばん実装が楽なオプション2、signInWithRedirect()
をやめ、 signInWithPopup()
を使うことにした。
ログインページの実装はこんな感じ:
<script lang="ts">
import { initializeApp, type FirebaseOptions, getApps } from 'firebase/app';
import { env } from '$env/dynamic/public';
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import { authStore } from '$lib/store';
import { goto } from '$app/navigation';
const firebaseConfig: FirebaseOptions = {
apiKey: env.PUBLIC_FIREBASE_API_KEY,
authDomain: env.PUBLIC_FIREBASE_AUTHDOMAIN,
projectId: env.PUBLIC_FIREBASE_PROJECTID,
storageBucket: env.PUBLIC_FIREBASE_STORAGEBUCKET,
messagingSenderId: env.PUBLIC_FIREBASE_MESSAGINGSENDERID,
appId: env.PUBLIC_FIREBASE_APPID
};
let app;
if (!getApps().length) {
app = initializeApp(firebaseConfig);
}
const auth = getAuth(app);
const handleGoogleLogin = async () => {
signInWithPopup(auth, new GoogleAuthProvider())
.then((res) => {
if (res === null) {
throw new Error("res is null")
}
authStore.set({ loggedIn: true, user: res.user });
goto('/');
})
.catch((e) => {
console.error(e);
});
};
</script>
<div>
<button type="button" on:click={handleGoogleLogin}> Sign In with Google </button>
</div>
ページをリロードするとログイン情報が失われてしまい、ログインしていないことになってしまう問題に直面してしまった。
最初はFirebase由来のAuthorizationヘッダをサーバに送りつける方法のみで認証していたが、Cookieでも認証できるようにしようと思っていたのだが、色々調べてみたところ、このページを見て解決した。
どうやらFirebase Authenticationがページ内で初期化される前にユーザがログインしているかどうかを確認してしまっていたようだ。