4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

管理者と利用者を同時に、しかもそれぞれ複数タブで。諦めた要件をGoogleの実測で見直した話

4
Posted at

はじめに

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.commyaccount.google.com で、実際の画面を描きます

利用者から見た動きはこうです。

  1. アカウントAでログインする
  2. 「アカウントを追加」からBでログインする
  3. 以降、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件 送られない それぞれのホストが自分用に置くもの(OSIDOTZ

28件の内訳がこの3行です。3行目は各ホストが自分用に置いているだけなので、マルチアカウントには関係しません。効いているのは最初の2行です。

ACCOUNT_CHOOSER はホスト限定なので、Gmailには届いていません。 RP側がこのCookieを読んで一覧を描くことはできない、という制約がここで決まります。

では、その一覧はどこから来ているのでしょうか。

1-4. 一覧は誰が描いているのか(実測)

切り替えの画面は2か所に出ます。IdPの選択画面と、Gmail右上のアバターを押したときのメニューです。見た目は似ていますが、仕組みは別でした。

アバターを押すと ogs.google.com のウィジェットがiframeとして読み込まれ、そこから accounts.google.comListAccounts を呼んで一覧を得ています。この要求にはCookieが23件付きました。1-3の15件と8件の両方です。宛先が accounts.google.com なので、ホスト限定の8件も届きます。

観点 IdPの選択画面 RP側のメニュー 第三者のRP
描くのは誰か IdPそのもの 共通ウィジェットをiframeで読み込む 自分自身
一覧の出どころ 自分のサーバー ウィジェットが ListAccounts を呼ぶ 無い
ACCOUNT_CHOOSER が届くか 届く ListAccounts にだけ届く 届かない

成立の条件は2つあり、どちらも同一事業者だから満たせています。ogs.google.comaccounts.google.com はオリジンこそ別ですが、ドメインの登録単位(eTLD+1)が同じ google.com なのでCookieが付くこと、そして応答が読み手を名指しで限定していることです。

第三者のRPはどちらも満たせません。 得られるのは認証後の subemail だけです。「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件 LSOLHSMSV。どちらも認証補助
値も長さも変わった 2件 ACCOUNT_CHOOSER が140→226バイト、NID が426→547バイト
値は変わったが長さは同じ 16件 SIDLSIDSAPISID ほか
値も変わらない 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, the sessionStorage is initially a copy of the opener's sessionStorage object.

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=noneinteraction_required で拒否される 同上

3つ目は裏返すと使い道になります。

ログイン状態 送ったもの 結果
1アカウント prompt=none 成功
2アカウント prompt=none interaction_required で拒否
2アカウント prompt=nonelogin_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コネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?