Skip to content

[workers-auth] Extract OAuth 2.0 + PKCE flow into a new package#14121

Merged
petebacondarwin merged 8 commits into
mainfrom
extract-workers-auth-oauth-flow
Jun 2, 2026
Merged

[workers-auth] Extract OAuth 2.0 + PKCE flow into a new package#14121
petebacondarwin merged 8 commits into
mainfrom
extract-workers-auth-oauth-flow

Conversation

@petebacondarwin

Copy link
Copy Markdown
Contributor

Refactor: extract the OAuth 2.0 + PKCE flow out of wrangler into a new internal @cloudflare/workers-auth package.

Move the OAuth login / logout / refresh logic, the auth-config TOML file IO, the OAuth token exchange + local callback server, and the Cloudflare Access detection helpers out of packages/wrangler/src/user/ and into a new internal @cloudflare/workers-auth package.

Wrangler now wires the OAuth flow up via a small glue module (packages/wrangler/src/user/oauth.ts) that injects its logger, browser opener, interactivity detector, and config-cache purge via a dependency-injection context (OAuthFlowContext).

What moved to @cloudflare/workers-auth:

  • OAuth-2.0-with-PKCE protocol code (PKCE generation, OAuth error hierarchy, token exchange, local callback server, authorize URL builder, random state generator)
  • Cloudflare Access detection (domainUsesAccess, getAccessHeaders, getCloudflareAccessHeaders)
  • OAuth-only env-var getters (WRANGLER_CLIENT_ID, WRANGLER_AUTH_DOMAIN, WRANGLER_AUTH_URL, WRANGLER_TOKEN_URL, WRANGLER_REVOKE_URL, WRANGLER_CF_AUTHORIZATION_TOKEN, CLOUDFLARE_ACCESS_CLIENT_ID, CLOUDFLARE_ACCESS_CLIENT_SECRET)
  • Auth-state TOML IO (getAuthConfigFilePath, readAuthConfigFile, writeAuthConfigFile, readStoredAuthState)
  • login, logout, loginOrRefreshIfRequired, getOauthToken, getOAuthTokenFromLocalState, refreshToken, isRefreshNeeded
  • MSW test handlers for OAuth + Access (exported via @cloudflare/workers-auth/test-helpers)

What stays in wrangler:

  • All yargs commands (login, logout, whoami, auth token)
  • Environment-based credential resolution (getAuthFromEnv, getAPIToken, requireApiToken, ApiCredentials)
  • Cloudflare account selection (requireAuth, getOrSelectAccountId, getActiveAccountId, getAccountFromCache)
  • The OAuth scope catalog (DefaultScopes, Scope, DefaultScopeKeys, validateScopeKeys, listScopes, getScopes, printScopes, setLoginScopeKeys) — passed into the OAuth flow as a generic string[]
  • whoami, fetchAllAccounts, fetchMembershipRoles
  • Non-OAuth env vars (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL, WRANGLER_R2_SQL_AUTH_TOKEN)

Preserved invariants:

  • On-disk TOML format and location unchanged (<globalWranglerConfigPath>/config/<env>.toml)
  • All telemetryMessage strings preserved verbatim
  • All ~107 existing from "../user" import sites continue to work — the OAuth-flow symbols are re-exported via thin wrappers in user.ts that supply default scopes from the wrangler-side catalog
  • Apache-2.0 attribution preserved on the BitySA-derived files (pkce.ts, errors.ts, token-exchange.ts, callback-server.ts, flow.ts)

Test surface:

  • access.test.ts moved to packages/workers-auth/tests/ and rewritten against the package's public API (9 tests passing)
  • The integration-style user.test.ts, logout.test.ts, and whoami.test.ts stay in wrangler (they exercise runWrangler("login") / runWrangler("logout") end-to-end) — all 82 of those continue to pass without modification
  • Wrangler's vitest.setup.ts continues to mock ../user/generate-auth-url and ../user/generate-random-state for stable snapshot URLs; the wrangler-side files are now re-export shims, and the wrangler glue passes the mocked symbols into the OAuth flow's DI context, so existing test seams continue to apply

