この記事では、Cloudflare Pages Functions、D1データベース、Google OAuth 2.0を使って、完全にCloudflare上で動作する認証システムを構築します。フロントエンドからバックエンド、データベースまで、すべてコピペで動くコードを提供します。

目次
  1. この記事で作るもの
  2. 前提条件
  3. Step 1: Google Cloud Consoleでの準備
  4. Step 2: プロジェクトのセットアップ
  5. Step 3: Cloudflare D1データベースのセットアップ
  6. Step 4: 環境変数の設定
  7. Step 5: バックエンド実装
  8. Step 6: フロントエンド実装
  9. Step 7: ローカルでの動作確認
  10. Step 8: 本番環境へのデプロイ
  11. Step 9: トラブルシューティング
  12. Step 9: トラブルシューティング
  13. Step 10: セキュリティの向上(オプション)
  14. まとめ
  15. 完成したコードの全体像

この記事で作るもの

  1. Google認証によるログイン・ログアウト機能
  2. 初回ログイン時の自動ユーザー登録
  3. Cloudflare D1データベースでのユーザー管理
  4. セッション管理

前提条件

  • Node.js(v18以降)がインストールされていること
  • Cloudflareアカウント(無料プランでOK)
  • Googleアカウント

Step 1: Google Cloud Consoleでの準備

1-1. Google Cloud Consoleにアクセス

  1. Google Cloud Consoleにアクセス
  2. 「プロジェクトを選択」→「新しいプロジェクト」をクリック
  3. プロジェクト名を入力(例: my-auth-app)して「作成」

1-2. OAuth 2.0クライアントIDの作成

  1. 左サイドバーから「APIとサービス」→「認証情報」を選択
  2. 「認証情報を作成」→「OAuthクライアントID」をクリック
  3. 「同意画面を構成」をクリック(初回のみ)
  4. User Typeは「外部」を選択して「作成」
  5. 以下の情報を入力:
    • アプリ名: My Auth App
    • ユーザーサポートメール: あなたのメールアドレス
    • デベロッパー連絡先: あなたのメールアドレス
  6. 「保存して次へ」を3回クリックして完了

1-3. OAuth認証情報の取得

  1. 再度「認証情報を作成」→「OAuthクライアントID」
  2. アプリケーションの種類: 「ウェブアプリケーション」
  3. 名前: Web Client
  4. 承認済みのリダイレクトURIに以下を追加: http://localhost:8788/api/auth/callbackhttps://あなたのプロジェクト名.pages.dev/api/auth/callback
  5. 「ログアウト」ボタンをクリックして、ログイン画面に戻ることを確認


Step 2: プロジェクトのセットアップ

2-1. プロジェクトの初期化

プロジェクトフォルダを作成

mkdir cloudflare-google-auth
cd cloudflare-google-auth

package.jsonを作成


npm init -y

必要なパッケージをインストール

npm install -D wrangler
npm install -D @cloudflare/workers-types

2-2. プロジェクト構造の作成

フォルダ構造を作成

mkdir -p functions/api/auth
mkdir public

最終的なフォルダ構造

cloudflare-google-auth/
├── functions/
│   └── api/
│       └── auth/
│           ├── login.ts
│           ├── callback.ts
│           ├── me.ts
│           └── logout.ts
├── public/
│   └── index.html
├── wrangler.toml
└── package.json


Step 3: Cloudflare D1データベースのセットアップ

3-1. D1データベースの作成

D1データベースを作成

npx wrangler d1 create auth-db

実行すると、以下のような出力が表示されます:

✅ Successfully created DB 'auth-db'!

[[d1_databases]]
binding = "DB"
database_name = "auth-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

この出力をコピーしておきます。

3-2. wrangler.tomlの作成

プロジェクトルートにwrangler.tomlを作成:

name = "cloudflare-google-auth"
compatibility_date = "2024-01-01"
pages_build_output_dir = "public"

[[d1_databases]]
binding = "DB"
database_name = "auth-db"
database_id = "ここに先ほどコピーしたdatabase_idを貼り付け"

GOOGLE_CLIENT_ID = "あなたのGoogleクライアントID" GOOGLE_REDIRECT_URI = "http://localhost:8788/api/auth/callback" # 本番環境用(デプロイ時にコメント解除)

