Skip to content

[wrangler] Read OAuth state from disk lazily so env auth takes priority#13954

Merged
petebacondarwin merged 6 commits into
mainfrom
fix-lazy-auth-state-read
May 19, 2026
Merged

[wrangler] Read OAuth state from disk lazily so env auth takes priority#13954
petebacondarwin merged 6 commits into
mainfrom
fix-lazy-auth-state-read

Conversation

@petebacondarwin

@petebacondarwin petebacondarwin commented May 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #13744.

What

Wrangler previously read its OAuth state from the user auth config file (e.g. ~/.config/.wrangler/config/default.toml) eagerly at module-import time. That happens before .env files are loaded, so the in-memory state would always hold the OAuth tokens even when the user only wanted to authenticate via CLOUDFLARE_API_TOKEN. If that stored OAuth token happened to be expired, Wrangler would try to refresh it (and fail), aborting the command with:

X [ERROR] Failed to fetch auth token: 400 Bad Request
X [ERROR] You are logged in with an API Token. Unset the CLOUDFLARE_API_TOKEN in the environment to log in via OAuth.
X [ERROR] Not logged in.

— even though a valid API token was in scope.

How

Read the auth config file on demand from every site that needs it, rather than caching it at module scope. The key fix is in isAccessTokenExpired(): it now short-circuits to false when env-based credentials are present, so the OAuth refresh endpoint is no longer called when env auth is going to be used.

