Skip to content

inject google oauth credentials at build time#154

Draft
sophiamariem wants to merge 4 commits into
ruzin:mainfrom
sophiamariem:chore/build-time-google-secrets
Draft

inject google oauth credentials at build time#154
sophiamariem wants to merge 4 commits into
ruzin:mainfrom
sophiamariem:chore/build-time-google-secrets

Conversation

@sophiamariem

@sophiamariem sophiamariem commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

The Google OAuth client ID and secret are currently hardcoded in app/main.js:360-361 and committed to source. They've been in public git history since whenever they landed — meaning every clone, fork, and scanner-database has them. This PR moves both values to build-time injection via a generated app/build-config.js module written by CI from GitHub Actions secrets.

What this PR does

  • app/main.js — replaces the two hardcoded constants with a try { require('./build-config') } lookup at startup. Empty strings on missing file → calendar surfaces as "not connected" rather than crashing.
  • app/build-config.example.js (new) — template module with empty strings + comments for local-dev setup.
  • .gitignoreapp/build-config.js is never committed.
  • .github/workflows/build-release.yml — new step "Inject build-time secrets" between npm ci and the build. Fails fast if either secret is missing; otherwise writes app/build-config.js via node -e + JSON.stringify so all escaping is handled correctly. electron-builder packages it into the asar at build time.
  • CONTRIBUTING.md — local-dev setup step pointing at build-config.example.js.
  • No new npm dependency. This was originally drafted with dotenv; we dropped it in favour of a plain require() since (a) it's two constants, not a config system, (b) you already do all your other env-var lookups with raw process.env (E2E vars at the top of main.js), and (c) it matches your "Node primitives + comment explaining why" pattern across the codebase.

What this PR does NOT do

Worth saying out loud so we don't oversell: this doesn't prevent someone unpacking the DMG with npx asar extract and grepping out the credentials. That's an unavoidable property of any OAuth secret shipped in a desktop binary, and Google's docs explicitly accept it for Desktop-application client type ("the client secret is obviously not treated as a secret in this context").

What it does change:

  • Git history — the secret no longer enters source. After rotation, the old value is dead; the new value lives only in CI secrets + signed binaries.
  • Rotation speed — change one GitHub Actions variable, push a tag. ~30 seconds vs a code release.
  • PR / AI agent safety — external contributors and AI coding tools running on the repo see an empty placeholder, not the production secret.
  • Scanner alerts — GitHub Secret Scanning / GitGuardian etc. stop firing on the source tree.

For a real fix to the brand-impersonation phishing vector (someone using the real client to spin up a fake "Steno Calendar Sync"), the only viable path is a serverless proxy that holds the secret server-side. That's a separate, larger change — sketched as a follow-up.

Action required to merge