# GOOGLE_REDIRECT_URI = "https://あなたのプロジェクト名.pages.dev/api/auth/callback"

3-3. テーブルの作成

schema.sqlファイルを作成:

CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT,
  picture TEXT,
  created_at INTEGER NOT NULL,
  last_login INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

テーブルを作成:

CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT,
  picture TEXT,
  created_at INTEGER NOT NULL,
  last_login INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

ローカル環境用

npx wrangler d1 execute auth-db --local --file=schema.sql

本番環境用(デプロイ前に実行)

npx wrangler d1 execute auth-db --remote --file=schema.sql

Step 4: 環境変数の設定

4-1. .dev.varsの作成

ローカル開発用の秘密情報を格納するため、プロジェクトルートに.dev.varsファイルを作成:

GOOGLE_CLIENT_SECRET=あなたのGoogleクライアントシークレット
SESSION_SECRET=ランダムな64文字以上の文字列

SESSION_SECRETは以下のコマンドで生成できます:

macOS/Linux

openssl rand -hex 32

Windows (PowerShellで実行)

-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | % {[char]$_})

4-2. .gitignoreの作成

node_modules/
.dev.vars
.wrangler/
dist/

Step 5: バックエンド実装

5-1. 型定義の作成

functions/types.tsを作成:

export interface Env {
  DB: D1Database;
  GOOGLE_CLIENT_ID: string;
  GOOGLE_CLIENT_SECRET: string;
  GOOGLE_REDIRECT_URI: string;
  SESSION_SECRET: string;
}

export interface User {
  id: string;
  email: string;
  name: string | null;
  picture: string | null;
  created_at: number;
  last_login: number;
}

export interface GoogleTokenResponse {
  access_token: string;
  expires_in: number;
  token_type: string;
  scope: string;
  id_token: string;
}

export interface GoogleUserInfo {
  id: string;
  email: string;
  verified_email: boolean;
  name: string;
  given_name: string;
  family_name: string;
  picture: string;
}

5-2. ユーティリティ関数の作成

functions/utils.tsを作成:

 import { Env } from './types';

// シンプルなセッショントークンの生成
export async function generateSessionToken(): Promise<string> {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}

// セッションCookieの作成
export function createSessionCookie(token: string, maxAge: number = 7 * 24 * 60 * 60): string {
  return `session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAge}`;
}

// Cookieからセッションを取得
export function getSessionFromCookie(cookieHeader: string | null): string | null {
  if (!cookieHeader) return null;
  
  const cookies = cookieHeader.split(';').map(c => c.trim());
  const sessionCookie = cookies.find(c => c.startsWith('session='));
  
  if (!sessionCookie) return null;
  
  return sessionCookie.split('=');
}

// CORS headers
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
};

// JSON response helper
export function jsonResponse(data: any, status: number = 200, headers: Record<string, string> = {}) {
  return new Response(JSON.stringify(data), {
    status,
    headers: {
      'Content-Type': 'application/json',
      ...corsHeaders,
      ...headers,
    },
  });
}

5-3. ログインエンドポイント

functions/api/auth/login.tsを作成:

 import { Env } from '../../types';

export const onRequestGet: PagesFunction<Env> = async (context) => {
  const { GOOGLE_CLIENT_ID, GOOGLE_REDIRECT_URI } = context.env;

  // Google OAuth 2.0の認証URLを生成
  const params = new URLSearchParams({
    client_id: GOOGLE_CLIENT_ID,
    redirect_uri: GOOGLE_REDIRECT_URI,
    response_type: 'code',
    scope: 'openid email profile',
    access_type: 'offline',
    prompt: 'select_account',
  });

  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;

  // Googleの認証ページにリダイレクト
  return Response.redirect(authUrl, 302);
};

5-4. コールバックエンドポイント

functions/api/auth/callback.tsを作成:

 import { Env, GoogleTokenResponse, GoogleUserInfo, User } from '../../types';
import { generateSessionToken, createSessionCookie, jsonResponse } from '../../utils';