Sibling-process refresh-token rotation (the case PR #13910 partially addressed) is also handled naturally because every check reads the current file contents.

Two commits:

  1. [wrangler] fix: read OAuth state from disk lazily so env auth takes priority — the actual fix in packages/wrangler/src/user/user.ts, plus three regression tests in user.test.ts. Includes several drive-by cleanups (dead localState mutations, dead logout() branch with accidental double-revoke, redundant reinitialiseAuthTokens in refreshToken, in-process selected-account cache).

  2. [wrangler] refactor: import runInTempDir from workers-utils directly — pure mechanical cleanup: the local packages/wrangler/src/__tests__/helpers/run-in-tmp.ts wrapper only existed to call reinitialiseAuthTokens() between tests. With that function gone, every caller can import runInTempDir directly from @cloudflare/workers-utils/test-helpers. 163 test files updated by a one-shot script, then oxfmt-ed.

Test plan

  • 3 new regression tests in user.test.ts cover the issue scenario directly:
    • getAPIToken returns the env token even when a stored OAuth token also exists
    • loginOrRefreshIfRequired does not attempt OAuth refresh when env auth is set
    • End-to-end wrangler whoami succeeds via env token when a stale OAuth token also exists on disk (asserting no oauth2/token request is fired)
  • Existing sibling-rotation regression test ([wrangler] fix: re-read refresh_token from disk to avoid 401 from sibling-process rotation #13910) updated to match the new lazy-read model — still validates the same invariant.
  • Full wrangler unit test suite: 4059 passing, 4 skipped, 9 todo (same shape as on main, plus the 3 new tests).

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: this is a bug fix that restores documented behaviour — env-based auth already takes priority over OAuth state per getAuthFromEnv() documentation in user.ts.

Open in Devin Review

@changeset-bot

changeset-bot Bot commented May 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fec8a84

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
wrangler Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

✅ All changesets look good

@workers-devprod workers-devprod requested review from a team and penalosa and removed request for a team May 18, 2026 16:02
@workers-devprod

workers-devprod commented May 18, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/cloudchamber
  • @cloudflare/d1
  • @cloudflare/workers-kv
  • ✅ @cloudflare/wrangler
Show detailed file reviewers
  • packages/wrangler/src/tests/containers/config.test.ts: [@cloudflare/cloudchamber]
  • packages/wrangler/src/tests/containers/deploy.test.ts: [@cloudflare/cloudchamber]
  • packages/wrangler/src/tests/containers/images.test.ts: [@cloudflare/cloudchamber]
  • packages/wrangler/src/tests/d1/create.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/d1.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/delete.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/execute.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/export.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/info.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/insights.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/list.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/migrate.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/migrations/helpers.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/d1/timeTravel.test.ts: [@cloudflare/d1]
  • packages/wrangler/src/tests/kv/bulk.test.ts: [@cloudflare/workers-kv]
  • packages/wrangler/src/tests/kv/help.test.ts: [@cloudflare/workers-kv]
  • packages/wrangler/src/tests/kv/key.test.ts: [@cloudflare/workers-kv]
  • packages/wrangler/src/tests/kv/local.test.ts: [@cloudflare/workers-kv]
  • packages/wrangler/src/tests/kv/namespace.test.ts: [@cloudflare/workers-kv]

@ask-bonk

ask-bonk Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Pure function, no side effects - fine to call multiple times.

Now I've done a thorough review. Let me assess the overall quality:

Summary of what this PR does:

  1. Removes module-level OAuth state caching (localState) in favor of on-demand disk reads (readStoredAuthState())
  2. Makes isAccessTokenExpired() short-circuit when env-based auth is present
  3. Fixes a double-revoke bug in logout()
  4. Removes reinitialiseAuthTokens() entirely (no more module-level cache to invalidate)
  5. Mechanical cleanup: replaces local run-in-tmp.ts wrapper with direct import from @cloudflare/workers-utils/test-helpers

Analysis of correctness:

  • The core fix is sound: getAPIToken() now checks env first, isAccessTokenExpired() short-circuits on env auth
  • The disk-read-on-every-call approach trades a tiny bit of I/O for correctness (avoids stale state bugs)
  • The logout() refactor fixes a real double-revoke bug
  • The sibling-process refresh token rotation still works because exchangeRefreshTokenForAccessToken reads from disk
  • The getOAuthTokenFromLocalState function properly re-reads after refresh
  • Test coverage is good: 3 new regression tests directly target the issue scenario

I don't see any logic bugs, security issues, backward compatibility violations, or incorrect API behavior. The change is well-structured and the approach is correct.

LGTM

github run

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 6 additional findings.

Open in Devin Review

@pkg-pr-new

pkg-pr-new Bot commented May 18, 2026

Copy link
Copy Markdown
create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@13954

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@13954

miniflare

npm i https://pkg.pr.new/miniflare@13954

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@13954

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@13954

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@13954

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@13954

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@13954

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@13954

wrangler

npm i https://pkg.pr.new/wrangler@13954

commit: fec8a84

Comment thread packages/wrangler/src/user/user.ts
Comment thread packages/wrangler/src/user/user.ts Outdated

@dario-piotrowicz dario-piotrowicz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread packages/wrangler/src/user/user.ts Outdated
Comment thread packages/wrangler/src/user/user.ts

@dario-piotrowicz dario-piotrowicz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Codeowners Bypass

petebacondarwin and others added 5 commits May 19, 2026 10:34
…riority

Wrangler previously read its OAuth state from the user auth config file
eagerly at module-import time, before `.env` files have been loaded. The
in-memory state therefore always held the OAuth tokens even when the
user only wanted to authenticate via `CLOUDFLARE_API_TOKEN`. If that
stored OAuth token happened to be expired, Wrangler would try to refresh
it (and fail), aborting the command with 'Failed to fetch auth token:
400 Bad Request' / 'Not logged in.' — even though a valid API token was
in scope.

Read the auth config file on demand from every site that needs it.
`isAccessTokenExpired()` short-circuits to `false` when env-based
credentials are present, so the OAuth refresh endpoint is no longer
called when env auth is going to be used. Sibling-process refresh-token
rotation is also handled naturally because every check reads the
current file contents.

Drive-by cleanups:
- Drop the dead in-memory mutations in `exchangeRefreshTokenForAccessToken`
  and `exchangeAuthCodeForAccessToken` — they were redundant with the
  `writeAuthConfigFile` call that immediately follows.
- Drop the dead inner `!localState.accessToken && localState.refreshToken`
  branch in `logout()` plus its accidental double-revoke.
- Remove the now-redundant `reinitialiseAuthTokens()` call from the top
  of `refreshToken()` (introduced in #13910 for sibling rotation; now
  naturally subsumed).
- Drop the in-process selected-account cache in favour of the existing
  file-based `wrangler-account.json` cache, which is naturally per-cwd.
- Remove the exported `reinitialiseAuthTokens()` function — there is no
  module-level cache left to invalidate.

Fixes #13744.
The local `packages/wrangler/src/__tests__/helpers/run-in-tmp.ts` helper
only existed to wrap `runInTempDir` from `@cloudflare/workers-utils/test-helpers`
with a `beforeEach(reinitialiseAuthTokens)` hook. Now that auth state is
read from disk on demand and `reinitialiseAuthTokens` has been removed,
the wrapper has no purpose.

Delete the wrapper and update every caller to import `runInTempDir`
directly from `@cloudflare/workers-utils/test-helpers`.
Co-authored-by: Dario Piotrowicz <dario@cloudflare.com>
@petebacondarwin petebacondarwin force-pushed the fix-lazy-auth-state-read branch from 50ae152 to 4777aeb Compare May 19, 2026 09:34

@workers-devprod workers-devprod 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.

Codeowners reviews satisfied

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk May 19, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

… one on refresh

RFC 6749 §6 allows the authorization server to return a successful refresh
response without a new refresh_token, in which case the previously issued
refresh token remains valid. The previous code wiped the stored value in
that case, which caused the next refresh attempt to fail with 'No refresh
token is present' and forced the user to re-run wrangler login.

exchangeRefreshTokenForAccessToken now falls back to the storedRefreshToken
already read from disk when the response omits a new refresh_token. Adds a
regression test alongside the existing sibling-rotation test.
@petebacondarwin petebacondarwin force-pushed the fix-lazy-auth-state-read branch from 8633154 to fec8a84 Compare May 19, 2026 13:15
@petebacondarwin petebacondarwin merged commit 62abf97 into main May 19, 2026
55 checks passed
@petebacondarwin petebacondarwin deleted the fix-lazy-auth-state-read branch May 19, 2026 13:47
@github-project-automation github-project-automation Bot moved this from Approved to Done in workers-sdk May 19, 2026
petebacondarwin added a commit that referenced this pull request May 20, 2026
The local helpers/run-in-tmp.ts wrapper was removed in #13954. New test files
must import runInTempDir directly from @cloudflare/workers-utils/test-helpers.
petebacondarwin added a commit that referenced this pull request May 20, 2026
The local helpers/run-in-tmp.ts wrapper was removed in #13954. New test files
must import runInTempDir directly from @cloudflare/workers-utils/test-helpers.
petebacondarwin added a commit to bimsonz/workers-sdk that referenced this pull request May 20, 2026
The local helpers/run-in-tmp.ts wrapper was removed in cloudflare#13954. New test files
must import runInTempDir directly from @cloudflare/workers-utils/test-helpers.
petebacondarwin added a commit that referenced this pull request May 20, 2026
The local helpers/run-in-tmp.ts wrapper was removed in #13954. New test files
must import runInTempDir directly from @cloudflare/workers-utils/test-helpers.
penalosa pushed a commit that referenced this pull request May 28, 2026
…ty (#13954)

Co-authored-by: Dario Piotrowicz <dario@cloudflare.com>
emily-shen pushed a commit that referenced this pull request Jun 11, 2026
The local helpers/run-in-tmp.ts wrapper was removed in #13954. New test files
must import runInTempDir directly from @cloudflare/workers-utils/test-helpers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

No longer able to use API token for auth

5 participants