[workers-auth] Extract OAuth 2.0 + PKCE flow into a new package#14121
Conversation
🦋 Changeset detectedLatest commit: 8a82838 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
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 |
|
Review posted successfully on PR #14121. Here's a summary of what I found: 3 actionable issues identified (all with suggestion comments posted):
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 |
create-cloudflare
@cloudflare/deploy-helpers
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-shared
@cloudflare/unenv-preset
@cloudflare/vite-plugin
@cloudflare/vitest-pool-workers
@cloudflare/workers-auth
@cloudflare/workers-editor-shared
@cloudflare/workers-utils
wrangler
@cloudflare/wrangler-bundler
commit: |
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.
|
Thanks @ask-bonk — all three addressed in 088aca0:
I also went ahead and registered an The |
There was a problem hiding this comment.
All four issues from the previous review are properly addressed in 088aca0:
-
token-exchange.ts:292-298—fetchAuthTokennow logs onlygrant_type=<value>instead of serializing the fullURLSearchParamsbody. Good comment explaining the rationale. -
access.ts:192-196—getCloudflareAccessHeaderslogs"Using WRANGLER_CF_AUTHORIZATION_TOKEN from environment"without the raw token value. -
callback-server.ts:145-158—exchangeAuthCodeForAccessTokenis wrapped intry/catchthat routes failures throughfinish(null, err), preventing unhandled rejections in the non-promise-awarehttp.createServercallback. -
callback-server.ts:177-189—server.once("error", ...)handler catchesEADDRINUSEand surfaces it as aUserErrorwithtelemetryMessage: "user oauth callback port in use", with a generic fallback for other listen errors. -
mock-http-server.ts:25-26—.onand.onceare stubbed asvi.fn().mockReturnThis()so wrangler's existing OAuth tests pass against the new error listener without needing realEventEmitterbehavior.
Clean work.
|
Reviewed and approved PR #14121. All three originally flagged issues plus the bonus
|
|
✅ All changesets look good |
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.
088aca0 to
3ad60ad
Compare
|
Codeowners approval required for this PR:
Show detailed file reviewers |
jamesopstad
left a comment
There was a problem hiding this comment.
I haven't studied every line but this looks generally good to me.
workers-devprod
left a comment
There was a problem hiding this comment.
Codeowners reviews satisfied
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.
b9165be to
39e03db
Compare
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.
Refactor: extract the OAuth 2.0 + PKCE flow out of wrangler into a new internal
@cloudflare/workers-authpackage.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-authpackage.Wrangler now wires the OAuth flow up via a small glue module (
packages/wrangler/src/user/oauth.ts) that injects itslogger, browser opener, interactivity detector, and config-cache purge via a dependency-injection context (OAuthFlowContext).What moved to
@cloudflare/workers-auth:domainUsesAccess,getAccessHeaders,getCloudflareAccessHeaders)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)getAuthConfigFilePath,readAuthConfigFile,writeAuthConfigFile,readStoredAuthState)login,logout,loginOrRefreshIfRequired,getOauthToken,getOAuthTokenFromLocalState,refreshToken,isRefreshNeeded@cloudflare/workers-auth/test-helpers)What stays in wrangler:
login,logout,whoami,auth token)getAuthFromEnv,getAPIToken,requireApiToken,ApiCredentials)requireAuth,getOrSelectAccountId,getActiveAccountId,getAccountFromCache)DefaultScopes,Scope,DefaultScopeKeys,validateScopeKeys,listScopes,getScopes,printScopes,setLoginScopeKeys) — passed into the OAuth flow as a genericstring[]whoami,fetchAllAccounts,fetchMembershipRolesCLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKEN,CLOUDFLARE_API_KEY,CLOUDFLARE_EMAIL,WRANGLER_R2_SQL_AUTH_TOKEN)Preserved invariants:
<globalWranglerConfigPath>/config/<env>.toml)telemetryMessagestrings preserved verbatimfrom "../user"import sites continue to work — the OAuth-flow symbols are re-exported via thin wrappers inuser.tsthat supply default scopes from the wrangler-side catalogpkce.ts,errors.ts,token-exchange.ts,callback-server.ts,flow.ts)Test surface:
access.test.tsmoved topackages/workers-auth/tests/and rewritten against the package's public API (9 tests passing)user.test.ts,logout.test.ts, andwhoami.test.tsstay in wrangler (they exerciserunWrangler("login")/runWrangler("logout")end-to-end) — all 82 of those continue to pass without modificationvitest.setup.tscontinues to mock../user/generate-auth-urland../user/generate-random-statefor 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 applyLocal results:
@cloudflare/workers-auth: 9/9 tests passing, build clean, type-check cleanpnpm prettifyclean, lint clean, package-dependency validation clean (only the pre-existing wrangler/viteissue remains)@cloudflare/workers-authis published withprerelease: trueand is not intended for external use — its APIs may change without notice.A picture of a cute animal (not mandatory, but encouraged)
🦦