export const onRequestGet: PagesFunction<Env> = async (context) => {
  const { request, env } = context;
  const { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, DB } = env;
  
  const url = new URL(request.url);
  const code = url.searchParams.get('code');
  const error = url.searchParams.get('error');

  // エラーチェック
  if (error) {
    return new Response(`Authentication error: ${error}`, { status: 400 });
  }

  if (!code) {
    return new Response('Missing authorization code', { status: 400 });
  }

  try {
    // 1. 認証コードをアクセストークンに交換
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        code,
        client_id: GOOGLE_CLIENT_ID,
        client_secret: GOOGLE_CLIENT_SECRET,
        redirect_uri: GOOGLE_REDIRECT_URI,
        grant_type: 'authorization_code',
      }),
    });

    if (!tokenResponse.ok) {
      const errorText = await tokenResponse.text();
      console.error('Token exchange failed:', errorText);
      return new Response('Failed to exchange token', { status: 500 });
    }

    const tokens: GoogleTokenResponse = await tokenResponse.json();

    // 2. アクセストークンを使ってユーザー情報を取得
    const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
      headers: {
        Authorization: `Bearer ${tokens.access_token}`,
      },
    });

    if (!userInfoResponse.ok) {
      return new Response('Failed to fetch user info', { status: 500 });
    }

    const googleUser: GoogleUserInfo = await userInfoResponse.json();

    // 3. データベースでユーザーを確認・作成
    const now = Date.now();
    
    // 既存ユーザーを確認
    const existingUser = await DB.prepare(
      'SELECT * FROM users WHERE email = ?'
    ).bind(googleUser.email).first<User>();

    let user: User;

    if (existingUser) {
      // 既存ユーザーの最終ログイン時刻を更新
      await DB.prepare(
        'UPDATE users SET last_login = ?, name = ?, picture = ? WHERE id = ?'
      ).bind(now, googleUser.name, googleUser.picture, existingUser.id).run();

      user = {
        ...existingUser,
        last_login: now,
        name: googleUser.name,
        picture: googleUser.picture,
      };
    } else {
      // 新規ユーザーを作成
      const userId = crypto.randomUUID();
      
      await DB.prepare(
        'INSERT INTO users (id, email, name, picture, created_at, last_login) VALUES (?, ?, ?, ?, ?, ?)'
      ).bind(
        userId,
        googleUser.email,
        googleUser.name,
        googleUser.picture,
        now,
        now
      ).run();

      user = {
        id: userId,
        email: googleUser.email,
        name: googleUser.name,
        picture: googleUser.picture,
        created_at: now,
        last_login: now,
      };
    }

    // 4. セッショントークンを生成
    const sessionToken = await generateSessionToken();
    
    // セッション情報をKVに保存(簡易版:D1に保存)
    await DB.prepare(
      'CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, user_id TEXT, created_at INTEGER, expires_at INTEGER)'
    ).run();

    const expiresAt = now + (7 * 24 * 60 * 60 * 1000); // 7日後
    await DB.prepare(
      'INSERT OR REPLACE INTO sessions (token, user_id, created_at, expires_at) VALUES (?, ?, ?, ?)'
    ).bind(sessionToken, user.id, now, expiresAt).run();

    // 5. Cookieをセットしてホームページにリダイレクト
    return new Response(null, {
      status: 302,
      headers: {
        'Location': '/',
        'Set-Cookie': createSessionCookie(sessionToken),
      },
    });

  } catch (error) {
    console.error('Authentication error:', error);
    return new Response('Internal server error', { status: 500 });
  }
};

5-5. ユーザー情報取得エンドポイント

functions/api/auth/me.tsを作成:

import { Env, User } from '../../types';
import { getSessionFromCookie, jsonResponse, corsHeaders } from '../../utils';

