コンテンツにスキップ

Claude Code でソーシャルログイン(Google・GitHub・X 等)の寄せ書きアプリを作る

この記事では、ソーシャルログイン(Google・GitHub・X・Facebook などの「◯◯でログイン」)で本人を確かめる「みんなの寄せ書き」アプリを作る。ユーザーは好きなサービスのアカウントでログインし、名前と一言を残すと、みんなの一言が並ぶ。登録・投稿・ログアウトまでを、Cloudflare の無料枠(Workers + D1)と各サービスの無料の OAuth だけで自前実装する。

「Google でログイン」「Facebook でログイン」のように、すでに持っているアカウントで別のサービスに入るしくみを、まとめてソーシャルログインと呼ぶ。これは機能の呼び名であって、規格の名前ではない。 中身を決めているのは次の2つ。

何を担うか
ソーシャルログイン「◯◯でログイン」という機能の呼び名
OAuth 2.0「このアプリに、ここまで許可する」を安全に受け渡すしくみ。もともとは許可(認可)のための規格で、「誰か」を伝えるためのものではない
OpenID ConnectOAuth 2.0 の上に、「この人が誰か」を伝える層を足したもの

本記事が使うのは OpenID Connect のほう。 Google に要求するスコープを openid だけにしているのがその合図で、受け取るのは「この人だ」という結果(sub)だけになる。

やっていることは、ログインを Google や Facebook のような信頼できる相手に代行してもらうこと。この代行してくれる側(Google・Facebook・X など)をプロバイダと呼ぶ。以降はこの呼び方を使う。

依頼する側から見た要点は3つ。

  • ユーザーはそのサービスの画面でログイン・同意し、こちらのアプリにはパスワードを一切渡さない。アプリは「この人はそのサービス上のこのユーザーだ」という結果だけを受け取る。
  • だからパスワードを保存しない(漏洩のリスクを持たない)。メール送信の仕組みも要らない。
  • アカウントを一意に識別するのは、各サービスが返す不変のユーザーID(Google なら sub、GitHub なら数値 id など)。メールアドレスではない(後述)。

1-2. 進め方:まず Google だけで作り、あとから増やす

Section titled “1-2. 進め方:まず Google だけで作り、あとから増やす”

いきなり全プロバイダを作らず、まず Google だけで一通り動かす。ログイン → 投稿 → ログアウトが動いたら、X や GitHub を1つずつ足していく。自分で作るときもハンズオンでも、この順が一番つまずかない。

作るのは、この画面に飛ばすところ。どこかで見たことがあるはずで、これが Google のソーシャルログインの入口になる。

Google のアカウント選択画面

「Google でログイン」を押すと出るアカウント選択画面。この記事で作るアプリからも、これが開く

プロバイダを増やすときに直すのは、次の3箇所だけ

  1. §6-1(ログインルート):そのプロバイダのルートと normalize の分岐を1つ足す。
  2. §8(ボタン):フロントにログインボタンを1つ足す。
  3. §2(プロバイダ登録):そのサービスの開発者コンソールで OAuth クライアントを作り、シークレットとリダイレクト URI を登録する。

変えないのは §4 スキーマ・§6-2 セッション・§7 アプリの API。プロバイダ非依存に作ってあるので、増やしても不変。プロバイダごとの差と具体的な足し方は、Google が動いたあとの §13・§14 で扱う。

2-1. 作るもの:みんなの寄せ書き

Section titled “2-1. 作るもの:みんなの寄せ書き”

付箋のようなカードで寄せ書きが並び、その下に Google でログインのボタンがある画面

完成イメージ。書いた一言が付箋のように並ぶ。ログインすると自分の分を書いて編集できる

  • 各ユーザーが好きなサービス(Google・Facebook・X など)でログインしてアカウントを作る。
  • 名前と一言を入力すると、全員の寄せ書きに並ぶ(1人1投稿、あとから編集可)。
  • 閲覧は公開、投稿・編集はログインした本人だけ。ログアウトもできる。

認証まわりで復旧の仕組み(パスワード再発行やリカバリコード)を自前で持たなくていいのがソーシャルログインの利点。アカウントの回復はログイン先のサービス(Google など)が持っているからだ。

このハンズオンは新規プロジェクトとして作る。

2-2. ベース名を決めて CLAUDE.md に書く

Section titled “2-2. ベース名を決めて CLAUDE.md に書く”

作業フォルダ・D1データベース・Worker の3つに名前を付ける。バラバラに決めると後で対応が分からなくなるので、最初に自分だけのベース名を1つ決めておき、3つともその名前から付ける。この記事では yosegaki-sol を例に使う。

付けるもの付け方
作業フォルダベース名~/claude/yosegaki-sol
D1データベースベース名のうしろに -dbyosegaki-sol-db
Worker(=公開URL)ベース名https://yosegaki-sol.<アカウント>.workers.dev

⚠️ ベース名はそのまま公開URLになる。人に見せてよい名前にする。

作業フォルダの名前を決めた時点で、公開URLも決まってしまう。wrangler は wrangler.jsoncname が無いと、フォルダ名をそのまま Worker 名に使う(Using the project name "..." as the Worker name. と表示して、そのまま進む)。あとから変えるなら wrangler.jsoncname を直すが、§9 で Google 側に登録するリダイレクト URI もつくり直しになるので、先に決めておくほうがよい。この記事では §5 で name を明示するようプロンプトに入れてある。

準備するのは3つ。

  1. ~/claude の中に、ベース名の作業フォルダを作る(例:~/claude/yosegaki-sol)。
  2. そのフォルダで Claude Code を使える状態にする。
  3. Cloudflare に wrangler でログイン済みか確認する。

作るものの仕様を、先に CLAUDE.md に書いてしまう。 以降のプロンプトでは「仕様のとおりに」と参照するだけでよくなる。

Claude
このプロジェクトのベース名と、これから作るアプリの仕様を CLAUDE.md に書いてください。
## 名前
- ベース名は yosegaki-sol
- アプリ名・Worker 名・作業フォルダ名 = ベース名
- D1データベース名 = ベース名のうしろに -db を付けた名前
## 作るもの:みんなの寄せ書き
- 名前と一言を書くと、みんなの寄せ書きに並ぶ
- 1人1件。あとから自分の分だけ編集できる
- 表示名は本人が決める。あとから変えられる
- 閲覧は誰でもできる。書き込みと編集は本人だけ
- 本人確認は Google などのソーシャルログインで行う。パスワードは自分のアプリで持たない
- 画面は上から「みんなの寄せ書き」の一覧、「あなたの寄せ書き」の入力欄、の順に並べる
- 寄せ書きは付箋のようなカードを並べ、本文の下に右寄せで「— 名前」を出す

先頭の yosegaki-sol(1箇所だけ)を、自分のベース名に置き換えて渡す。以降のプロンプトは「CLAUDE.md のとおりに」で通じるようになる。なお、この記事がターミナル用に載せているコマンド例は yosegaki-sol のままなので、そちらは自分で読み替える。

