{"record":{"id":"6a42f12ac543f7c6","repo":"paperclipai/paperclip","slug":"cloud-control-assertion-is-expired-or-has-an-invalid","errorCode":null,"errorMessage":"Cloud control assertion is expired or has an invalid lifetime","messagePattern":"Cloud control assertion is expired or has an invalid lifetime","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/cloud-runtime-identity.ts","lineNumber":570,"sourceCode":"    !(CLOUD_CONTROL_ACTIONS as readonly string[]).includes(payload.action)\n    || payload.action !== input.expectedAction\n  ) {\n    throw new Error(\"Cloud control assertion does not authorize this action\");\n  }\n  if (\n    !payload.requestId\n    || payload.requestId.trim() !== payload.requestId\n    || payload.requestId.length > 256\n  ) {\n    throw new Error(\"Cloud control assertion request id is invalid\");\n  }\n  if (\n    payload.exp <= nowSeconds\n    || payload.iat > nowSeconds + MAX_CLOCK_SKEW_SECONDS\n    || payload.exp <= payload.iat\n    || payload.exp - payload.iat > CLOUD_CONTROL_MAX_LIFETIME_SECONDS\n  ) {\n    throw new Error(\"Cloud control assertion is expired or has an invalid lifetime\");\n  }\n  // Consumed LAST, only after every other check passed: a rejected\n  // assertion must not burn its request id, or an attacker could deny a\n  // legitimate call by replaying a mangled copy of it first.\n  if (!consumeControlRequestId(payload.requestId, payload.exp + MAX_CLOCK_SKEW_SECONDS, now.getTime())) {\n    throw new Error(\"Cloud control assertion has already been used\");\n  }\n  return payload as CloudControlClaims;\n}\n","sourceCodeStart":552,"sourceCodeEnd":580,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/cloud-runtime-identity.ts#L552-L580","documentation":"The final temporal check in verifyCloudControlAssertion enforces the assertion's validity window: exp must be in the future, iat must not be more than MAX_CLOCK_SKEW_SECONDS ahead of now, exp must be after iat, and the total lifetime (exp - iat) must not exceed CLOUD_CONTROL_MAX_LIFETIME_SECONDS. This error means the assertion is expired, not-yet-valid due to clock skew, or declares an unreasonably long lifetime. The request id is only consumed after this passes, so rejected assertions do not burn replay tokens.","triggerScenarios":"Sending an assertion after its exp has passed (cached or reused token); the verifying server's clock is far behind/ahead of the signer's clock; the issuer set a lifetime longer than CLOUD_CONTROL_MAX_LIFETIME_SECONDS; iat set in the future by a misconfigured signer.","commonSituations":"Long-lived cached assertions reused across control calls instead of per-request minting; NTP drift or wrong timezone/clock on a VM or container; an issuer hard-coding a 24h expiry that exceeds the max lifetime; a test fixture with frozen timestamps.","solutions":["Mint a fresh assertion immediately before each control request with a short lifetime under CLOUD_CONTROL_MAX_LIFETIME_SECONDS (e.g. 5 minutes)","Synchronize clocks (NTP/chrony) on both the signing client and the verifying instance","Fix the issuer so exp = iat + small delta and iat = now, both epoch-seconds integers","Stop caching/persisting assertions; if retries are needed, mint a new token with a new requestId"],"exampleFix":"// before\nconst iat = 0, exp = iat + 60 * 60 * 24; // 24h lifetime\n// after\nconst iat = Math.floor(Date.now() / 1000);\nconst exp = iat + 300; // within CLOUD_CONTROL_MAX_LIFETIME_SECONDS\nconst assertion = mintAssertion({ sub: stackId, action, requestId: crypto.randomUUID(), iat, exp });","handlingStrategy":"retry","validationCode":"function lifetimeOk(iat, exp, maxLifetime) {\n  const now = Math.floor(Date.now() / 1000);\n  return exp > now && iat <= now + 60 && exp > iat && (exp - iat) <= maxLifetime;\n}\nif (!lifetimeOk(claims.iat, claims.exp, MAX_LIFETIME)) throw new Error(\"assertion expired or over-long lifetime; re-mint\");","typeGuard":null,"tryCatchPattern":"async function withFreshAssertion(action) {\n  try {\n    return await control(action, mintAssertion({ ...claims(), iat: now(), exp: now() + 300 }));\n  } catch (e) {\n    if (e.message === \"Cloud control assertion is expired or has an invalid lifetime\" && attempts < 2) {\n      // only safe retry path: mint a brand-new assertion (and new requestId) — never resend the old token\n      return control(action, mintAssertion({ ...claims(), iat: now(), exp: now() + 300 }));\n    }\n    throw e;\n  }\n}","preventionTips":["Mint assertions immediately before use with lifetimes well under CLOUD_CONTROL_MAX_LIFETIME_SECONDS","Run NTP/chrony on all signers and verifiers to keep skew within MAX_CLOCK_SKEW_SECONDS","Never cache, persist, or retry with an old assertion; each request gets a fresh token and fresh requestId","Add a canary verify at deploy time to catch clock drift early"],"tags":["jws","expired-token","clock-skew","security","replay-protection"],"backgroundTag":"jwt-token-expired","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}