export const onRequestGet: PagesFunction<Env> = async (context) => {
  const { request, env } = context;
  const { DB } = env;

  const sessionToken = getSessionFromCookie(request.headers.get('Cookie'));

  if (!sessionToken) {
    return jsonResponse({ error: 'Not authenticated' }, 401);
  }

  try {
    // セッションを確認
    const session = await DB.prepare(
      'SELECT user_id, expires_at FROM sessions WHERE token = ?'
    ).bind(sessionToken).first<{ user_id: string; expires_at: number }>();

    if (!session) {
      return jsonResponse({ error: 'Invalid session' }, 401);
    }

    // セッションの有効期限を確認
    if (session.expires_at < Date.now()) {
      // 期限切れセッションを削除
      await DB.prepare('DELETE FROM sessions WHERE token = ?').bind(sessionToken).run();
      return jsonResponse({ error: 'Session expired' }, 401);
    }

    // ユーザー情報を取得
    const user = await DB.prepare(
      'SELECT id, email, name, picture, created_at, last_login FROM users WHERE id = ?'
    ).bind(session.user_id).first<User>();

    if (!user) {
      return jsonResponse({ error: 'User not found' }, 404);
    }

    return jsonResponse({ user });

  } catch (error) {
    console.error('Error fetching user:', error);
    return jsonResponse({ error: 'Internal server error' }, 500);
  }
};

// OPTIONSリクエスト対応(CORS)
export const onRequestOptions: PagesFunction = async () => {
  return new Response(null, {
    status: 204,
    headers: corsHeaders,
  });
};

5-6. ログアウトエンドポイント

functions/api/auth/logout.tsを作成:

import { Env } from '../../types';
import { getSessionFromCookie, jsonResponse } from '../../utils';

export const onRequestPost: PagesFunction<Env> = async (context) => {
  const { request, env } = context;
  const { DB } = env;

  const sessionToken = getSessionFromCookie(request.headers.get('Cookie'));

  if (sessionToken) {
    try {
      // セッションを削除
      await DB.prepare('DELETE FROM sessions WHERE token = ?').bind(sessionToken).run();
    } catch (error) {
      console.error('Error deleting session:', error);
    }
  }

  // Cookieを削除
  return new Response(null, {
    status: 302,
    headers: {
      'Location': '/',
      'Set-Cookie': 'session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',
    },
  });
};

Step 6: フロントエンド実装

6-1. HTMLファイルの作成

