コンテンツにスキップ

Claude Code でメールに届くリンクのログイン機能(マジックリンク)を作る

この記事では、メールアドレスを入れると届くリンクでログインする機能を作る。この方式はマジックリンクと呼ばれる。パスワードは使わず、メールを受け取れることを本人確認の代わりにする。

題材は「みんなの寄せ書き」。同じ寄せ書きを、パスキー(PKA)、ログインキー(ACC)、ソーシャルログイン(SOL)でも作っている。違うのは本人確認の方法だけで、アプリは同じ。

寄せ書きが付箋のように並んだ画面

完成した寄せ書き。ログインしていなくても見られる

メールの送信には Cloudflare Email Service を使う。認証の外部サービスは使わず、Workers・D1・Email Service の Cloudflare の中だけで作る。ただし、メールを送るには有料の Workers Paid プラン(月 $5)と、Cloudflare で管理している独自ドメインが要る。

ログイン方式の選び方は AUM にまとめてある。特定の少人数に使わせるなら、合言葉(BAU)か Cloudflare Access(CFA)で足りる。この記事は、不特定多数が登録して使うアプリ向けの方式の1つ。

方式本人確認を任せる先使う人に求めるもの記事
メールに届くリンク本人のメールメールアドレスだけこの記事
ソーシャルログインGoogle などのサービスGoogle などのアカウントSOL
パスキー本人の端末対応した端末PKA
ログインキー本人が保管する文字列文字列を自分で保管することACC

メールに届くリンクは、使う人に求めるものがいちばん少ない。メールさえ受け取れれば、どの端末からでも入れる。端末をなくしても、メールを開ければまた入れるので、回復用コードのような仕組みも作らなくてよい。

そのかわり、作る側には用意するものがある。

用意するもの理由費用
Workers Paid プラン無料プランでは、自分で確認したアドレスにしかメールを送れない月 $5(メールは月3,000通まで込み)
Cloudflare で DNS を管理しているドメインメールの送信元にするため。workers.dev からは送れないドメイン代

ドメインをまだ持っていなければ、先に DOM で用意する。前提として、D1W で Worker と D1 を一度使ったことがあるものとする。

名前と一言を書くと、みんなの寄せ書きに並ぶ。閲覧はだれでもでき、書き込みはログインした本人だけができる。ログインの流れは次のとおり。

メールアドレスを入れるログイン画面

メールアドレスを入れて「ログイン用のリンクを送る」を押す。初めての人も、このまま登録される

メールを確認してくださいの画面

送ったあとの画面。登録済みのアドレスかどうかに関係なく、同じ画面が出る

届いたログイン用のメール

届いたメール。リンクは15分で切れ、1回しか使えない。このときは Gmail の迷惑メールのフォルダに入った(→ 7-2)

ログインの確認画面

リンクを開くと確認の画面が出る。ここで「ログインする」を押して、初めてログインする

ログインした直後の画面

ログインできた。表示名と一言を書く欄が出る

寄せ書きを書いたあとの画面

書くと、みんなの寄せ書きに付箋が並ぶ。ログアウトとアカウントの削除もここからできる

リンクを開いたあとに確認の画面を挟むのは、メールソフトの中には、届いたリンクを安全確認のために先に開くものがあるため。開いただけでログインする作りだと、本人が押す前にリンクが使われてしまう。

1-2. メールアドレスを保存しない

Section titled “1-2. メールアドレスを保存しない”

この記事で作る寄せ書きは、メールアドレスを保存しない。

保存しなくても動くのは、マジックリンクではログインのたびに本人がアドレスを入力するから。メールはその入力されたアドレスに送ればよく、保存しておいたアドレスを使う場面がない。

ただ、同じ人がまたログインしたときに、同じ人だと分かる必要はある。そこで、アドレスそのものの代わりに、運営だけが知る合言葉を混ぜてハッシュにした値を保存する。この方式は HMAC と呼ばれる。同じアドレスからはいつも同じ値ができるので、同じ人だと分かる。値からアドレスに戻すことはできず、合言葉を知らなければ「このアドレスは登録しているか」を確かめることもできない。

メールアドレスをそのまま持つか、HMAC で持つかは、選べるトレードオフ。

