Webサービスで「ユーザーページに独自ドメインを登録できる」機能を提供するのがなぜ難しいか
コンテンツ投稿系のWebサービスでは「ユーザーのページに好きな独自ドメインを登録できる」という機能をつけたくなることがあります。ユーザーからすると「コンテンツが自分自身の所有物であること」を感じやすいですし、コンテンツのポータビリティが上がりますし、とても夢がありますよね。僕もいつか実装してみたい機能のひとつです。
しかし、この機能を提供するには、以下のようなハードルがあります。
- 料金
- ベンダーロックイン
- 複雑な実装(とくに認証)
(1)の料金についてはデプロイ先によります。例えばVercelであればProプラン以上であれば無制限に独自ドメインを登録できます(Unlimited custom domains for all Pro teams)。
Google Cloudの場合にはCertificate Managerで独自ドメインごとの証明書を管理するのに「ひとつあたり○USD」という感じでお金がかかったりします。Pricing - Certificate Manager
Vercelにたくさんのユーザーが独自ドメインを登録してしまえば、後からGoogle Cloudに移行したくなったときには移行作業や料金の面ですごく大変そうです。これが(2)のベンダーロックインですね。
個人的には一番ネックだと感じているのが(3)の複雑な実装です。とくに、ユーザーページからも「いいねボタン」や「フォローボタン」のようなものを設置するには各独自ドメインからのリクエストに対しても認証ができる必要があります。
(ユーザーが設定した独自ドメインのページから認証が必要なリクエストを送ることがないのであれば実装が楽なんですが、ほとんどのサービスでは「ヘッダーにログイン中のユーザーのアイコンを表示してほしい!」みたいなものが要件に入ってきてしまいますよね…)
これを実現するにはいくつか力技でのワークアラウンドっぽいことをやる必要があり、それゆえにパフォーマンスが低下したり、セキュリティホールが生まれやすくなったりします。
というわけでこの記事ではユーザーのページごとに独自ドメインをあてられるようにしたい!認証もしたい! がなぜ難しいかに焦点をあてて説明していきたいと思います。
APIサーバーとドメインが異なるページからのリクエストをどう認証するか
仮にapp.example
というドメインでサービスを提供しているとします。とあるユーザーAさんは自分のページにcustom.example
というドメインを紐付けました。
では別のユーザーBさんがcustom.example
上のLikeボタンを押したとき、リクエストを受け取ったサーバー側ではどうやって「Bさんであること」を確認すれば良いでしょうか?
サードパーティCookieは避けたい
「普通にapp.example
にリクエストを送れば?」と思う方もいるかもしれません。
しかしcustom.example
からapp.example
へ送られるCookieはサードパーティCookieにあたります。サードパーティCookieは今後廃止されていく運命にあります。一部のブラウザではすでにデフォルトでブロックされてしまいますし、別の方法を考える必要がありそうです。
custom.example
に対するCookieを持っていれば良いのですが、いきなりcustom.example
にアクセスがあったときにはCookieを持っていないわけで「これはBさんによるリクエストか?」を判定する何かが必要になります。
既存のサービスの取っている方法から学んでみる
Medium、note、はてなブログなどのサービスでは(有料プランへの加入者限定ですが)自分のブログに独自ドメインをあてることができ、ログインユーザーはそれらのページからも「いいね」や「フォロー」のような機能を利用できます。ではこれらのサービスではどうやって認証を行っているのでしょうか。
本ドメインへリダイレクト → 認証後に再リダイレクトする方法
ブラウザのDevToolでNetworkのログやCookieを見て限り、2022年10月時点でMediumとnoteは同じような方法を取っているようです。
例えば、Mediumのとある独自ドメインのページにはじめてアクセスすると、以下のようにリダイレクトが繰り返されていることが分かります。
ユーザーがMediumに紐付けたドメインがcustom.example
だとすると、以下のようなリダイレクトが行われることになります。
- アクセス:
https://custom.example
- リダイレクト:
https://medium.com/m/global-identity?redirectUrl=[1のURL]
- リダイレクト:
https://custom.example/?gi=[トークン]
これらのリダイレクトが何を行っているかを推測してみます。
まず(1)においてMediumのサーバーはcustom.example
へのリクエストヘッダに有効なセッションCookieが付与されていない場合に(2)のmedium.com
の認証用エンドポイントにリダイレクトを行います。
リダイレクトによりmedium.com
へ直接アクセスするため、Cookieはファーストパーティとなり、サーバーはmedium.com
に対するCookieで認証を行うことができます[1]。
しかしmedium.com
へのリクエストに対するレスポンスヘッダでSet-Cookie
を含めてもそれはcustom.example
には付与されません。
そこでcustom.example/?gi=[トークン]
のようにクエリ文字列にトークンを含むURLにリダイレクトを行います(3)。サーバーはこのURLへのリクエストに対して「トークンが妥当であれば、Set-Cookie
でcustom.example
でユーザーを認証するためのCookieを付与する」といった処理を行っているのだと推測します。
※ このURLに付与されるトークンはセキュリティホールになりやすい部分であり、おそらく有効期限はかなり短く設定されているはずです。
以上の流れを踏むことでブラウザはcustom.example
でも認証が可能なCookieを保持することになります[2]。
noteでもMediumと同様にリダイレクトを繰り返すことで認証が行われているようです[3]。
noteの独自ドメインページにはじめてアクセスしたとき
パフォーマンスへの影響
上の方法では、独自ドメインのページはじめてアクセスがあったときにリダイレクトを繰り返す必要があり、これは表示速度に大きな影響を与えるはずです。特にオリジンサーバーが地球の裏側にあるような場合、リダイレクトを複数回行うことはユーザー体験に大きなインパクトがあるはずです。
また、CDNにページファイル(HTML)をキャッシュすることも難しくなるかもしれません。この方法を採用する際には、ユーザー体験へのインパクトと天秤にかける必要があるでしょう。
セキュリティ面でも注意が必要
また、セキュリティホールが生まれないように細かいところまで注意する必要があります。
Cookieがサブドメインに送られないようにする
例えば、悪意のあるユーザーがサービスにcustom.example
というドメインを登録しながらattack.custom.example
というサイトをホスティングしてcustom.example
のCookieを奪おうとする可能性があります。
Cookieのdomain
属性の値が.custom.example
となっている場合、そのCookieはサブドメインのホストにも送られてしまいます。
これはSet-Cookie
においてdomain
属性を空にすることで回避できます(同一のホストに対してのみCookieが送られるようになります)。詳しくは以下のページが参考になると思います。
攻撃者のサイトにしれっとドメインを付け替えられたときの対策もしておきたい
悪意のあるユーザーがDNSのレコード設定をしれっと変えてしまう可能性もあります。サービスへのドメインの紐付けを解除された場合、サービスが発行したCookieが収集されてしまう可能性があります。
対策としては例えば以下のような方法が考えられると思います。
- Cookieだけでは認証ができないように、Local Storage等でダブルチェック用のトークン等を保持しておき、リクエスト時にヘッダに付与する
- サーバーではセッションCookieとトークンの両方をバリデーションする
Cookieの値はドメインごとにユニークにする
万が一ユーザーのcustom.example
に対するCookieを攻撃者に奪われた場合にセッションハイジャックの被害が最小限になるように、認証に関わるCookieの値はドメインごとにユニークにするのが良いと思います。また、ユーザーの独自ドメイン経由でリクエスト可能なエンドポイントは必要なものだけに絞るべきでしょう。
ドメインごとにCookieを発行する方法についてのまとめ
色々と話が広がってしまったので簡単にまとめると以下のようになります。
- リダイレクトを繰り返すことで、独自ドメインごとにセッションCookieを付与することはできる
- ただしパフォーマンス面がネックになる
- Cookieが奪われないようにすごーーーく注意する必要がある
まさに「技術的には可能ですが…」な案件ですね。
はてなブログはiframeを使ってなんとかしてる(?)
ちなみにはてなブログでは(2022年10月時点で)おそらくiframeを経由してサーバーにリクエストを送っている気がします。custom.example
のページ上にs.hatena.ne.jp
のページを<iframe>
で埋め込み、その<iframe>
を経由してリクエストを送ることでサードパーティーCookieを回避しているのかな(?)と思いました。
これも実装の難易度が高い気がしますがどうなんでしょう。ぜひこのような機能を提供してるサービスの中の人に色々とお話を聞きたいところです。
(おわり)
-
CookieのSameSite属性は
None
もしくはLax
となっている必要があります ↩︎ -
この有効なCookieを持った状態でもう一度
custom.example
にリクエストをした場合にはリダイレクトは行われません。 ↩︎ -
noteの場合は(1)
custom.example
のレスポンスヘッダでセッションCookieを付与したうえでリダイレクト → (2)note.com
で認証したうえでリダイレクト → (3)custom.example?gs=トークン
でトークンの値が妥当であれば、バックエンドのDBで(1)のセッションCookieの値を特定のユーザーに紐付ける といった処理を行っているように見えました。 ↩︎
Discussion
参考になりました!
関連記事↓
情報ありがとうございます。具体的なコードが載っていてとても参考になりました!