public/index.htmlを作成:

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Google認証デモ</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
    }

    .container {
      background: white;
      border-radius: 12px;
      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
      padding: 40px;
      max-width: 400px;
      width: 100%;
      text-align: center;
    }

    h1 {
      color: #333;
      margin-bottom: 10px;
      font-size: 28px;
    }

    .subtitle {
      color: #666;
      margin-bottom: 30px;
      font-size: 14px;
    }

    .login-section, .user-section {
      display: none;
    }

    .login-section.active, .user-section.active {
      display: block;
    }

    .google-btn {
      background: #4285f4;
      color: white;
      border: none;
      padding: 12px 24px;
      border-radius: 6px;
      font-size: 16px;
      font-weight: 500;
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 10px;
      transition: background 0.3s;
      text-decoration: none;
    }

    .google-btn:hover {
      background: #357ae8;
    }

    .google-icon {
      width: 20px;
      height: 20px;
      background: white;
      border-radius: 3px;
      padding: 3px;
    }

    .user-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 15px;
      margin-bottom: 25px;
    }

    .user-avatar {
      width: 80px;
      height: 80px;
      border-radius: 50%;
      border: 3px solid #667eea;
    }

    .user-name {
      font-size: 20px;
      font-weight: 600;
      color: #333;
    }

    .user-email {
      color: #666;
      font-size: 14px;
    }

    .user-info {
      background: #f8f9fa;
      padding: 15px;
      border-radius: 8px;
      margin-bottom: 20px;
      text-align: left;
    }

    .info-row {
      display: flex;
      justify-content: space-between;
      padding: 8px 0;
      border-bottom: 1px solid #e9ecef;
    }

    .info-row:last-child {
      border-bottom: none;
    }

    .info-label {
      color: #666;
      font-size: 13px;
    }

    .info-value {
      color: #333;
      font-size: 13px;
      font-weight: 500;
    }

    .logout-btn {
      background: #dc3545;
      color: white;
      border: none;
      padding: 10px 30px;
      border-radius: 6px;
      font-size: 14px;
      cursor: pointer;
      transition: background 0.3s;
    }

    .logout-btn:hover {
      background: #c82333;
    }

    .loading {
      color: #666;
      padding: 20px;
    }

    .error {
      background: #fff3cd;
      color: #856404;
      padding: 15px;
      border-radius: 6px;
      margin-top: 20px;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>🔐 Google認証デモ</h1>
    <p class="subtitle">Cloudflare Pages + D1 + Google OAuth</p>

    <!-- ローディング -->
    <div id="loading" class="loading">
      読み込み中...
    </div>

    <!-- ログインセクション -->
    <div id="login-section" class="login-section">
      <p style="color: #666; margin-bottom: 20px;">
        Googleアカウントでログインしてください
      </p>
      <a href="/api/auth/login" class="google-btn">
        <svg class="google-icon" viewBox="0 0 24 24">
          <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
          <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
          <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
          <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
        </svg>
        Googleでログイン
      </a>
    </div>

    <!-- ユーザーセクション -->
    <div id="user-section" class="user-section">
      <div class="user-card">
        <img id="user-avatar" class="user-avatar" src="" alt="User Avatar">
        <div>
          <div id="user-name" class="user-name"></div>
          <div id="user-email" class="user-email"></div>
        </div>
      </div>

      <div class="user-info">
        <div class="info-row">
          <span class="info-label">ユーザーID</span>
          <span id="user-id" class="info-value"></span>
        </div>
        <div class="info-row">
          <span class="info-label">登録日時</span>
          <span id="user-created" class="info-value"></span>
        </div>
        <div class="info-row">
          <span class="info-label">最終ログイン</span>
          <span id="user-login" class="info-value"></span>
        </div>
      </div>

      <button id="logout-btn" class="logout-btn">ログアウト</button>
    </div>

    <!-- エラー表示 -->
    <div id="error" class="error" style="display: none;"></div>
  </div>

  <script>
    // ページ読み込み時にユーザー情報を取得
    async function checkAuth() {
      const loading = document.getElementById('loading');
      const loginSection = document.getElementById('login-section');
      const userSection = document.getElementById('user-section');
      const errorDiv = document.getElementById('error');

      try {
        const response = await fetch('/api/auth/me', {
          credentials: 'include',
        });

        loading.style.display = 'none';

        if (response.ok) {
          const data = await response.json();
          displayUser(data.user);
          userSection.classList.add('active');
        } else {
          loginSection.classList.add('active');
        }
      } catch (error) {
        console.error('Error checking auth:', error);
        loading.style.display = 'none';
        loginSection.classList.add('active');
        errorDiv.textContent = '認証状態の確認に失敗しました';
        errorDiv.style.display = 'block';
      }
    }

    // ユーザー情報を表示
    function displayUser(user) {
      document.getElementById('user-avatar').src = user.picture || 'https://via.placeholder.com/80';
      document.getElementById('user-name').textContent = user.name || 'No Name';
      document.getElementById('user-email').textContent = user.email;
      document.getElementById('user-id').textContent = user.id.substring(0, 8) + '...';
      document.getElementById('user-created').textContent = formatDate(user.created_at);
      document.getElementById('user-login').textContent = formatDate(user.last_login);
    }

    // 日時フォーマット
    function formatDate(timestamp) {
      const date = new Date(timestamp);
      return date.toLocaleString('ja-JP', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit',
      });
    }

    // ログアウト処理
    document.getElementById('logout-btn').addEventListener('click', async () => {
      try {
        const response = await fetch('/api/auth/logout', {
          method: 'POST',
          credentials: 'include',
        });

        if (response.redirected) {
          window.location.href = response.url;
        } else {
          window.location.reload();
        }
      } catch (error) {
        console.error('Logout error:', error);
        alert('ログアウトに失敗しました');
      }
    });

    // 初期化
    checkAuth();
  </script>
</body>
</html>

Step 7: ローカルでの動作確認

7-1. 開発サーバーの起動

npx wrangler pages dev public --compatibility-date=2024-01-01

ブラウザで http://localhost:8788 にアクセスします。

7-2. 動作確認手順

  1. 「Googleでログイン」ボタンをクリック
  2. Googleアカウントを選択
  3. アプリへのアクセスを許可
  4. 自動的にホームページにリダイレクトされ、ユーザー情報が表示される
  5. 「ログアウト」ボタンをクリックして、ログイン画面に戻ることを確認

