はじめに
GMOコネクトの永田です。
前回の記事で、Googleのマルチアカウントを実測しました。管理者と利用者のアカウントを同時に、しかもそれぞれ複数タブで開きたい、という要件が出発点です。Googleは「いま誰として動いているか」をどこにも保存しておらず、URLだけで運んでいる、という話でした。
書きながら、ずっと引っかかっていたことがあります。1社しか見ていないので、Googleの作りのどこまでがWebの制約で、どこからがGoogleの選択なのかを区別できませんでした。「Googleがそうしているから」を、自分で作るときの根拠にしてよいのかが分からないままでした。
そこでGitHubとMicrosoftを足して、同じ観点で測りました。同じ問題に、3社が3通りの答えを出していた記録です。
先にまとめ
Webの仕様で決まっている部分は少なく、ほとんどは各社の設計判断でした。表のIdP(Identity Provider)はログインを受け付けて認証状態を持つ側、RP(Relying Party)は認証をIdPに任せる側です。
| 観点 | GitHub | Microsoft | |
|---|---|---|---|
| 認証セッションの実体 | IdPのサーバー。CookieにはセッションIDだけ | Cookie 1本に全アカウント分のトークン | 認証基盤ごとにCookie一式 |
| アカウントを増やすと |
ACCOUNT_CHOOSER が伸びる |
saved_user_sessions が伸びる |
別の基盤のCookie一式が増える(条件は1章) |
| 「いま誰か」の置き場所 | URL | Cookie | RPごと |
| 2タブで別アカウント | 使える | 使えない。表示だけ食い違う | 未測定 |
| 片方だけのログアウト | 提供なし | できる。記憶も消える | できる。記憶は残る |
| RPから再認証を要求 | できない | 対象外(OpenID Connectを通らない) | できる |
- 前回「Googleには当てはまらない」と書いた「全アカウントを1つのCookieに持つ」という説明は、GitHubにはほぼ当てはまりました。 ただし伸びるCookieの中身は、Googleとは正反対でした
- 前回、推論として書いた「開いたままのタブが黙って別アカウントとして動く」事故が、GitHubで実際に起きました
- 前回「Googleが相手だとできない」と書いた片方だけのログアウトと再認証の要求は、Microsoftでは標準のパラメータでできました
- 一方で、割れなかった点も13ありました。そのうち9点は、自分で作るときにそのまま採ってよいものです
前回と同じく、見出しに印を付けます。
- 実測: 2026年9月に観測した各社の挙動。公開文書に根拠が無いものが多く、変わっても不思議はありません
- 仕様: HTML StandardやOpenID Connect(OIDC)の仕様に書かれていること。相手が誰でも変わりません
1. 測った条件
3社で条件は揃っていない
| 条件 | GitHub | Microsoft | |
|---|---|---|---|
| 観測日 | 2026-09-15〜17 | 2026-09-20 | 2026-09-21〜22 |
| 2アカウントの構成 | 個人2つ(同じ認証基盤) | 個人1つ+組織が払い出したアカウント1つ | 業務(Entra ID)1つ+個人(Microsoft アカウント)1つ |
| ブラウザ | Chrome / Edge / Firefox / Safari | Chrome | Chrome |
| IdPとRPの関係 | 分離。同じeTLD+1 | 一体型。ログイン画面とサービスが同じアプリ | 分離。IdPとRPでeTLD+1が別 |
eTLD+1はドメインの登録単位で、mail.google.com と accounts.google.com はどちらも google.com になります。
Microsoftは、業務用と個人用で認証基盤が別です。 2章の「Cookie一式が増える」は、別々の基盤に1つずつ入ったときの結果です。同じ基盤に2つ入れた場合は、2つ目の業務アカウントが無く測れていません。Googleと同じ形になる可能性も残ります。
GitHubはOIDCのRPとして観測していません。 github.com自身のログインは独自方式で、OIDCを通りません。prompt や max_age などのパラメータの比較は、GoogleとMicrosoftの2社だけです。
Microsoftで2タブの挙動は測っていません。 3章の表では「未測定」と書いています。
GitHubの2アカウントの用意
GitHubの利用規約は、1人が持てる無料アカウントを1つに限っています。
One person or legal entity may maintain no more than one free Account (if you choose to control a machine account as well, that's fine, but it can only be used for running a machine).
— GitHub Terms of Service, B.3 Account Requirements(2026-09-24確認)
調査用に新しく作ることはできないので、個人アカウント1つと、組織が払い出した Enterprise Managed Users のアカウント1つで測りました。EMUは組織が発行するもので、個人が維持する無料アカウントには当たりません。
2. アカウントを増やすと何が伸びるか(実測)
| 事例 | アカウント数に連動して伸びたもの | 実測値 |
|---|---|---|
ACCOUNT_CHOOSER(記憶しているアカウントの一覧。以下、記憶リスト)だけ |
140→226バイト。認証Cookieは本数も長さも変わらない | |
| GitHub |
saved_user_sessions だけ |
59→122バイト。新しいCookieは0件 |
| Microsoft | 別の基盤のCookie一式 | 28→52件。新しく現れた26件のうち21件は個人アカウント側の基盤(live.com)のドメイン |
同じ「1本が伸びる」でも、GoogleとGitHubは正反対
GoogleもGitHubも、アカウント数に連動して伸びるCookieは1本だけでした。ところが、その1本の中身がまったく違います。
| 観点 | Googleの ACCOUNT_CHOOSER
|
GitHubの saved_user_sessions
|
|---|---|---|
| 中身 | 表示用の候補。メールアドレスも sub も入っていない |
セッショントークンそのもの |
| 認証に使えるか | 使えない | 使える。切り替えの実体がこれ |
| 全ログアウトで | 残る。値まで変わらない | 消える |
| 漏れたときの影響 | 選択画面の表示が変わるだけ | ログイン中の全アカウントに及ぶ |
GitHubでは、saved_user_sessions の中に、いまのアカウントの user_session の値がそのまま含まれていました。切り替え前も切り替え後も値は同じで(sha256が一致)、そのまま両方のアカウントのトークンが取り出せています。
区切り文字の数から見ると、1エントリは <数値の識別子>:<48文字のトークン> で、エントリの間は | です。1アカウントあたり約58バイト伸びます。
切り替えとは、saved_user_sessions から1本取り出して user_session に据える操作でした。 サーバー側の状態は関与していません。
前回の記事では、「複数アカウントの認証トークンを単一の共有Cookie空間にまとめて持っている」という説明はGoogleに当てはまらず、過去の作りだった可能性もあると書きました。実際には、GitHubの現役の実装でした。
どちらが良いかは引き換えです。GitHubの作りは、サーバー側にセッションの表を持たなくても、切り替えも片方だけのログアウトもCookieの書き換えで完結します。そのかわり、1本漏れれば全アカウント分になります。Googleはその逆で、漏れても選択画面の表示が変わるだけですが、状態はサーバー側に持つことになります。
Microsoftは、既存のCookieが1バイトも伸びなかった
業務アカウント1つの状態から個人アカウントを足すと、Cookieは28件から52件に増えました。新しく現れたのが26件、消えたのが2件です。残った26件は、長さが1バイトも変わっていません。
| 持ち主 | ドメイン | 件数 |
|---|---|---|
| Entra ID(業務) |
.login.microsoftonline.com など |
27 |
| Microsoft アカウント(個人) |
.live.com など |
23 |
| RP |
.portal.azure.com、.microsoft.com
|
2 |
1つのセッションの置き場所にN個入れる方式ではなく、基盤ごとにCookie一式が別にあり、login.microsoftonline.com/common がそれを束ねています。
eTLD+1にCookieを置くかも割れた
| 事例 | eTLD+1に置いた件数 | RPのドメイン |
|---|---|---|
| 15件 |
mail.google.com、drive.google.com など。いずれもIdPと同じ google.com
|
|
| Microsoft(Entra ID側) | 1件(wlidperf という21バイトの計測用Cookieだけ) |
portal.azure.com、entra.microsoft.com、myaccount.microsoft.com など。いずれもIdPの microsoftonline.com とは別のeTLD+1 |
Googleは、各サービスが自分で「いま誰か」を解決できるように、.google.com に15件を置いていました。前回はこれを「同じeTLD+1にRPが並ぶなら自然な作り」として読みましたが、Microsoftは、RPのドメインがばらばらで、Cookieを共有する作りになっていません。代わりに login.microsoftonline.com へのリダイレクトだけでSSOを成立させていました。
違いは SameSite の内訳にも出ていました。Googleは28件中Laxが20件、Noneが8件でした。Microsoftは52件中Noneが50件、Laxは2件だけです。SameSite=None は、クロスサイトの文脈(RPのページに埋め込んだiframeなど)でも送る指定です。サードパーティCookieを制限するブラウザではこの経路が止まるので、影響を受けるCookieの範囲はGoogleより広いと考えられます(制限下での挙動は測っていません)。
3. 「いま誰か」の置き場所とタブ(実測)
| 事例 | 置き場所 | 2タブで別アカウント | 開いたままのタブ |
|---|---|---|---|
URL(/u/N/) |
使える | そのタブのURLのアカウントで動き続ける | |
| GitHub | Cookie(user_session) |
使えない | 黙って別アカウントとして動く |
| Microsoft | RPごと | 未測定 | 未測定 |
GitHubで、表示と応答が食い違った
Aだけでログインしていた時点のタブを、リロードせずに残しておきます。別のタブでBを追加し、そのあと両方のタブから同じオリジンへGETを出しました。画面に表示されているアカウントと、サーバーが応答したアカウントを比べています。
| タブ | 画面上のアカウント | サーバーが応答したアカウント |
|---|---|---|
| Bを追加したタブ | B | B |
| Aのまま残したタブ | A | B |
画面上は2つのアカウントが並んでいるように見えますが、どちらのタブもBとして動いています。同じCookieを送っているので、当然の結果です。全ログアウトのあとも、残したタブは画面上ログイン済みのまま、サーバーから見るとログインしていない状態でした。
前回の記事の3章で推論として書いた「開いたままのタブが、黙って別アカウントとして動く」事故を、GitHubで観測できました。
これはGitHubの不具合ではなく、Cookieに置くという選択の裏表です。全タブで「いま誰か」が常に1つに決まる代わりに、画面に古いアカウントが残ったタブが生まれます。
4. ログアウトの単位(実測)
片方だけのログアウト
| 事例 | 片方だけのログアウト | 仕組み | 記憶リスト |
|---|---|---|---|
| 提供なし。対象を指定しても全アカウントのセッションが消えた | 提供なし | 残る | |
| GitHub | できる |
saved_user_sessions からエントリを1つ削る |
一緒に消える |
| Microsoft | できる |
logout_hint(OpenID Connect の標準パラメータ) |
残る |
GitHubでBだけをログアウトすると、saved_user_sessions は122から59バイトに縮み、いま使っているAの user_session は値まで変わりませんでした。
Microsoftの logout_hint には、IDトークンに入る login_hint という省略可能なクレームの値を渡します。アプリ登録で有効にすると、業務側(Entra ID)のアカウントでは150文字の値が入りました。省略可能なクレームのリファレンスはこの値を「opaque」、つまり中身を解釈せずにそのまま渡す前提の値と説明し、改変しないよう求めています。ただし、ランダムな文字列ではありませんでした。業務側の値はbase64で、デコードすると oid・tid・ユーザー名がそのまま読めました。形式は公開されていないので、これは2026年9月時点の実装を業務側の1アカウントで観測した結果です。
logout_hint にUPN(user@domain 形式の識別子)を渡すことは、MicrosoftのOpenID Connect解説ページが禁じています。
これでAだけをログアウトすると、業務側のCookieは ESTSAUTH が1343から57バイトに、SignInStateCookie が927から661バイトに縮みました。個人側のCookieはバイト単位で変わっていません。Bはそのあと資格情報なしで通り、IDトークンに auth_time は付きませんでした。認証は起きていない、ということです。logout_hint を付けないログアウトでは、アカウントの選択画面を挟みました。
ただし、logout_hint でAをログアウトした直後は、残ったBでも prompt=none が一度失敗しました。画面上は「サインイン済み」で、実際に資格情報なしで通るのに、無操作の認証には使えません。
片方だけのログアウトは、記憶リストの作りとは関係なかった
Googleに片方だけのログアウトが無いのは、作れないからではなく、提供していないからでした。
Googleの認証セッションはアカウントごとにサーバー側にあります。全ログアウトのあとにAだけ再ログインすると、「Aはログイン中、Bは記憶されているだけ」という状態が成立しました。片方だけのログアウトで作りたい状態を、Googleのサーバーはすでに表現できています。欠けているのは、そこへ移す操作だけです。
記憶リストの作りが決めるのは、別のことでした。
| 事例 | 記憶リストの中身 | 「IDだけ記憶」の状態 |
|---|---|---|
| 表示用の候補 | 作れる | |
| GitHub | セッショントークンそのもの | 作れない |
| Microsoft | セッションとは別の層 | 作れる |
GitHubは記憶リストがセッションそのものなので、「記憶だけ残してセッションを切る」ができません。全ログアウトすると記憶も消え、次回は素のログイン画面に戻りました。
Microsoftは選択画面の各行に、セッションだけを切る「サインアウト」と、記憶からも消す「サインアウトして破棄」を用意していました。「ログイン中」「記憶だけ」「何も無い」の3つの状態を利用者に見せたいなら、Microsoftの形が一番近い実装例です。
他のRPへのログアウトの伝播
front-channel logout は、IdPがブラウザに各RPのログアウト用URLを読み込ませて(iframe)、ログアウトを知らせる仕組みです。Microsoftでは、別のアプリでサインアウトすると、こちらのRPのfront-channel logout URLにGETが届き、付いてきた sid はサインイン時に受け取った session_state と一致しました。複数のアカウントを持つRPでも、「全部捨てる」ではなく「このセッションだけ捨てる」が実装できます。ただし、個人アカウントのIDトークンには sid クレームが返りませんでした(業務アカウントには返ります)。
届いたのは sid だけで、iss は付いていませんでした。Front-Channel Logout 1.0 の2章は、どちらも任意としつつ、片方を含めるなら両方含めなければならない(MUST)と定めているので、ここは仕様との差です。
ハマり: front-channel logout が飛んでこないと思い込んだ
front-channel logout URLはHTTPS必須で、リダイレクトURIと違って http://localhost の例外がありません。そこで https://localhost を用意しましたが、最初はGETが届かず、IdP側の設定を疑って遠回りしました。
原因はChromeの Local Network Access(公開サイトからローカルのサーバーへのリクエストに、利用者の許可を求める仕組み)でした。login.microsoftonline.com から https://localhost へのiframeがこれに掛かり、許可を求めるダイアログは数秒で消えていました。ブラウザが黙って止めていると、IdPが送っていないように見えます。
5. 標準パラメータは、あるから使えるわけではない(実測・仕様)
ここはGoogleとMicrosoftの2社です。GitHubはOIDCを通らないので対象外です。
| パラメータ | 定義している仕様 | Microsoft | |
|---|---|---|---|
max_age |
OIDC Core 1.0 | 無視する。auth_time も返らない |
解釈する。再認証を求め auth_time を返す |
id_token_hint |
OIDC Core 1.0 | 効く(文書化されていない) | 効かない |
logout_hint |
RP-Initiated Logout 1.0 | 無い | 効く |
互いに、相手が実装していない方を実装していました。 どれも仕様にある標準のパラメータなので、「仕様にあるか」ではなく「そのIdPが実装しているか」で判断するしかありません。
Microsoftで max_age=60 を送ると、パスワードの再入力を求められ、成功後のIDトークンに auth_time が返りました。送らなかったときは返りません。max_age を送ったら auth_time は必須、というOIDC Coreの規定どおりです。
login_hint と prompt=select_account を併用したとき
| 事例 | どちらが勝つか | 利用者から見ると |
|---|---|---|
login_hint |
選択画面がhintのアカウントだけに絞られ、ほかのアカウントを選べない | |
| Microsoft | prompt=select_account |
hintが捨てられ、両方のアカウントが並ぶ |
Googleを相手にするときは、選択画面で全アカウントから選ばせたいなら login_hint を送らない、が回避策になります。自分でIdPを作るなら、prompt=select_account を優先するMicrosoftの形のほうが、RPが親切のつもりで添えたhintで利用者が選べなくなる事態を避けられます。
文書と実装のずれは、3社ともにあった
| 事例 | 文書 | 実装 |
|---|---|---|
id_token_hint の記載が無い |
効く | |
| GitHub | 切り替えリストからアカウントを外す Remove がある |
画面に無い。アカウントごとの Sign out と、全アカウントの Sign out from all accounts だけ |
| Microsoft | 認可リクエストのパラメータ表に max_age が無い |
実装されている |
| Microsoft | エラー表で、login_required の説明が「Too many or no users found.」になっている |
複数ログイン中の prompt=none は interaction_required だった |
| Microsoft | 「You can't use both login_hint and select_account.」とある | エラーにならず、hintが捨てられる |
Googleの記載は OpenID Connect、GitHubは Switching between accounts、Microsoftは OpenID Connect のページと認可コードフローのページで確認しました(2026-09-24)。どちらのページにも max_age は1回も出てきません。
文書に無いから使えない、とも、文書にあるからそのとおり動く、とも言えませんでした。気になるパラメータは、相手のIdPに実際に投げて確かめる必要があります。
6. 割れなかった点(仕様・実測)
割れなかった点は13ありました。ただ、どれも同じ重みで参考になるわけではありませんでした。
| 区分 | 件数 | 扱い |
|---|---|---|
| 仕様どおり | 3 | 仕様に沿って作ればよい |
| 仕様が決めていないのに、同じ答えへ収束していた | 6 | 自分で作るときに一番参考になる |
| 各社の設計の前提や選択の結果として揃っていた | 4 | 条件が合えば参考になる |
仕様どおりの3つ
| 挙動 | 仕様 | 一致した事例 |
|---|---|---|
prompt=none は画面を出さず、出せなければエラーを返す |
OIDC Core 1.0 の3.1.2.1節 | Google、Microsoft |
prompt=select_account はアカウントの選択画面を出す |
OIDC Core 1.0 の3.1.2.1節 | Google、Microsoft |
sub はログインし直しても変わらない |
OIDC Core 1.0 の2節・5.7節 | Google、Microsoft |
sub の配り方は違いました。Googleはすべての RP に同じ値を、Microsoftはアプリごとに別の値(pairwise)を返します。どちらも仕様が認める方式で、Microsoftはアプリをまたいで同じ利用者を扱うための oid も返しています。
仕様が決めていないのに、収束していた6つ
| 収束していた挙動 | 一致した事例 |
|---|---|
複数ログイン中に prompt が無ければ選択画面を出す。IdPが推測で選ばない |
Google、Microsoft |
prompt=none と login_hint を併せて送ると、複数ログイン中でも無操作で指定したアカウントを返す |
Google、Microsoft |
複数ログイン中の prompt=none の失敗は interaction_required で返す |
Google、Microsoft |
| 切り替えは認証ではない。ログイン済みのアカウントへの切り替えでは資格情報を求めず、追加するときだけ認証する | Google、GitHub、Microsoft |
| 選択画面が、アカウントごとに「ログイン中」か「記憶だけ」かを表示する | Google、Microsoft |
| 認証基盤自身のセッションCookieは、基盤のホストに閉じる | Google、GitHub、Microsoft |
自分で作るときは、こちらに揃えておけば利用者の予想を外しにくくなります。
そのまま採るときは、次の点に気をつけてください。
-
interaction_requiredには「複数ログイン中で決められない」場合が含まれる前提で組む。 仕様にはこの場面専用のaccount_selection_requiredもありますが、どちらも「返してよい(MAY)」という規定で、2社とも汎用のinteraction_requiredを返しました。専用のエラーが来るのを待ってはいけません -
切り替えのときに再認証を挟みたいなら、切り替え自体は無操作にしておき、必要な場面だけRPから
max_ageで要求する。 5章のとおり、max_ageが効くかはIdP次第です - Cookieで一致したのは「基盤のセッションをホストに閉じる」ほうだけ。 eTLD+1に何を置くかは割れています(2章)
条件が合えば参考になる4つ
| 一致していた点 | 参考になる条件 |
|---|---|
| 認証する側が、ログイン済みのセッションを持っている(3社) | 自分の認証基盤もログイン済みのセッションを持つなら、その上にマルチアカウントを足せる。持たないなら、セッションの仕組みから作ることになる |
| アカウントごとに名前を分けたCookieを作っていない(3社) | 状態をサーバー側に置く(Google)か、1本のCookieにまとめる(GitHub)なら、名前を分ける必要がない |
| Web Storageのキーをアカウントの識別子で分けている(Google、GitHub) | アカウントごとのデータをブラウザに残すなら、同じ手当てが要る |
sessionStorage を「いま誰か」の置き場所に使っていない(Google、GitHub) |
「いま誰か」をURL(Google)かCookie(GitHub)で運ぶなら、使う必要がない |
最後の行は、「3社とも使っていないから sessionStorage は避ける」と読むと間違えます。URLやCookieで運ばない設計なら、sessionStorage は選択肢に残ります。前回の記事で書いたとおり、タブごとに「いま誰か」を持てる代わりに、開き方によって引き継がれ方が変わるという癖を承知で選ぶものです。
まとめ
- 同じ問題に、3社が3通りの答えを出していました。認証状態をIdPのサーバーに置く(Google)、Cookie 1本に全アカウント分のトークンを詰める(GitHub)、認証基盤ごとにCookie一式を分ける(Microsoft)
- 標準のパラメータは、仕様にあるから使えるわけではありません。GoogleとMicrosoftは互いに相手の実装していない方を実装しており、文書と実装のずれは3社ともにありました
「Googleがそうしているから」が根拠になるのは、割れなかった13点のうち、仕様どおりの3点と収束していた6点です。残る4点は条件が合うときに参考になる程度で、3社で割れた点は、Googleがそう選んだというだけでした。なお各社の挙動は2026年9月時点のもので、公開文書に根拠が無い部分は変わっても不思議はありません。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。