この仕様は3つの記事で共通。違うのは「本人確認を何でやるか」の1行だけで、アプリそのものは同じ。同じ寄せ書きを、ログインキーパスキーで作るハンズオンもある。

なぜ先に書くのか。仕様を毎回のプロンプトに書くと、作りたいものの話と、どう作るかの話が混ざる。先に1回書いておけば、以降のプロンプトは「どう作るか」だけになる。自分のアプリを作るときは、ここを差し替えるだけでよい。

3. Google 側で OAuth クライアントを作る

Section titled “3. Google 側で OAuth クライアントを作る”

Google のログインを使うには、Google Cloud Console(認証まわりは現在「Google Auth Platform」に整理されている)で OAuth クライアントを1つ作り、クライアント ID とクライアントシークレットを受け取る。以下は 2026 年 9 月時点の実際の画面。

UI は変わりうる:Google のコンソールは頻繁に画面が変わる。ボタン名が違ったら、同じ意味の項目を探して読み替える。

(0) 初めて Google Cloud を使うとき。そのアカウントで Google Cloud Console を初めて開くと、プロジェクト作成の画面にかぶさって「〈アカウント名〉 へようこそ」というダイアログが出る。(日本)を確認し、「同意して続行」を押す。クレジットカードの登録は求められない。

チェックボックスは2つあり、どちらも最初は外れている。入れるのは上だけでよい。

チェック扱い
利用規約(Google Cloud Platform の利用規約 および 適用されるサービスと API の利用規約に同意します)入れる。 これを入れないと先へ進めない
最新情報に関する通知メール入れない。 Google やパートナーからの案内メールを受け取るだけの任意項目

Google Cloud の初回ダイアログ。国の選択、利用規約と通知メールのチェック、同意して続行のボタン

初回だけ出るダイアログ。チェックは2つとも外れた状態で開き、利用規約に入れるまで「同意して続行」は押せない

🎁 無料トライアルの帯は押さなくてよい。画面の上部に「$300 相当の無料トライアルをご利用ください」「有効化」といった案内が出るが、押すとクレジットカードの登録に進む。OAuth クライアントの作成に課金は不要なので、このハンズオンでは最後まで使わない。

「割り当て内の残りのプロジェクト数は ◯ projects 件です」という警告が出ることがある。プロジェクトをいくつまで作れるかの残数で、数はアカウントによって違う(手元では別々のアカウントで 12 と 17 だった)。このハンズオンで作るのは1つなので気にしなくてよい。

(1) プロジェクトを作るGoogle Cloud Console を開き、「新しいプロジェクト」で、プロジェクト名(例:yosegaki-sol)を入れて作成する。作成の画面を直接開くなら https://console.cloud.google.com/projectcreate

プロジェクト名には My Project 22604 のような既定値が最初から入っている。消してから自分のベース名を入れる。入れ直すと、下の「プロジェクト ID」もその名前になる(yosegaki-sol と入れれば ID も yosegaki-sol)。ID は「後で変更することはできません」と書かれているが、このハンズオンでは使わないので気にしなくてよい。

「組織」「親リソース」の欄は「組織なし」のままでよい。会社などの Google Workspace に属していないアカウントでは、これ以外を選べない。アカウントによっては「組織」の欄自体が出ない(そのときは「親リソース」だけがある)。

新しいプロジェクトの作成画面。プロジェクト名にベース名を入れた状態

プロジェクト名に自分のベース名を入れる。プロジェクト ID も同じ名前になる

🚨 作っただけでは、そのプロジェクトに切り替わらない。 作成しても画面上部のプロジェクト選択は前に見ていたプロジェクトのままなので、明示的に切り替える。作成直後なら、右上の通知に出る「プロジェクトを選択」を押すのが早い。以降の作業はすべてこのプロジェクトの中で行うので、ここを飛ばすと別のプロジェクトを設定してしまう。

作成完了の通知。プロジェクトを選択のリンクがある

作成の通知から「プロジェクトを選択」で切り替える

(2) OAuth 同意画面を構成する次の URL を直接開くのが確実。

https://console.cloud.google.com/auth/overview

新しいプロジェクトでは「Google Auth Platform はまだ構成されていません」と出るので「開始」を押す。

なぜメニューから辿らずに URL を開くのか。Google Auth Platform は左メニューの初期表示に出ていないことがあるうえ、ホームのダッシュボードには常設の左メニューがそもそも無い(ハンバーガーから開く形になる)。URL で開けばどちらも関係なく着くし、着いた先ではその画面用の左メニューが出るので、以降の移動も楽になる。メニューから辿るなら、「すべてのプロダクトを表示」の中に「Google Auth Platform」がある。

⚠️ 画面上部のプロジェクト選択が (1) で作ったプロジェクトになっているか確認する。URL を直接開いた場合も、表示されるのは「いま選ばれているプロジェクト」の設定なので、別プロジェクトのまま進めると設定先を間違える。URL の末尾に ?project=<プロジェクト ID> が付いていれば、そのプロジェクトが選ばれている。

Google Auth Platform 未構成の画面。左に概要・ブランディング・対象・クライアントなどのメニューがある

まだ構成されていない状態。「開始」から設定する。URL の末尾に project=yosegaki-sol が付いていて、正しいプロジェクトを見ていることが分かる

この画面の左メニューが、以降の行き先になる。 (3) のテストユーザー登録は「対象」、(4) の OAuth クライアント作成は「クライアント」で行う。

「開始」を押すと「プロジェクト構成」という4ステップのウィザードが開く。上から順に埋め、各ステップの「次へ」で下に進み、最後に「作成」を押す。

ステップ入れるもの
アプリ情報アプリ名(例:みんなの寄せ書き。ログインの同意画面に出る名前)と、ユーザー サポートメール(自分のアドレスを選ぶ)
対象外部」を選ぶ
連絡先情報メールアドレス(自分のアドレス。プロジェクトの変更を Google が知らせるのに使う)
終了Google API サービス: ユーザーデータに関するポリシーに同意します。」にチェックして「続行

プロジェクト構成のウィザード。アプリ情報にアプリ名とサポートメールを入れた状態

4ステップのウィザード。①にアプリ名とサポートメールを入れ、「次へ」で②に進む

②の「対象」は「外部」を選ぶ。 「内部」は組織(Google Workspace)のアカウント向けなので、個人の Gmail では選べない。「外部」を選ぶと、アプリはテストモードで起動し、テストユーザーのリストに追加した人だけが使える状態になる(→ (3))。

対象の選択。内部と外部のラジオボタン

「外部」を選ぶ。テストモードで起動し、テストユーザーだけが使える状態になる

作成すると「OAuth の構成を作成しました。」と出て、「OAuth クライアントを作成」のボタンが現れる。そのまま (4) へ進める。

OAuth の構成が作成され、OAuth クライアントを作成のボタンが出た状態

