セキュリティ

カスタムドメインで Preview と Production 間の Cookie 共有を実現する方法

Vercel Preview Deployment Suffix と Better Auth の crossSubDomainCookies を使用して、Preview 環境と Production 環境間で OAuth 認証の Cookie を共有する方法を解説します。

本記事では、Vercel の Preview デプロイメントと Production デプロイメント間で Cookie を共有する方法を解説します。特に、Better Auth の oAuthProxy プラグインを使用して Preview 環境でも OAuth 認証を動作させるために必要な設定について詳しく説明します。

oAuthProxy の仕組み

Better Auth の oAuthProxy プラグインは、開発環境や Preview 環境で OAuth 認証を動作させるためのプラグインです。

OAuth プロバイダー(Google など)は、セキュリティ上の理由からリダイレクト URI として登録できる URL に制限があります。動的に変わる Preview URL(例: https://my-app-git-feature-xxx.vercel.app)をすべて登録することは現実的ではありません。

oAuthProxy は以下のフローで動作します:

1. Preview で「ログイン」クリック

2. Google OAuth 開始(state を Cookie に保存)

3. Google が Production の callback URL にリダイレクト
   (Production URL は Google に登録済み)

4. Production が認証を処理し、Preview にリダイレクト

5. Preview でセッション Cookie を確認してログイン状態を維持

このフローには致命的な問題があります。ステップ 4 で Production サーバーが Preview の Cookie(state)を読み取る必要がありますが、vercel.app のサブドメイン間では Cookie を共有できません

Public Suffix List(PSL)とは

PSL の概要

**Public Suffix List(PSL)**は、ブラウザが Cookie のスコープを決定するために参照するリストです。

通常、Cookie は親ドメインに対して設定でき、サブドメイン間で共有できます:

example.com に設定した Cookie
  ├─ app.example.com     ✅ 共有可能
  ├─ api.example.com     ✅ 共有可能
  └─ staging.example.com ✅ 共有可能

しかし、PSL に登録されているドメインは「公開サフィックス」として扱われ、そのサブドメイン間での Cookie 共有が禁止されます。

なぜ PSL が必要なのか

PSL がなければ、悪意のあるサイトが他のユーザーのサイトの Cookie にアクセスできてしまいます:

悪意のあるサイト: evil.vercel.app
ターゲット: victim.vercel.app

もし PSL がなければ:
evil.vercel.app が vercel.app に Cookie を設定
  → victim.vercel.app でもその Cookie が読み取れてしまう

これを防ぐため、vercel.appgithub.ionetlify.app などの共有ホスティングドメインは PSL に登録されています。

vercel.app は PSL に登録されている

publicsuffix.org で確認すると、vercel.app が登録されていることがわかります:

// Vercel, Inc : https://vercel.com/
// Submitted by Connor Davis <connor@vercel.com>
vercel.app
vercel.dev
now.sh

これにより、以下のような Cookie 共有は不可能です:

linto-dev.vercel.app に設定した Cookie
  ├─ linto-dev-git-feature.vercel.app  ❌ 共有不可
  └─ linto-dev-xxx.vercel.app          ❌ 共有不可

解決策: カスタムドメインの使用

カスタムドメイン(例: example.com)は PSL に登録されていないため、サブドメイン間で Cookie を共有できます:

example.com に設定した Cookie
  ├─ preview-feature.example.com  ✅ 共有可能
  ├─ staging.example.com          ✅ 共有可能
  └─ app.example.com              ✅ 共有可能

Vercel Preview Deployment Suffix

Vercel では Preview Deployment Suffix という機能を使用して、Preview デプロイメントにカスタムドメインを割り当てることができます。

この機能を使用すると、Preview URL が以下のように変わります:

変更前(デフォルト):
https://my-app-git-feature-username.vercel.app

変更後(カスタムドメイン):
https://my-app-git-feature.preview.example.com

注意: Preview Deployment Suffix は Vercel の Pro プランまたは Enterprise プランで利用可能です。

設定手順

1. Vercel でカスタムドメインを追加

  1. Vercel ダッシュボードでプロジェクトを選択
  2. SettingsDomains に移動
  3. カスタムドメイン(例: example.com)を追加
  4. DNS レコードを設定

2. Preview Deployment Suffix を設定

  1. SettingsDomains に移動
  2. Preview Deployment Suffix セクションを探す
  3. サブドメインを入力(例: preview.example.com
  4. DNS で以下のレコードを追加:
*.preview.example.com  CNAME  cname.vercel-dns.com

3. Better Auth で crossSubDomainCookies を設定

src/lib/auth.ts を更新して、Cookie がサブドメイン間で共有されるように設定します:

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { oAuthProxy } from "better-auth/plugins";
import { db } from "@/db";
import { env } from "@/env";

// 本番ドメイン
const PRODUCTION_DOMAIN = "example.com";
const PRODUCTION_URL = `https://app.${PRODUCTION_DOMAIN}`;

export const auth = betterAuth({
  baseURL: env.BETTER_AUTH_URL,
  trustedOrigins: [
    `https://*.${PRODUCTION_DOMAIN}`, // すべてのサブドメインを信頼
  ],
  database: drizzleAdapter(db, {
    provider: "sqlite",
  }),
  advanced: {
    // サブドメイン間で Cookie を共有
    crossSubDomainCookies: {
      enabled: true,
      domain: `.${PRODUCTION_DOMAIN}`, // 先頭のドットが重要
    },
  },
  plugins: [
    oAuthProxy({
      currentURL: env.BETTER_AUTH_URL,
      productionURL: PRODUCTION_URL,
    }),
  ],
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      redirectURI: `${PRODUCTION_URL}/api/auth/callback/google`,
    },
  },
});

