{"record":{"id":"aa44ade1778ca408","repo":"NousResearch/hermes-agent","slug":"loopback-callback-missing-authorization-code","errorCode":null,"errorMessage":"Loopback callback missing authorization code","messagePattern":"Loopback callback missing authorization code","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/native-oauth.ts","lineNumber":161,"sourceCode":" * `expectedState` MUST match (CSRF defense — RFC 6749 §10.12); a mismatch\n * throws rather than proceeding.\n */\nexport function parseLoopbackCallback(requestUrl: string, expectedState: string): { code: string } {\n  // requestUrl is the path+query the loopback server received, e.g.\n  // \"/callback?code=...&state=...\". Resolve against a dummy origin to parse.\n  const parsed = new URL(requestUrl, 'http://127.0.0.1')\n  const error = parsed.searchParams.get('error')\n\n  if (error) {\n    const desc = parsed.searchParams.get('error_description') || ''\n    throw new Error(`Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''}`)\n  }\n\n  const code = parsed.searchParams.get('code') || ''\n  const state = parsed.searchParams.get('state') || ''\n\n  if (!code) {\n    throw new Error('Loopback callback missing authorization code')\n  }\n\n  if (!expectedState || state !== expectedState) {\n    // Never redeem a code that arrived with a mismatched state — it may be a\n    // forged callback trying to inject an attacker's code.\n    throw new Error('Loopback callback state mismatch (possible CSRF)')\n  }\n\n  return { code }\n}\n\n/**\n * Normalize a `/auth/native/token` (or refresh) JSON response into a\n * NativeTokenSet, validating the shape. Throws on a missing/short access\n * token so a malformed response fails loudly rather than storing junk.\n */\nexport function parseTokenResponse(body: any): NativeTokenSet {\n  const accessToken = String(body?.access_token || '')","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/native-oauth.ts#L143-L179","documentation":"In parseLoopbackCallback, the loopback redirect arrived without an 'error' param but also without a 'code' param. The authorization-code half of the OAuth exchange is impossible without the code, so the parse fails loudly rather than proceeding with an empty code. Usually means the redirect came from something other than a successful authorize response (a login page bounce, a partial redirect, or a stray request hitting the loopback port).","triggerScenarios":"A GET to the loopback callback URL carrying neither code nor error — e.g. a favicon.ico request from the browser, an aborted login redirecting early, a health-check/probe hitting the loopback port, or a gateway redirect that dropped the query string.","commonSituations":"Browsers auto-requesting /favicon.ico on the loopback redirect page; security software probing the briefly-open loopback listener; gateway misconfiguration stripping query params on redirect.","solutions":["Log the full requestUrl that triggered the parse — stray paths like /favicon.ico explain most cases and can be ignored rather than failed","Retry the login; a one-off stray request to the loopback port does not indicate broken credentials","If reproducible every time, capture the gateway's redirect Location header and check why 'code' is missing (authorizer misroute, proxy stripping query)"],"exampleFix":"// before\nconst { code } = parseLoopbackCallback(req.url, expectedState)\n\n// after\nif (new URL(req.url, 'http://127.0.0.1').pathname !== '/callback') return // ignore favicon/probes\nconst { code } = parseLoopbackCallback(req.url, expectedState)","handlingStrategy":"try-catch","validationCode":"// Ignore non-callback requests hitting the loopback listener\nconst u = new URL(requestUrl, 'http://127.0.0.1')\nif (u.pathname !== '/callback') return // favicon.ico, probes, etc.\nconst { code } = parseLoopbackCallback(requestUrl, expectedState)","typeGuard":"function looksLikeAuthCallback(requestUrl: string): boolean {\n  const u = new URL(requestUrl, 'http://127.0.0.1')\n  return u.pathname === '/callback' && (u.searchParams.has('code') || u.searchParams.has('error'))\n}","tryCatchPattern":"try { const { code } = parseLoopbackCallback(requestUrl, expectedState) } catch (e) { if (e instanceof Error && e.message === 'Loopback callback missing authorization code' && !looksLikeAuthCallback(requestUrl)) return // ignore stray request; keep listening\n throw e }","preventionTips":["Filter loopback listener requests by path before parsing them as OAuth callbacks","Treat a single stray missing-code request as ignorable; only fail when the real redirect lacks the code","Retry the login after transient parse failures"],"tags":["oauth","authentication","loopback","validation","desktop"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}