Local results:

  • @cloudflare/workers-auth: 9/9 tests passing, build clean, type-check clean
  • Wrangler: 4262/4262 tests passing (matches main: same 4 skipped, 9 todo), build clean, type-check clean
  • pnpm prettify clean, lint clean, package-dependency validation clean (only the pre-existing wrangler/vite issue remains)

@cloudflare/workers-auth is published with prerelease: true and is not intended for external use — its APIs may change without notice.


  • 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: internal refactor with no user-facing API or behavior change

A picture of a cute animal (not mandatory, but encouraged)

🦦

@changeset-bot

changeset-bot Bot commented May 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8a82838

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

This PR includes changesets to release 5 packages
Name Type
wrangler Patch
@cloudflare/workers-auth Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
@cloudflare/wrangler-bundler 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

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully on PR #14121. Here's a summary of what I found:

3 actionable issues identified (all with suggestion comments posted):

  1. Sensitive credentials in debug logs (Security) — token-exchange.ts:292 logs the full URLSearchParams body including refresh_token/code_verifier/code, and access.ts:194 logs the raw CF_Authorization token value. Suggested redacting both.

  2. Unhandled async exception in HTTP callback server (Bug) — callback-server.ts:138 awaits exchangeAuthCodeForAccessToken inside an http.createServer callback that isn't promise-aware. If the exchange throws, it becomes an unhandled rejection that crashes the process. Suggested wrapping in try/catch.

  3. No EADDRINUSE handler (noted in summary) — callback-server.ts:164 calls server.listen() without an error handler, so if port 8976 is in use, the process crashes with an unhelpful stack trace.

The overall extraction is clean — backward compatibility is fully preserved, the DI context is well-designed, and all existing test seams continue to work. Issues #1 and #2 are the ones I'd recommend addressing before merge; #1 and the access.ts log are pre-existing from wrangler but this extraction is a good opportunity to fix them.

github run

@pkg-pr-new

pkg-pr-new Bot commented May 30, 2026

Copy link
Copy Markdown
create-cloudflare

npm i https://pkg.pr.new/cloudflare/workers-sdk/create-cloudflare@14121

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/deploy-helpers@14121

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/kv-asset-handler@14121

miniflare

npm i https://pkg.pr.new/cloudflare/workers-sdk/miniflare@14121

@cloudflare/pages-shared

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/pages-shared@14121

@cloudflare/unenv-preset

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/unenv-preset@14121

@cloudflare/vite-plugin

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/vite-plugin@14121

@cloudflare/vitest-pool-workers

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

@cloudflare/workers-auth

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-auth@14121

@cloudflare/workers-editor-shared

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

@cloudflare/workers-utils

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

wrangler

npm i https://pkg.pr.new/cloudflare/workers-sdk/wrangler@14121

@cloudflare/wrangler-bundler

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/wrangler-bundler@14121

commit: 8a82838

petebacondarwin added a commit that referenced this pull request May 30, 2026
Three fixes from the PR #14121 review:

1. Security: stop logging the full URLSearchParams body in fetchAuthToken
   (token-exchange.ts). The body contains the refresh_token, auth code, and
   code_verifier — if debug logging is enabled and logs are persisted, this
   leaks credentials. Now only the grant_type is logged.

2. Security: stop logging the raw CF_Authorization token value when reading
   WRANGLER_CF_AUTHORIZATION_TOKEN from the environment (access.ts). Same
   credential-leak concern.

3. Bug: wrap the await exchangeAuthCodeForAccessToken call in the OAuth
   callback server in a try/catch (callback-server.ts). The await is inside
   an http.createServer callback that is not promise-aware, so a rejection
   would become an unhandled promise rejection and crash the process in
   Node >= 15. The error now routes through finish(null, err) so the caller
   promise rejects cleanly.

Additionally, register an 'error' listener on server.listen so EADDRINUSE
(port 8976 already in use) surfaces as an actionable UserError instead of an
unhandled 'error' event crashing the process.

The mock-http-server test helper grows .on/.once no-op stubs to keep wrangler's
existing OAuth login tests passing against the new error listener.
@petebacondarwin

Copy link
Copy Markdown
Contributor Author