構成が完了した状態。ここから OAuth クライアントの作成に進める

⚠️ 「アプリの OAuth 構成が完了していません」という警告が出るが、このまま進めてよい。 「対象」ページの上部に「不足している情報を入力する必要があります。[ブランディング] ページにアクセスしてください」と出ることがある。だがブランディングを開くと、必須(*)のアプリ名・ユーザーサポートメール・デベロッパーの連絡先メールはウィザードで入れた値で埋まっていて、「保存」も押せない(変えるものが無い)。不足しているのはロゴ・ホームページ・プライバシーポリシー・利用規約という、公開(審査)のための任意項目で、「アプリを公開」がグレーアウトしているのはそのため。テストモードで動かすぶんには要らない(同じブランディングの画面にも「公開ステータスが『テスト中』の場合は、送信は不要です」と書かれている)。この警告が出たままでも、次の (4) の OAuth クライアントは作れる(手元で確認済み)。

(3) テストユーザーを登録する。外部=テストモードのアプリは、原則としてテストユーザーに登録した Google アカウントに限定される。「対象」ページのテストユーザーで「Add users」を押し、自分の Google アカウントのメールを追加しておく(テスト中にログインする人はここに入れる)。

「対象」ページ。公開ステータス・ユーザーの種類・OAuth ユーザー数の上限・テストユーザー

「対象」ページ。ここでテストユーザーを足す。上の「公開ステータス」と「OAuth ユーザー数の上限」もあとで関わってくる

実際の挙動は要確認:手元で試したところ、本記事の最小スコープ(openid だけ)だとテストユーザー未登録のアカウントでもそのままログインできた(Google がこの制限を厳密に適用しないことがあるようだ)。逆に、アカウントによっては「アクセスをブロック:このアプリは Google の審査プロセスを完了していません」と弾かれることもある。弾かれたら、そのアカウントをテストユーザーに追加すれば入れる。確実にしたいなら、ログインさせたいアカウントは先に登録しておく。

同じ画面の上部にある 「公開ステータス:テスト中」「OAuth ユーザー数の上限」 も見ておく。テスト中の間はテストユーザーしかアクセスできず、しかも「アプリの確認前の許可済みユーザー数の上限は 100 で、この上限はアプリの全期間でカウントされる」と書かれている。

ハンズオンを進めるあいだはテストモードのままでよい。ただし作ったアプリを本当に不特定多数へ公開するなら、この画面の「アプリを公開」で本番に切り替える必要がある。切り替えないと、100人を上限に、しかも一人ずつメールアドレスを登録した人しかログインできない。自分のアプリを公開する段になったら、ここに戻ってくること(→ §15)。

(4) OAuth クライアントを作る。左メニューの「クライアント」を開き、「+ クライアントを作成」を押す((2) の完了画面から「OAuth クライアントを作成」で来てもよい)。アプリケーションの種類で「ウェブ アプリケーション」を選ぶ。

アプリケーションの種類を選ぶドロップダウン

「ウェブ アプリケーション」を選ぶ

名前には「ウェブ クライアント 1」という既定値が入っているので、消して自分の名前を入れる(例:yosegaki-sol-web)。この名前はコンソールで見分けるためだけのもので、ログインする人には表示されない。

クライアント名を入れた状態。承認済みドメインが自動追加されるという注記がある

名前を入れる。既定値は消してよい

「承認済みの JavaScript 生成元」は空のままでよい。ブラウザから直接 Google を呼ぶ作りのときに使うもので、本記事は Worker 側で受けるので要らない。

ブランディングの「承認済みドメイン」も自分で埋めなくてよい。この画面に「下で追加する URI のドメインは、OAuth 同意画面に承認済みドメインとして自動で追加されます」と書かれているとおり、次に入れるリダイレクト URI から自動で登録される。

承認済みのリダイレクト URI の「+ URI を追加」で、ログインから戻ってくる URL を登録する。§2-2 で決めたベース名から、次の形になる。

https://<ベース名>.<アカウント>.workers.dev/api/auth/google

承認済みのリダイレクト URI を入力した状態。設定の反映に時間がかかるという注記がある

リダイレクト URI にコールバックの URL を登録して「作成」

🚨 登録してもすぐには効かないことがある。同じ画面に「設定が有効になるまで 5 分から数時間かかることがあります」と書かれている。URL が完全に合っているのに redirect_uri_mismatch が出るときは、これを疑う。打ち間違いを探す前に、少し待ってから試す。

<アカウント> の部分が分からなければ、ここは空のままでよい。Cloudflare の Workers サブドメインは、§9 でデプロイすると公開 URL としてそのまま表示される。リダイレクト URI はあとから追加できるので、§9 のあとに戻ってきて登録してもかまわない(登録前にログインを試すと redirect_uri_mismatch になる、というだけ)。

ローカルでも動かすならhttp://localhost:8787/api/auth/google も足しておく(→ §15-1)。

(5) クライアント ID とシークレットを控える。作成すると「OAuth クライアントを作成しました」のダイアログが出て、クライアント IDクライアント シークレットが並ぶ。控え方は2つあり、それぞれ右のコピーアイコンで写すか、ダイアログの一番下にある「JSON をダウンロード」で両方まとめて落とす(下までスクロールしないと見えない)。

この2つは扱いが違う。

あとから見られるか
クライアント ID見られる。 画面にも「Google Auth Platform の [クライアント] タブでいつでも確認できます」とある
クライアント シークレット🚨 見られない。このダイアログを閉じると、クライアント シークレットを表示およびダウンロードできなくなります」と書かれている

OAuth クライアント作成完了ダイアログ。クライアント ID とクライアント シークレットが並ぶ

作成完了。閉じる前にシークレットをコピーする。「JSON をダウンロード」は一番下にある

🚨 シークレットを控えずに閉じたら、追加で発行する。クライアント詳細の「クライアント シークレット」で「シークレットを追加(Add secret)」を押して新しいものを作る(Google は既存シークレットを後から表示してくれない)。クライアントごと作り直す必要はない。

同じダイアログに「OAuth アクセスは、OAuth 同意画面に表示されているテストユーザーに制限されます」と出る。(3) で登録したテストユーザーのことで、テストモードのあいだはこの制限がかかる。

受け取った値は秘密なので、コードに書かない。Cloudflare のシークレットとして入れておけば、Worker 側から読めるようになり、@hono/oauth-providers が自動で拾う。

入れるのは §9 のデプロイのあと(→ §9-1)。Worker がまだ存在しないと、ダッシュボードに設定欄が出ないため。いまはクライアント ID とシークレットを手元に控えておくだけにする。

⚠️ redirect_uri は登録と完全一致が必須。1文字(スラッシュ・http/https・ポート)でも違うと Google が redirect_uri_mismatch を返す。

GitHub・X・Facebook を足すときも、各社のコンソール(GitHub: Developer settings → OAuth Apps/X: X Developer Portal/Facebook: Meta for Developers)で同じように OAuth アプリを作り、<プロバイダ>_ID / <プロバイダ>_SECRET を足す(→ §14)。

