transloadit/uppy · error · Error

Invalid OAuth state payload

Error message

Invalid OAuth state payload

What it means

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.

Source

Thrown at packages/@uppy/companion/src/server/helpers/oauth-state.ts:30

  authCallbackToken?: string
}

export const encodeState = (
  state: OAuthState,
  secret: string | Buffer,
): string => {
  const encodedState = Buffer.from(JSON.stringify(state)).toString('base64')
  return encrypt(encodedState, secret)
}

export const decodeState = (
  state: string,
  secret: string | Buffer,
): OAuthState => {
  const encodedState = decrypt(state, secret)
  const parsed: unknown = JSON.parse(atob(encodedState))
  if (!isOAuthState(parsed)) {
    throw new Error('Invalid OAuth state payload')
  }
  return parsed
}

export const generateState = (): OAuthState => {
  return {
    id: crypto.randomBytes(10).toString('hex'),
  }
}

function isOAuthState(value: unknown): value is OAuthState {
  return isRecord(value) && typeof value['id'] === 'string'
}

export const getFromState = <T extends keyof OAuthState>(
  state: string,
  name: T,
  secret: string | Buffer,

View on GitHub (pinned to 5d4dedd02a)

Solutions

  1. Ensure all Companion instances behind a load balancer share the same COMPANION_SECRET so state encrypts/decrypts consistently
  2. Restart the OAuth flow to obtain a fresh state rather than replaying an old callback URL
  3. Verify COMPANION_CLIENT_ID/SECRET and the provider OAuth app's redirect URI match the Companion host serving the callback

Example fix

# before (two companions with different secrets)
COMPANION_SECRET=aaaa  # instance 1 starts flow
COMPANION_SECRET=bbbb  # instance 2 receives callback -> Invalid OAuth state payload

# after
COMPANION_SECRET=shared-secret  # both instances
Defensive patterns

Strategy: validation

Validate before calling

const stateOk = (s: string) => { try { return Boolean(decodeState(s, secret)) } catch { return false } }
if (!stateOk(req.query.state)) return res.status(400).send('bad state')

Try / catch

try { getFromState(req, key, secret) } catch (err) {
  if (err.message === 'Invalid OAuth state payload') { res.redirect('/connect/provider') /* restart flow */ }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of transloadit/uppy@5d4dedd02a (2026-08-28). Data as JSON: /api/errors/1e9a3055a67b99c8. Report an issue: GitHub.