そのまま持つHMAC で持つ(この記事)
データベースが漏れたときアドレスの一覧が漏れるアドレスは出ない
運営から全員へのお知らせ送れる送れない
「登録したアドレスを忘れた」への対応調べられる調べられない(本人が思い当たるアドレスで試す)

お知らせを送る予定がないなら、持たないほうが預かる責任が小さくなる。

まず、自分のドメインからメールを送れるようにする。ここはダッシュボードでの操作になる。

アカウントが Workers の無料プランのままなら、Workers Paid に切り替える。左のメニューの Compute → Workers plans から切り替えられる。

左のメニューの Compute → Email Service → Email Sending を開く。まだ何も登録していないと、次の画面になる。Onboard Domain を押す。

Email Sending の最初の画面

Email Sending の最初の画面。Onboard Domain を押す

Zone の欄で、送信元にするドメインを選ぶ。

Zone の一覧からドメインを選ぶ

Cloudflare で管理しているドメインが一覧に出る

ドメインを選ぶと Subdomain の欄が出る。空欄のままにすると、ドメインそのもの([email protected] の形)から送れるようになる。Continue を押す。

Subdomain の欄

「Leave blank to use the top level」とある。空欄のまま Continue

追加される DNS レコードの一覧が出る。メールが本当にこのドメインから送られたことを、受け取る側が確かめるためのもの。Activate を押すと自動で入る。

Verify DNS records の画面

MX・SPF・DKIM のレコードが cf-bounce の下に、DMARC が _dmarc に入る。Activate を押す

登録が終わると、ドメインの画面になる。Sending status が Enabled になっていれば、送れる状態。

登録後の Email Sending の画面

右上の Configuration summary で、Sending status と DNS records がどちらも緑になる

作業フォルダ・D1 データベース・Worker に付ける名前の元になるベース名を決めて、作るものの仕様と一緒に AGENTS.md に書いてもらう。この記事では yosegaki-mla を例に使う。

プロンプト
このプロジェクトのベース名と、これから作るアプリの仕様を、以下のとおり AGENTS.md に書いてください。CLAUDE.md からは AGENTS.md を読み込むようにしてください。
====
## 名前
- ベース名は yosegaki-mla
- アプリ名・Worker 名・作業フォルダ名 = ベース名
- D1データベース名 = ベース名のうしろに -db を付けた名前
## 作るもの:みんなの寄せ書き
- 名前と一言を書くと、みんなの寄せ書きに並ぶ
- 1人1件。あとから自分の分だけ編集できる
- 表示名は本人が決める。あとから変えられる
- 閲覧は誰でもできる。書き込みと編集は本人だけ
- 画面は上から「みんなの寄せ書き」の一覧、「あなたの寄せ書き」の入力欄、の順に並べる
- 寄せ書きは付箋のようなカードを並べ、本文の下に右寄せで名前を出す
- ログアウトと、アカウントの削除(自分の寄せ書きも消える)ができる
## ログイン
- 本人確認は、メールに届くリンクで行う。パスワードは使わない
- メールは Cloudflare Email Service で [email protected] から送る
- メールアドレスは個人情報なので、できるだけ保存したくない

yosegaki-mla を自分のベース名に、example.com を 2-2 で登録したドメインに置き換えて渡す。

最後の行「メールアドレスは個人情報なので、できるだけ保存したくない」が、安全のために足した行。この1行が無いと、Claude Code はメールアドレスをそのまま保存する(次の章で説明する)。

仕様が書けたら、まとめて作ってもらう。

プロンプト
AGENTS.md の仕様のとおりに、寄せ書きアプリを作って。D1 データベースがまだ無ければ作って。
作ったら、ローカルで動かして、ブラウザで開いて、ログインして寄せ書きを書けるところまで確かめて。

2行目は、作ったものを Claude Code 自身に試させるための行。ローカルでは、メールは実際には送られず、送るはずだった中身がファイルに書き出される。Claude Code はそのファイルからリンクを取り出して、ログインまで試せる。

「ブラウザで開いて」は、あとから足した。この言葉が無いと、Claude Code はブラウザを使わず、テスト用のプログラムから画面と同じ通信を送って確かめた。そのテストは通ったが、公開してブラウザで開くと、確認画面の「ログインする」で「リクエストの送信元が正しくありません」と出て、ログインできなかった。ブラウザがフォームに付ける情報を、テストでは自分で付けて送っていたため、食い違いに気づけなかった。

