Skip to content

security(sync): Google OAuth as a public client with PKCE, no secret - #32

Merged
vietanhdev merged 1 commit into
mainfrom
feat/oauth-pkce-public-client
Jul 20, 2026
Merged

security(sync): Google OAuth as a public client with PKCE, no secret#32
vietanhdev merged 1 commit into
mainfrom
feat/oauth-pkce-public-client

Conversation

@vietanhdev

Copy link
Copy Markdown
Owner

Removes the client secret entirely, rather than replacing the leaked one.

Stacked on #30 (oauth2 5.0), which it builds against.

Why no secret at all

A desktop binary cannot keep one. Anything compiled in is readable by anyone holding the binary — not theoretical here: it's exactly how this project's first client secret ended up public, in the initial commit, readable for the life of the repo.

Google's guidance for installed apps is a public client with PKCE, which this flow already used. PKCE is what actually protects the exchange — the verifier is generated per authorisation, never leaves the process, and an intercepted authorisation code is useless without it.

The secret was adding nothing an attacker couldn't read, while creating something that had to be rotated when it leaked. So THINKUTILS_GOOGLE_CLIENT_SECRET is gone, not merely unset. Only the client ID is supplied at build time, and a client ID isn't sensitive.

Tests, and what writing them taught

Three assertions hold the line. They check the source, because the property is the absence of a call — there's no runtime state to inspect, and someone re-adding set_client_secret to fix an auth error would otherwise pass review unnoticed.

It took three attempts, and each failure is worth keeping:

  1. The comments explaining the rule matched the rule.
  2. Then the assertion strings in the test module matched it too.
  3. And a scan scoped too aggressively would pass while inspecting nothing.

So the scan covers production code only, strips comments, and asserts it saw the module body. A guard that inspects nothing passes for the wrong reason — that's the failure mode I'd been bitten by twice already in this repo's packaging tests.

Docs

Setup no longer tells anyone to paste a secret into , and says plainly that the secret Google offers for a Desktop app client should be ignored.

What you still need to do

Create a new OAuth client (type: Desktop app) and build with:

THINKUTILS_GOOGLE_CLIENT_ID="...apps.googleusercontent.com" npm run tauri build

The flow gets its first real run then — it can't be exercised without a live client.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 19, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
thinkutils eb316fc Jul 20 2026, 03:27 AM

Removes the client secret entirely rather than replacing the leaked one.

A desktop binary cannot keep a secret: anything compiled in is readable
by anyone holding the binary. That is not theoretical here -- it is
exactly how this project's first client secret ended up public, in the
initial commit, and it stayed readable for the life of the repository.

Google's guidance for installed apps is a public client with PKCE, which
this already used. PKCE is what actually protects the exchange: the
verifier is generated per authorisation, never leaves the process, and an
intercepted authorisation code is useless without it. The secret was
adding nothing an attacker could not read, while creating something that
had to be rotated when it leaked.

So THINKUTILS_GOOGLE_CLIENT_SECRET is gone, not merely unset, and
set_client_secret is no longer called. Only the client ID is supplied at
build time, and a client ID is not sensitive.

Three tests hold the line, asserted against the source because the
property is the ABSENCE of a call and there is no runtime state to
inspect. Someone re-adding set_client_secret to fix an auth error would
otherwise pass review unnoticed.

Writing those tests took three attempts, each failure worth keeping:

  the comments explaining the rule matched the rule
  then the assertion strings in the test module matched it too
  and a scan scoped too aggressively would pass while inspecting nothing

So the scan covers production code only, strips comments, and asserts it
saw the module body -- a guard that inspects nothing passes for the wrong
reason.

Docs updated: the setup no longer tells anyone to paste a secret into
sync.rs, and says plainly that the secret Google offers for a Desktop app
client should be ignored.
@vietanhdev
vietanhdev force-pushed the feat/oauth-pkce-public-client branch from 6ce35a2 to eb316fc Compare July 20, 2026 03:24
@vietanhdev
vietanhdev merged commit 995761b into main Jul 20, 2026
9 of 10 checks passed
@vietanhdev
vietanhdev deleted the feat/oauth-pkce-public-client branch July 20, 2026 03:36
vietanhdev added a commit that referenced this pull request Jul 20, 2026
The concurrency block intended never to cancel on main -- the comment said
so -- and did the opposite:

  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

The expression renders to the STRING "false", and a non-empty string is
truthy in that position, so main was cancelled like any other ref. It
failed silently for exactly as long as nobody merged twice in quick
succession.

Four main runs were cancelled during this batch of merges (#36, #37, #39,
#32), each with ZERO jobs recorded -- so those commits have no evidence
they ever built. The runs that were supposed to be the record of what
shipped are the ones that got killed.

Encoding the rule in the concurrency GROUP is unambiguous: on main the SHA
gives every run its own group, so there is nothing to supersede; every
other ref keeps a per-ref group, so a force-push still cancels the old run.

tests/workflow_concurrency.rs guards both halves -- an expression-valued
cancel-in-progress, and a group that lost its per-SHA component (which
with cancel-in-progress: true would cancel main on every push, strictly
worse than the bug it replaced). Mutation-verified: restoring the original
two lines fails both.

118 tests.
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.

1 participant