4. 【データベース】D1 の作成とスキーマ

Section titled “4. 【データベース】D1 の作成とスキーマ”

まず D1 データベースを作る(初回だけ)。名前は CLAUDE.md に書いてあるので、それを使ってもらう。

Claude
CLAUDE.md の名前の決まりのとおりに、D1 データベースを作って

ターミナルでやる場合npx wrangler d1 create yosegaki-sol-db を実行する(yosegaki-sol は自分のベース名に読み替え)。

表示される database_id は §5 の wrangler.jsonc で使う(同じセッションで作っていれば、エージェントが覚えている)。

テーブルの設計に入る。ソーシャルログインならテーブルは2つでよい。肝は users を(プロバイダ+プロバイダ側ユーザーID)で一意にすること。

Claude
D1 マイグレーション migrations/0001_init.sql を作って。テーブルは2つ。
- users は「認証の列」+「アプリの列」で作る。
認証の列: id TEXT PK=内部用ランダムUUID, provider TEXT=プロバイダ名, provider_user_id TEXT=そのプロバイダのユーザーID, created_at, updated_at
アプリの列: CLAUDE.md の仕様が要求するもの(この記事では display_name と message)
UNIQUE(provider, provider_user_id) を必ず付ける。(provider, provider_user_id) にインデックス
email は保存しない(使わないので列を作らない)
- sessions(id TEXT PK=ランダムなセッショントークン, user_id FK→users ON DELETE CASCADE, created_at, expires_at)
補足:
- アカウントの同一性は (provider, provider_user_id) の組。初回ログイン時だけ users 行を作る。
- email など個人情報は取得・保存しない。同一性は (provider, provider_user_id) だけで判定する。
- アプリの列は NULL 可にし、DEFAULT は設定しない(空文字を既定値にしない)。プロバイダから取れる実名などを初期値にせず、本人に入力してもらうため。

認証の列とアプリの列を分けて頼んでいる。 上の5列と sessionsソーシャルログインならどのアプリでも同じで、変わるのは「アプリの列」だけ。自分のアプリを作るときは、そこを差し替える。

ここではファイルを作るだけで、適用はしない。 本記事はデプロイした本番だけで動かすので、適用は §9 で本番の D1 に対して行う。手元でも動かしたい場合だけ、ローカルの D1 にも適用する(→ §15-1)。

このアプリは Cloudflare Workers + Static Assets で作り、ルーティングに Hono を使う。

依存を入れる。

Claude
Hono と @hono/oauth-providers を入れて

ターミナルでやる場合npm install hono @hono/oauth-providers を実行する。

「CommonJS になっていますが ESM にしますか」と聞かれたら、はいでよい。エージェントが先に npm init -y を走らせると、package.json"type": "commonjs" が書かれるため。そのままでもビルドは通る(手元で確認)が、Workers も Hono も ESM が標準なので "type": "module" にしておくほうが素直。

Claude
wrangler.jsonc を作って。Workers + Static Assets 構成にする。
- name は CLAUDE.md のベース名(省略するとフォルダ名が使われるので必ず書く。これが公開URLになる)
- main は src/index.js
- assets は public/ を binding 名 ASSETS で配信
- d1_databases に binding "DB"、database_name は CLAUDE.md のベース名-db、database_id は d1 create で出た値
- compatibility_date は UTC(協定世界時)での今日の日付にする
- compatibility_flags に "nodejs_compat" を入れる
- 各プロバイダの <PROVIDER>_ID / <PROVIDER>_SECRET は Cloudflare のシークレットとして別に入れるので、wrangler.jsonc には書かない