この章のプロンプトを決めるときに、仕様の最後の1行(保存したくない)がある場合と無い場合で、Claude Code に作らせて比べた。安全のための作りは、どちらでも次のものが入った。

安全策何を防ぐか
リンクは15分で切れ、1回しか使えない古いメールや、転送されたメールのリンクで入られること
リンクの合言葉(トークン)はハッシュで保存するデータベースが漏れたときに、そのリンクで入られること
開いたあとに確認の画面を挟むメールソフトの先読みでリンクが使われてしまうこと
同じアドレスへの送信は、短い時間に数通まで他人のアドレスにメールを大量に送りつけられること
登録済みかどうかに関係なく、同じ画面を出す「このアドレスは登録しているか」を調べられること
ログインは HttpOnly などを付けた Cookie で保つページに入り込んだスクリプトに、ログインを盗まれること

違いが出たのは、メールアドレスの持ち方だけだった。保存したくない、と書かないと、アドレスをそのまま保存した。書くと、HMAC だけを保存する作りになり、合言葉を Secret に分けて置くところまで提案された。

試したのは Opus 5.5 と Sonnet 5.5 で、どちらも同じだった。ただし Sonnet は、「動かして確かめて」の行が無いと、作ったところで止めて動かさなかった。この行を入れておくと、どちらのモデルでも試してから報告が返ってきた。

プロンプト
本番に公開して。D1 の表を本番にも作って。メールアドレスのハッシュに使う合言葉は、乱数で作って Secret に登録して。合言葉の値は会話に出さなくてよい。

合言葉は、Claude Code が乱数を作って、そのまま Secret に登録する。値を自分で入力する場面はない。会話にも出ないので、自分も Claude Code も値を知らないまま進む。Secret に入れた値は、ダッシュボードからも Wrangler からも読み出せない。読めるのは、動いている Worker のコードだけ。そのため、データベースが漏れても合言葉は一緒に漏れず、アドレスを片端から試して当てることもできない。

公開すると、https://yosegaki-mla.<アカウント>.workers.dev の形の URL が出る。ドメインを Worker に割り当てていなくても、メールの送信元は 2-2 で登録したドメインになる。

公開した URL を開いて、自分のアドレスでログインする。1-1 の流れのとおりに進めば、寄せ書きを書けるようになる。

続けて、次のことを確かめる。

確かめることやり方こうなればよい
リンクは1回だけ同じメールのリンクをもう一度開いて「ログインする」「ログインできませんでした」になる
同じ人に戻れるログアウトして、同じアドレスでもう一度ログイン自分の寄せ書きがそのまま出る
アドレスを保存していない下のプロンプトでデータベースの中を見るアドレスが出てこない
閲覧はだれでもシークレットウィンドウで開く寄せ書きは見えるが、書く欄は出ない

ログインできませんでしたの画面

使い終わったリンクをもう一度使った画面

データベースの中は、Claude Code に見てもらう。

プロンプト
本番の D1 の中身を見て、メールアドレスがどこかに保存されていないか確かめて。

ユーザーの表に、64文字の英数字(HMAC の値)があって、@ を含む値がどこにも無ければよい。

メールが届かないときは、まず迷惑メールのフォルダを見る。それでも無ければ、ダッシュボードの Email Sending でドメインを開き、Activity log を見る。

Activity log の画面

送ったメールが並ぶ。Result が Delivered なら、Cloudflare からは届けられている

  • Delivered になっている:Cloudflare からは届けられている。受け取る側で迷惑メールに分けられたか、別のフォルダに入っている
  • 一覧に出てこない:アプリがメールを送れていない。Claude Code にエラーを調べてもらう

Gmail なら、メールの詳細から SPF・DKIM・DMARC の判定を見られる。3つとも PASS なら、2-2 の DNS レコードが正しく入っている。

Gmail のメッセージのソース

Gmail で メッセージのソースを表示 を開いたところ。SPF・DKIM・DMARC がすべて PASS

7-1. 漏れたときと、乗っ取られたとき

Section titled “7-1. 漏れたときと、乗っ取られたとき”

安全さは、2つに分けて考える。

