郭立 (leeguoo)

# agent に「Sign in with Google」を自分でクリックさせる——chrome-use が OAuth ポップアップログインを攻略

AI agent に自分でアカウントへログインさせたい。アカウントとパスワードの経路は bitwarden-use で通せたが、OAuth の経路で半日以上つまずいた。「Sign in with Google」の裏で 3 つのウィンドウがどうやり取りするのか、なぜ自動化するとすぐ切れるのか、chrome-use が入れた 3 つの修正、そして最後に x へ全自動でログインできた流れを説明する。

2026年6月30日 · 記事 · 公開

このページの目次

chrome-use が id_token を Google popup から gsi/transform 経由で opener に返し、opener が破棄されないように保つ

hermes(べつ機械きかいうごいている、ぼくの agent)に、自分でアカウントへログインしてほしかった。先にぼくがログインして cookie を渡すのではなく、最初から自分でログインする形だ。

ログイン方法はだいたい 2 種類しかない。1 つはアカウントとパスワード。agent がぼくの Bitwarden 金庫きんこからユーザー名、パスワード、2FA 認証にんしょうコードを取り出して、入力すれば終わり——この経路は bitwarden-use で通せた。人の手をまったく使わずに github にログインできたし、passkey までいける(秘密鍵ひみつかぎ金庫きんこから取り出してローカルで WebAuthn に署名しょめいするので、スマホを取り出して指紋しもんを触る必要はない)。もう 1 つは「Sign in with Google」のような第三者だいさんしゃ OAuth。こっちで半日以上つまずいた。

以下では、なぜ難しいのか、そして chrome-use が最終的にどう通したのかを説明する。

1 回のクリックの裏で、3 つのウィンドウが会話している

x のログインページで「Google アカウントでログイン」をクリックすると、見た目は 1 回の操作だけだが、裏ではこうなっている。

  1. その Google ボタンは x が自分で描いているものではなく、accounts.google.comクロスオリジン iframe が差し込まれている。
  2. クリックすると popup ウィンドウ が開き、その中でアカウントを選ぶ。
  3. 選び終えると、Google は token を URL に書くのではなく、この popup を中継ページ accounts.google.com/gsi/transform にリダイレクトする。
  4. gsi/transform がやることは 1 つだけ。window.opener を通じて id_token を最初のログインページ(opener)へ postMessage で返し、その後自分を閉じる。
  5. opener が token を受け取って、x 側のログインが成功し、/home に移動する。

重要じゅうようなのは 4 番目だ。token は popup から opener へ、ウィンドウ越しに呼びかける形で返される。その土台どだいwindow.opener関係かんけいだ。この関係かんけいが切れると、token は空気に向かって叫ぶだけになる。popup は gsi/transform の上で止まったまま閉じず、x 側はいつまでも受け取れず、数秒後にログイン選択ページへ戻ってしまう。

自動化すると、なぜ触っただけで切れるのか

ぼくは chrome-use で本物の Chrome を操作している(ブラウザ拡張を通じてタブを引き受ける仕組みで、relay モードと呼んでいる)。問題は 3 か所にあった。

1 つ目、opener を自分たちで誤って消していた。 relay は各タブのデバッグセッションに tabId をキーとして使っている。x のログインページが一度遷移せんいすると(クロスプロセス遷移せんいなので)、Chrome はこのタブに新しい tabId を割り当て、古いキーは無効むこうになる。さらに悪いことに、その時点の「復旧ふっきゅう」ロジックは、無効むこうなセッションに当たると、そのタブを破棄し、新しい空白タブを開いて置き換えていた。破棄されていたのは、まさに token を待っている opener だった。ログを半日ほど見続けたが、毎回 token が戻る瞬間で止まっていた。opener が自分たちの復旧ふっきゅうロジックに消されていたのだ。

