JuliusBrussee/caveman · error
device login refused a redirected token endpoint
Error message
device login refused a redirected token endpoint
What it means
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.
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
Example fix
// before --instance=http://auth.example.com // 301 -> https://auth.example.com // after --instance=https://auth.example.com // no redirect
Defensive patterns
Strategy: validation
Validate before calling
const u = new URL(instance);
if (u.protocol !== "https:") console.warn("http instance URLs may trigger token-endpoint redirects; use the canonical https URL"); Try / catch
try { await login({ instance }) } catch (e) { if (e.message.includes("redirected token endpoint")) console.error("Use the final non-redirecting https URL for --instance"); } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- cave_redirect_not_allowed
- device authorization failed: HTTP
- fetch failed: too many redirects
- fetch failed: unexpected redirect
- private device authorization returned an unsafe browser URL
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/44f95b74b218ca24.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:9723
tokenStatus = tokResp.status;
const retryAfter = tokResp.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds) && seconds >= 0) retryAfterMs = seconds * 1000;
}
tok = tokenStatus >= 300 && tokenStatus < 400 ? {} : await tokResp.json() as Record<string, unknown>;
} catch (error) {
// RFC 8628 polling is retryable: a dropped connection or malformed
// transient response must not consume the approved code or abort login
// before the bounded device deadline. The next poll can reclaim the
// server-side lease and replay the same durable bundle.
if (Date.now() >= deadline) {
throw new Error(`device login polling failed: ${error instanceof Error ? error.message : String(error)}`);
}
await sleep(Math.max(intervalMs, retryAfterMs, 200));
continue;
}
if (tokenStatus >= 300 && tokenStatus < 400) throw new Error("device login refused a redirected token endpoint");
if (tokenStatus === 429) {
// rateLimitAuth returns a nested cave error envelope rather than the RFC
// `error` string. Status is the authoritative retry signal here.
await sleep(Math.max(intervalMs, retryAfterMs, 200));
continue;
}
const accessToken = typeof tok.access_token === "string" ? tok.access_token : "";
if (accessToken) {
if (instance && (tokenStatus < 200 || tokenStatus >= 300 || tok.credential_kind !== "none" ||
["gateway_api_key", "gateway_key_id", "gateway_url"].some((key) => tok[key] != null) ||
typeof tok.refresh_token !== "string" || !tok.refresh_token || typeof tok.project_id !== "string" || !tok.project_id ||
typeof tok.delivery_ack_token !== "string" || !tok.delivery_ack_token || typeof tok.scope !== "string" || !tok.scope ||
tok.scope.split(/\s+/).some((scope) => scope === "proxy:write" || scope === "sdk:write"))) {
throw new Error("private device login requires a keyless project grant with a refresh token and delivery acknowledgement");
}
const credentials: StoredCredentials = {
access_token: accessToken,
...(typeof tok.refresh_token === "string" && tok.refresh_token ? { refresh_token: tok.refresh_token } : {}),View on GitHub (pinned to 3ee70a1026)