v2.6.1: rebuild Google OAuth to emulate Gemini CLI

- Uses Google's public OAuth client_id (no client_secret.json needed)
- PKCE + CSRF state protection for secure auth
- Scopes: cloud-platform, generative-language, userinfo
- Just click OAuth Login -> browser -> authorize -> done
- Zero setup required
This commit is contained in:
Roman
2026-05-20 17:38:08 +04:00
Unverified
parent bd4ccf1635
commit 8343837b3c
4 changed files with 78 additions and 96 deletions

View File

@@ -1,5 +1,16 @@
# Changelog
## v2.6.1 (2026-05-20)
- **Google OAuth rebuilt to emulate Gemini CLI**
- Uses Google's public OAuth client_id (same as gemini-cli)
- No `client_secret.json` needed — zero setup required
- PKCE (S256 code challenge) + CSRF state protection
- Scopes: cloud-platform, generative-language, userinfo.email, userinfo.profile
- Redirects to Google's success/failure pages (same as gemini-cli)
- Just click "OAuth Login" → browser opens → authorize → done
- Token file permissions set to 0600 for security
## v2.6.0 (2026-05-20)
- **Usage Dashboard** — per-provider tracking with visual cards

Binary file not shown.

Binary file not shown.

View File

@@ -25,6 +25,13 @@ model_catalog_json = ""
"""
CHANGELOG = [
("2.6.1", "2026-05-20", [
"Google OAuth rebuilt to emulate Gemini CLI — no client_secret.json needed",
"Uses Google's public OAuth client_id (same as gemini-cli)",
"PKCE + CSRF state protection for secure auth",
"Just click OAuth Login → browser opens → authorize → done",
"Includes cloud-platform scope for Gemini Code Assist compatibility",
]),
("2.6.0", "2026-05-20", [
"Usage Dashboard — per-provider request/token/latency tracking",
"Visual cards with success rate bars, model breakdown, error tracking",
@@ -640,7 +647,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label(label="<b>Codex Launcher v2.6.0</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v2.6.1</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1824,84 +1831,41 @@ class EditEndpointDialog(Gtk.Dialog):
def _google_oauth_flow(self):
token_path = os.path.expanduser("~/.cache/codex-proxy/google-oauth-token.json")
default_cs_path = os.path.expanduser("~/.cache/codex-proxy/client_secret.json")
client_secret_path = None
if os.path.exists(default_cs_path):
client_secret_path = default_cs_path
else:
dlg = Gtk.Dialog(title="Google OAuth Setup", parent=self, modal=True)
dlg.add_button("Open Google Console", 1)
dlg.add_button("Browse for client_secret.json", 2)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.set_default_size(500, 320)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
area.set_margin_top(12)
area.set_margin_bottom(12)
area.set_spacing(8)
area.pack_start(Gtk.Label(label="<b>One-time Google OAuth Setup</b>", use_markup=True), False, False, 0)
steps = Gtk.Label(label=(
"1. Create project at Google Cloud Console\n"
" (enable 'Generative Language API')\n"
"2. APIs & Services → Credentials\n"
" → Create OAuth 2.0 Client ID (Desktop app)\n"
"3. Add redirect URI: http://localhost:8085\n"
"4. Download client_secret.json\n\n"
"Then click 'Browse for client_secret.json'\n"
f"It will be auto-copied to: {default_cs_path}"
), xalign=0)
area.pack_start(steps, False, False, 4)
area.show_all()
r = dlg.run()
dlg.destroy()
if r == 1:
subprocess.Popen(["xdg-open", "https://console.cloud.google.com/apis/credentials"])
return
if r == 2:
chooser = Gtk.FileChooserDialog(
title="Select client_secret.json",
parent=self,
action=Gtk.FileChooserAction.OPEN,
)
chooser.add_button("Cancel", Gtk.ResponseType.CANCEL)
chooser.add_button("Open", Gtk.ResponseType.OK)
filt = Gtk.FileFilter()
filt.set_name("JSON files")
filt.add_pattern("*.json")
chooser.add_filter(filt)
chooser.set_current_folder(os.path.expanduser("~/Downloads"))
if chooser.run() == Gtk.ResponseType.OK:
src = chooser.get_filename()
chooser.destroy()
if src and os.path.exists(src):
import shutil as _shutil
os.makedirs(os.path.dirname(default_cs_path), exist_ok=True)
_shutil.copy2(src, default_cs_path)
client_secret_path = default_cs_path
else:
chooser.destroy()
if not client_secret_path:
return
CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
CLIENT_SECRET = "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w"
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/generative-language.retriever",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
]
import http.server, hashlib, secrets, socket
with open(client_secret_path) as f:
cs = json.load(f)
installed = cs.get("installed", cs.get("web", {}))
client_id = installed["client_id"]
client_secret = installed["client_secret"]
redirect_uri = "http://localhost:8085"
scope = "https://www.googleapis.com/auth/generative-language.retriever"
port = 8085
state = secrets.token_hex(32)
verifier = secrets.token_urlsafe(32)
challenge = hashlib.sha256(verifier.encode()).digest()
challenge_b64 = urllib.parse.quote_plus(__import__('base64').urlsafe_b64encode(challenge).rstrip(b'=').decode())
redirect_uri = f"http://127.0.0.1:{port}/oauth2callback"
scope_str = " ".join(SCOPES)
auth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth?"
f"client_id={client_id}&redirect_uri={urllib.parse.quote(redirect_uri)}"
f"&response_type=code&scope={urllib.parse.quote(scope)}&access_type=offline&prompt=consent"
f"client_id={CLIENT_ID}"
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
f"&response_type=code"
f"&scope={urllib.parse.quote(scope_str)}"
f"&access_type=offline"
f"&prompt=consent"
f"&state={state}"
f"&code_challenge={challenge_b64}"
f"&code_challenge_method=S256"
)
dlg = Gtk.Dialog(title="Google OAuth", parent=self, modal=True)
dlg = Gtk.Dialog(title="Google OAuth (Gemini Mode)", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.set_default_size(520, 260)
dlg.set_default_size(520, 280)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
@@ -1909,15 +1873,16 @@ class EditEndpointDialog(Gtk.Dialog):
area.set_margin_bottom(12)
area.set_spacing(8)
area.pack_start(Gtk.Label(label="<b>Step 1: Authorize with Google</b>", use_markup=True, xalign=0), False, False, 0)
link_lbl = Gtk.Label(label=f'<a href="{auth_url}">Click here to open Google authorization</a>', use_markup=True)
area.pack_start(Gtk.Label(label="<b>Sign in with Google</b>", use_markup=True, xalign=0), False, False, 0)
area.pack_start(Gtk.Label(label="Emulating Gemini CLI OAuth — no client_secret.json needed.", xalign=0), False, False, 0)
link_lbl = Gtk.Label()
link_lbl.set_markup(f'<a href="{auth_url}">Click here to open Google authorization</a>')
link_lbl.set_line_wrap(True)
area.pack_start(link_lbl, False, False, 4)
area.pack_start(Gtk.Label(label="Opening browser automatically…", xalign=0), False, False, 0)
area.pack_start(Gtk.Label(label="<b>Waiting for authorization…</b>", use_markup=True, xalign=0), False, False, 12)
self._oauth_status = Gtk.Label(label="Listening on http://localhost:8085 …", xalign=0)
area.pack_start(self._oauth_status, False, False, 0)
self._oauth_status = Gtk.Label(label="Opening browser…", xalign=0)
area.pack_start(self._oauth_status, False, False, 4)
spinner = Gtk.Spinner()
spinner.start()
@@ -1925,36 +1890,39 @@ class EditEndpointDialog(Gtk.Dialog):
area.show_all()
import http.server
code_holder = [None]
error_holder = [None]
received_state = [None]
class OAuthHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self2):
qs = urllib.parse.urlparse(self2.path).query
params = urllib.parse.parse_qs(qs)
received_state[0] = params.get("state", [None])[0]
if "code" in params:
if received_state[0] != state:
self2.send_response(400)
self2.send_header("Content-Type", "text/html")
self2.end_headers()
self2.wfile.write(b"<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>"
b"<h2 style='color:#e74c3c'>CSRF state mismatch.</h2></body></html>")
error_holder[0] = "CSRF state mismatch"
return
code_holder[0] = params["code"][0]
self2.send_response(200)
self2.send_header("Content-Type", "text/html")
self2.send_response(302)
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_success_gemini")
self2.end_headers()
self2.wfile.write(b"<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>"
b"<h2 style='color:#27ae60'>&#10003; Authorization successful!</h2>"
b"<p>You can close this tab and return to Codex Launcher.</p></body></html>")
else:
error_holder[0] = params.get("error", ["unknown"])[0]
self2.send_response(400)
self2.send_header("Content-Type", "text/html")
self2.send_response(302)
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini")
self2.end_headers()
self2.wfile.write(b"<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>"
b"<h2 style='color:#e74c3c'>Authorization failed.</h2>"
b"<p>Please close this tab and try again.</p></body></html>")
def log_message(self2, *a): pass
try:
server = http.server.HTTPServer(("127.0.0.1", 8085), OAuthHandler)
server = http.server.HTTPServer(("127.0.0.1", port), OAuthHandler)
except OSError:
self._oauth_status.set_text("Port 8085 already in use — close other apps and retry.")
self._oauth_status.set_text(f"Port {port} already in use — close other apps and retry.")
spinner.stop()
dlg.run(); dlg.destroy()
return
@@ -1962,16 +1930,16 @@ class EditEndpointDialog(Gtk.Dialog):
def wait_for_code():
server.handle_request()
server.server_close()
GLib.idle_add(self._google_oauth_complete, dlg, code_holder, error_holder,
client_id, client_secret, redirect_uri, token_path, spinner)
GLib.idle_add(self._google_oauth_complete_gemini, dlg, code_holder, error_holder,
CLIENT_ID, CLIENT_SECRET, redirect_uri, token_path, spinner, verifier)
threading.Thread(target=wait_for_code, daemon=True).start()
subprocess.Popen(["xdg-open", auth_url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
dlg.run()
dlg.destroy()
def _google_oauth_complete(self, dlg, code_holder, error_holder,
client_id, client_secret, redirect_uri, token_path, spinner):
def _google_oauth_complete_gemini(self, dlg, code_holder, error_holder,
client_id, client_secret, redirect_uri, token_path, spinner, verifier):
spinner.stop()
if error_holder[0]:
self._oauth_status.set_markup(f'<span foreground="#e74c3c">Error: {error_holder[0]}</span>')
@@ -1988,6 +1956,7 @@ class EditEndpointDialog(Gtk.Dialog):
"client_secret": client_secret,
"redirect_uri": redirect_uri,
"grant_type": "authorization_code",
"code_verifier": verifier,
}).encode()
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
@@ -1996,10 +1965,12 @@ class EditEndpointDialog(Gtk.Dialog):
tokens["client_id"] = client_id
tokens["client_secret"] = client_secret
tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600)
os.makedirs(os.path.dirname(token_path), exist_ok=True)
with open(token_path, "w") as f:
json.dump(tokens, f, indent=2)
os.chmod(token_path, 0o600)
self._entry_key.set_text(tokens.get("access_token", ""))
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">&#10003; Authorization successful! Token saved.</span>')
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">Authorization successful! Token saved.</span>')
dlg.set_title("Google OAuth — Success")
except Exception as e:
self._oauth_status.set_markup(f'<span foreground="#e74c3c">Token exchange failed: {e}</span>')