Let a user whose remote endpoint (SPEC-044) sits behind Cloudflare Access authenticate with a browser SSO login instead of a pasted key: fill the endpoint URL, click Sign in, complete the company IdP flow in the browser — no credential to obtain, store, or rotate by hand. The auth layer stays organisation-agnostic: the only parameter is the URL the user typed; the code never knows (or names) which IdP sits behind it.
SPEC-044 shipped .none / .bearer / .header(name:) — enough for hosted APIs and simple gateways, but structurally unable to reach an Access-protected endpoint: Access accepts only a browser-SSO JWT or an admin-issued service token (two headers), neither of which a user can express as a single pasted key. For teams whose STT gateway lives behind Access, SSO login is therefore the only practical path, not a convenience.
RemoteAuth was designed as a closed enum precisely so this lands as an added case with the transport untouched.
public enum RemoteAuth { // existing cases unchanged
case cloudflareAccess // ← new; app URL = the endpoint's origin
}
The browser round-trip is delegated to the cloudflared CLI (user-installed, e.g. brew install cloudflared) — no OAuth implementation of our own:
cloudflared access login <origin> # opens the browser, blocks until the IdP flow completes
cloudflared access token --app <origin> # prints the freshly minted JWT
CloudflareAccessClient (OpenQuackKit/Auth) drives both via Process with an argument array (no shell interpolation), a timeout, and shape validation on the result (three dot-separated base64url segments). The JWT then goes into the existing CredentialStore as the endpoint host’s secret — SPEC-044’s host-keying and “edited URL can’t carry the old credential” invariant are inherited, not re-implemented.
At request time RemoteEngine.attachAuth adds one branch: read the host’s secret, decode its exp, and attach cf-access-token: <jwt> — or refuse.
cloudflared’s own ~/.cloudflared cache is its business and is noted in the UI copy).The SPEC-044 Authentication picker gains Company SSO (Cloudflare Access). Selecting it replaces the API-key field with a status row driven by a four-state machine:
brew install cloudflared), sign-in disabled; detection (which cloudflared + common install paths) runs when the pane appears, so the user is told before clicking, not failed after.Sign in with your browser… button; runs login → token → Keychain, with a progress state while the browser flow is pending.✓ Signed in · expires <relative time> + Sign out.Session expired + Sign in again.The Access “app URL” is not a separate field: it defaults to the endpoint URL’s origin, which is what Access binds to in practice. (An advanced override can be added later if a real deployment needs it.)
Strictly narrower than a pasted key, and the SPEC-044 fences all still apply (opt-in, user-typed destination, overlay indicator, Keychain-only, host-keyed, redirects refused):
cloudflared never see the password or MFA.cloudflared binary itself (user-installed via their own package manager, never bundled) and its on-disk token cache in ~/.cloudflared (outside our control; our copy lives in the Keychain).RemoteAuth.cloudflareAccess attaches cf-access-token with a valid stored JWT; existing .none/.bearer/.header behaviour unchanged (unit tests).exp parsing handles base64url padding variants and rejects malformed tokens (unit test).cloudflared absent → Settings shows the install hint and sign-in is disabled; present → sign-in runs login + token and the row flips to signed-in with an expiry (manual).CF-Access-Client-Id/-Secret pair) — headless/CLI concern; needs a two-header auth case and admin-issued credentials. Follow-up when CLI remote support lands.ASWebAuthenticationSession flow (removing the cloudflared dependency) — requires reimplementing Access’s login URL construction and callback server; revisit if the CLI dependency proves to be real friction.