Skip to content

fix(desktop): rehydrate persisted native OAuth tokens with a stored-set parser (#73271) - #73273

Closed
PRATHAMESH75 wants to merge 1 commit into
NousResearch:mainfrom
PRATHAMESH75:fix/desktop-native-oauth-stored-token-rehydrate
Closed

fix(desktop): rehydrate persisted native OAuth tokens with a stored-set parser (#73271)#73273
PRATHAMESH75 wants to merge 1 commit into
NousResearch:mainfrom
PRATHAMESH75:fix/desktop-native-oauth-stored-token-rehydrate

Conversation

@PRATHAMESH75

Copy link
Copy Markdown
Contributor

Fixes #73271

Problem

Desktop native OAuth sign-in works during a process lifetime, but a cold restart reports the user as signed out even though a valid, encrypted native token record was persisted. The Desktop log shows failed to load tokens: Gateway token response missing access_token.

Root cause

The persist and reload paths disagree on the token payload shape:

  • _persistNativeTokens writes JSON.stringify(tokens), where tokens is an internal camelCase NativeTokenSet (accessToken, refreshToken, expiresAt, provider, userId).
  • _loadNativeTokens fed that decrypted JSON to parseTokenResponse, which parses the raw gateway response shape (snake_case access_token, …).

A persisted set has no access_token, so parseTokenResponse throws Gateway token response missing access_token; the surrounding catch swallows it and returns null, stranding a signed-in user on the sign-in recovery screen after every restart.

Fix

Add parseStoredTokenSet — the reload counterpart of parseTokenResponse — which validates/normalizes a persisted camelCase NativeTokenSet, and use it on the load path (_loadNativeTokens). The token-response path (parseTokenResponse) is unchanged for the live /auth/native/token + /auth/native/refresh exchanges. Existing on-disk tokens are already camelCase, so they rehydrate without a re-login.

Tests

apps/desktop/electron/native-oauth.test.ts:

  • parseStoredTokenSet round-trips a persisted set through JSON.stringifyJSON.parse (the on-disk encoding) and rehydrates it.
  • A regression guard asserting parseTokenResponse throws on a persisted camelCase set — encoding why the reload path must use parseStoredTokenSet.
  • Missing-token and absent-refresh/expiry cases, mirroring the existing parseTokenResponse coverage.

No token value is exposed to reproduce or fix this; the failure is entirely after safe-storage decryption and before any token use.

electron vitest project: 837 passed. parseStoredTokenSet/parseTokenResponse files lint- and typecheck-clean (tsconfig.electron.json).

The arm64 fork-Docker CI job is expected to fail on fork PRs and is unrelated to this change.

…et parser (NousResearch#73271)

Native OAuth sign-in worked during a process lifetime but a cold Desktop
restart reported the user as signed out even though a valid encrypted token
record was persisted.

`_persistNativeTokens` writes `JSON.stringify(tokens)` where `tokens` is an
internal camelCase `NativeTokenSet` (`accessToken`, `refreshToken`,
`expiresAt`, …). On reload, `_loadNativeTokens` fed that decrypted JSON to
`parseTokenResponse`, which reads the raw gateway response shape (snake_case
`access_token`, …). The persisted set has no `access_token`, so the parser
threw "Gateway token response missing access_token"; the surrounding catch
swallowed it and returned null, stranding a signed-in user on the sign-in
recovery screen after every restart.

Add `parseStoredTokenSet`, the reload counterpart of `parseTokenResponse`,
that validates/normalizes a persisted camelCase set, and use it on the load
path. Existing on-disk tokens (already camelCase) now rehydrate. Covered by
tests that round-trip a persisted set and guard against regressing to the raw
parser on the reload path.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data duplicate This issue or pull request already exists labels Jul 28, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #71524: both live diffs add the same camelCase stored-token parser and route _loadNativeTokens() through it. #71524 is the earlier open implementation and also retains load-error diagnostics.

@PRATHAMESH75

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #71524 by @Doud-FR, which is the earlier open implementation of the same fix: it adds the same camelCase parseStoredTokenSet and routes _loadNativeTokens() through it instead of parseTokenResponse. #71524 additionally preserves a load-error diagnostic in the catch (via rememberLog), which this PR drops, so it's the stronger version. Deferring to it.

If useful, the extra regression test here — asserting parseTokenResponse throws on a persisted camelCase set (guarding against re-swapping the parsers) — is easy to cherry-pick onto #71524. Thanks @Doud-FR.

@Doud-FR

Doud-FR commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@PRATHAMESH75 Thanks for the thoughtful comparison and for closing this in favor of #71524.

I also appreciate you pointing out the additional regression guard around parseTokenResponse() rejecting a persisted camelCase token set. I’m going to review that test against the current coverage in #71524, and if it adds useful protection without duplicating an existing assertion, I’ll add the equivalent check there.

Thanks again for taking the time to investigate the issue and for sharing the extra test — it’s genuinely helpful.

@Doud-FR

Doud-FR commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Thanks again @PRATHAMESH75 for pointing this out.

I reviewed the additional regression guard from #73273 and agreed that it usefully documents the parser boundary, even though the underlying rejection was already indirectly covered.

I’ve now added the equivalent test to #71524 in commit 071baf5ca. It verifies that a persisted camelCase token set is rejected by parseTokenResponse() and accepted by parseStoredTokenSet().

All 19 native OAuth tests and the Electron typecheck pass.

Thanks for contributing the extra safeguard — it makes the intent of the fix much clearer for future changes.

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

Labels

area/auth Authentication, OAuth, credential pools comp/desktop Electron desktop app (apps/desktop/*) duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Desktop native OAuth tokens cannot rehydrate after restart — persisted token set is parsed as raw OAuth response

3 participants