feat(auth)!: OAuth login as the default for mux login - #74
Open
daniel-hayes wants to merge 10 commits into
Open
Conversation
daniel-hayes
marked this pull request as ready for review
August 19, 2026 16:30
daniel-hayes
marked this pull request as draft
August 19, 2026 16:30
daniel-hayes
marked this pull request as ready for review
August 19, 2026 17:01
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6cc4e63. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
mux loginnow opens the browser by default to kick off the OAuth flow. You pick an organization and environment in the Mux Dashboard, the CLI catches the redirect on a loopback port, exchanges the code for tokens, and handles refresh from then on. Mux API access tokens keep working exactly as before.The four ways to authenticate are now explicit and mutually exclusive:
--oauth(default),--interactive,--env-file,--from-env.Breaking changes
Read these before approving. The first two are deliberate; the third is a consequence of the config format.
mux loginerrors whenMUX_TOKEN_ID/MUX_TOKEN_SECRETare set. It used to save them silently. Since env vars always outrank the config, that produced an entry that did nothing until you unset them. It now prints the four explicit options and exits 1 without writing. CI that runsmux loginwith injected credentials needs--from-env.mux loginwith no flags opens a browser instead of prompting for a Token ID and Secret. Anything scripted against those prompts needs--interactive.oauth/tokenblocks. Reading old flat entries works fine and needs no migration, but not the reverse — and note that any write converts every entry, including ones the command didn't touch. A downgrade means re-runningmux login. Nothing is lost: the credentials are still inconfig.json, just nested one level deeper. Worth saying explicitly in the release notes, since Mux only shows an access token secret once at creation and people may otherwise mint a replacement.--oauthand--interactiverequire a TTY. They fail immediately under--json, in agent mode, or with piped stdin rather than hanging.mux env listandmux auth statusoutput changed shape, so text parsing of them breaks. Both have--json.mux logoutnow makes a network call to revoke refresh tokens. Failure prints a warning and still removes the local credentials.mux login --jsonoutput and itssourcevalues are unchanged.What's new
mux auth status— every credential source, which is active, and why. No network calls, never prints token material.mux logout --all, and revocation on logout.mux env switchwith no argument gives an interactive picker;--jsononenv list,env switch, andlogout.muxprocesses coordinate through a lock file so a rotating refresh token is never spent twice.webhooks listenrefreshes and reconnects instead of dying mid-stream.mux auth statusexplains it, and a token pair on the same environment silently takes over.Read these six, in this order
src/lib/credentials.ts(236) — start here. The credential model: what an environment holds, how the two historical config layouts are normalized on read, andgetPreferredCredential, which decides OAuth-vs-token including the fallback when a credential is flagged as failing. Everything else assumes this.src/lib/mux.ts(+264) — the choke point.resolveCredentials()returns a discriminatedResolvedCredentials, which is whyBearervsBasicneeded no changes in ~98 command files. Also where the SDK client is built, and where the pre-existingMUX_AUTHORIZATION_TOKENfootgun is defused.src/lib/token-refresh.ts+src/lib/refresh-lock.ts(130 + 170) — the riskiest code in the PR. Proactive refresh, the re-read under lock that stops two processes spending one rotating refresh token, and flag-don't-delete on terminal failure. The lock deliberately fails rather than breaking a live holder's lock; that tradeoff is the thing to argue with.src/lib/oauth-loopback.ts(299) — the security surface. Binds 127.0.0.1 only, one path, one accepted callback, constant-timestatecompare, forced close. If you're only going to scrutinize one file for safety, this is it.src/lib/oauth.ts(503) — endpoint resolution (env override → discovery → built-in) the three grant calls, scope policy, and error normalization. The top ~90 lines are configuration and comments explaining why each default is what it is.src/commands/login-mode.ts(88) — pure function, no I/O, and it encodes the breaking behavior: four mutually exclusive methods, and the error when shell credentials are set. Quickest way to review the UX contract.Worth a reviewer's attention
The SDK already supported bearer auth.
@mux/mux-nodehas anauthorizationTokenoption, so no custom client was needed. It also revealed a pre-existing bug: that option defaults toprocess.env.MUX_AUTHORIZATION_TOKEN, and the SDK builds the bearer header after the Basic one, so a stray variable in someone's shell would silently override Basic auth. We now passnullexplicitly for whichever credential kind isn't in use.Endpoints derive from one base, and are discoverable. All three live on the API host (
/ui/v1/oauth/authorizefor the browser leg,/auth/v1/oauth/{token,revoke}for the back channel), soMUX_BASE_URLmoves the API calls, discovery, and the sign-in flow together — the token endpoint can't end up on a different host than the authorize endpoint. On top of that, RFC 8414 / OIDC discovery is consulted on login and refresh (cached a day) so Mux can move endpoints without stranding installed binaries. Discovery is never load-bearing: any failure falls back to the built-ins. Discovered endpoints are validated to behttps:on a.mux.comhost (dot-boundary matched, somux.com.evil.testfails) or the document's own origin, because a document that can repointtoken_endpointcould otherwise collect authorization codes.Scopes are hardcoded deliberately.
video/data/robots/systemread+write — the union of what the commands need. Not taken from the server'sscopes_supported, which would mean silently requesting any scope Mux adds later. Noopenid/profile/email: noid_tokenis requested or consumed, and identity comes from/system/v1/whoami, which reports what the access token can actually do. That also keeps JWKS out of the client.The 401 retry is one function, not 98 changes. It's the SDK's
fetchimplementation, so every command inherits refresh-and-retry.Refresh is lock-guarded. Acquisition uses
link()of a fully-written temp file — anopen('wx')lock is briefly empty, and a competitor reading that mistakes a live holder for a crashed one. A waiter never breaks a live holder's lock, and release is ownership-checked.Caveats
client_idis still a placeholder. It's the one value discovery can't supply, so it has to be right before this ships.removeCredential(name, kind)exists and is tested, butmux logoutremoves the whole environment.ssh -L 51372:127.0.0.1:51372plusmux login --port 51372, or--interactive.0600config file that already holds token secrets and signing keys.webhooks listenis only exercised against a local fake server — the function it calls is unit-tested, the SSE loop wiring isn't.Testing
1200 tests pass, up from 1014 on
main. Beyond unit coverage, the flows were driven against real staging and a local fake authorization server, and verified on the wire:Bearerfor OAuth vsBasicfor token pairs on the same commandBasicon the same environmentNo secrets in the diff: scanned the commits, working tree, and untracked files for
sk-ant-,eyJ, private key blocks,client_secret, and the staging client ID. The only high-entropy strings are the RFC 7636 Appendix B PKCE test vectors.Note
High Risk
Changes authentication defaults, config on-disk shape, credential precedence, and token refresh/revocation across all API commands—security-critical behavior with deliberate breaking changes for CI and 2.x configs.
Overview
Browser sign-in is now the default for
mux login, with explicit alternatives (--interactive,--env-file,--from-env,--oauth) that cannot be combined. IfMUX_TOKEN_IDandMUX_TOKEN_SECRETare already set, baremux loginrefuses to guess and exits without writing config (CI should use--from-env).Stored credentials move to nested
oauthandtokenblocks per environment; legacy flattokenId/tokenSecretentries are read on load. One environment can hold both kinds—OAuth is preferred for API calls, with fallback to the token pair when OAuth is flagged dead. Config writes are atomic, and re-login updates entries in place so default environment and signing keys are not accidentally dropped.New
mux authcommand group (auth status, pluslogin/logoutaliases) reports every credential source locally with no secrets in output.mux env list/switch/logoutgain richer human output,--json, interactiveenv switch,logout --all, and server-side refresh-token revocation on OAuth logout (best-effort).Runtime auth in
lib/muxresolves Bearer vs Basic from the preferred credential, refreshes OAuth proactively, and wires a 401 retry fetch into the SDK.webhooks listenrefreshes and reconnects on auth failure; asset signing can run with signing keys only when the active login has no token pair.Reviewed by Cursor Bugbot for commit b5cff85. Bugbot is set up for automated code reviews on this repo. Configure here.