Thanks @ask-bonk — all three addressed in 088aca0:

  1. token-exchange.tsfetchAuthToken no longer serializes the full URLSearchParams body to debug logs. Only grant_type is logged now.

  2. access.tsgetCloudflareAccessHeaders no longer logs the raw CF_Authorization token value when reading WRANGLER_CF_AUTHORIZATION_TOKEN.

  3. callback-server.ts — the await exchangeAuthCodeForAccessToken is now wrapped in try/catch that routes failures through finish(null, err) so the caller promise rejects cleanly instead of producing an unhandled rejection.

I also went ahead and registered an error listener on server.listen so EADDRINUSE surfaces as a UserError with telemetry message user oauth callback port in use, rather than crashing with an unhandled error event.

The mock-http-server test helper grew .on/.once no-op stubs so wrangler's existing OAuth login tests pass against the new error listener — all 4262 wrangler tests + 9 workers-auth tests still pass locally.

@ask-bonk ask-bonk 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.

All four issues from the previous review are properly addressed in 088aca0:

  1. token-exchange.ts:292-298fetchAuthToken now logs only grant_type=<value> instead of serializing the full URLSearchParams body. Good comment explaining the rationale.

  2. access.ts:192-196getCloudflareAccessHeaders logs "Using WRANGLER_CF_AUTHORIZATION_TOKEN from environment" without the raw token value.

  3. callback-server.ts:145-158exchangeAuthCodeForAccessToken is wrapped in try/catch that routes failures through finish(null, err), preventing unhandled rejections in the non-promise-aware http.createServer callback.

  4. callback-server.ts:177-189server.once("error", ...) handler catches EADDRINUSE and surfaces it as a UserError with telemetryMessage: "user oauth callback port in use", with a generic fallback for other listen errors.

  5. mock-http-server.ts:25-26.on and .once are stubbed as vi.fn().mockReturnThis() so wrangler's existing OAuth tests pass against the new error listener without needing real EventEmitter behavior.

Clean work.

@ask-bonk

ask-bonk Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Reviewed and approved PR #14121. All three originally flagged issues plus the bonus EADDRINUSE handler are correctly implemented in commit 088aca016:

  • token-exchange.ts — only grant_type is logged, no credential leakage
  • access.ts — raw CF_Authorization token value removed from debug log
  • callback-server.tstry/catch around exchangeAuthCodeForAccessToken prevents unhandled rejections; server.once("error", ...) handles EADDRINUSE as a UserError
  • mock-http-server.ts.on/.once stubs added so existing wrangler OAuth tests work with the new error listener

github run

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

✅ All changesets look good

petebacondarwin added a commit that referenced this pull request Jun 1, 2026
Three fixes from the PR #14121 review:

1. Security: stop logging the full URLSearchParams body in fetchAuthToken
   (token-exchange.ts). The body contains the refresh_token, auth code, and
   code_verifier — if debug logging is enabled and logs are persisted, this
   leaks credentials. Now only the grant_type is logged.

2. Security: stop logging the raw CF_Authorization token value when reading
   WRANGLER_CF_AUTHORIZATION_TOKEN from the environment (access.ts). Same
   credential-leak concern.

3. Bug: wrap the await exchangeAuthCodeForAccessToken call in the OAuth
   callback server in a try/catch (callback-server.ts). The await is inside
   an http.createServer callback that is not promise-aware, so a rejection
   would become an unhandled promise rejection and crash the process in
   Node >= 15. The error now routes through finish(null, err) so the caller
   promise rejects cleanly.

Additionally, register an 'error' listener on server.listen so EADDRINUSE
(port 8976 already in use) surfaces as an actionable UserError instead of an
unhandled 'error' event crashing the process.

The mock-http-server test helper grows .on/.once no-op stubs to keep wrangler's
existing OAuth login tests passing against the new error listener.
@petebacondarwin petebacondarwin force-pushed the extract-workers-auth-oauth-flow branch from 088aca0 to 3ad60ad Compare June 1, 2026 08:38
@petebacondarwin petebacondarwin marked this pull request as ready for review June 1, 2026 10:37
@workers-devprod workers-devprod requested review from a team and jamesopstad and removed request for a team June 1, 2026 10:38
@workers-devprod

