🍪

Webサービスで「ユーザーページに独自ドメインを登録できる」機能を提供するのがなぜ難しいか

2022/10/18に公開2

コンテンツ投稿系のWebサービスでは「ユーザーのページに好きな独自ドメインを登録できる」という機能をつけたくなることがあります。ユーザーからすると「コンテンツが自分自身の所有物であること」を感じやすいですし、コンテンツのポータビリティが上がりますし、とても夢がありますよね。僕もいつか実装してみたい機能のひとつです。

しかし、この機能を提供するには、以下のようなハードルがあります。

  1. 料金
  2. ベンダーロックイン
  3. 複雑な実装(とくに認証)

(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にリクエストを送れば?」と思う方もいるかもしれません。

サードパーティーcookieとファーストパーティcookie

しかし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で独自ドメインページにはじめてアクセスしたときのネットワーク

ユーザーがMediumに紐付けたドメインがcustom.exampleだとすると、以下のようなリダイレクトが行われることになります。

  1. アクセス: https://custom.example
  2. リダイレクト: https://medium.com/m/global-identity?redirectUrl=[1のURL]
  3. リダイレクト: 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-Cookiecustom.exampleでユーザーを認証するためのCookieを付与する」といった処理を行っているのだと推測します。

※ このURLに付与されるトークンはセキュリティホールになりやすい部分であり、おそらく有効期限はかなり短く設定されているはずです。

以上の流れを踏むことでブラウザはcustom.exampleでも認証が可能なCookieを保持することになります[2]

noteでもMediumと同様にリダイレクトを繰り返すことで認証が行われているようです[3]

noteの独自ドメインページにリダイレクトしたときのログnoteの独自ドメインページにはじめてアクセスしたとき

パフォーマンスへの影響

上の方法では、独自ドメインのページはじめてアクセスがあったときにリダイレクトを繰り返す必要があり、これは表示速度に大きな影響を与えるはずです。特にオリジンサーバーが地球の裏側にあるような場合、リダイレクトを複数回行うことはユーザー体験に大きなインパクトがあるはずです。

また、CDNにページファイル(HTML)をキャッシュすることも難しくなるかもしれません。この方法を採用する際には、ユーザー体験へのインパクトと天秤にかける必要があるでしょう。

セキュリティ面でも注意が必要

また、セキュリティホールが生まれないように細かいところまで注意する必要があります。

Cookieがサブドメインに送られないようにする

例えば、悪意のあるユーザーがサービスにcustom.exampleというドメインを登録しながらattack.custom.exampleというサイトをホスティングしてcustom.exampleのCookieを奪おうとする可能性があります。

Cookieのdomain属性の値が.custom.exampleとなっている場合、そのCookieはサブドメインのホストにも送られてしまいます。

これはSet-Cookieにおいてdomain属性を空にすることで回避できます(同一のホストに対してのみCookieが送られるようになります)。詳しくは以下のページが参考になると思います。

https://blog.tokumaru.org/2011/10/cookiedomain.html

攻撃者のサイトにしれっとドメインを付け替えられたときの対策もしておきたい

悪意のあるユーザーが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を回避しているのかな(?)と思いました。

これも実装の難易度が高い気がしますがどうなんでしょう。ぜひこのような機能を提供してるサービスの中の人に色々とお話を聞きたいところです。

(おわり)

脚注
  1. CookieのSameSite属性NoneもしくはLaxとなっている必要があります ↩︎

  2. この有効なCookieを持った状態でもう一度custom.exampleにリクエストをした場合にはリダイレクトは行われません。 ↩︎

  3. noteの場合は(1)custom.exampleのレスポンスヘッダでセッションCookieを付与したうえでリダイレクト → (2)note.comで認証したうえでリダイレクト → (3)custom.example?gs=トークンでトークンの値が妥当であれば、バックエンドのDBで(1)のセッションCookieの値を特定のユーザーに紐付ける といった処理を行っているように見えました。 ↩︎

Discussion