Vercel + Firebase AuthenticationでSafari対応しようとしたら4度くらい罠にハマった

投稿日: 更新日:

はじめに

プライベートでSolidStart + Firebase Authentication + Vercelで作ろうとしてたら罠にハマったのでメモ。

概要

  1. Safari, Firefoxではサードパーティのストレージアクセスがブロックされる(Chromeでもブロックされる予定)
  2. リバースプロキシ設定しようとしたらSolidStartがVercelのoutput.jsonに未対応
  3. リバースプロキシ設定できたけど今度はブランチごとのプレビューに未対応(OAuthのredirect_uriの問題)
  4. 仕方ないのでFirebaseのスクリプトをデプロイする方式で対応
  5. プロジェクトを分けてGoogle認証(リバースプロキシ) + パスワード認証の2つに分離

SafariでFirebase Authenticationが使えない問題

理由はこれです。Chromeだとできたのですが、Safariだとダメでした。試してないけどFirefoxでもダメで、Chromeもダメになる予定です。

解決方法は5つありますが、オプション1はFirebase Hostingに乗り換える予定はなかったしVercelならいけるだろうと思って取り組まず。オプション2はモバイルの時面倒なのが目に見えたので却下。

次のオプション3、firebaseapp.com へのプロキシ認証リクエストを試そうとしたんですが、SolidStartが対応していなかったのと、そもそも書き方が分からんということで悪戦苦闘。

SolidStartが未対応なのでビルドスクリプトを作って対応したのですが、今度はプレビュー環境でredirect_uriが不正だとエラーに。今度はGoogle Cloudのこの画面をいじることに。

ここに入れることで動いたものの、そもそもOAuth 2.0のredirect_uriはワイルドカードが使えないということで諦め。その後「オプション 4: ログイン ヘルパー コードを自社ドメイン内でホストする」を試したんですが、単にリバースプロキシでない方法というわけで本質的には何も変わってないので取りやめ。

それで行き詰まったのですが、最終的には次の2つを併用することで対応しました。

  • mainブランチ: 「オプション 3: firebaseapp.com へのプロキシ認証リクエスト」
  • プレビュー: パスワード認証を使う別のプロジェクトを使用

方針が決まればあとは何とかなりました。最終的に使ったコードはこんな感じです。まずオプション3を実現するための、output.jsonを更新するコード。

import { readFileSync, writeFileSync } from "fs";

type HandleValue =
  | "rewrite"
  | "filesystem" // check matches after the filesystem misses
  | "resource"
  | "miss" // check matches after every filesystem miss
  | "hit"
  | "error"; //  check matches after error (500, 404, etc.)

type Handler = {
  handle: HandleValue;
  src?: string;
  dest?: string;
  status?: number;
};

type Source = {
  src: string;
  dest?: string;
  headers?: Record<string, string>;
  methods?: string[];
  continue?: boolean;
  caseSensitive?: boolean;
  check?: boolean;
  status?: number;
  middlewareRawSrc?: string[];
  middlewarePath?: string;
};

type Route = Source | Handler;

type Config = {
  version: 3;
  routes?: Route[];
};

const configJson = JSON.parse(
  readFileSync(".vercel/output/config.json", "utf8"),
) as Config;

const routes = configJson.routes ?? [];

const source: Source = {
  src: "/__/auth/(.*)",
  dest: "https://<projectId>.firebaseapp.com/__/auth/$1",
};

routes.unshift(source);

writeFileSync(
  ".vercel/output/config.json",
  JSON.stringify(configJson, null, 2),
);

それからプロジェクトを切り替えるためのコード。概要は次のとおり。

  • authDomainをVITE_PRODUCTION_DOMAINまたはVITE_VERCEL_BRANCH_URLにする
    • VITE_PRODUCTION_DOMAINは独自で設定した環境変数。Vercelのドメインではなく、独自ドメインに割り当てているため。
  • VITE_VERCEL_ENVを見て本番環境のときはGoogle認証にする。
    • Google Cloud側のOAuth設定に登録が必要。
  • VITE_ENABLE_GOOGLE_AUTHが “true” のときもGoogle認証にする。
    • 普段は使わないが、ブランチでGoogle認証に変更が入ったときに検証できるようにするため。
    • Google Cloud側のOAuth設定に登録が必要。
  • パスワード認証のときはプロフィールアイコンが取得できないため、そこだけ固定値をセット
const productionDomain =
  import.meta.env.VITE_PRODUCTION_DOMAIN ??
  import.meta.env.VITE_VERCEL_BRANCH_URL;

const prodFirebaseConfig = {
  apiKey: "...",
  authDomain: productionDomain,
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
};

const devFirebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
};

export const useGoogleAuth =
  import.meta.env.VITE_VERCEL_ENV === "production" ||
  import.meta.env.VITE_ENABLE_GOOGLE_AUTH === "true";

export const firebaseConfig = useGoogleAuth
  ? prodFirebaseConfig
  : devFirebaseConfig;

export const MyPhotoURL =
  "...";

おわりに

SolidStartを使ったのは最近仕事でSolidJSを使っているので、プライベートでもちょっと触れる場所を作りたいのと、何となく面白そうだからです。まだβ版なので荒削りのところが目立ちますが・・・