2 つ目、relay から Google ボタンが見えなかった。 クロスオリジン iframe は Chrome 内では独立どくりつしたレンダラープロセス(OOPIF)になる。chrome-use が CDP に直接ちょくせつつなぐ場合は自動的に中へ入り、iframe 内の本物の「Continue as 立」ボタンが見える。だが relay モードでは、拡張がタブの最上位にしか attach しておらず、iframe はブラックボックスだった。座標を頼りに当てずっぽうでクリックするしかなく、開けたのも「別のアカウントを使う」という汎用はんようフローで、個人化されたワンクリックログインではなかった。

3 つ目、relay が popup に attach できなかった。 Google のアカウント選択 popup が開いた瞬間に切り替えてアカウントをクリックしようとすると、拡張が「タブが応答しない」と返した。popup のロード完了を待ってからゆっくり attach しようとしていたが、一瞬で通り過ぎる OAuth ウィンドウには遅すぎた。

さらにかくれた落とし穴もあった。長く調べてようやく分かったのだが、拡張が使う native-messaging host は、インストール済みの古いバイナリを指していて、ローカルで新しくビルドしたものではなかった。つまり、それまでの「パッチを当てたテスト」は、全部古いロジックで走っていた。その wrapper スクリプトを新しくビルドしたバイナリに向けて、ようやくパッチが本当に有効な経路に入った。

3 つの修正

上の 3 か所に対して、それぞれ 1 つずつ手を入れた。

opener を守る。 セッションが無効むこうになってもタブを破棄しないようにし、非破壊はかい的な再接続に変えた。同じ targetId はまだ存在している(sessionId が入れ替わっただけ)ので、再 attach して新しい sessionId を取ればよい。読み取りやクリックのようなコマンドで無効むこうセッションに当たっても、拡張側がセッションを復旧ふっきゅうするまで数回リトライを許し、最初の 1 回で強くエラーにしないようにした。

iframe を貫通する。 attach 時に拡張が自分から Target.setAutoAttach({flatten:true}) を開くようにし、Google iframe という子セッションのイベントをデーモンへ転送させた。コマンド側も子 sessionId に合わせて、その iframe 内へルーティングする。これでスナップショットが中まで入れるようになり、「Continue as 立」にクリック可能な ref が付く——CDP 直接ちょくせつ接続と同じになった。

popup についていく。 拡張は chrome.tabs.onCreated を使い、popup が作られた瞬間に attach するようにした(opener が自分の管理下にあるタブのものだけを対象にし、別のウィンドウを誤って巻き込まないようにしている)。デーモンはそれを副次タブとして保持するが、メインタブは奪わない。opener はそのまま固定され、token を待ち続ける。

通った

パッチを入れて、拡張をリロードしてから、もう一度走らせた。

  • スナップショットで、GSI iframe 内の「Continue as 立, leeguooooo@gmail.com」が見えるようになり、それをクリックする。
  • アカウント選択 popup が出るので切り替える(今回は「応答しない」とは出ない)。そこでアカウントを選ぶ。
  • 選んだ後は何も触らず、gsi/transform に token を opener へ postMessage で返させる。
  • opener は破棄されず、token を受け取る——x は /home に移動し、ログイン状態になる。

最初から最後まで、人のクリックは 1 回もない。この硬い骨は、どうにか噛み砕けた。

仕上げ

agent が自分でアカウントにログインする経路は、これで 2 本とも通った。OAuth のような第三者だいさんしゃ遷移せんいは chrome-use が自分でクリックし切る。アカウントとパスワード、2FA、passkey のようなものは、bitwarden-use があなたの金庫きんこから取り出して入力する。

← 前の記事
微信にバックグラウンドでメッセージを送らせようとして、データベースを壊したことがある —— 「デバッガをアタッチ」から「ゼロアタッチ」へ
次の記事 →
agent にクロスオリジン iframe をクリックさせる:chrome-use がこの難題を突破した話

コメント

コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。

最大 1000 文字。