Three steps, all yours @ruzin: ⚠️⚠️⚠️

  1. Rotate the secret in Google Cloud Console. The current value should be treated as compromised (it's been public). If the OAuth client isn't already type "Desktop application," create a new one. Revoke the old client.

  2. Add the new values as GitHub Actions secrets at Settings → Secrets and variables → Actions:

    • GOOGLE_OAUTH_CLIENT_ID
    • GOOGLE_OAUTH_CLIENT_SECRET
  3. Merge. The next tagged release picks up the new values automatically. No code change needed for future rotations — just update the secret variables.

If steps 1–2 aren't done before the next release, the inject step fails the build loudly with a ::error:: annotation pointing at exactly where to add the secrets. No silent ship of an empty-credential DMG.

Local dev impact

Contributors who skip the setup launch the app normally — calendar surfaces "not connected", everything else works. Setup is cp build-config.example.js build-config.js + their own Google Cloud project credentials. CONTRIBUTING.md walks through it.

Test plan

  • node --check app/main.js clean.
  • npm install clean; package-lock.json no longer has a direct dotenv entry (transitive via electron-builder is fine, unchanged).
  • AST + workflow YAML validated.
  • After adding the GitHub secrets, run a workflow_dispatch build and confirm the resulting DMG authenticates to Google Calendar (full end-to-end: install DMG → connect calendar → see events).
  • Verify the fail-fast guard: run the workflow once without the secrets set, confirm it errors with the inject-step ::error:: message instead of silently shipping.
  • Local dev: confirm the app launches with no build-config.js and surfaces "calendar not connected" cleanly (no crash).
  • Local dev with build-config.js populated: confirm calendar connect still works.

Follow-ups (out of scope)

  • POSTHOG_API_KEY at main.js:356 is the same kind of leaked credential. Same fix pattern, ~3 lines, intentionally left out of this PR to keep the diff focused.
  • OUTLOOK_CLIENT_ID at main.js:367 is also hardcoded. Microsoft public clients don't use a secret so the risk profile is different, but the externalisation is good hygiene.
  • The serverless-proxy approach (the one that actually defeats binary extraction) — ~50 lines on Vercel/Cloudflare, would eliminate the need for any secret in the binary.
  • Windows port: same build-config.js + workflow pattern works on windows-latest runners with no changes — just swap electron-builder --mac for --win in the build script.
  • The old leaked secret stays in git log forever as a fact of git history. Mitigation = having rotated it. git filter-branch / BFG can rewrite history but breaks every fork and is rarely worth the disruption.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/workflows/build-release.yml Outdated
Comment thread .github/workflows/build-release.yml Outdated
@sophiamariem sophiamariem marked this pull request as draft June 6, 2026 15:02
@sophiamariem

sophiamariem commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

marked as draft - as requires manual steps by @ruzin to be safe for merge

@ruzin

ruzin commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Thanks for moving the OAuth creds out of source — good change. One cross-platform blocker before this can merge though:

🚫 Breaks Google Calendar on Windows builds

This PR removes the hardcoded GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET from app/main.js and switches to require('./build-config') with an empty-string fallback. But the new "Inject build-time secrets" step is only added to .github/workflows/build-release.yml (the macOS workflow). .github/workflows/build-windows.yml — which runs npx electron-builder --win — has no equivalent step.

So on a Windows release: build-config.js is never written → buildConfig = {}GOOGLE_CLIENT_ID = ''Google Calendar auth silently fails for every Windows user. That's exactly the "silently ship with empty OAuth credentials and every user's calendar would fail to authenticate" failure your own comment guards against — but the guard only covers macOS.

Fix: add the same secret-injection step to build-windows.yml before the electron-builder --win step. Two Windows-specifics to double-check while you're there:

  • Windows uses asar: false, so confirm build-config.js lands next to main.js in the packaged resources/app/ (it should, since it's a normal require, but worth a smoke test).
  • The PowerShell step will need its own env block / if guard rather than the bash [ -z ... ] form.

Minor (non-blocking)

  • Lockfile churn: app/package-lock.json is heavily touched here, including a 0.2.13 → 0.3.8 version bump and a lot of peer flag flips. That'll conflict with other open PRs — worth isolating to just the dependency change if possible.
  • Rotate the secret: the old client secret is still in git history. Since you're already wiring up the GitHub Actions secret, rotating it in Google Cloud Console closes that off for good.

Happy to help wire up the Windows workflow step if useful.

@Optic00

Optic00 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Status check as part of a housekeeping pass - this PR has been sitting since June 11 and now conflicts with main, while the underlying issue is still live: the Google client ID + secret are still hardcoded on main (app/main.js:448-449) and in public git history.

One nuance worth stating for the decision: for a desktop app's installed-app OAuth flow, Google's own docs treat the client secret as not confidential - it ships inside every binary regardless of how it gets there. So this is repo hygiene and rotation policy rather than an acute leak. But that cuts both ways: build-time injection (this PR) only keeps it out of the repo, not out of the app.

@ruzin I think this deserves an explicit call rather than staying parked:

  1. Revive this PR - needs a rebase plus the two secrets in CI, keeps the repo clean going forward.
  2. Close with rationale - accept that installed-app credentials are public by design, document that, and optionally rotate the current pair once.
  3. Different approach - e.g. move Google OAuth to the PKCE flow without a client secret where possible.

Happy to help with whichever direction you pick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants