9.1 KiB
AGENTS.md — workers-auth
OAuth-2.0-with-PKCE flow against Cloudflare's dash.cloudflare.com (or staging /
custom-overridden) endpoints. Used by wrangler and (in future) other Cloudflare
CLIs. Internal-only — published as prerelease: true.
STRUCTURE
src/pkce.ts— PKCE code-verifier / code-challenge generation (RFC 7636)src/errors.ts—ErrorOAuth2class hierarchy +toErrorClassmappersrc/generate-auth-url.ts— authorize URL buildersrc/generate-random-state.ts— CSRF state generatorsrc/env-vars.ts—WRANGLER_*andCLOUDFLARE_AUTH_*env-var getterssrc/access.ts— Cloudflare Access detection + service-token /cloudflaredheaderssrc/config-file/auth.ts— theAuthConfigStorage/UserAuthConfigstorage contract (interfaces only). The default plaintext-TOML implementation now lives alongside the credential-store layer atsrc/credential-store/file-store.ts(see "Credential storage" below). Wrangler'ssrc/user/auth-config-file.tsexposes a genericcreateTomlFileStorage<T>helper for non-credential TOML stores (the temporary-preview-account storage) and re-exportsgetAuthConfigFilePathfor back-compat.src/config-file/temporary.ts—TemporaryAccountStorage/TemporaryPreviewAccountstorage contract for the temporary-preview-account flowsrc/config-file/index.ts— genericConfigStorage<T>interface shared by the auth and temporary-account contractssrc/state.ts—readStoredAuthState()+StoredAuthStateshapesrc/token-exchange.ts— auth-code → token + refresh-token rotation +fetchAuthTokensrc/callback-server.ts— local HTTP server for the OAuth callback (listens on the host/port from the consumer'sredirectUri)src/flow.ts—createOAuthFlow(ctx)factory wiring everything togethersrc/context.ts—OAuthFlowContextinterface (DI surface)src/credential-store/— opt-in OS-keyring-backed credential persistence (see below)src/test-helpers/— MSW handlers for consumers' tests (@cloudflare/workers-auth/test-helpers)
Credential storage (src/credential-store/)
Pluggable credential persistence layer that consumers can wire into the
OAuth flow via ctx.storageFactory. Default backend is the plaintext
TOML file (FileCredentialStore); an opt-in EncryptedFileCredentialStore
writes AES-256-GCM-encrypted credentials to a sibling .enc file using a
key held in the OS keyring. Every store is profile-aware: the file path,
the encrypted .enc path, and the keyring account name are all derived from
resolveAuthProfileBaseName(profile) (default profile → environment-based
name; named profile → the profile name), so each auth profile gets its own
files and its own encryption key.
interface.ts—CredentialStoreinterface (extendsAuthConfigStoragewithkindanddescribe())file-store.ts—FileCredentialStore(plaintext TOML, default)encrypted-file-store.ts—EncryptedFileCredentialStore+ plaintext-TOML migrationcrypto.ts— AES-256-GCMencryptString/decryptStringhelpersresolver.ts—createCredentialStorageContext({...})factory; returns{ storageFactory, getActiveStore }(both take an optionalprofile) for the consumer to plug intocreateOAuthFlow(asctx.storageFactory) andwhoami-style reporting respectivelystate.ts— module-level per-session resolver flags (one-time warnings, the Windows install-failed latch)key-providers/— per-platform OS-keyring backends that store only the 32-byte encryption key (never the credential blob itself, so the macOS Keychain 2.5 KB item limit is never a concern):interface.ts—KeyProviderinterfacemac-security.ts—/usr/bin/securityshell-outlinux-secret-tool.ts—secret-toolshell-out (probeslibsecret-tools)napi-keyring.ts—@napi-rs/keyringwincred binding on Windowslazy-installer.ts— Windows-onlynpm install @napi-rs/keyringon first opt-infactory.ts—resolveKeyProvider(serviceName)picks the right per-platform implementationshared.ts— account-name derivation + keyring JSON envelope encoding
DI SURFACE
createOAuthFlow(ctx) accepts a context object:
logger— drop-in replacement for wrangler's logger singletonisNonInteractiveOrCI()— whether to suppress interactive promptsopenInBrowser(url)— opens the browser to the OAuth authorize URLhasEnvCredentials()— short-circuits refresh logic when env-based auth is setclientId(required) — the consumer's registered OAuth app ID;stringor() => stringfor lazy (e.g. env-driven prod/staging) resolutionconsent(required) — the consumer's branded granted/denied consent pagesredirectUri(required) — the registered redirect URI / local callback URL. The callback server's listen host/port and route path are all derived from it (per-call bind overrides viaLoginProps.callbackHost/callbackPort)storageFactory(required) — maps an auth profile to the consumer'sAuthConfigStoragetoken-persistence backend. The flow calls it with the active profile on every credential access. For keyring opt-in, pass thestorageFactoryfromcreateCredentialStorageContext(...)rather than a raw file store, so both the active profile and the encrypted-file / plaintext choice are re-resolved on every credential operation.purgeOnLoginOrLogout?()— invalidate consumer-side caches after login/logoutgenerateAuthUrl?/generateRandomState?— test overrides for deterministic snapshot tests (defaults pull from./generate-auth-url/./generate-random-state)
clientId, consent, redirectUri, and storageFactory are consumer-specific
(Wrangler's live in packages/wrangler/src/user/), so they are required rather
than defaulted here.
Wrangler wires the credential-storage layer once in
packages/wrangler/src/user/user.ts via createCredentialStorageContext(...)
and exposes the resulting getActiveStore (called with the active profile) as
getCredentialStore() for whoami-style code that wants to surface the active
storage location.
CONVENTIONS
- License: dual MIT/Apache-2.0. Files derived from BitySA/oauth2-auth-code-pkce carry the Apache-2.0 header.
- No
console.*— use the injectedctx.logger. - No global
fetch— use undici'sfetch. UserErrorinstances must carry stabletelemetryMessagelabels (<area> <sub-area> <failure>, e.g.user oauth invalid scope). These labels are part of the telemetry contract — preserve them verbatim.- No direct Cloudflare REST API calls. This package talks to OAuth endpoints
(
/oauth2/auth,/oauth2/token,/oauth2/revoke) only. - OAuth callback server listens on the host/port derived from the consumer's
required
ctx.redirectUri; override the bind address per-call viaLoginProps.callbackHost/callbackPort.
BUILD
- tsup: two entry points —
src/index.tsandsrc/test-helpers/index.ts - ESM-only output to
dist/ @cloudflare/*,undici,msw, andvitestare kept external
CREDENTIAL STORAGE NOTES
- The encrypted file uses
AES-256-GCMvianode:crypto(no third-party crypto deps). The 12-byte IV is generated fresh per write; the 16-byte GCM auth tag is verified on every read. - The keyring entry holds only a 32-byte AES key wrapped in a small JSON
envelope (
{v, key, created}). It's well under the macOS Keychain ~2.5 KB per-item limit no matter how the credential schema grows. @napi-rs/keyring(the Windows backend's native binding) is installed lazily on first opt-in vianpm installinto<configPath>/native/keyring/, whereconfigPathis the consumer-provided global config directory (seegetConfigPathbelow) — so each CLI's binding lives under its own config dir. Pinned toPINNED_KEYRING_VERSIONso CI users runningnpm install -g @napi-rs/keyringby hand get the same version as the lazy-install path.- The credential files (
.toml/.enc) and the keyring install dir are all rooted at the consumer's config directory.@cloudflare/workers-authnever resolves that path itself (wrangler and a futurecfCLI use different global config paths); instead the consumer passesgetConfigPath: () => stringintocreateCredentialStorageContext, and the path helpers (getAuthConfigFilePath/getEncryptedAuthConfigFilePath/getKeyringInstallDir) and store constructors all take it explicitly. - The consumer's
createCredentialStorageContextcall capturesserviceName,getConfigPath,isKeyringEnabled,logger,isNonInteractiveOrCI, andcliNamein a closure. The returnedstorageFactory(profile)re-resolves the active store on every call so the active profile,--use-keyring/--no-use-keyring, and theCLOUDFLARE_AUTH_USE_KEYRINGenv var all take effect without rebuilding the OAuth flow. Per-session memoization flags (hasWarnedAboutKeyringFallback,installFailedThisSession, ...) still live at module scope instate.ts; tests useresetCredentialStorageStateto clear them between cases andsetKeyProviderFactoryForTestingto swap in stubs.