7-3. データベースの確認

ユーザーが正しく登録されているか確認:

# ローカルD1のデータを確認
npx wrangler d1 execute auth-db --local --command="SELECT * FROM users"

# セッション情報を確認
npx wrangler d1 execute auth-db --local --command="SELECT * FROM sessions"

7-4. デバッグのコツ

問題が発生した場合は、以下を確認:

コンソールログの確認

# wranglerのログを詳細モードで表示
npx wrangler pages dev public --compatibility-date=2024-01-01 --log-level=debug

ブラウザの開発者ツール

  • Network タブでAPIリクエストを確認
  • Console タブでJavaScriptエラーを確認
  • Application > Cookies でセッションCookieを確認

よくあるローカル開発の問題

  1. ポート8788が使用中の場合
# 別のポートを指定
npx wrangler pages dev public --port=8789 --compatibility-date=2024-01-01
  1. D1データベースが見つからない
# .wranglerフォルダを削除して再起動
rm -rf .wrangler
npx wrangler pages dev public --compatibility-date=2024-01-01
  1. 環境変数が読み込まれない
  • .dev.varsファイルがプロジェクトルートにあることを確認
  • ファイル名が正確に.dev.varsであることを確認(スペースなし)
  • wranglerを再起動

Step 8: 本番環境へのデプロイ

8-1. Cloudflare Pagesプロジェクトの作成

# Cloudflareにログイン
npx wrangler login

# 初回デプロイ
npx wrangler pages deploy public --project-name=cloudflare-google-auth

デプロイが完了すると、以下のようなURLが表示されます:

✨ Success! Uploaded 1 file
✨ Deployment complete! Take a peek over at https://xxxxxxxx.cloudflare-google-auth.pages.dev

8-2. 本番用データベースのセットアップ

# 本番環境のD1にテーブルを作成
npx wrangler d1 execute auth-db --remote --file=schema.sql

# 確認
npx wrangler d1 execute auth-db --remote --command="SELECT name FROM sqlite_master WHERE type='table'"

8-3. Cloudflare Dashboardでの設定

ステップ1: プロジェクト設定を開く

  1. Cloudflare Dashboardにログイン
  2. 左サイドバーから「Workers & Pages」を選択
  3. 作成したcloudflare-google-authプロジェクトをクリック

ステップ2: 環境変数の設定

  1. 「Settings」タブをクリック
  2. 「Environment variables」セクションを見つける
  3. 「Add variables」をクリック

以下の変数をProduction環境に追加:

変数名タイプ
GOOGLE_CLIENT_SECRETあなたのGoogleクライアントシークレットSecret
SESSION_SECRETランダムな64文字以上の文字列Secret

重要: 「Encrypt」オプションを有効にして保存してください。

ステップ3: D1バインディングの確認

  1. 「Settings」→「Functions」に移動
  2. 「D1 database bindings」セクションでDBバインディングが設定されていることを確認
  3. なければ「Add binding」から追加:
    • Variable name: DB
    • D1 database: auth-dbを選択

8-4. wrangler.tomlの本番用更新

wrangler.tomlを本番環境用に更新:

name = "cloudflare-google-auth"
compatibility_date = "2024-01-01"
pages_build_output_dir = "public"

[[d1_databases]]
binding = "DB"
database_name = "auth-db"
database_id = "ここにあなたのdatabase_idを入力"

GOOGLE_CLIENT_ID = "あなたのGoogleクライアントID.apps.googleusercontent.com" # 本番環境用のリダイレクトURI GOOGLE_REDIRECT_URI = "https://cloudflare-google-auth.pages.dev/api/auth/callback"

注意: あなたの実際のプロジェクト名に合わせてURLを変更してください。

8-5. Google Cloudでのリダイレクト承認

ステップ1: Google Cloud Consoleにアクセス

  1. Google Cloud Consoleにアクセス
  2. 左サイドバーから「APIとサービス」→「認証情報」を選択
  3. 作成したOAuthクライアントIDをクリック

ステップ2: 本番URLを追加

「承認済みのリダイレクトURI」セクションで「URIを追加」をクリックし、以下を追加:

https://あなたのプロジェクト名.pages.dev/api/auth/callback

