🌐

localhost:3000 で動いてるのに localhost/ も必要?FedCMのOrigin検証を理解する

に公開

きっかけ:FedCMで遭遇した謎の設定要求

最近、Next.jsアプリにFedCM(Federated Credential Management API)を使ってGoogle認証を実装していたときのこと。Google Consoleで承認済みのJavaScript生成元を設定する際、こんな指示に遭遇しました。

承認済みのJavaScript生成元:
- http://localhost:3000  ← 開発サーバーが動いているポート
- http://localhost/      ← ???なぜこれも必要?

開発サーバーは http://localhost:3000 で動いているのに、なぜ http://localhost/(ポート80)も登録する必要があるのか?80番ポートなんて使っていないのに...

そしてちょうど同じタイミングで、Xでこんなポストを見かけたのです。

エンジニア歴の浅い私は、「なんとなく3000はフロントエンド、8080はバックエンドとだけ思ってた。あと6006はStorybook、5000台は...何かあったななんだっけ?」と、普段何気なく使っているポート番号について改めて整理することにしました。

ポート番号と世代の関係

localhost:3000 - モダンフロントエンド世代

特徴:

  • Node.js、React、Next.js、Viteなどのモダンなフロントエンドツールのデフォルトポート
  • 2010年代中盤以降に開発を始めた世代に馴染み深い

背景:
Create React Appが 3000 をデフォルトとして採用したことで、React開発者の間で定着しました。その後、多くのフロントエンドツールがこれに倣い、今では「開発サーバー = 3000番」というのが暗黙の了解になっています。

$ npm start
# Compiled successfully!
# You can now view your app in the browser.
# Local: http://localhost:3000

localhost:8080 - アプリケーションサーバー世代

特徴:

  • Java (Tomcat、Jetty)、Spring Boot、Apacheなどで頻繁に使用
  • 2000年代〜2010年代前半の開発経験者に馴染み深い
  • ポート80が使えない時の「定番の代替ポート」

背景:
Apache Tomcatがデフォルトで8080番を使用していたことから、Javaのサーバーサイド開発では8080が標準的になりました。また、Unix系システムでは1024番未満のポート(ウェルノウンポート)を使うには管理者権限が必要なため、開発時は8080のような高番号ポートを使うのが一般的でした。

<!-- Tomcat server.xml -->
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

localhost/ (ポート80) - Webサーバー直起動世代

特徴:

  • ポート番号を省略 = HTTPのデフォルトポート80番
  • Apache、IIS、nginxを直接80番で動かしていた時代
  • 1990年代〜2000年代初頭のスタイル

背景:
かつては開発環境でもApacheやIISを80番ポートで起動し、本番環境に近い形で開発するのが主流でした。XAMPPなどのローカル開発環境パッケージは、デフォルトで80番を使用する設定になっていました。

# Apache httpd.conf
Listen 80
<VirtualHost *:80>
    DocumentRoot "/var/www/html"
    ServerName localhost
</VirtualHost>

現代の開発における役割分担

実は、現代の開発では同時に複数のポート番号を使い分けることが一般的です。

典型的な構成例

http://localhost:3000  → フロントエンド (React/Vue/Next.js)
http://localhost:8080  → バックエンドAPI (Spring Boot/Express)
http://localhost:5432  → PostgreSQL
http://localhost:6379  → Redis

「3000がフロントエンド、8080がバックエンド」という理解は現代の開発において一般的な構成です。ただし、これは広く使われている慣習であって、厳密なルールではありません。Next.jsのAPI routesのようにフロントエンドとバックエンドが統合される場合や、プロジェクトの要件によって異なるポート構成を取ることもあります。

世代ネタは「どの時代の開発経験が長いか」を表現しているだけで、技術的な役割分担とは別の軸の話なのです。

本題:なぜFedCMは localhost/ を要求するのか

ここからが本題です。冒頭で触れた疑問に戻りましょう。

状況の整理:

  • Next.jsの開発サーバーは http://localhost:3000 で起動している
  • 80番ポートでは何も動かしていない
  • それなのにFedCMの設定で http://localhost/ の登録が必要だった

