{"record":{"id":"d33f2a28bc829a0c","repo":"NousResearch/hermes-agent","slug":"gateway-rejected-native-login-error-desc","errorCode":null,"errorMessage":"Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''}","messagePattern":"Gateway rejected native login: (.+?)(.+?)\\)` : ''\\}","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/native-oauth.ts","lineNumber":154,"sourceCode":"\n  return `${parsed.protocol}//${parsed.host}${prefix}/auth/native/refresh`\n}\n\n/**\n * Parse the loopback redirect the gateway sends the browser to. Returns the\n * `code` + `state`, or throws with the gateway's `error` if the flow failed.\n * `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","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/native-oauth.ts#L136-L172","documentation":"Part of the native (RFC 8252 loopback) OAuth flow: after the gateway redirects back to 127.0.0.1 with ?code=...&state=..., parseLoopbackCallback checks for an OAuth 2.0 'error' query parameter. If present, the gateway explicitly rejected the login and the error code (plus error_description) is rethrown here. This is the gateway speaking, not a desktop bug — common codes are access_denied, invalid_request, unauthorized_client.","triggerScenarios":"The gateway's authorize endpoint redirects to the loopback callback with ?error=access_denied (user denied consent), ?error=invalid_request (malformed authorize request), or a provider-side failure — the desktop then parses that redirect and throws.","commonSituations":"User clicks 'deny' on the consent screen; the OAuth client_id/redirect_uri registered on the gateway doesn't match what the desktop sent; upstream IdP outage; attempting native login when the gateway has native flow disabled.","solutions":["Read the error code and description in the message — access_denied means user refusal (retry and approve), others point at gateway/provider config","Verify the gateway's OAuth client registration: redirect_uri must be the loopback origin the desktop uses","Confirm the gateway supports/enables the native (loopback) flow; otherwise use the browser-cookie flow or a token","Check gateway logs at the moment of the redirect for the authorize failure reason"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isGatewayLoginRejection(e: unknown): e is Error {\n  return e instanceof Error && e.message.startsWith('Gateway rejected native login:')\n}","tryCatchPattern":"try { const { code } = parseLoopbackCallback(requestUrl, expectedState) } catch (e) { if (isGatewayLoginRejection(e)) { surfaceAuthError(e.message); abortFlow() } else throw e }","preventionTips":["Register the exact loopback redirect_uri on the gateway before shipping","Verify the gateway enables the native OAuth flow","Surface error/error_description to the user instead of retrying blindly"],"tags":["oauth","authentication","loopback","gateway","desktop"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}