NousResearch/hermes-agent · error · Error
Gateway rejected native login: ${error}${desc ? ` (${desc})`
Error message
Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''} What it means
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.
Source
Thrown at apps/desktop/electron/native-oauth.ts:154
return `${parsed.protocol}//${parsed.host}${prefix}/auth/native/refresh`
}
/**
* Parse the loopback redirect the gateway sends the browser to. Returns the
* `code` + `state`, or throws with the gateway's `error` if the flow failed.
* `expectedState` MUST match (CSRF defense — RFC 6749 §10.12); a mismatch
* throws rather than proceeding.
*/
export function parseLoopbackCallback(requestUrl: string, expectedState: string): { code: string } {
// requestUrl is the path+query the loopback server received, e.g.
// "/callback?code=...&state=...". Resolve against a dummy origin to parse.
const parsed = new URL(requestUrl, 'http://127.0.0.1')
const error = parsed.searchParams.get('error')
if (error) {
const desc = parsed.searchParams.get('error_description') || ''
throw new Error(`Gateway rejected native login: ${error}${desc ? ` (${desc})` : ''}`)
}
const code = parsed.searchParams.get('code') || ''
const state = parsed.searchParams.get('state') || ''
if (!code) {
throw new Error('Loopback callback missing authorization code')
}
if (!expectedState || state !== expectedState) {
// Never redeem a code that arrived with a mismatched state — it may be a
// forged callback trying to inject an attacker's code.
throw new Error('Loopback callback state mismatch (possible CSRF)')
}
return { code }
}
View on GitHub (pinned to c896c09c42)
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
Defensive patterns
Strategy: try-catch
Type guard
function isGatewayLoginRejection(e: unknown): e is Error {
return e instanceof Error && e.message.startsWith('Gateway rejected native login:')
} Try / catch
try { const { code } = parseLoopbackCallback(requestUrl, expectedState) } catch (e) { if (isGatewayLoginRejection(e)) { surfaceAuthError(e.message); abortFlow() } else throw e } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Loopback callback missing authorization code
- Gateway token response missing access_token
- Loopback callback state mismatch (possible CSRF)
- Gateway did not return a WS ticket.
- Remote gateway session token is required.
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/d33f2a28bc829a0c.
Report an issue: GitHub.