{"record":{"id":"1e9a3055a67b99c8","repo":"transloadit/uppy","slug":"invalid-oauth-state-payload","errorCode":null,"errorMessage":"Invalid OAuth state payload","messagePattern":"Invalid OAuth state payload","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@uppy/companion/src/server/helpers/oauth-state.ts","lineNumber":30,"sourceCode":"  authCallbackToken?: string\n}\n\nexport const encodeState = (\n  state: OAuthState,\n  secret: string | Buffer,\n): string => {\n  const encodedState = Buffer.from(JSON.stringify(state)).toString('base64')\n  return encrypt(encodedState, secret)\n}\n\nexport const decodeState = (\n  state: string,\n  secret: string | Buffer,\n): OAuthState => {\n  const encodedState = decrypt(state, secret)\n  const parsed: unknown = JSON.parse(atob(encodedState))\n  if (!isOAuthState(parsed)) {\n    throw new Error('Invalid OAuth state payload')\n  }\n  return parsed\n}\n\nexport const generateState = (): OAuthState => {\n  return {\n    id: crypto.randomBytes(10).toString('hex'),\n  }\n}\n\nfunction isOAuthState(value: unknown): value is OAuthState {\n  return isRecord(value) && typeof value['id'] === 'string'\n}\n\nexport const getFromState = <T extends keyof OAuthState>(\n  state: string,\n  name: T,\n  secret: string | Buffer,","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/transloadit/uppy/blob/5d4dedd02a1ac0ae022c75c54aca76558f88e256/packages/@uppy/companion/src/server/helpers/oauth-state.ts#L12-L48","documentation":"decodeState decrypts the OAuth `state` parameter, base64-decodes and JSON.parses it, then validates it with isOAuthState. If the shape check fails, it throws 'Invalid OAuth state payload' — the state did not originate from Companion's generateState.","triggerScenarios":"Hitting an OAuth callback URL with a `state` query param that is malformed, truncated, from a different secret/environment, or hand-crafted — anything that decrypts+parses but doesn't satisfy isOAuthState.","commonSituations":"OAuth callbacks crossing environments (token created with a different COMPANION_SECRET), load-balanced Companions with mismatched secrets, stale/cached callback URLs, or manual testing with fabricated state values.","solutions":["Ensure all Companion instances behind a load balancer share the same COMPANION_SECRET so state encrypts/decrypts consistently","Restart the OAuth flow to obtain a fresh state rather than replaying an old callback URL","Verify COMPANION_CLIENT_ID/SECRET and the provider OAuth app's redirect URI match the Companion host serving the callback"],"exampleFix":"# before (two companions with different secrets)\nCOMPANION_SECRET=aaaa  # instance 1 starts flow\nCOMPANION_SECRET=bbbb  # instance 2 receives callback -> Invalid OAuth state payload\n\n# after\nCOMPANION_SECRET=shared-secret  # both instances","handlingStrategy":"validation","validationCode":"const stateOk = (s: string) => { try { return Boolean(decodeState(s, secret)) } catch { return false } }\nif (!stateOk(req.query.state)) return res.status(400).send('bad state')","typeGuard":null,"tryCatchPattern":"try { getFromState(req, key, secret) } catch (err) {\n  if (err.message === 'Invalid OAuth state payload') { res.redirect('/connect/provider') /* restart flow */ }\n}","preventionTips":["Share COMPANION_SECRET across all Companion replicas","Never bookmark/replay OAuth callback URLs; restart the flow instead"],"tags":["companion","oauth","state","encryption"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"5d4dedd02a1ac0ae022c75c54aca76558f88e256","analyzedAt":"2026-08-28T12:18:41.267Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}