「オリジンが違う」ということは理解していました。しかし、使ってもいないポート番号のオリジンをなぜ登録する必要があるのか? これが最大の謎でした。

オリジン(Origin)の仕組み

Webのセキュリティモデルでは、オリジンという概念が重要です。オリジンは以下の3つの要素で構成されます。

スキーム://ホスト:ポート

具体例で見てみましょう:

URL スキーム ホスト ポート オリジン
http://localhost:3000 http localhost 3000 http://localhost:3000
http://localhost:8080 http localhost 8080 http://localhost:8080
http://localhost/ http localhost 80 http://localhost
http://localhost:80 http localhost 80 http://localhost

重要なのは、ポート番号が異なれば別のオリジンとして扱われることです。

補足: localhost は特殊なホスト名として、ブラウザによって「安全なローカル環境(Secure Context)」として扱われることがあります。ただし、オリジンの識別自体は通常のドメインと同じルールに従い、ポート番号の違いによって別のオリジンとして区別されます。

FedCMの内部動作:デフォルトポートのOriginもチェックされる

答えは、FedCMの仕様にありました。

FedCM APIは、Origin検証時に以下の2つのOriginをチェックします:

  1. 実際のOrigin: http://localhost:3000 - アプリケーションが実際に動作しているOrigin
  2. デフォルトポートのOrigin: http://localhost/ (= http://localhost:80) - HTTPのデフォルトポート80番のOrigin
// frontend/hooks/useGoogleOneTap.ts
const useFedCm = 'IdentityCredential' in window

window.google.accounts.id.initialize({
  client_id: GOOGLE_CLIENT_ID,
  use_fedcm_for_prompt: useFedCm,  // FedCMが有効化される
  ...
})

このコードでFedCMを有効化すると、ブラウザは以下のプロセスでOriginを検証します:

FedCMのOrigin検証フロー:

1. 実際のページのOriginを取得
   → http://localhost:3000

2. デフォルトポート(80番)のOriginも生成
   → http://localhost/ (= http://localhost:80)

3. Identity Providerに両方のOriginで問い合わせ
   → 両方が登録されていないとエラー

つまり、FedCMはセキュリティチェックの一環として、デフォルトポート(80番)のOriginも内部的に参照するのです。これは、本番環境で https://example.com/ のようにポート番号を省略するのが一般的であることを考慮した設計だと考えられます。

重要なポイント:

  • 80番ポートで実際にサーバーが動いている必要はない
  • FedCMが「デフォルトポート(80番)のOriginも確認する」という仕様のため
  • Google Consoleには、実際に使用するOrigin + デフォルトポートのOriginの両方を登録する必要がある
Origin 役割 実際の使用
http://localhost:3000 実際のアプリが動作するOrigin ✅ 実際に使用
http://localhost/ FedCMが内部的に参照するデフォルトポートのOrigin ❌ 使用していないが登録必須

補足: ChromeのFedCM実装では、localhost のポート差異を厳密に区別します。そのため、開発環境で複数のローカルサーバーを併用している場合は、使用するすべてのポート番号を個別に登録する必要があります。これは環境やツールの構成によって異なる可能性があります。

オリジンの違いを再確認

改めて整理すると、以下はすべて異なるオリジンです:

// 同一オリジン
http://localhost:3000/page1
http://localhost:3000/page2
// → 同じオリジン

// 異なるオリジン
http://localhost:3000
http://localhost:8080
http://localhost/
// → すべて異なるオリジン!

ポート番号省略の挙動

HTTPでは、ポート番号を省略すると自動的に80番として扱われます。

http://localhost/        → http://localhost:80 と同じ
http://localhost:80/     → http://localhost:80 と同じ
https://localhost/       → https://localhost:443 と同じ

つまり、http://localhost/ は実質的に http://localhost:80 であり、これは http://localhost:3000 とは完全に別のオリジンなのです。

開発環境での注意点

CORS設定での失敗例

オリジンを意識していないと、CORS設定でハマることがあります。

// バックエンド (localhost:8080) の設定
app.use(cors({
  origin: 'http://localhost:3000'
}));