例:

https://cloudflare-google-auth.pages.dev/api/auth/callback

保存をクリック。

8-6. 再デプロイ

設定を反映させるため、再度デプロイ:

npx wrangler pages deploy public --project-name=cloudflare-google-auth

8-7. 本番環境での動作確認

  1. デプロイされたURL(https://あなたのプロジェクト名.pages.dev)にアクセス
  2. 「Googleでログイン」をクリック
  3. 認証フローが正常に動作することを確認
  4. ユーザー情報が表示されることを確認
  5. ログアウトが正常に動作することを確認

8-8. 本番データベースの確認

# 本番環境のユーザーを確認
npx wrangler d1 execute auth-db --remote --command="SELECT id, email, name, created_at FROM users"

# セッション数を確認
npx wrangler d1 execute auth-db --remote --command="SELECT COUNT(*) as session_count FROM sessions"

Step 9: トラブルシューティング

よくある問題と解決方法

1. 「redirect_uri_mismatch」エラー

エラーメッセージ:

Error 400: redirect_uri_mismatch

原因: Google Cloudで設定したリダイレクトURIと実際のURIが一致していない

解決方法:

  1. wrangler.tomlGOOGLE_REDIRECT_URIを確認
  2. Google Cloud Consoleの承認済みリダイレクトURIを確認
  3. 両方が完全に一致していることを確認(プロトコル、ドメイン、パス、末尾のスラッシュすべて)

チェックリスト:

  • [ ] https://で始まっている(http://ではない)
  • [ ] ドメインが正確(.pages.devのスペルミスなし)
  • [ ] パスが/api/auth/callbackで一致
  • [ ] 末尾にスラッシュがない(あるいは両方にある)

2. 「Database not found」エラー

エラーメッセージ:

Error: Database 'auth-db' not found

原因: D1データベースが作成されていない、またはバインディングが正しくない

解決方法:

# データベースのリストを確認
npx wrangler d1 list

# データベースが存在しない場合は作成
npx wrangler d1 create auth-db

# テーブルを作成
npx wrangler d1 execute auth-db --local --file=schema.sql
npx wrangler d1 execute auth-db --remote --file=schema.sql

wrangler.tomlのバインディングを確認:

[[d1_databases]]
binding = "DB"  # ← この名前がコード内と一致しているか確認
database_name = "auth-db"
database_id = "正しいID"

3. 「Invalid session」エラー

原因: セッションテーブルが作成されていない、または期限切れ

解決方法:

# セッションテーブルを確認
npx wrangler d1 execute auth-db --local --command="SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'"

# テーブルが存在しない場合は作成
npx wrangler d1 execute auth-db --local --command="CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, user_id TEXT, created_at INTEGER, expires_at INTEGER)"

# 本番環境にも適用
npx wrangler d1 execute auth-db --remote --command="CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, user_id TEXT, created_at INTEGER, expires_at INTEGER)"

4. CORSエラー

エラーメッセージ(ブラウザコンソール):

Access to fetch at '...' from origin '...' has been blocked by CORS policy

原因: CORS設定が不足している

解決方法: functions/utils.tscorsHeadersを確認し、必要に応じてAccess-Control-Allow-Originを更新:

export const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://あなたのドメイン.pages.dev',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Cookie',
  'Access-Control-Allow-Credentials': 'true',
};

5. 「Failed to exchange token」エラー

原因: GOOGLE_CLIENT_SECRETが正しく設定されていない

解決方法:

  1. .dev.varsファイルを確認(ローカル)
  2. Cloudflare Dashboardの環境変数を確認(本番)
  3. Google Cloud Consoleで正しいシークレットをコピー
  4. 前後にスペースが入っていないか確認
# ローカルで環境変数を再確認
cat .dev.vars

# wranglerを再起動
npx wrangler pages dev public --compatibility-date=2024-01-01

6. セッションが保存されない・すぐ切れる

原因: Cookieの設定やセッションの有効期限の問題

解決方法:

functions/utils.tscreateSessionCookieを確認:

export function createSessionCookie(token: string, maxAge: number = 7 * 24 * 60 * 60): string {
  // 本番環境ではSecureが必須
  return `session=${token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=${maxAge}`;
}

