Skip to content

feat: Get tokens for Grafana Assistant via OAuth#1118

Draft
rafa-munoz wants to merge 1 commit intomainfrom
grafana-assistant-token-retrieval
Draft

feat: Get tokens for Grafana Assistant via OAuth#1118
rafa-munoz wants to merge 1 commit intomainfrom
grafana-assistant-token-retrieval

Conversation

@rafa-munoz
Copy link
Copy Markdown

@rafa-munoz rafa-munoz commented Mar 13, 2026

Ticket

https://github.com/grafana/k6-cloud/issues/4152

Description

This is a PoC that reuses the OAuth flow from Assistant CLI as it is explained in the Proposal 2 of the Design Doc.

k6-studio-assistant-integration.mov

Things that still need to be done:

  • Integrate it properly with the UI.
  • Write tests.
  • Polish code.
  • Check for corner cases.
  • Add the refresh token logic.

How to Test

  1. Go to Console in "Developer Tools".
  2. Type the command: const result = await window.studio.grafanaAssistant.connect('https://mystack.grafana.net')
  3. Type the command: JSON.stringify(result)
  4. Check cat ~/Library/Application\ Support/k6\ Studio/k6-studio-grafana-assistant-dev.json (for OSX).

Checklist

  • I have performed a self-review of my code.
  • I have added tests for my changes.
  • I have commented on my code, particularly in hard-to-understand areas.

Related PR(s)/Issue(s)


Note

Cursor Bugbot is generating a summary for commit 252a414. Configure here.

@rafa-munoz rafa-munoz requested a review from a team as a code owner March 13, 2026 14:20
@rafa-munoz rafa-munoz marked this pull request as draft March 13, 2026 14:20
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Abort during authorization returns error instead of aborted
    • The abort handler now rejects with signal.reason, preserving the AbortError shape so the state machine correctly returns an aborted result.
  • ✅ Fixed: HTTP server not closed on callback error paths
    • All callback rejection paths now remove the abort listener and close the HTTP server before rejecting, preventing leaked listeners and occupied ports.

Create PR

Or push these changes by commenting:

@cursor push e54149f6bb
Preview (e54149f6bb)
diff --git a/src/services/grafana-assistant/callback-server.ts b/src/services/grafana-assistant/callback-server.ts
--- a/src/services/grafana-assistant/callback-server.ts
+++ b/src/services/grafana-assistant/callback-server.ts
@@ -21,7 +21,7 @@
   signal: AbortSignal
 ): Promise<{ port: number; result: Promise<CallbackParams> }> {
   let resolveCallback!: (params: CallbackParams) => void
-  let rejectCallback!: (error: Error) => void
+  let rejectCallback!: (error: unknown) => void
 
   const result = new Promise<CallbackParams>((resolve, reject) => {
     resolveCallback = resolve
@@ -44,6 +44,10 @@
     if (!code || !state || !endpoint) {
       res.writeHead(400)
       res.end('Missing code, state, or endpoint')
+      signal.removeEventListener('abort', handleAbort)
+      if (server.listening) {
+        server.close()
+      }
       rejectCallback(new Error('Missing code, state, or endpoint in callback'))
       return
     }
@@ -51,6 +55,10 @@
     if (state !== expectedState) {
       res.writeHead(400)
       res.end('Invalid state')
+      signal.removeEventListener('abort', handleAbort)
+      if (server.listening) {
+        server.close()
+      }
       rejectCallback(new Error('State mismatch: possible CSRF attack'))
       return
     }
@@ -60,13 +68,19 @@
       '<html><body><h1>Authentication successful</h1><p>You can close this tab and return to k6 Studio.</p></body></html>'
     )
 
-    server.close()
+    signal.removeEventListener('abort', handleAbort)
+    if (server.listening) {
+      server.close()
+    }
     resolveCallback({ code, state, endpoint })
   })
 
   const handleAbort = () => {
-    server.close()
-    rejectCallback(new Error('Authentication aborted'))
+    signal.removeEventListener('abort', handleAbort)
+    if (server.listening) {
+      server.close()
+    }
+    rejectCallback(signal.reason)
   }
   signal.addEventListener('abort', handleAbort, { once: true })

const handleAbort = () => {
server.close()
rejectCallback(new Error('Authentication aborted'))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Abort during authorization returns error instead of aborted

Medium Severity

The handleAbort callback rejects with new Error('Authentication aborted'), which has .name === 'Error'. The wasAborted function checks for .name === 'AbortError', so this error is never recognized as an abort. When the user aborts during the authorization phase, the result will be { type: 'error', message: 'Authentication aborted' } instead of { type: 'aborted' }. The existing pattern in src/handlers/utils.ts correctly uses reject(signal.reason) to propagate the default AbortError from the signal.

Additional Locations (1)
Fix in Cursor Fix in Web

res.end('Invalid state')
rejectCallback(new Error('State mismatch: possible CSRF attack'))
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP server not closed on callback error paths

Medium Severity

When rejectCallback is called on error paths (missing parameters or state mismatch), server.close() is never called. The server continues listening on the port. Only the success path and the abort handler close the server. This leaks the HTTP server and occupies a port from the limited range (54321–54399), potentially exhausting available ports on retries.

Fix in Cursor Fix in Web

@rafa-munoz rafa-munoz force-pushed the grafana-assistant-token-retrieval branch from 252a414 to d95d8a7 Compare March 17, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant