cloud login: add browser-based OAuth login behind --oauth - #6226
Draft
dgzlopes wants to merge 5 commits into
Draft
cloud login: add browser-based OAuth login behind --oauth#6226dgzlopes wants to merge 5 commits into
dgzlopes wants to merge 5 commits into
Conversation
`k6 cloud login --oauth` authenticates in the browser instead of asking the user to paste a token. It runs the OAuth PKCE flow served by the Grafana Assistant app on each stack, as gcx does: k6 opens the stack's CLI auth page, receives the authorization code on a short-lived callback server on localhost, and exchanges it for a Grafana access token. That access token is used once, to read the user's k6 API token through the k6 app plugin's resource API, and is then discarded. Only the k6 API token is persisted, so login needs no token refresh or revocation machinery, and the credential k6 stores is the same one `--token` takes by hand. The token is read rather than regenerated, so logging in on one machine does not invalidate a token already in use on another. --stack is required, since the stack serves the login page; the user is prompted for it when the flag is omitted. The flow depends on the Grafana Assistant app being installed on the stack, and on the user holding its tokens.gcx:access permission.
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 08:22 — with
GitHub Actions
Inactive
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 08:24 — with
GitHub Actions
Inactive
Self-review fixes on the browser login: - Fail the login when the CSRF state cannot be generated, instead of falling back to an empty string. An empty expected state compared equal to an absent state parameter, so the check passed and the protection was silently disabled; the comment claimed the opposite. The state and PKCE secrets now live in a per-login session value generated once, up front, and the handler rejects an empty expected state outright. - Refuse redirects to local addresses during the token exchange. The endpoint check exempts them so the flow can be pointed at a development server, but a redirect target is entirely server-chosen, so exempting it there would let a redirect hand the bearer token to any process listening on the machine. - Reject --token combined with --oauth rather than silently ignoring the token. - Report a login that timed out as such, rather than as "context deadline exceeded". - Compare the stack the browser reports against the requested one after normalising both, so a trailing slash or different casing no longer looks like a different stack. - Drop the untested fallback that re-tried the token read against the stack itself. It never ran, and removing it means the access token goes to exactly one host. - Size the error channel for both of its senders, so the serving goroutine cannot block forever on a full channel. Tests cover the happy path, state mismatch and absent state, the vacuous empty-state case, untrusted endpoints in the callback and in the exchange response, denied logins, control-character stripping, callback replay, cancellation, port release, and the token read.
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 08:42 — with
GitHub Actions
Inactive
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 08:44 — with
GitHub Actions
Inactive
CI lint caught 17 issues the local golangci-lint could not report, as the Homebrew build crashes on the Go 1.26 toolchain. Installing the version pinned in .golangci.yml reproduces CI exactly; the package now lints clean. Substantive fixes: - Replace os.Hostname as the device label. The os package is forbidden outside k6's own wrappers, and there is no wrapper for this, so the caller now supplies a DeviceName and cloud login passes k6/<version>. The token is still identifiable in the user's device list, though less precisely than a hostname would be. - Take a context in OpenInBrowser and use exec.CommandContext, per noctx. The opener hands the URL over and exits immediately, so the browser still outlives it. - Strip cancellation from the shutdown context with context.WithoutCancel rather than starting from Background. The login's context is usually already cancelled by then, but its values should still apply. This also removes the nolint that contextcheck reported as unused, because the directive was on the function rather than the call site it fired on. - Drop os.Stderr as the default progress writer; messages are discarded when no writer is set. Every caller in k6 passes one. - Tag Result.AccessToken as json:"-" so the access token cannot be serialized by accident (gosec G117). - Parse the callback page template per call instead of holding it in a package-level variable (gochecknoglobals), keeping html/template so the server-supplied error stays contextually escaped. - Explain the two G704 SSRF findings and the three G204 subprocess findings rather than suppressing them silently: each URL is allowlisted before use, and every redirect target is re-checked. - Check the progress-message write errors, dropping them deliberately. Test fixes: - Use assert instead of require on the fake server's goroutines. FailNow may only be called from the test goroutine, so a failing require there was undefined behaviour rather than a clean failure. - Use t.Context() instead of context.Background() (usetesting).
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 08:56 — with
GitHub Actions
Inactive
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 08:58 — with
GitHub Actions
Inactive
The two failures a user is most likely to hit — the Grafana Assistant app not being installed on the stack, and the user lacking permission to mint CLI tokens — were surfacing as a bare HTTP status. Reading the k6 API token now maps 401 and 403 to the gcx User role, and 404 to the app not being installed. The status code alone cannot tell these apart with certainty, so the server's own message is kept alongside the hint rather than replaced by it. An account with no k6 token now says so, and points at opening the k6 app once to have one created. The timeout carries the same hint. When the login page itself fails to load, nothing reaches k6 at all, so the browser shows the error and k6 just waits — making the timeout the only place those causes can be mentioned.
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 12:54 — with
GitHub Actions
Inactive
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 12:55 — with
GitHub Actions
Inactive
The local-address exemption in URL validation exists so the flow can be pointed at a development server, but it was also being applied to the endpoint and instanceEndpoint values that arrive from the browser. A page that induced a login could therefore name a local address, and k6 would POST the authorization code and the PKCE verifier to whatever was listening on it — handing over exactly the secret that makes an intercepted code useless. The exemption now depends on the flow's own target: a login against a local stack may be sent to local endpoints, a login against Grafana Cloud may not. The exchange response's api_endpoint is held to the same rule. Reverting the check makes the new test fail with "token exchange failed with status 404", which is the vulnerability rather than a near miss: the request was sent, and a process on the developer's machine answered it. Also clip the server-supplied detail in a failed token read to 256 characters. It was interpolated into an error at up to the 1 MiB read limit, letting a server flood the terminal.
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 13:04 — with
GitHub Actions
Inactive
dgzlopes
temporarily deployed
to
azure-trusted-signing
July 30, 2026 13:06 — with
GitHub Actions
Inactive
joanlopez
self-requested a review
July 30, 2026 14:13
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.
What?
k6 cloud login --oauthauthenticates in the browser instead of asking the user to paste a token.It runs the OAuth PKCE flow served by the Grafana Assistant app on each stack, as gcx does:
Screen.Recording.2026-07-30.at.10.38.27.mov
Why?
It is a much better experience than having to find tokens, copy them, etc.
Problems
Once the user lands in assistant, the copy mentions gcx.
We could make a small change in their app to be able to change that text on demand.
Checklist
make check) and all pass.Checklist: Documentation (only for k6 maintainers and if relevant)
Please do not merge this PR until the following items are filled out.
Related PR(s)/Issue(s)