はじめに
GMOコネクトの永田です。
以前、こんな要件の前で手が止まったことがあります。利用者アカウントと管理者アカウントを、同じブラウザの別タブで同時に使いたい。そのうえで、1つのアカウントについて一覧と詳細を別タブで開いて見比べたい。
アカウントの軸と画面の軸が掛かっています。「いま誰として動いているか」をタブごとに持ちつつ、同じアカウントのタブは何枚でも開ける必要があります。当時は諦めました。トークンを localStorage に置いていたので、タブごとに別のアカウントにする方法が思いつかなかったからです。
Gmailは普通にこれをやっています。/u/0/ と /u/1/ を並べれば、別々のアカウントで同時に動きます。何が違うのか、ブラウザの開発者ツールで26ケース測った記録です。
先にまとめ
答えは「保存していないから」でした。Googleは「いま誰として動いているか」をCookieにもストレージにも置かず、URLだけで運んでいます。
| 測ったこと | 結果 |
|---|---|
| アカウントを1つ増やしたときのCookie | 26件 → 28件。増えた2件は認証補助で、アカウント別の名前空間は0件 |
| 長さがアカウント数に連動したCookie |
ACCOUNT_CHOOSER の1件だけ。その中身にメールアドレスも sub も表示名も入っていない |
/u/0/ と /u/1/ が送るCookie |
どちらも2099バイトで名前の集合も一致。それで別のアカウントが描画される |
- 「いま誰か」の置き場所は6つあり、管理単位は置き場所のスコープがそのまま決まります。 URL・JSの変数・
sessionStorageに置けばタブごと、localStorage・Cookie・サーバー側セッションに置けばブラウザ全体で1つになります - どちらが優れているという話ではなく、要件で決まります。 タブごとは別アカウントを並べられる代わりに、切り替えが他のタブに伝わりません。ブラウザ全体は全タブで一貫する代わりに、開いたままのタブが黙って別アカウントとして動きます
- 冒頭の2つ、つまりタブごとに別アカウントと、同じアカウントを別タブでも、を両方満たすのはURLだけでした
- Googleが選んだURL方式にも弱点が2つあります。パラメータが落ちたときのフォールバック先と、インデックスが安定しないことです
性質の違う話が混ざるので、見出しに印を付けます。
- 実測: 2026年9月に観測したGoogleの挙動。公開文書に根拠が無いので、変わっても不思議はありません
- 仕様: HTML StandardやOpenID Connectの仕様に書かれていること。相手がGoogleでなくても変わりません
「複数アカウントの認証トークンを単一の共有Cookie空間にまとめて持っている」という説明を見かけますが、いま測るとそうなっていませんでした。過去にそういう作りだった可能性もあるので、以下は2026年9月時点のスナップショットとして読んでください。測定はChrome 152 / Edge 153 / Firefox 156 / Safari 26.6、テストアカウント2つで、以下A・Bと書きます。
1. Googleのマルチアカウントはこう動いている
1-1. 登場人物
Googleでは、認証を担う側と、それに任せる側が別のホストに分かれています。以降は認証の用語をそのまま使います。
- IdP(Identity Provider): ログインを受け付けて認証状態を持つ側。OpenID Connectの仕様ではOpenID Providerと呼ばれます。Googleでは
accounts.google.comで、ログイン画面とアカウント選択画面を出します - RP(Relying Party): 認証をIdPに任せて、その結果を受け取る側。Googleでは
mail.google.comやmyaccount.google.comで、実際の画面を描きます
利用者から見た動きはこうです。
- アカウントAでログインする
- 「アカウントを追加」からBでログインする
- 以降、URLが
/u/0/や/u/1/という形になる
この /u/N/ が全部の鍵でした。
1-2. 状態は3つに分かれている(実測)
| 層 | 何を持つか | 管理単位 | 置き場所 | 寿命 |
|---|---|---|---|---|
| 記憶しているアカウントの一覧 | 表示用の候補 | ブラウザに1つ | Cookie(ACCOUNT_CHOOSER) |
ログアウトしても残る |
| 認証セッション | アカウントごとの認証状態 | アカウントごと | IdPのサーバー。CookieにはセッションIDだけ | ログアウトで消える |
| いま利用中 | どのアカウントとして描画するか | タブごと | 保存しない。URLに載せる | リクエスト単位 |
真ん中の「CookieにはセッションIDだけ」は、Cookieの中に認証状態そのものが入っていない、という意味です。入っているのはサーバー側の状態を指す識別子で、JWTのように中身を自分で持ち歩くトークンではありません。実際にそうなっているかは2章で確かめます。
ログアウトで消えるのは真ん中だけです。 ログアウトしたのに選択画面に名前が残っているのは、一番上の層が別に生きているからです。
切り替えとは、真ん中の層を変える操作です。 認証のやり直しではありません。ここを分けていないと「切り替え=ログアウトしてログインし直すこと」になってしまいます。
1-3. Cookieは2つの範囲に配り分けられている(実測)
アカウント2つでログインした時点のCookieは、保管ドメインで分かれていました。
| 保管ドメイン | 件数 | 他のRPへ送られるか | 役割 |
|---|---|---|---|
.google.com |
15件 | 送られる。mail.google.com にも届く |
各RPが「いま誰か」を解決し、API要求に資格情報を付ける |
accounts.google.com |
8件 | 送られない | IdPだけが読む。記憶しているアカウントの一覧を持つ ACCOUNT_CHOOSER もここ |
myaccount.google.com など各ホスト |
5件 | 送られない | それぞれのホストが自分用に置くもの(OSID、OTZ) |
28件の内訳がこの3行です。3行目は各ホストが自分用に置いているだけなので、マルチアカウントには関係しません。効いているのは最初の2行です。
ACCOUNT_CHOOSER はホスト限定なので、Gmailには届いていません。 RP側がこのCookieを読んで一覧を描くことはできない、という制約がここで決まります。
では、その一覧はどこから来ているのでしょうか。
1-4. 一覧は誰が描いているのか(実測)
切り替えの画面は2か所に出ます。IdPの選択画面と、Gmail右上のアバターを押したときのメニューです。見た目は似ていますが、仕組みは別でした。
アバターを押すと ogs.google.com のウィジェットがiframeとして読み込まれ、そこから accounts.google.com の ListAccounts を呼んで一覧を得ています。この要求にはCookieが23件付きました。1-3の15件と8件の両方です。宛先が accounts.google.com なので、ホスト限定の8件も届きます。
| 観点 | IdPの選択画面 | RP側のメニュー | 第三者のRP |
|---|---|---|---|
| 描くのは誰か | IdPそのもの | 共通ウィジェットをiframeで読み込む | 自分自身 |
| 一覧の出どころ | 自分のサーバー | ウィジェットが ListAccounts を呼ぶ |
無い |
ACCOUNT_CHOOSER が届くか |
届く |
ListAccounts にだけ届く |
届かない |
成立の条件は2つあり、どちらも同一事業者だから満たせています。ogs.google.com と accounts.google.com はオリジンこそ別ですが、ドメインの登録単位(eTLD+1)が同じ google.com なのでCookieが付くこと、そして応答が読み手を名指しで限定していることです。
第三者のRPはどちらも満たせません。 得られるのは認証後の sub と email だけです。「Googleにログイン済みのアカウント一覧を自前の画面に出したい」は、原理的に実現できません。
なお、配布範囲と HttpOnly は別の軸として掛けられていました。認証セッションのCookieでも SAPISID はJSから読めます。Gmailが付ける Authorization ヘッダの値をページ内で計算するのに、Cookieの値そのものが要るからです。入力はメールアドレス・SAPISID の値・origin・時刻の4つで、Chromiumのソースに記述があります(lens_sapisid_generator.h)。Bearer相当をJSで組み立てる設計を採ると HttpOnly を外すことになる、という実例です。
2. 測る前に思い込んでいた2つのこと
思い込み1: アカウントが増えればCookieも増える(実測)
アカウントAでログイン中に、Bで追加ログインしたときの差分です。
| 項目 | 件数 | 内訳 |
|---|---|---|
| 新規に増えた | 2件 |
LSOLH、SMSV。どちらも認証補助 |
| 値も長さも変わった | 2件 |
ACCOUNT_CHOOSER が140→226バイト、NID が426→547バイト |
| 値は変わったが長さは同じ | 16件 |
SID、LSID、SAPISID ほか |
| 値も変わらない | 8件 | — |
| 消えた | 0件 | — |
| アカウント別に名前空間が分かれたもの | 0件 |
SID_1 のようなものは存在しない |
合計は26件から28件になりました。増えたのは2件だけで、どちらも認証補助です。
見るべきは「長さが変わった2件」です。中にアカウントの情報を持つCookieなら、1つ増えたぶん長くなるはずだからです。逆に、値だけが変わって長さが動かない16件は、アカウント数に比例する中身を持っていないと言えます。
| 記憶しているアカウント数 | ACCOUNT_CHOOSER |
NID |
|---|---|---|
| 0 | 無し | 319バイト |
| 1 | 140バイト | 426バイト |
| 2 | 226バイト | 547バイト |
| 2(全ログアウト後) | 226バイト | 395バイト |
連動していたのは ACCOUNT_CHOOSER だけでした。NID も伸びましたが、全ログアウト後に採り直すと ACCOUNT_CHOOSER は226バイトのままなのに NID は547から395へ縮みます。広告と設定のCookieで、別の要因で伸び縮みしています。
その ACCOUNT_CHOOSER の中身も調べました。
| 観点 | 結果 |
|---|---|
メールアドレス・sub・表示名を含むか |
含まない。生の値・URLデコード後・base64デコード後のすべてで不検出 |
| 長さの法則 | デコード後の総バイト数 = 41 + 64 × アカウント数 |
| 統計的性質 | 乱数と区別がつかない |
| ログアウトでの変化 | 変化しない。sha256まで一致 |
| 削除すると | 記憶が完全に消え、素のログイン画面に戻る。再発行もされない |
サーバーが封をして発行し、そのまま提示すれば通る持参物でした。どのアカウントを記憶しているかという集合はCookieが決め、氏名やメールアドレスという表示用の情報はサーバー側が持つ、という分担です。だから中身が空っぽでも選択画面には名前が並びます。
つまり認証状態はIdPのサーバー側にあり、Cookieが持っているのはそこを指すセッションIDだけです。1-2で置いた読み方が、ここで裏付けられました。アカウントが増えても認証Cookieの本数も長さも増えないのは、その結果です。
思い込み2: 「いま誰か」はどこかに保存されている(実測)
myaccount.google.com の /u/0/ と /u/1/ を続けて開いて、送信Cookieを比べました。
| 観測項目 | /u/0/ |
/u/1/ |
|---|---|---|
| 表示されたアカウント | B | A |
| Cookieヘッダ長 | 2099バイト | 2099バイト |
| Cookie名の集合 | 一致 | 一致 |
値が違ったのは SIDCC 系の3件だけで、これは同じURLを2回続けて開いても値が変わるものです。その分を除くと、パスによって異なるCookieは1つもありませんでした。
完全に同じCookieを送って、違うアカウントの画面が描画されます。 アカウントの区別がURLだけで行われていることの、直接の証明でした。
選択画面でアカウントを選んだ結果も、Cookieではなく authuser=N としてURLに載り、以降の全リクエストで引き回されます。「いま誰か」を記録するCookieは書かれていません。
mail.google.com/mail/u/0/ と /u/1/ を同時に開くと、両方が別のアカウントで動きました。RP側に「現在のアカウント」という単一の変数が存在しないからです。
3. 置き場所のスコープが、そのまま管理単位になる(仕様)
ここからは自分のアプリの話です。
置き場所は6つある
「いま誰として動いているか」の置き場所は、「URLか、サーバー側セッションか」の2択ではありません。6つあります。そして管理単位は、置き場所のスコープがそのまま決まります。
| 置き場所 | スコープ | タブ2枚で別アカウントを開けるか | 同じアカウントの別タブに引き継がれるか | リロードで残るか | 切り替えの反映 |
|---|---|---|---|---|---|
| URL | リクエスト | 開ける | リンクにアカウントが載るので引き継がれる | 残る | そのタブだけ |
| JSの変数 | 文書 | 開ける | 引き継がれない | 消える | そのタブだけ |
sessionStorage |
タブ | 開ける | 開き方による(下記) | 残る | そのタブだけ |
localStorage |
オリジン | 開けない | 引き継がれる | 残る | 全タブ |
| Cookie | オリジン | 開けない | 引き継がれる | 残る | 全タブ |
| サーバー側セッション | セッション | 開けない | 引き継がれる | 残る | 全タブ |
上の3つがタブごと、下の3つがブラウザ全体になります。Googleは一番上を採っています。
4列目が冒頭の要件のもう一方、一覧から詳細を別タブで開く、という動きにあたります。同じアカウントを複数のタブで開くこと自体は、どれを選んでもできます。 違うのは、新しく開いたタブが最初から同じアカウントで始まるかどうかです。
sessionStorage は開き方で変わる
「タブごと」と言っても一様ではありません。
After creating a new auxiliary browsing context and document, the session storage is copied over.
— HTML Standard, Web storage(2026-09-18確認)
新しく開いた補助的なブラウジングコンテキストにはコピーされる、という規定です。ここでいう補助的とは、開いた側との関係を持っているかという意味になります。
If the page has an
opener, thesessionStorageis initially a copy of the opener'ssessionStorageobject.— MDN, Window: sessionStorage property(2026-09-18確認)
つまり window.open で開いたタブにはコピーされます。一方、リンクを右クリックして「新しいタブで開く」ではコピーされません。 Ctrl+クリックや中クリック、ブックマーク、URLの直打ちも同じで、開いた側との関係を持たない新しいタブになります。
target="_blank" は挙動が割れています。いまの仕様では rel="noopener" が暗黙に付くのでコピーされないはずですが、Huli氏の記事「Starting a Journey with SessionStorage」では、Firefoxはコピーせず、ChromeとSafariはコピーする、と実測されています。2020年の記事なので現在も同じとは限らず、今回は測っていません。
同じ「一覧から詳細を別タブで開く」でも、リンクの書き方と利用者の開き方で結果が変わります。sessionStorage を選ぶなら、対象ブラウザで開き方を変えて実際に確かめる必要があります。
どちらにも代償がある
上の3つが安全で下の3つが危ない、という話ではありません。
| 観点 | タブごと(URL / JSの変数 / sessionStorage) |
ブラウザ全体(localStorage / Cookie / サーバー側セッション) |
|---|---|---|
| できること | 別アカウントを並べて開ける | 「いま誰か」が常に1つに決まる |
| 切り替えたとき | そのタブだけ変わる | 全タブに即座に効く |
| 起きる事故 | 切り替えたつもりのタブが、古いアカウントのまま残る | 開いたままのタブが、黙って別アカウントとして動く |
| サーバーから見ると | 毎リクエストで名乗りを受け取って検証する | セッションを引けば分かる |
| 同じアカウントの別タブ | 開き方によっては引き継がれない | そのまま引き継がれる |
| 実装の重さ | 全画面でアカウントを引き回す必要がある | 1か所で解決できる |
どちらにも「送信ボタンを押した先が、意図と違うアカウントだった」という結末があります。違うのは事故の形です。 タブごとなら切り替え忘れ、ブラウザ全体なら知らないうちの切り替わりになります。
分離をブラウザ側に任せる
アカウントの分離自体をブラウザ側に任せる、という手もあります。 今回の測定でも、Edgeの隔離プロファイルを2つ同時に起動すると完全に独立していました。片方はGoogle関連Cookieが28件で認証Cookieあり、もう片方は3件で認証Cookieなし。同じ myaccount.google.com を開いても、後者は未ログイン扱いで案内ページに飛びます。
サイトから見れば、別のブラウザでしかありません。
プロファイルを使用すると、Chrome 上の情報全体(ブックマーク、履歴、パスワードなど)を、プロファイルごとに分離できます。
— Google Chrome ヘルプ, 複数のプロファイルを使用して Chrome を管理する(2026-09-18確認)
これが使えるなら「ブラウザ全体」を選んで実装を軽くし、同時利用はプロファイルに任せられます。ただし条件は付きます。
- 同時に使う数だけプロファイルが要ります。 シークレットウィンドウは何枚開いても1つのセッションで、すべて閉じれば消えます
- 別ウィンドウになります。 同じウィンドウの隣のタブで別アカウント、という形にはなりません
- 利用者の操作が前提です。 製品側の機能ではないので、「アプリとしてマルチアカウントに対応している」とは言えません
利用者と端末が限られる社内システムなら、現実的な落とし所だと思います。一方、冒頭のように一覧と詳細を並べて見比べたい、という要件は、ウィンドウが分かれる時点で満たせません。
どれを選んでも決めることがある
| 置き場所 | 付随して決めること |
|---|---|
| URL | 毎リクエストで、そのアカウントが本当にセッションに紐づいているかを検証する。パラメータが落ちたときのフォールバックを決める |
| JSの変数 | リロードで消えるので、消えた後の導線を決める |
sessionStorage |
上のコピーの差。加えてSafariの7日削除の対象になる |
localStorage |
同じく7日削除の対象。storage イベントで他タブへ即時に伝わるが、それは「他タブが黙って切り替わる」ことそのもの |
| Cookie | 毎リクエスト送られるのでサーバー側セッションと同じ性質になる。GETのリンクで切り替えを起こさない |
| サーバー側セッション | 同じURLが利用者によって違う内容を返すので、キャッシュ制御が要る |
7日削除というのは、Safariが操作のないサイトのスクリプト書き込み可能なストレージを消す挙動です。
deleting all of a website's script-writable storage after seven days of Safari use without user interaction on the site
— WebKit, Full Third-Party Cookie Blocking and More(2026-09-18確認)
冒頭の要件は、どれで解けたか
必要だったのは2つでした。利用者と管理者をタブごとに分けられること、そして一覧から詳細を別タブで開いても同じアカウントのままであること。
表で両方を満たすのはURLだけです。JSの変数は新しいタブに引き継がれず、sessionStorage は開き方によって引き継がれたりされなかったりします。Googleが /u/N/ をURLに載せていると、この2つが同時に満たされます。
当時は、ブラウザ全体で1つになる置き方しか候補に上げていませんでした。
Googleは保存しないほうを選び、そのかわりURL方式の弱点を引き受けています。それが次の章です。
4. URL方式にも弱点が2つある(実測)
弱点1: パラメータが落ちるとフォールバックする
| アクセス先 | 表示されたアカウント |
|---|---|
myaccount.google.com/(指定なし) |
B |
myaccount.google.com/u/0/ |
B |
myaccount.google.com/u/1/ |
A |
このときのログイン順はBが先です。authuser を指定しないと authuser=0、つまり最初にログインしたアカウントが使われました。新規ウィンドウやディープリンクで意図しないアカウントが開く現象は、これが原因です。
弱点2: インデックスが安定しない
authuser の番号はログイン順で割り当てられます。ログインし直すと入れ替わります。実測でも、同じ authuser=1 が1回目はBを、2回目はAを指しました。UIにも手掛かりはありません。
こちらは相手がGoogleかどうかに関係なく成り立ちます。URLに載せるならインデックスではなく、安定した識別子を使う、ということです。
5. Googleが相手だと、できないこと(実測)
この章に並ぶのは、Googleの実装がそうである、という話です。Webの制約ではありません。
| できないこと | 実測 | 自分がIdPを作る場合 |
|---|---|---|
| 片方のアカウントだけログアウト | 対象を指定しても全アカウントのセッションが消えた。認証Cookie 17件が消え、ACCOUNT_CHOOSER は226バイトのまま残る |
ログアウトの単位は自分で決められる |
| RPから再認証を要求 |
prompt=login は選択画面を出すだけで再認証させず、max_age は無視される。auth_time すら返らない |
どちらもOpenID Connectの仕様(OIDC Core 1.0)にある標準パラメータなので、実装すれば要求できる |
| 暗黙のアカウント選択 | 2アカウントで prompt=none は interaction_required で拒否される |
同上 |
3つ目は裏返すと使い道になります。
| ログイン状態 | 送ったもの | 結果 |
|---|---|---|
| 1アカウント | prompt=none |
成功 |
| 2アカウント | prompt=none |
interaction_required で拒否 |
| 2アカウント |
prompt=none + login_hint
|
無操作で成功し、指定したアカウントが返る |
アカウントを切り替えるたびに画面を出す必要はありません。 誰なのかを login_hint で名指しすれば、画面遷移なしでそのアカウントのトークンを取れます。
ただし1つだけ、相手が誰でも変わらない注意点があります。login_hint に強制力は無く、存在しないメールアドレスを渡してもエラーにならず黙って無視されました。hintが効いたかどうかを、RPは戻り値から判断できません。返ってきた sub の検証は必須です。
まとめ
- Googleは「いま誰として動いているか」をどこにも保存していません。アカウントを増やしてもCookieは26件から28件になるだけで、認証Cookieは1件も増えず、アカウント別の名前空間も作られません。
/u/0/と/u/1/はバイト単位で同じCookieを送っています - 置き場所は6つあり、管理単位はスコープがそのまま決まります。URL・JSの変数・
sessionStorageならタブごとに別アカウントを開けて、localStorage・Cookie・サーバー側セッションならブラウザ全体で1つになります - どれが正解ということはなく、要件で決まります。タブごとは切り替え忘れたタブが残り、ブラウザ全体は開いたままのタブが黙って切り替わります。どちらも送信先を間違える形の事故です
- 実装の重さは引き換えになります。同時利用をブラウザのプロファイルに任せられるなら、実装の軽い「ブラウザ全体」も現実的な選択です
- Googleが選んだURL方式にも弱点があります。パラメータが落ちたときのフォールバック先と、インデックスが安定しないことです。載せるなら安定した識別子にします
冒頭の要件は「タブごとに別アカウント」と「同じアカウントを別タブでも」の2つだったので、両方を満たすのはURLでした。トークンの置き場所は動かせない前提で考えていましたが、動かすべきだったのはそこでした。なお5章に並べたGoogle側の制約は、測った時点の挙動です。公開文書に根拠が無いので、将来変わっても不思議はありません。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。