Skip to content

refactor(auth): make the token cipher async#3444

Merged
trunk-io[bot] merged 6 commits into
mainfrom
posthog-code/async-token-cipher
Jul 15, 2026
Merged

refactor(auth): make the token cipher async#3444
trunk-io[bot] merged 6 commits into
mainfrom
posthog-code/async-token-cipher

Conversation

@gantoine

Copy link
Copy Markdown
Member

Summary

Makes IAuthTokenCipher.encrypt/decrypt async (Promise-returning) so a host can back the refresh-token-at-rest cipher with an asynchronous primitive — specifically the browser's Web Crypto API, which is async.

  • Threads await through the AuthService call sites (persistSession, commitSessionState, resolveStoredSession, getSessionInputForRefresh, plus an extracted recoverSession that keeps the synchronous re-entrancy guard).
  • The desktop adapter (TokenCipherPortAdapter) wraps its existing synchronous node:crypto cipher in Promise.resolve, so desktop behavior is unchanged.

Why

This is the first of a few small enabling changes peeled out of the apps/web browser-host work (previously #3353). It stands on its own: the interface is genuinely async-capable now, and desktop keeps working exactly as before.

Testing

  • @posthog/core typecheck + full auth test suite (2250 tests) pass.
  • apps/code typecheck passes.
  • Biome clean.

Created with PostHog Code

IAuthTokenCipher.encrypt/decrypt become Promise-returning so a host can
back them with an async primitive (e.g. the browser's Web Crypto, which is
async). Thread await through the AuthService call sites; the desktop adapter
wraps its existing synchronous node:crypto cipher in Promise.resolve, so
desktop behavior is unchanged.

Enabling change for the browser web host, but self-contained and useful on
its own.

Generated-By: PostHog Code
Task-Id: 77d13a30-444d-4045-9d80-5e2a9f2e68ae
@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 55b9a9d.

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security Review

Concurrent refresh callers can submit the same rotating refresh token twice, allowing the failed request to clear a session created by the successful request. No authorization bypass was identified.

Reviews (1): Last reviewed commit: "refactor(auth): make the token cipher as..." | Re-trigger Greptile

Comment thread packages/core/src/auth/auth.ts Outdated
Comment thread packages/core/src/auth/auth.ts
Comment thread packages/core/src/auth/auth.ts
gantoine added 3 commits July 14, 2026 16:25
commitSessionState mutated this.session and saved the project preference
before awaiting persistSession. Now that encryption is async and can
reject, a failure left the service on the new project in memory (and in the
saved preference) while the stored session and published state stayed on
the old one — a partial, inconsistent commit the caller saw only as a
rejected promise.

Build the next session locally, await the persist first (the only fallible
step), then assign this.session, save the preference, and publish state —
so a rejection leaves every layer on the prior session. Add a regression
test that fails encryption mid-selectProject and asserts nothing moved.

Generated-By: PostHog Code
Task-Id: 77d13a30-444d-4045-9d80-5e2a9f2e68ae
Making getSessionInputForRefresh async put an await (stored-token
decryption) between ensureValidSession's `if (this.refreshPromise)` guard
and the `this.refreshPromise = ...` assignment. Two concurrent callers
could both observe a null refreshPromise, both decrypt, then both start a
refresh — spending the rotating refresh token twice, so the second request
fails with auth_error and clears the session the first one established,
logging the user out.

Move the decryption inside refreshAndSync so refreshPromise is assigned
synchronously right after the guard, restoring single-flight dedup. Add a
regression test firing two concurrent refreshAccessToken() calls and
asserting one underlying token request.

Generated-By: PostHog Code
Task-Id: 77d13a30-444d-4045-9d80-5e2a9f2e68ae
With async encryption, two overlapping project selections could interleave
across the await in commitSessionState: an earlier selection completing
last would overwrite the newer stored session and published state (and
persistSession's own read of the prior selection could race), leaving the
committed selection stale — not the one the user picked last.

Serialize commits onto a promise chain so each commit's persist-then-publish
runs to completion before the next begins; the latest selection now wins
consistently across the in-memory session, storage, and subscribers. The
stored chain swallows rejections so a failed commit can't wedge later ones,
while the returned promise still rejects for the caller. Add a regression
test that overlaps two selections, drains encryption newest-first, and
asserts the latest selection wins everywhere.

Generated-By: PostHog Code
Task-Id: 77d13a30-444d-4045-9d80-5e2a9f2e68ae
@gantoine gantoine requested a review from a team July 15, 2026 11:55
Generated-By: PostHog Code
Task-Id: f67230a6-4c1c-4433-91cc-ee487135fead
@gantoine gantoine force-pushed the posthog-code/async-token-cipher branch from 39e1fea to 4a90dec Compare July 15, 2026 14:19
@posthog

posthog Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

@gantoine

Copy link
Copy Markdown
Member Author

/trunk merge

@trunk-io trunk-io Bot merged commit 413f1b5 into main Jul 15, 2026
31 checks passed
@trunk-io trunk-io Bot deleted the posthog-code/async-token-cipher branch July 15, 2026 14:53
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.

2 participants