{"record":{"id":"44f95b74b218ca24","repo":"JuliusBrussee/caveman","slug":"device-login-refused-a-redirected-token-endpoint","errorCode":null,"errorMessage":"device login refused a redirected token endpoint","messagePattern":"device login refused a redirected token endpoint","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":9723,"sourceCode":"      tokenStatus = tokResp.status;\n      const retryAfter = tokResp.headers.get(\"retry-after\");\n      if (retryAfter) {\n        const seconds = Number(retryAfter);\n        if (Number.isFinite(seconds) && seconds >= 0) retryAfterMs = seconds * 1000;\n      }\n      tok = tokenStatus >= 300 && tokenStatus < 400 ? {} : await tokResp.json() as Record<string, unknown>;\n    } catch (error) {\n      // RFC 8628 polling is retryable: a dropped connection or malformed\n      // transient response must not consume the approved code or abort login\n      // before the bounded device deadline. The next poll can reclaim the\n      // server-side lease and replay the same durable bundle.\n      if (Date.now() >= deadline) {\n        throw new Error(`device login polling failed: ${error instanceof Error ? error.message : String(error)}`);\n      }\n      await sleep(Math.max(intervalMs, retryAfterMs, 200));\n      continue;\n    }\n    if (tokenStatus >= 300 && tokenStatus < 400) throw new Error(\"device login refused a redirected token endpoint\");\n    if (tokenStatus === 429) {\n      // rateLimitAuth returns a nested cave error envelope rather than the RFC\n      // `error` string. Status is the authoritative retry signal here.\n      await sleep(Math.max(intervalMs, retryAfterMs, 200));\n      continue;\n    }\n    const accessToken = typeof tok.access_token === \"string\" ? tok.access_token : \"\";\n    if (accessToken) {\n\t  if (instance && (tokenStatus < 200 || tokenStatus >= 300 || tok.credential_kind !== \"none\" ||\n\t      [\"gateway_api_key\", \"gateway_key_id\", \"gateway_url\"].some((key) => tok[key] != null) ||\n\t      typeof tok.refresh_token !== \"string\" || !tok.refresh_token || typeof tok.project_id !== \"string\" || !tok.project_id ||\n\t      typeof tok.delivery_ack_token !== \"string\" || !tok.delivery_ack_token || typeof tok.scope !== \"string\" || !tok.scope ||\n\t      tok.scope.split(/\\s+/).some((scope) => scope === \"proxy:write\" || scope === \"sdk:write\"))) {\n\t    throw new Error(\"private device login requires a keyless project grant with a refresh token and delivery acknowledgement\");\n\t  }\n\t  const credentials: StoredCredentials = {\n\t    access_token: accessToken,\n\t    ...(typeof tok.refresh_token === \"string\" && tok.refresh_token ? { refresh_token: tok.refresh_token } : {}),","sourceCodeStart":9705,"sourceCodeEnd":9741,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/cli/src/index.ts#L9705-L9741","documentation":"Security check during token polling: if the token endpoint responds with an HTTP 3xx redirect, the CLI refuses to follow it and throws immediately. OAuth token endpoints must be called directly; following a redirect could leak the device_code/credentials to another host.","triggerScenarios":"The token-endpoint poll returns a status in 300–399 (e.g. 301/302), typically because the instance URL is http and gets redirected to https, a trailing-slash redirect, a misconfigured reverse proxy, or a load balancer bouncing the path.","commonSituations":"Using http:// --instance where the server redirects to https; proxy appending/removing a trailing slash; misconfigured load balancer redirecting /oauth/token; pointing at a host that redirects all traffic.","solutions":["Use the final, canonical https URL of the instance in --instance so no redirect occurs","Fix the reverse proxy/load balancer so the token endpoint is served directly without redirects","Check for trailing-slash or http→https redirect rules affecting the token path"],"exampleFix":"// before\n--instance=http://auth.example.com   // 301 -> https://auth.example.com\n// after\n--instance=https://auth.example.com  // no redirect","handlingStrategy":"validation","validationCode":"const u = new URL(instance);\nif (u.protocol !== \"https:\") console.warn(\"http instance URLs may trigger token-endpoint redirects; use the canonical https URL\");","typeGuard":null,"tryCatchPattern":"try { await login({ instance }) } catch (e) { if (e.message.includes(\"redirected token endpoint\")) console.error(\"Use the final non-redirecting https URL for --instance\"); }","preventionTips":["Always configure --instance with the canonical https URL","Remove redirect rules (trailing slash, http→https) from the token path","Test the token endpoint with curl -i to confirm no 3xx"],"tags":["oauth","security","redirect","http"],"backgroundTag":"redirect-blocked","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}