workers-devprod commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

devin-ai-integration[bot]

This comment was marked as resolved.

@jamesopstad jamesopstad 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.

I haven't studied every line but this looks generally good to me.

Comment thread packages/workers-auth/scripts/deps.ts
Comment thread packages/workers-auth/src/auth-config-file.ts Outdated
Comment thread packages/workers-auth/tsup.config.ts

@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 Jun 2, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Move the OAuth login / logout / refresh logic, the auth-config TOML file IO,
the OAuth token exchange + local callback server, and the Cloudflare Access
detection helpers out of packages/wrangler/src/user/ and into a new internal
@cloudflare/workers-auth package.

Wrangler now wires the OAuth flow up via a small glue module
(packages/wrangler/src/user/oauth.ts) that injects its logger, browser opener,
interactivity detector, and config cache via a dependency-injection context.

What stays in wrangler:
- The yargs login / logout / whoami / auth token commands
- Environment-based credential resolution (CLOUDFLARE_API_TOKEN etc.)
- Cloudflare account selection (requireAuth, getOrSelectAccountId)
- The OAuth scope catalog (passed into the OAuth flow as a generic string[])
- whoami / account fetching

No behavior change for end users. The on-disk TOML format and location remain
identical, and all telemetry message labels are preserved verbatim.

@cloudflare/workers-auth is published with prerelease: true and is not intended
for external use — its APIs may change without notice.
Three fixes from the PR #14121 review:

1. Security: stop logging the full URLSearchParams body in fetchAuthToken
   (token-exchange.ts). The body contains the refresh_token, auth code, and
   code_verifier — if debug logging is enabled and logs are persisted, this
   leaks credentials. Now only the grant_type is logged.

2. Security: stop logging the raw CF_Authorization token value when reading
   WRANGLER_CF_AUTHORIZATION_TOKEN from the environment (access.ts). Same
   credential-leak concern.

3. Bug: wrap the await exchangeAuthCodeForAccessToken call in the OAuth
   callback server in a try/catch (callback-server.ts). The await is inside
   an http.createServer callback that is not promise-aware, so a rejection
   would become an unhandled promise rejection and crash the process in
   Node >= 15. The error now routes through finish(null, err) so the caller
   promise rejects cleanly.

Additionally, register an 'error' listener on server.listen so EADDRINUSE
(port 8976 already in use) surfaces as an actionable UserError instead of an
unhandled 'error' event crashing the process.

The mock-http-server test helper grows .on/.once no-op stubs to keep wrangler's
existing OAuth login tests passing against the new error listener.
…s helpers

The `options` object on `getAccessHeaders` and `getCloudflareAccessHeaders`
is now required (and both fields within it). The `logger` argument on
`domainUsesAccess` is also required.

This removes optional-chaining noise inside `access.ts` and forces every
call site to thread its CI predicate through, which previously silently
defaulted to `() => false` — easy to miss when the helpers were used
from token-exchange paths during refresh.

To support that, `isNonInteractiveOrCI` is now threaded through
`fetchAuthToken`, `exchangeAuthCodeForAccessToken`, and
`exchangeRefreshTokenForAccessToken`, so the indirect `getCloudflareAccessHeaders`
call inside `fetchAuthToken` can satisfy the new contract.

Tests in `tests/access.test.ts` updated to pass the full options shape.
@petebacondarwin petebacondarwin force-pushed the extract-workers-auth-oauth-flow branch from b9165be to 39e03db Compare June 2, 2026 19:36
@petebacondarwin petebacondarwin merged commit 7539a9b into main Jun 2, 2026
62 checks passed
@petebacondarwin petebacondarwin deleted the extract-workers-auth-oauth-flow branch June 2, 2026 20:38
@github-project-automation github-project-automation Bot moved this from Approved to Done in workers-sdk Jun 2, 2026
petebacondarwin added a commit that referenced this pull request Jun 3, 2026
The OAuth callback fix now lives in @cloudflare/workers-auth (since
PR #14121 extracted the OAuth flow out of wrangler). Add the package
to the changeset so its CHANGELOG reflects the fix too.
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.

3 participants