crossSubDomainCookies の設定詳細

オプション説明
enabledクロスサブドメイン Cookie を有効化true
domainCookie のドメイン属性。先頭にドットを付ける.example.com

重要: domain の先頭にドット(.)を付けることで、そのドメインのすべてのサブドメインで Cookie が共有されます。

完全な設定例

環境変数

# .env.local(ローカル開発)
BETTER_AUTH_URL=http://localhost:3000

# .env.production(本番)
BETTER_AUTH_URL=https://app.example.com

# Preview 環境では Vercel が自動設定
# BETTER_AUTH_URL=https://my-app-git-feature.preview.example.com

GitHub Actions での設定

.github/workflows/preview-deploy.yml:

- name: Deploy to Vercel
  run: |
    BRANCH_SLUG=$(echo "${{ github.head_ref }}" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-')
    PREVIEW_URL="https://my-app-git-${BRANCH_SLUG}.preview.example.com"

    vercel deploy --prebuilt \
      --env BETTER_AUTH_URL=$PREVIEW_URL \
      --token=${{ secrets.VERCEL_TOKEN }}

セキュリティ上の考慮事項

trustedOrigins の設定

crossSubDomainCookies を有効にする場合、trustedOrigins でサブドメインのワイルドカードを許可する必要があります:

trustedOrigins: [
  `https://*.${PRODUCTION_DOMAIN}`,
],

これにより、以下のオリジンからのリクエストが許可されます:

  • https://app.example.com
  • https://preview-feature.example.com
  • https://staging.example.com

Better Auth は自動的に以下のセキュリティ属性を設定します:

属性説明
SecuretrueHTTPS でのみ送信
HttpOnlytrueJavaScript からアクセス不可
SameSiteLaxCSRF 対策
Domain.example.comサブドメイン間で共有

サブドメインの管理

カスタムドメインを使用する場合、そのドメイン配下のすべてのサブドメインを管理する責任が生じます。第三者がサブドメインを悪用できないよう、DNS とサーバー設定を適切に管理してください。

代替案: oAuthProxy を使用しない方法

カスタムドメインを使用できない場合、以下の代替案を検討してください:

1. Preview 環境で OAuth を無効化

最もシンプルな解決策です。Preview 環境ではテストアカウントでのログインのみを許可します:

// src/lib/auth.ts
const isPreview = env.VERCEL_ENV === "preview";

export const auth = betterAuth({
  // Preview では OAuth を無効化
  socialProviders: isPreview ? {} : {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
    },
  },
});

2. 共有ストレージ(Redis)を使用

Redis などの外部ストレージを使用して、Preview と Production 間で OAuth state を共有します:

import { redis } from "@/lib/redis";

export const auth = betterAuth({
  account: {
    stateStore: {
      async get(state) {
        return await redis.get(`oauth:state:${state}`);
      },
      async set(state, value, expiresIn) {
        await redis.set(`oauth:state:${state}`, value, "EX", expiresIn);
      },
      async delete(state) {
        await redis.del(`oauth:state:${state}`);
      },
    },
  },
});

3. 単一データベースの使用

Preview と Production で同じデータベースを使用する場合、oAuthProxy は問題なく動作します。ただし、データの分離ができなくなるため、本番データへの影響に注意が必要です。

まとめ

Vercel の Preview デプロイメントと Production デプロイメント間で Cookie を共有するには、以下の設定が必要です:

  1. カスタムドメインの取得: PSL に登録されていないドメインを使用
  2. Preview Deployment Suffix の設定: Preview URL にカスタムドメインを適用
  3. crossSubDomainCookies の有効化: Better Auth でサブドメイン間の Cookie 共有を設定

この設定により、oAuthProxy を使用した OAuth 認証が Preview 環境でも正常に動作します。

方法メリットデメリット
カスタムドメイン + crossSubDomainCookies完全な OAuth 対応Pro プラン以上が必要
Preview で OAuth 無効化シンプル、追加コストなしPreview で OAuth テスト不可
Redis 共有ストレージ既存ドメインで動作追加インフラが必要
単一データベース追加設定不要データ分離ができない

プロジェクトの要件と予算に応じて、適切な方法を選択してください。