ルーティングは Hono に任せる。/api/* を各ルートで処理し、それ以外はすべて静的ファイル(public/)に委ねる形にしてもらう。

6. 【バックエンド】ソーシャルログインとセッション

Section titled “6. 【バックエンド】ソーシャルログインとセッション”

6-1. ログインルート(まず Google だけ)

Section titled “6-1. ログインルート(まず Google だけ)”

プロバイダごとに1本のルートを用意し、そこへ @hono/oauth-providers のミドルウェアを適用する。各ルートが「送り出す」役と「戻ってきた code を処理する」役を兼ねる。戻ってきた後の処理(ユーザー正規化・upsert・セッション)は共通化して、プロバイダが増えても足すのは分岐1つだけにする。ここではまず Google だけ作る。

Claude
src/index.js に Google のソーシャルログインを実装して。@hono/oauth-providers を使う。あとで他プロバイダを足せる形にする。
- ルートは GET /api/auth/google。ここに googleAuth ミドルウェアを適用する。
client_id / client_secret は c.env.GOOGLE_ID / GOOGLE_SECRET。scope は ["openid"] だけ(email もプロフィールも取得しない)。
redirect_uri はこのルートの絶対URL(デプロイ先の公開URL + /api/auth/google)に一致させる。
- プロバイダごとの user を共通形に正規化する関数 normalize(provider, user) を作る(増やすとき、ここに分岐を1つ足すだけにする):
戻り値 { provider, providerUserId }(本人の一意ID だけ。email も表示名も受け取らない)
・Google: user-google → id(sub)を providerUserId にする
- 正規化後の共通処理:
・(provider, providerUserId) で users を検索し、無ければ作る(display_name の初期値は空=NULL。Google の実名を入れない)。あれば既存を使う。
・セッションを発行して Cookie をセットし、トップ(/)へリダイレクト。
セッションまわりはこのあと共通のモジュールに切り出すので、ここでは簡素でよい。

normalize は「プロバイダの差を1箇所に閉じ込める関数」。いまは Google の分岐だけを持ち、プロバイダを増やすときはここに分岐を1つ足していく(具体例は後述の「プロバイダを増やす」節)。

なぜ表示名を空で作るか。Google の name(実名のことが多い)を初期値にすると、本人が気づかないまま実名が寄せ書きに公開されうる。だから初期値は空(NULL)にして、本人が入力した名前だけを出す。email を取らないのと同じ「個人情報は最小限」の考え方(§11-2)。scope を openid だけにしているので、そもそも nameemail も受け取っていない。

セッションは D1 に持ち、不透明なトークンを Cookie に入れる。Cookie は HttpOnly; Secure; SameSite=Lax

5-1 の時点では、セッションの処理は src/index.js の中に書かれている。 ここで共通の部品として切り出し、index.js はそれを使う形に置き換える。このあと §7 の API でも同じ部品を使うので、1箇所にまとめておく。

Claude
src/session.js にセッションの共通処理を作って。
- createSession(env, userId): ランダムトークンを sessions に保存(expires_at は例えば30日後)。HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age付き の Cookie を返す。
- getSessionUser(c): Cookie のトークンで sessions を引き、期限内なら user_id を返す。無効なら null。
- deleteSession(c): セッション削除+Cookie失効。
- 保護ルート用の認証ミドルウェア requireAuth を用意し、未ログインは 401、成功時は c に user_id を渡す。
- src/index.js に同じ処理が書かれていたら、このモジュールを使う形に置き換える(重複を残さない)。

なぜ SameSite=Lax。OAuth はプロバイダから自分のサイトへリダイレクトで戻ってくる流れがある。Strict すぎると戻り直後の遷移でセッションが送られず不都合が出やすい。ログインセッションは Lax が無難な既定。

7. 【バックエンド】アプリの API とアカウント操作

Section titled “7. 【バックエンド】アプリの API とアカウント操作”

ここも2つに分けて頼む。 アプリの API は作るものによって変わるが、アカウント操作の3つはどのアプリでも同じ

Claude
CLAUDE.md の仕様のとおりに、アプリのAPIとアカウント操作のAPIを Hono で追加して。
【アプリのAPI】仕様に沿って作る。誰でも見られるものと、ログインが要るものを分ける。
この記事では、公開の一覧 GET /api/board と、本人だけが更新できる PUT /api/me/post。
- 公開の一覧では、users の id・provider・provider_user_id など内部情報を返さない(表示に必要な項目だけ)
- 一覧に出すのは、アプリの列が埋まっている(NULL でも空文字でもない)ユーザーだけ。新しい順
- 本人のデータを更新するAPIは要ログインにし、必ず自分の users 行だけを対象にする
(行はログイン時に作られているので、新規作成は要らない)
- 空の入力は空文字ではなく NULL として保存する(空文字を作らない)
【アカウント操作のAPI】どのアプリでも同じ。
- GET /api/me: 要ログイン。自分のアプリの列を返す
- POST /api/logout: セッション削除+Cookie失効
- DELETE /api/me: 要ログイン。自分の users 行を削除する(=アカウント削除)。
sessions は ON DELETE CASCADE で一緒に消える。あわせて現在のセッション Cookie も失効させる。
本人が書いたものは users 行に載っているので、削除すると一覧からも消える。
本人の行だけを消し、他人の行は絶対に消さない。

安全の指示は仕様から導けない。 「内部情報を返さない」「自分の行だけを対象にする」「他人の行は絶対に消さない」は、CLAUDE.md に書いた仕様のどこにも書いていない。アプリが変わっても、この3つはそのまま持っていく。

public/index.htmlapp.jsstyle.css を作る。ログインは各プロバイダのボタンを /api/auth/<プロバイダ> に飛ばすだけ

Claude
CLAUDE.md の仕様のとおりに、public/index.html, app.js, style.css を作って。
【アプリの画面】仕様に沿って作る。この記事では、一覧を GET /api/board から取り、
本人の入力を PUT /api/me/post で保存する。ユーザー入力にはXSS防止の処置を入れる。
【ログイン周り】どのアプリでも同じ。
- ログイン状態は GET /api/me が 200 か 401 かで判定する
- 未ログイン: 「Google でログイン」ボタン。リンク先は /api/auth/google(プロバイダを増やしたらボタンも足す)
- ログイン済み: 本人の入力欄と保存、ログアウト(POST /api/logout)、アカウント削除(DELETE /api/me)
- アカウント削除は取り消せないので、実行前に強い確認(例:「本当に削除しますか? あなたの投稿も消えます」)を出す。成功したらログアウト状態(未ログイン表示)に戻す

⚠️ ここでローカルの wrangler dev を起動しても、ログインはまだ動かない。エージェントが「動作確認しますか」と提案してくることがあるが、シークレットをまだ入れていないうえ、Google 側に登録したリダイレクト URI は本番の URL だけだから。画面の見た目と「Google でログイン」ボタンが出るところまでは見えるが、押すと失敗する。ログインまで通すのは、次の §9 でデプロイして §9-1 でシークレットを入れてから(ローカルでログインまで動かしたい場合は → §15-1)。

public/index.html をファイルとして直接開いた場合も同じ。API が動かないので、見出しだけが出て寄せ書きは空になる。

本番の D1 にマイグレーションを適用し、デプロイする。

Claude
本番の D1 にマイグレーションを適用(migrations apply)してから、デプロイして

ターミナルでやる場合npx wrangler d1 migrations apply yosegaki-sol-db --remote のあと npx wrangler deploy を実行する。

完了すると https://<ベース名>.<アカウント>.workers.dev の形で公開 URL が出る(§2-2 で決めたベース名がそのまま出る)。この時点ではログインはまだ動かない。シークレットを入れていないため。

リダイレクト URI をまだ登録していないなら、ここで登録する。上に出た公開 URL に /api/auth/google を付けたものを、Google のクライアント設定(→ §3)に足す。プロバイダを増やしたら、その分も同じように足す(登録前に試すと redirect_uri_mismatch になる)。

9-1. クライアント ID とシークレットを入れる

Section titled “9-1. クライアント ID とシークレットを入れる”

§3 で控えた2つの値を、Cloudflare のダッシュボードから入れる。ブラウザだけで完結し、2つまとめて入れられる。

  1. 左メニューの ComputeWorkers & Pages を開き、自分の Worker(ベース名)を選ぶ
  2. Settings タブ → 「Runtime variables and secrets」の「+ Add variable
  3. Add environment variable」のダイアログで、KeyGOOGLE_IDValue にクライアント ID を入れ、Value 欄の右にある「Secret」にチェックを入れる
  4. + Add」で行を足し、2つ目に GOOGLE_SECRET とクライアントシークレットを入れる(こちらも「Secret」にチェック)
  5. Add 2 variables」を押す

Settings タブの Runtime variables and secrets のセクション

Settings タブを開いた直後に出る「Runtime variables and secrets」。ここの「+ Add variable」から入れる

環境変数を追加するダイアログ。Key と Value の欄、右に Secret のチェックボックス

2つまとめて入れた状態。どちらも「Secret」にチェックが入っている

🚨 「Secret」のチェックを忘れない。チェックしないとただの環境変数として保存され、あとから値が画面に表示される。クライアントシークレットは秘密なので、必ずチェックを入れる。チェックボックスなので、2行目で入れ忘れやすい。

チェックが効いていれば、Value が伏せ字(●●●)になる。入力した文字がそのまま見えていたら、チェックが外れている。

なぜブラウザで入れるかwrangler secret put でも入るが、対話プロンプトを出せない環境で実行すると、値が空のまま Success と表示されることがある。シークレットは書き込み専用で、入った値を後から読んで確かめられない(wrangler secret list は名前しか見せない)ので、空で入ったことに気づけない。ダッシュボードなら入力欄に値が見えていて、入っていなければ入っていないと分かる。ターミナルから入れる方法は §15-1 にある。

🚨 シークレットを入れたら、もう一度デプロイする。追加した時点では動いている Worker にはまだ反映されない(Workers は動かす構成をバージョンごとに固めるため、シークレットを足す前のバージョンが動いたままになる)。この状態でログインを押すと、次のエラーが出る。

Required parameters were not found. Please provide them to proceed.

@hono/oauth-providers が、クライアント ID とシークレットを受け取れていないときに出すもの。もう一度デプロイすれば直る。

Claude
デプロイして

入れたシークレットは、次に wrangler deploy しても消えない。前のバージョンから引き継がれるので、デプロイのたびに入れ直す必要はない。入れた直後の1回だけ、反映のためにデプロイが要る。

10. 動かす・動作確認(自分でテストする)

Section titled “10. 動かす・動作確認(自分でテストする)”

まず「Google でログイン」を押すと、Google のアカウント選択画面に飛ぶ。

⚠️ アプリ名ではなく、ドメイン名が表示される。「<アカウント>.workers.dev に移動」のように出て、§3 で設定したアプリ名(「みんなの寄せ書き」)は出てこない。アプリの確認(審査)を受けていないためで、テストモードのあいだはこうなる。間違っていないので、そのまま進めてよい。

Google のアカウント選択画面

アカウントを選ぶ(テストモードでは、弾かれたらそのアカウントをテストユーザーに追加する)

アカウントを選ぶと、続行の確認が出る。本記事は openid だけしか要求しないので、許可する項目は「Google で公開されているお客様の個人情報とお客様を関連付ける」の1つだけ。名前やメールアドレスを渡す確認は出ない。「次へ」で進めるとアプリに戻る。もしここで「アクセスをブロック」と出たら、そのアカウントを Google 側のテストユーザーに追加する(§3)。

アプリへのアクセス許可の同意画面

openid だけなので、許可する項目は1つだけ。名前もメールも渡さない

アプリに戻ると、ログイン状態になる。名前の欄は空(Google の実名を初期値にしていない → §11-2)。ここに自分の名前を入力する。

ログイン後の編集パネル

ログイン後。お名前は空なので、自分で入力して投稿する

一言を入れて保存すると、寄せ書きに自分のカードが並ぶ。

投稿が寄せ書きに並んだ画面

投稿が寄せ書きに反映される

別の Google アカウントでログインすると、別のユーザーとして扱われる。同じ Google でも、アカウントが違えば provider_user_idsub)が違うため、別の users 行になり、寄せ書きにもう1枚の別カードが並ぶ。実際に2つ目のアカウントで入って投稿すると、こうなる(手元で確認済み)。

2つの Google アカウントの投稿が別カードで並んだ画面

別の Google アカウントは別ユーザー。カードが2枚に増える(連携でまとめるのは §15 の宿題)

続けて、仕様どおり安全に動くかを自分で確かめる(まだ Google だけ)。

確認することやり方期待
公開閲覧シークレットウィンドウでトップを開く寄せ書きは見えるが投稿欄は出ない
Google でログイン「Google でログイン」同意後に戻り、投稿できる
1人1投稿もう一度投稿カードが増えず上書きされる
未ログインで保護APIシークレットで GET /api/me を直接開く401
一意キーは(provider+id)同じ Google で再ログイン同じ1行が使われる(users が増えない)
ログアウトログアウト後に GET /api/me401 に戻る
アカウント削除ログイン後に「アカウント削除」→ 確認自分の行と投稿が消え、board から消える。GET /api/me は 401 に戻る
内部情報を出さないGET /api/board の応答を見る名前と一言だけ(provider・id は無い)

users の中身を目視する例。

Terminal window
npx wrangler d1 execute yosegaki-sol-db --remote --command "SELECT provider, display_name FROM users"

この記事には、作る前に決めてある方針が2つある。何で作るか(どの方式を選ぶか)と、何を持たないか(どこまでデータを取るか)。

「◯◯でログイン」を載せる素直な方法は Cloudflare Access だが、無料枠が50ユーザーで、その先が per-seat(1ユーザーいくら)課金になる。個人やコミュニティの無料アプリでは、人が増えるほどコストが線形に膨らんで使いづらい。

一方、50人の壁は Access という製品の制限であって、外部サービスでログインさせること自体の制限ではない。Google も GitHub も OAuth ログインに課金しないので、OAuth を自分の Worker で実装すれば無料・人数無制限になる。しかも Cloudflare Access が組み込みで対応していない X(Twitter) のようなプロバイダも、自前なら扱える。

自前といっても、OAuth の難所(認可リダイレクト・CSRF 対策の state・トークン交換)は @hono/oauth-providers ライブラリに任せる。プロバイダを差し替えるだけで Google でも GitHub でも X でも同じ形で書けるのがこのライブラリの利点。セキュリティの中核は枯れたライブラリに寄せ、自分はアプリのロジックに集中する。

認証の載せ方無料の上限コスト構造対応プロバイダ
Cloudflare Access50ユーザーper-seat(人数で増える)Google/GitHub/Facebook/LinkedIn ほか(X は無い)
ソーシャルログイン自前(本記事)無制限フラット(Workers/D1 の無料枠〜数ドル)Google/GitHub/X/Facebook/LinkedIn ほか

本サイトのハンズオンは、個人情報をできるだけ持たないことを基本方針にしている。理由は、これらを作って運用するのが IT に不慣れな人であることを想定しているから。個人情報を持つほど、漏洩時の被害も、個人情報保護法の観点での責任も重くなる。持たなければ、漏らしようがない

ソーシャルログインは、この方針と相性がいい。

  • 本人を見分けるのに必要なのは sub(各プロバイダが返す不変のユーザーID)だけ。email は寄せ書きに使わない(一意キーにも使わない)。だから Google に要求するスコープを openid だけにして、email もプロフィールも取得しない。「保存しない」ではなく「そもそも受け取らない」ところまで踏み込む。
  • 表示名も Google の実名を初期値にしない。ログイン直後の名前欄は空にして、本人に入力してもらう。実名を出したい人だけが出せばよく、意図しない実名の公開を防げる。

結果、このアプリが持つ個人に関わる情報は、sub(それ単体では個人に結びつかない仮名の ID)と、本人が自分で決めた表示名だけ。パスワードもメールアドレスも保存しない。

email を後から使いたくなったら:scope に email を足し、users に列を戻せばよい(Google なら verified_email で確認済みかも見られる)。ただし使う当てが無いなら取らないのが原則。必要になってから足す。

「取っていない」は、利用者に確かめてもらえる。作る側が説明するだけでは、使う側には確かめようがない。アプリが何を受け取るかは、ログインを押す前に Google の同意画面に出る。Google が出す画面なので、作る側の説明より強い材料になる。

本記事のアプリは openid しか要求しないので、出るのは「Google で公開されているお客様の個人情報とお客様を関連付ける」の1項目だけになる(→ §10 の画面)。ではメールアドレスも取るアプリだとどう変わるのか。同じクライアントのまま、要求するスコープだけを変えて確かめた。

scope に openid と email を要求したときの同意画面。メールアドレスの項目が1つだけ並んでいる

openidemail を足したとき。項目は1つのままで、中身が本人のメールアドレスに変わる

scope に openid と email と profile を要求したときの同意画面。名前とプロフィール写真、メールアドレスの2項目が並んでいる

さらに profile を足したとき。項目が2つに増え、上に名前とプロフィール写真が出る

利用者に確かめてもらうなら、下記を伝える。

同意画面のリストに、自分のメールアドレスが表示されているかどうかを見る。出ていればアプリが受け取る。出ていなければ受け取らない。

OAuth の仕組みを知らなくても、自分のアドレスが画面に出ているかを確認してもらうだけでよい。

以下の §2(スコープ)・§4(スキーマ)・§6(normalize)は、すべてこの方針の実装になっている。

ここまでで動くものはできている。あとから、いま作ったログインの中で何が起きていたのか、そして「どこを自分が守っているのか」を押さえておく。プロバイダを増やすときも、この形は変わらない。

flowchart LR
  B["ブラウザ"] -->|1 ログイン開始| W["自分の Worker"]
  W -->|2 プロバイダへリダイレクト| G["Google / GitHub / X …"]
  G -->|3 同意| B
  B -->|4 code を持って戻る| W
  W -->|5 code を情報に交換| G
  G -->|6 ユーザー情報| W
  W -->|7 セッション発行| B
  1. ユーザーが「◯◯でログイン」を押す → Worker がそのプロバイダの認可画面へリダイレクト(state を付ける)
  2. ユーザーが同意 → プロバイダがこちらの redirect_uri に一時的な code を付けて戻す
  3. Worker が codeclient_secret と一緒に交換し、ユーザー情報を取得
  4. Worker がユーザーを D1 に作る/引く → 自前のセッション Cookie を発行

この 1〜3 の往復(state の生成・照合=CSRF 対策、code の交換)を @hono/oauth-providers が内部でやってくれる。こちらが書くのは 4 のアプリ側だけで、しかもプロバイダが変わっても 4 の形は同じ。

12-2. ライブラリに任せる部分/自分で守る部分

Section titled “12-2. ライブラリに任せる部分/自分で守る部分”
  • ライブラリが担保:認可リダイレクト、state(CSRF 対策)、code → トークン交換、ユーザー情報の取得。
  • 自分で守るclient_secret を秘密に保つ、redirect_uri を正確に登録、一意キーは(プロバイダ+そのユーザーID)要求スコープを最小にする(使わない個人情報は取得しない)、セッション Cookie の属性、公開データに内部情報を混ぜない。

次の章でプロバイダを増やす前に、プロバイダごとの差を見ておく@hono/oauth-providers は同じ書き方で複数プロバイダを扱えるが、返ってくるユーザー情報はプロバイダごとに違う。ただし本記事は §11-2 の方針どおり sub(一意ID)と、本人が入力する表示名しか使わないので、実際に気にするのは「一意 ID がどのフィールドか」だけ。下の表の「メール」「確認フラグ」列は、取ろうと思えば取れる参考情報として載せてあるが、本記事では取得しない。

プロバイダimport / ミドルウェアcontext key一意IDメール確認フラグ表示名の元
Google.../google / googleAuthuser-googleid(sub)ありverified_emailname
GitHub.../github / githubAuthuser-githubidnull のことありなしnamelogin
X (Twitter).../x / xAuthuser-xidなしなしname / username
Facebook.../facebook / facebookAuthuser-facebookidありなしname
LinkedIn.../linkedin / linkedinAuthuser-linkedinsubありemail_verifiedname

設計上の結論はこう。

  • 一意キーは「プロバイダ名 + そのプロバイダのユーザーID」の組にする(google12345github12345 は別人)。
  • email は取得しない(寄せ書きに不要・§11-2)。だから「メールが無い/空/未確認」というプロバイダ差を、そもそも気にしなくてよくなる。X がメールを返さないことも問題にならない。
  • 表示名は取得しても初期値にしない。本人入力を基本にして、意図しない実名公開を防ぐ。

同じ人でもプロバイダが違えば別アカウント。ある人が Google で入って寄せ書きし、後日 GitHub で入ると、別の新規アカウントになる(ユーザーIDが違うため)。これを1つにまとめる「アカウント連携」は本記事の範囲外(宿題)。まずは「1ログイン=1アカウント」で作る。

14. プロバイダを増やす(例:X を足す)

Section titled “14. プロバイダを増やす(例:X を足す)”

Google が動いたら、他のプロバイダを1つずつ足す。直すのは前述の3箇所だけ。ここでは X(Twitter)を例にする。

(1) X 側の登録(§2):X Developer Portal で OAuth 2.0 を有効にしたアプリを作り、リダイレクト URI に https://<ベース名>.<アカウント>.workers.dev/api/auth/x を登録。X_ID / X_SECRET は、Google のときと同じくダッシュボードのシークレットに入れる(→ §9-1)。

(2) ログインルートと normalize に X を足す(§6-1):

Claude
X(Twitter)のログインを足して。
- GET /api/auth/x に xAuth ミドルウェアを適用する。client_id/secret は c.env.X_ID / X_SECRET。
scope は ["tweet.read","users.read"](本人の一意ID が取れればよい。offline.access は継続アクセス用なので今回は不要)。
- normalize に x の分岐を足す: user-x → providerUserId=id だけを取る(email も表示名も受け取らない)。

normalize に足すのは X の1分岐(user-xidproviderUserId にする)だけ。email 非取得の方針は Google と同じなので、X がメールを返さないことも最初から関係ない。

(GitHub を足すなら githubAuthuser-githubidproviderUserId にする。email を取りに行く user:email スコープは付けない。)

(3) ボタンを足す(§8):フロントに「X でログイン」ボタンを1つ加え、リンク先を /api/auth/x にする。

足して(ローカルで試し、再デプロイして)確認する。

確認することやり方期待
X でログイン「X でログイン」同意後に戻り、投稿できる
個人情報を取らないX でログイン後に users を見るprovider_user_id と(本人が入れた)表示名だけ。email 列は無い
プロバイダ違いは別人Google と X の両方で入るusers が2行できる(設計どおり。連携は宿題)

同じ要領で GitHub・Facebook なども足せる。差は normalize の分岐とボタンとコンソール登録に閉じる。

  • state(CSRF 対策)とトークン交換はライブラリ任せ@hono/oauth-providers が担保。OAuth の往復を手書きしない。
  • client_secret は Worker のシークレットwrangler secret に入れ、コード・フロント・リポジトリに置かない。
  • redirect_uri は登録と完全一致:プロバイダごとに、ローカルと本番の両方を登録。
  • 一意キーは(provider+provider_user_id)UNIQUE(provider, provider_user_id)。表示名などで同一性を判断しない。
  • 個人情報は取らない(データ最小化・§11-2):要求スコープは openid だけ。email もプロフィールも受け取らず、保存もしない。
  • 表示名は本人入力・実名を初期値にしない:プロバイダが返す実名を勝手に公開しない。
  • セッション CookieHttpOnly; Secure; SameSite=LaxlocalStorage にセッションを置かない(サーバー発行 Cookie にすることで、Safari の localStorage 自動削除の影響も受けない)。
  • 公開データに内部情報を混ぜない/api/board は名前と一言だけ。provideridprovider_user_id)を返さない。
  • ユーザー入力の XSS 対策:名前・一言の表示時に無害化する。
  • アカウント削除は本人だけ・取り消し不可DELETE /api/me は requireAuth で自分の行だけを削除する。sessions は ON DELETE CASCADE で消え、Cookie も失効させる。削除後に同じ Google で入り直すと、別の新規アカウントになる(前の投稿は戻らない)。

15-1. ローカルでも動かすなら(.dev.varswrangler secret

Section titled “15-1. ローカルでも動かすなら(.dev.vars と wrangler secret)”

本記事はデプロイした本番だけで動かす手順にしてある。手元の wrangler dev でも動かしたいなら、次の4つが要る。ダッシュボードに入れたシークレットはローカルには来ないためだ(Cloudflare の説明でも、ローカル開発用は .dev.vars.env に置くとされている)。

やるなら §8 のあと、§9 のデプロイの前。 (1)(2) は §3 で OAuth クライアントを作ったあとならいつでもできるが、(3) は wrangler.jsonc(§5)が、(4) は画面(§8)まで揃っていないと確認にならない。

(1) .dev.vars に値を置く.gitignore に入れて、リポジトリに含めない。値はクオートで囲まない。

.dev.vars(ローカル用・gitignore する)
GOOGLE_ID=(クライアント ID)
GOOGLE_SECRET=(クライアントシークレット)

(2) Google 側にローカルのリダイレクト URI を足す。§3(4) のクライアント設定に、次を追加する。ローカルでも動かすと最初から決めているなら、§3(4) でクライアントを作るときに本番のぶんと一緒に登録しておけばよい(そうすればここで Google に戻らなくて済む)。

http://localhost:8787/api/auth/google

(3) ローカルの D1 にマイグレーションを適用する。本線では本番にしか適用していないので、手元の D1 は空のまま。

Terminal window
npx wrangler d1 migrations apply yosegaki-sol-db --local

wrangler.jsonc ができたあとに実行するmigrations applywrangler.jsonc の D1 の設定を読むため、先に流すと wrangler.jsonc がまだ無い で失敗する)。

(4) 開発サーバーを起動する--port でポートを固定する。

Terminal window
npx wrangler dev --port 8787

http://localhost:8787 を開く。

🚨 --port を付ける理由リダイレクト URI はポートまで完全一致が必須で、1つ違えば redirect_uri_mismatch になる。wrangler dev の既定は 8787 だが、別のプロジェクトが使っていると 8788 などに変わる。ポートを固定しないと、(2) で登録した URI と食い違って、起動するまでどのポートになるか分からないまま登録することになる。8787 が塞がっていて起動できないときは、空いているポートを1つ決めて、(2) の登録と --port の両方をその番号に揃える。

ターミナルからシークレットを入れたい場合は、.dev.vars の値をそのままパイプで渡す。

Terminal window
grep '^GOOGLE_ID=' .dev.vars | cut -d= -f2- | tr -d '\r\n"' | npx wrangler secret put GOOGLE_ID
grep '^GOOGLE_SECRET=' .dev.vars | cut -d= -f2- | tr -d '\r\n"' | npx wrangler secret put GOOGLE_SECRET

npx wrangler secret put GOOGLE_ID だけを実行すると値を手入力するプロンプトが出るが、手で貼り付ける経路には事故が多いので、入れるならこのパイプ方式にする。

  • 値を画面にもシェル履歴にも出さない.dev.vars にすでにある正しい値を、そのまま送る
  • 貼り間違い・貼り忘れが起きないcut -d= -f2- は最初の = より後ろを全部取るので、値に = が含まれていても欠けない
  • クオートと改行を落とすtr -d '\r\n"' があるので、.dev.vars をうっかり "…" で囲んでいても、末尾に改行が紛れ込んでも、正しい値だけが入る
  • AI コーディングエージェントに任せられる。Claude Code などから実行しても、対話プロンプトで止まらない

⚠️ 手入力するなら、値をクオートで囲まない.dev.vars のように "…" で囲んだまま入れると、クオートも値の一部として保存され、本番で invalid_client エラー(The OAuth client was not found.)になる。.dev.varswrangler dev がクオートを外して読むので囲ってもよいが、secret put は入力をそのまま保存する点が違う。パイプ方式ならこの問題が起きない。

⚠️ プロンプトが出ないまま「Success」と表示されたら、空文字が入っている。対話プロンプトを出せない環境(AI エージェント経由の実行など)で npx wrangler secret put GOOGLE_ID を単体で実行すると、入力を待たずに空の値で成功してしまうことがある。シークレットは書き込み専用なので気づけない。§9-1 のダッシュボードから入れ直せば上書きできる。

  • プロバイダ側の連携解除は別:アプリのアカウントを削除しても、Google などプロバイダ側の「このアプリへのアクセス許可」は残る。完全に切りたいユーザーには、各プロバイダのアカウント設定で連携を解除してもらう(アプリ側からは制御できない)。削除完了画面にその案内を添えるとていねい。
  • アカウント連携:同じ人が別プロバイダで入ると別アカウントになる。1人にまとめたいなら、ログイン中に「別のログイン方法を追加」して同じ users 行に紐づける仕組みを足す(本記事では未対応)。
  • テストモードから本番への切り替え:Google は「対象」ページが「テスト中」のあいだ、テストユーザーに登録した人しかログインできず、その上限は 100 人(アプリの全期間で通算)(→ §3(3))。不特定多数に使ってもらうなら「アプリを公開」で本番に切り替える。本記事のように openid だけを要求する場合、押すとその場で「本番環境」になり、審査は求められなかった(機密スコープを含まないため。手元で確認済み)。X や Facebook などでも同様に、開発者コンソール側の審査・本番切り替えが要ることがある。要求スコープは最小限に(本記事は openid だけ。必要になってから足す)。
  • セッションの掃除と失効:期限切れ sessions の定期削除(Cron Triggers など)、ログアウトの確実な失効。
  • レート制限:ログイン開始や API に Cloudflare の Rate Limiting を足す。

これで、複数のソーシャルログインによる一般ユーザーのログイン・投稿・ログアウトが、パスワード保管もメール送信もなしに、Cloudflare の無料枠だけで、しかも人数無制限で完成した。プロバイダの差は正規化の1箇所に閉じ込めたので、対応サービスを増やすのも容易になっている。

別の認証方法:ログインを外部サービスに預けたくない場合は、端末の生体認証などでパスワードもアカウント登録も無しにする「パスキー」という方法もある。同じ寄せ書きをパスキーで作る版が パスキーで複数ユーザーのアカウントを作る(PKA)にある。