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:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -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.
BIN
codex-launcher_2.6.1_all.deb
Normal file
BIN
codex-launcher_2.6.1_all.deb
Normal file
Binary file not shown.
@@ -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:
|
||||
code_holder[0] = params["code"][0]
|
||||
self2.send_response(200)
|
||||
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:#27ae60'>✓ 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]
|
||||
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'>Authorization failed.</h2>"
|
||||
b"<p>Please close this tab and try again.</p></body></html>")
|
||||
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(302)
|
||||
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_success_gemini")
|
||||
self2.end_headers()
|
||||
else:
|
||||
error_holder[0] = params.get("error", ["unknown"])[0]
|
||||
self2.send_response(302)
|
||||
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini")
|
||||
self2.end_headers()
|
||||
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">✓ 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>')
|
||||
|
||||
Reference in New Issue
Block a user