IDaaS なしで OAuth ログインを自前実装する — Google / X、PKCE、メールを平文で持たない設計

Ember 編集部 ・ 2026-07-15 ・ 約6分 ・ security

Ember のログインは Google と X(旧 Twitter)の OAuth ですが、Auth0 も Clerk も Firebase Auth も使っていません。 Web Crypto と fetch だけ で Authorization Code + PKCE を自前実装しています。依存ライブラリはゼロ、ランタイムは Cloudflare Workers と Node 22 の共通サブセット、もちろん月額0円です。 「認証の自前実装はやめろ」はパスワードを自分で預かる場合の鉄則で、OAuth のリライング・パーティ側の実装は、仕様の要点さえ押さえれば数百行で安全に書けます。この記事で扱うのは: PKCE フローの実装と、 state / code verifier をサーバに保存しない ステートレス設計(短命 HttpOnly cookie に載せる) Google の id token の 署名検証を省略してよい条件 (なぜ今回は JWKS 検証が不要か、どうなったら必須か) X 特有の制約(メールが取れない)を逆手に取った、 メールアドレスを平文で一切保存しない 個人情報最小化設計 handle 自動生成となりすまし予約語、複数プロバイダの紐付け、管理者権限の安全な自動付与 認証を外部 SaaS に出すか自前で書くか迷っている人が、判断材料にできる粒度で書きます。

全体構造: 2エンドポイント + cookie 1枚 エンドポイントは GET /auth/:provider/start と GET /auth/:provider/callback の2つだけです。 PKCE とステートレス state code verifier はランダム32バイト、 code challenge はその SHA 256(S256)です。多くの実装例は state をサーバ側セッションに保存しますが、Ember は 署名も保存もせず、フロー継続に必要な値を丸ごと短命 cookie に載せます 。 これが成立する理由: cookie は HttpOnly なので JS から読めず、攻撃者が state を知るには cookie 自体を握る必要がある(その時点でより深刻な侵害)。改竄されても callback で s とクエリの state の一致・ p のプロバイダ一致を検査するので、別フローへの流用はできません。サーバに「進行中フロー」の状態が無いため、水平スケールや DO 再起動とも無縁です。 id token の署名検証を省略できる条件 OIDC の教科書は「id token は JWKS で署名検証せよ」と言います。Ember は あえて検証していません 。 署名検証が守るのは「 信頼できない経路で受け取ったトークン 」です。implicit flow やフロントから渡された id token なら必須。しかし今回の id token は、 サーバ自身が https://oauth2.googleapis.com/token へ client secret 付きで POST した応答 として受け取っています。TLS でオリジンを検証済みの、Google 本人からの直接応答です。ここで署名を検証しても、守れるものが増えません。 この判断はコメントとして実装に残しています。 条件が崩れたら(トークンをフロント経由で受ける、検証を別サービスに委ねる等)即座に必須になる 種類の省略なので、「なぜ省略できているか」をコードに書いておくことが安全装置になります。 X の制約を設計に取り込む: メールを平文で持たない X の OAuth 2.0 ( users.read tweet.read ) では メールアドレスが取得できません 。つまりメール必須の設計にすると X ログインが成立しない。逆に「メールが無くても成立する設計」に振り切ると、副産物として個人情報の保有量を最小化できます。 Ember の users / identities テーブルには メールアドレスの平文カラムがありません 。 Google から得た検証済みメールは、 小文字化して SHA 256 ハッシュのみ を email hash に保存(将来の照合・重複判定用) 平文メールは管理者判定(後述)にその場で使うだけで、 profile json からも除去してから保存 漏洩時に出ていくのは復元不能なハッシュだけ 「取らない・持たない」は、暗号化や権限管理よりも強いプライバシー対策です。 アカウント解決: identity → account OAuth の実装よりむしろ設計が要るのはここです。Ember は identities(provider, provider user id) → account id の紐付けテーブルを介し、1トランザクションで解決します。 既知の identity → そのアカウントでログイン(プロバイダのアバターが未設定なら取り込むバックフィル付き) ログイン済みユーザーの /start → 新規作成ではなく 既存アカウントへの紐付け (Google と X を同一アカウントに) 未知の identity → アカウント自動作成 自動作成の handle はプロフィール由来ですが、2つ守ることがあります。 1つ目、 なりすまし予約語 。運営や公式を名乗れる handle は新規生成で使わせない。2つ目、 衝突時の確実な脱出 。連番サフィックスで探索しつつ、想定外の大量衝突ではランダムサフィックスに切り替えて無限ループを断ちます。地味ですが、handle 自動生成は「誰でも任意の文字列に寄せられる」入力なので、ここの詰めが甘いと表示名詐称の温床になります。 管理者権限の自動付与は「検証済みメール」だけを見る 運営アカウントは、環境変数の許可リストとメールが一致した場合に自動付与されます。このとき必ず email verified を確認した値だけ を使います。 Google は未検証メールも id token に載せてくることがあります。未検証メールで管理者判定をすると、「他人のメールアドレスを自分の Google アカウントに未検証のまま登録する」だけで権限が取れてしまう。1行の条件ですが、これを落とすと即・権限昇格です。 その他、踏んで学んだこと state cookie の Path は /api/auth に絞る 。全リクエストに乗せる意味がなく、攻撃面も減る。 callback は必ず「state 破棄 → リダイレクト」で終える 。成功・失敗どちらの分岐でも Max Age=0 で消す。失敗時にフローが残ると、エラー画面からの再送で不可解な挙動になります。 凍結ユーザーの検査はセッション発行の直前に 。OAuth が成功しても、アカウント状態で止める最後の関門を認可側に持たせる。 X のアバター URL は normal (48px)で返ってくる 。 400x400 に置換してから保存しないと、後で全員のアイコンがぼやけます。 自前実装は「あり」か リライング・パーティ側の OAuth は、(1) パスワードを預からない、(2) フローが2エンドポイントに閉じる、(3) 仕様の落とし穴が既知で有限 — という点で、自前実装のリスクが誤解されている領域だと思います。IDaaS の本当の価値は MFA・組織管理・監査ログのような周辺機能で、「Google でログイン」だけが要件なら、この記事の分量で実装は終わります。 次回は Ember の心臓部、ポイント台帳の話 — 「残高カラムを持たない」追記型台帳で、FIFO 失効・冪等発行・没収を SQLite の1テーブルに畳む設計です。