JuliusBrussee/caveman · error

device authorization failed: missing device code

Error message

device authorization failed: missing device code

What it means

After a successful (2xx) device-authorization response, the CLI requires the JSON body to contain a device_code, which is needed to poll the token endpoint. A 2xx response without device_code means the server answered but not with a valid RFC 8628 payload, so login cannot proceed.

Solutions

  1. Verify the --instance URL targets the actual authorization server, not a proxy or wrong service
  2. Inspect the raw response of the device/code endpoint (curl with -i) to see what is actually returned
  3. Confirm the identity provider supports the device authorization grant (RFC 8628)
  4. Check for captive portals or middleware rewriting responses

Example fix

// before
const code = await codeResp.json();
// after (server-side fix: return a proper device grant)
res.json({ device_code, user_code, verification_uri, verification_uri_complete, interval });
Defensive patterns

Strategy: type-guard

Validate before calling

const body = await resp.json();
if (typeof body.device_code !== "string" || !body.device_code) throw new Error("device code endpoint returned an invalid payload");

Type guard

function hasDeviceCode(c) { return c != null && typeof c.device_code === "string" && c.device_code.length > 0; }

Try / catch

try { await login({ instance }) } catch (e) { if (e.message.includes("missing device code")) console.error("Instance did not return an RFC 8628 device grant; check proxy/IdP"); }

Prevention

When it happens

Trigger: The device/code endpoint returns 200 with JSON that lacks a truthy device_code field — e.g. an error envelope with HTTP 200, a proxy returning an HTML/empty page as JSON, or a non-standard authorization server response shape.

Common situations: Reverse proxy or captive portal intercepting the request and returning 200 with unexpected content; identity provider returning an error object instead of a device grant; pointing --instance at the wrong service that returns 200 for any path.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/a3db8d535b8db7ab. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/index.ts:9683

function blockCloudLoginWhileBeta(): void {
  throw new Error("Caveman Cloud platform is still in beta.");
}

async function login(argv: string[] = []) {
  if (!argv.some((arg) => arg === "--instance" || arg.startsWith("--instance="))) blockCloudLoginWhileBeta();
  const { noBrowser, instance } = validateLoginArgs(argv);
  const baseURL = instance ?? resolveLoginBaseUrl(argv);

  const codeResp = await fetch(`${baseURL}/api/v1/auth/device/code`, {
    method: "POST",
    redirect: "error",
    headers: { "content-type": "application/json" },
    body: "{}",
    signal: AbortSignal.timeout(5000),
  });
  if (!codeResp.ok) throw new Error(`device authorization failed: HTTP ${codeResp.status}`);
  const code = await codeResp.json();
  if (!code.device_code) throw new Error("device authorization failed: missing device code");

  const verificationURL = instance ? privateVerificationURL(code, instance) : code.verification_uri_complete ?? code.verification_uri;
  console.error(`\n  Authorize this device in your browser:`);
  console.error(`    ${verificationURL}`);
  console.error(`    code: ${code.user_code}\n`);
  if (typeof verificationURL === "string" && shouldOpenLoginBrowser(noBrowser)) openLoginBrowser(verificationURL);

  let intervalMs = Math.max(0, Number(code.interval ?? 5)) * 1000;
  const deadline = Date.now() + Number(code.expires_in ?? 600) * 1000;
  while (Date.now() < deadline) {
    let tok: Record<string, unknown>;
    let tokenStatus = 0;
    let retryAfterMs = 0;
    try {
      const tokResp = await fetch(`${baseURL}/api/v1/auth/device/token`, {
        method: "POST",
        redirect: "manual",
        headers: { "content-type": "application/json" },

View on GitHub (pinned to 3ee70a1026)