Cloudflare Pages FunctionsでGoogle認証を実装する完全ガイド
この記事では、Cloudflare Pages Functions、D1データベース、Google OAuth 2.0を使って、完全にCloudflare上で動作する認証システムを構築します。フロントエンドからバックエンド、データベースまで、すべてコピペで動くコードを提供します。
この記事で作るもの
- Google認証によるログイン・ログアウト機能
- 初回ログイン時の自動ユーザー登録
- Cloudflare D1データベースでのユーザー管理
- セッション管理
前提条件
- Node.js(v18以降)がインストールされていること
- Cloudflareアカウント(無料プランでOK)
- Googleアカウント
Step 1: Google Cloud Consoleでの準備
1-1. Google Cloud Consoleにアクセス
- Google Cloud Consoleにアクセス
- 「プロジェクトを選択」→「新しいプロジェクト」をクリック
- プロジェクト名を入力(例:
my-auth-app)して「作成」
1-2. OAuth 2.0クライアントIDの作成
- 左サイドバーから「APIとサービス」→「認証情報」を選択
- 「認証情報を作成」→「OAuthクライアントID」をクリック
- 「同意画面を構成」をクリック(初回のみ)
- User Typeは「外部」を選択して「作成」
- 以下の情報を入力:
- アプリ名:
My Auth App - ユーザーサポートメール: あなたのメールアドレス
- デベロッパー連絡先: あなたのメールアドレス
- アプリ名:
- 「保存して次へ」を3回クリックして完了
1-3. OAuth認証情報の取得
- 再度「認証情報を作成」→「OAuthクライアントID」
- アプリケーションの種類: 「ウェブアプリケーション」
- 名前:
Web Client - 承認済みのリダイレクトURIに以下を追加:
http://localhost:8788/api/auth/callbackhttps://あなたのプロジェクト名.pages.dev/api/auth/callback - 「ログアウト」ボタンをクリックして、ログイン画面に戻ることを確認
Step 2: プロジェクトのセットアップ
2-1. プロジェクトの初期化
プロジェクトフォルダを作成
mkdir cloudflare-google-auth
cd cloudflare-google-authpackage.jsonを作成
npm init -y必要なパッケージをインストール
npm install -D wrangler
npm install -D @cloudflare/workers-types2-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 32Windows (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. 動作確認手順
- 「Googleでログイン」ボタンをクリック
- Googleアカウントを選択
- アプリへのアクセスを許可
- 自動的にホームページにリダイレクトされ、ユーザー情報が表示される
- 「ログアウト」ボタンをクリックして、ログイン画面に戻ることを確認
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を確認
よくあるローカル開発の問題
- ポート8788が使用中の場合
# 別のポートを指定
npx wrangler pages dev public --port=8789 --compatibility-date=2024-01-01
- D1データベースが見つからない
# .wranglerフォルダを削除して再起動
rm -rf .wrangler
npx wrangler pages dev public --compatibility-date=2024-01-01
- 環境変数が読み込まれない
.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.dev8-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: プロジェクト設定を開く
- Cloudflare Dashboardにログイン
- 左サイドバーから「Workers & Pages」を選択
- 作成した
cloudflare-google-authプロジェクトをクリック
ステップ2: 環境変数の設定
- 「Settings」タブをクリック
- 「Environment variables」セクションを見つける
- 「Add variables」をクリック
以下の変数をProduction環境に追加:
| 変数名 | 値 | タイプ |
|---|---|---|
GOOGLE_CLIENT_SECRET | あなたのGoogleクライアントシークレット | Secret |
SESSION_SECRET | ランダムな64文字以上の文字列 | Secret |
重要: 「Encrypt」オプションを有効にして保存してください。
ステップ3: D1バインディングの確認
- 「Settings」→「Functions」に移動
- 「D1 database bindings」セクションで
DBバインディングが設定されていることを確認 - なければ「Add binding」から追加:
- Variable name:
DB - D1 database:
auth-dbを選択
- Variable name:
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にアクセス
- Google Cloud Consoleにアクセス
- 左サイドバーから「APIとサービス」→「認証情報」を選択
- 作成した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. 本番環境での動作確認
- デプロイされたURL(
https://あなたのプロジェクト名.pages.dev)にアクセス - 「Googleでログイン」をクリック
- 認証フローが正常に動作することを確認
- ユーザー情報が表示されることを確認
- ログアウトが正常に動作することを確認
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が一致していない
解決方法:
wrangler.tomlのGOOGLE_REDIRECT_URIを確認- Google Cloud Consoleの承認済みリダイレクトURIを確認
- 両方が完全に一致していることを確認(プロトコル、ドメイン、パス、末尾のスラッシュすべて)
チェックリスト:
- [ ]
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.tsのcorsHeadersを確認し、必要に応じて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が正しく設定されていない
解決方法:
.dev.varsファイルを確認(ローカル)- Cloudflare Dashboardの環境変数を確認(本番)
- Google Cloud Consoleで正しいシークレットをコピー
- 前後にスペースが入っていないか確認
# ローカルで環境変数を再確認
cat .dev.vars
# wranglerを再起動
npx wrangler pages dev public --compatibility-date=2024-01-01
6. セッションが保存されない・すぐ切れる
原因: Cookieの設定やセッションの有効期限の問題
解決方法:
functions/utils.tsのcreateSessionCookieを確認:
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が適切(秒単位)
ブラウザの開発者ツールで確認:
- F12を押して開発者ツールを開く
- Application > Cookies
sessionCookieが存在し、有効期限が正しいことを確認
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で確認:
- プロジェクト > Deployments
- 最新のデプロイメントが表示されているか確認
- 必要に応じて「Retry deployment」をクリック
Step 9: トラブルシューティング
よくある問題と解決方法
1. 「redirect_uri_mismatch」エラー
原因: Google Cloudで設定したリダイレクトURIと実際のURIが一致していない
解決方法:
wrangler.tomlのGOOGLE_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.tsのcorsHeadersが正しく設定されているか確認
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