// この設定だと...
// ✅ http://localhost:3000 からのリクエスト → OK
// ❌ http://localhost/ からのリクエスト → CORS エラー
// ❌ http://localhost:8080 からのリクエスト → CORS エラー

OAuth/OpenID Connect での設定

リダイレクトURIの登録も同様です。

許可されたリダイレクトURI:
http://localhost:3000/callback  ✅
http://localhost/callback       ❌ 別途登録が必要

Cookieの domain 属性でも注意が必要です。

// localhost に対しては domain 属性を省略するのが正しい
document.cookie = "token=xxx; path=/";  // ✅ 有効

// domain 属性にポート番号は含められない
document.cookie = "token=xxx; domain=localhost:3000"; // ❌ 無効

// localhost に対して domain 属性を指定すると無効になるブラウザが多い
document.cookie = "token=xxx; domain=localhost";      // ⚠️ 非推奨

重要な注意点:

  • localhost はドメインではなくホスト名のため、多くのブラウザでは domain=localhost の指定が無効扱いされます
  • domain 属性を省略した場合、Cookieはそのホスト名(localhost)全体で有効になります
  • つまり、domain 属性を省略すれば、すべてのポート(3000、8080、80など)でCookieが共有されます
// domain 属性を省略した場合の挙動
document.cookie = "token=xxx; path=/";

// このCookieは以下すべてでアクセス可能
// http://localhost:3000 ✅
// http://localhost:8080 ✅
// http://localhost/     ✅

ベストプラクティス

1. ポート番号を統一する

開発チーム内でポート番号の規約を決めておくと混乱が減ります。

# docker-compose.yml
services:
  frontend:
    ports:
      - "3000:3000"
  backend:
    ports:
      - "8080:8080"
  db:
    ports:
      - "5432:5432"

2. 環境変数で管理する

// .env
VITE_API_URL=http://localhost:8080
VITE_APP_PORT=3000

// アプリケーション内
const apiUrl = import.meta.env.VITE_API_URL;

3. 開発環境専用の設定を用意する

// FedCM や OAuth の開発用設定
const devOrigins = [
  'http://localhost:3000',
  'http://localhost:8080',
  'http://localhost',  // 念のため
];

const allowedOrigins = process.env.NODE_ENV === 'development'
  ? devOrigins
  : ['https://example.com'];

4. ドキュメントに明記する

# 開発環境セットアップ

## 使用ポート
- フロントエンド: 3000
- バックエンドAPI: 8080
- データベース: 5432

## 注意事項
OAuth設定では以下のリダイレクトURIを登録してください:
- http://localhost:3000/callback

まとめ

http://localhost:3000 で開発しているのに、なぜ http://localhost/ も必要なのか?

答えは、FedCMが「デフォルトポート(80番)のOrigin」も内部的にチェックするという仕様にありました。これは本番環境での標準的なURL形式(ポート番号省略)を考慮した設計ですが、開発時には「使ってもいないポート番号を登録する」という一見不可解な状況を生み出します。

そしてこの問題を理解する過程で、localhostのポート番号には開発ツールの歴史が反映されていることも見えてきました:

  • 3000番: モダンなフロントエンド開発のデファクトスタンダード
  • 8080番: アプリケーションサーバー時代の定番
  • 80番(省略形): Webサーバー直起動時代の名残、そしてFedCMが参照するデフォルトポート

重要なポイント:

  • FedCM、CORS、OAuth、Cookieなど、多くのWeb技術がオリジンを基準にセキュリティモデルを構築している
  • localhost:3000localhost/ は完全に別のオリジンとして扱われる
  • FedCMは実際に使用していないポート番号のOriginも検証に使用する可能性がある

開発環境で予期しないOriginエラーに遭遇したら、「デフォルトポート」の登録を忘れていないか確認してみてください。

注意:
この記事は、FedCMで遭遇した問題を調べる過程で学んだ内容をまとめたものです。筆者自身も今回初めて知ったことばかりなので、もし技術的な誤りや不正確な記述がありましたら、コメント等でご指摘いただけると幸いです。

参考資料

Discussion