チェックポイント:

  • Secure属性がHTTPS環境で有効
  • SameSite=LaxまたはStrictに設定
  • Max-Ageが適切(秒単位)

ブラウザの開発者ツールで確認:

  1. F12を押して開発者ツールを開く
  2. Application > Cookies
  3. session Cookieが存在し、有効期限が正しいことを確認

7. デプロイ後に環境変数が反映されない

原因: 環境変数の設定後、再デプロイが必要

解決方法:

# 再デプロイ
npx wrangler pages deploy public --project-name=cloudflare-google-auth

# または強制的に再ビルド
npx wrangler pages deploy public --project-name=cloudflare-google-auth --branch=main

Cloudflare Dashboardで確認:

  1. プロジェクト > Deployments
  2. 最新のデプロイメントが表示されているか確認
  3. 必要に応じて「Retry deployment」をクリック

Step 9: トラブルシューティング

よくある問題と解決方法

1. 「redirect_uri_mismatch」エラー

原因: Google Cloudで設定したリダイレクトURIと実際のURIが一致していない

解決方法:

  • wrangler.tomlGOOGLE_REDIRECT_URIを確認
  • Google Cloud Consoleの承認済みリダイレクトURIを確認
  • 両方が完全に一致していることを確認(末尾のスラッシュにも注意)

2. 「Database not found」エラー

原因: D1データベースが作成されていない、またはバインディングが正しくない

解決方法:

# データベースの存在を確認
npx wrangler d1 list

# テーブルが作成されているか確認
npx wrangler d1 execute auth-db --local --command="SELECT name FROM sqlite_master WHERE type='table'"

3. セッションが保存されない

原因: セッションテーブルが作成されていない

解決方法:

# セッションテーブルを手動で作成
npx wrangler d1 execute auth-db --local --command="CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, user_id TEXT, created_at INTEGER, expires_at INTEGER)"

4. CORSエラー

原因: クロスオリジンリクエストが許可されていない

解決方法: すでにコードに含まれていますが、utils.tscorsHeadersが正しく設定されているか確認


Step 10: セキュリティの向上(オプション)

10-1. CSRF対策の追加

より安全にするため、CSRF対策を追加できます。

functions/utils.tsにCSRFトークン生成を追加:

export async function generateCSRFToken(): Promise<string> {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}

10-2. レート制限の実装

functions/api/auth/login.tsにレート制限を追加:

// IPアドレスベースの簡易レート制限
const rateLimitKey = request.headers.get('CF-Connecting-IP') || 'unknown';
// 実装はKVまたはDurable Objectsを使用(今回は省略)

10-3. セキュアなCookie設定

本番環境では以下のCookie属性を推奨:

export function createSessionCookie(token: string, maxAge: number = 7 * 24 * 60 * 60): string {
  return `session=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${maxAge}`;
}


まとめ

お疲れ様でした!これで以下の機能が実装できました:

✅ Googleアカウントでのログイン ✅ 初回ログイン時の自動ユーザー登録 ✅ Cloudflare D1でのユーザー管理 ✅ セッション管理とログアウト ✅ 完全にCloudflare上で動作するアプリケーション

次のステップ

この基盤を使って、以下のような機能を追加できます:

  • ユーザープロフィール編集: ユーザーが自分の情報を更新
  • 保護されたページ: 認証が必要なページの作成
  • ロール管理: 管理者と一般ユーザーの区別
  • Cloudflare Workers KV: より高速なセッション管理
  • Durable Objects: リアルタイム機能の追加

参考リンク


完成したコードの全体像

cloudflare-google-auth/
├── functions/
│   ├── types.ts (型定義)
│   ├── utils.ts (ユーティリティ関数)
│   └── api/
│       └── auth/
│           ├── login.ts (ログイン開始)
│           ├── callback.ts (OAuth コールバック)
│           ├── me.ts (ユーザー情報取得)
│           └── logout.ts (ログアウト)
├── public/
│   └── index.html (フロントエンド)
├── schema.sql (データベーススキーマ)
├── wrangler.toml (Cloudflare設定)
├── .dev.vars (ローカル環境変数)
├── .gitignore
└── package.json