データベースが漏れたときに出るもの他人に乗っ取られにくいか
メールに届くリンク(この記事)ほぼ無い(アドレスは HMAC だけ)本人のメールボックスの守り次第
ソーシャルログイン(SOL)ほぼ無い(本人を見分ける番号だけ)Google などの2段階認証やパスキーで守れる
パスキー(PKA)公開鍵だけ(漏れても使えない)いちばん強い(端末に結びつき、偽のサイトでは使えない)

漏れたときに出るものは、ソーシャルログインと同じくらい少ない。乗っ取られにくさは、本人がメールにどれだけ鍵をかけているかで決まる。Gmail に2段階認証をかけている人なら、メールボックスへの侵入は難しい。

ただし、利用者が書き込む寄せ書きの本文には、個人情報が書かれることがある。これはログイン方式とは別に残る。

7-2. 迷惑メールに入ることがある

Section titled “7-2. 迷惑メールに入ることがある”

Yahoo! メールに送ったところ、迷惑メールのフォルダに入った(2026-10-29)。SPF・DKIM・DMARC はすべて PASS で、本文をログイン用のメールらしく整えても変わらなかった。送信の実績がない新しいドメインだから、と考えている。

Gmail でも迷惑メールに入ることがある。最初の試作版では受信トレイに入ったが、同じ日に寄せ書きを作り直して送ると、2回とも迷惑メールのフォルダに入った。理由の欄には「以前迷惑メールと判断されたメールに類似しています」と出た。

Yahoo! メールの迷惑メールフォルダ

Yahoo! メールでは迷惑メールのフォルダに入った

送ったあとの画面に「届かないときは、迷惑メールのフォルダも見てください」と出しておく。

  • 1日に送れる数の上限は、公開されていない。新しいアカウントは少なめの数から始まり、送信の実績に合わせて増える、とだけ書かれている。オフ会などで大勢が一斉にログインすると、止まるかもしれない
  • 合言葉はあとから変えられない。変えると、同じアドレスから違う値ができ、全員が別人扱いになる
  • 合言葉はだれも控えていない。Worker を消すと合言葉も消え、別のアカウントや別のサービスに引っ越すときも同じ値を作れない。どちらも全員が別人扱いになる。引っ越す予定があるなら、合言葉をパスワードマネージャーに控えておく
  • Worker のコードは合言葉を読めるので、Cloudflare のアカウントに入れる人なら合言葉を取り出せる。守りの要は Cloudflare のアカウントのほう
  • 運営からメールを送ることはできない。お知らせを送りたくなったら、メールアドレスの持ち方を変えることになる
  • だれでもアドレスを入れられるので、他人のアドレスにメールを送りつけるいたずらはできる。同じアドレスへの送信回数の上限で数は抑えられる。さらに防ぐなら、ボットを見分ける Cloudflare Turnstile を足す

月3,000通の枠は、登録した人数ではなく、ログインの回数で減る。マジックリンクはログインのたびに1通送るため。

ログインした状態を30日保つ作りなら、1人あたりおよそ月1通になる。3,000通を超えた分は、1,000通ごとに $0.35 かかる。

8-2. 認証の外部サービスを使わない理由

Section titled “8-2. 認証の外部サービスを使わない理由”

メールでのログインは、Clerk などの外部サービスでも作れる。この記事でそれを使わないのは次の理由から。

  • 本番で使うには、外部サービスでも独自ドメインが必要になることが多い
  • 無料のままだと、届くメールの文面を日本語にできないことがある
  • ユーザーの情報を、そのサービスに預けることになる

ユーザーの管理ごと外に任せたい人には、外部サービスが向いている。

8-3. ローカルで本当にメールを送るには

Section titled “8-3. ローカルで本当にメールを送るには”

ローカルでは、メールは送られずにファイルに書き出される。ローカルからも実際に送りたいときは、wrangler.jsonc のメールの設定に remote を付ける。付けたまま試すと、本物の宛先に届く。

  • 2026-10-29 に、Email Service の登録、Gmail と Yahoo! メールへの送信、マジックリンクのログインを試した
  • Email Service のメール送信は beta(2026-04-16 から公開 beta)。画面や制限が変わることがある
  • 認証のように間違えると困る部分では、Claude Code のモデルを上位のものに切り替えておくのも手。この記事のプロンプトは、Opus 5.5 と Sonnet 5.5 のどちらでも同じ作りになった