Budibase/budibase · error · Error

Slack OAuth state is invalid or expired

Error message

Slack OAuth state is invalid or expired

What it means

The OAuth state value is used as a cache key holding { agentId, workspaceId } for the pending flow. The handler deletes the cached entry immediately after reading (single-use), and throws if the payload is missing or lacks agentId/workspaceId — meaning the state was unknown, already consumed, or expired.

Source

Thrown at packages/server/src/api/controllers/ai/agents.ts:582

      state,
    }),
  }
  ctx.status = 200
}

export async function completeSlackOAuth(ctx: UserCtx<void, void>) {
  const state = String(ctx.query.state || "").trim()
  if (!state) {
    throw new Error("Slack OAuth callback is missing state")
  }

  const cacheKey = getSlackOAuthStateCacheKey(state)
  const statePayload = (await cache.get(cacheKey, {
    useTenancy: false,
  })) as SlackOAuthState | undefined
  await cache.destroy(cacheKey, { useTenancy: false })
  if (!statePayload?.agentId || !statePayload.workspaceId) {
    throw new Error("Slack OAuth state is invalid or expired")
  }

  const oauthError = String(ctx.query.error || "").trim()
  if (oauthError) {
    throw new Error("Slack OAuth authorization failed")
  }

  const code = String(ctx.query.code || "").trim()
  if (!code) {
    throw new Error("Slack OAuth callback is missing the authorization code")
  }

  await context.doInWorkspaceContext(statePayload.workspaceId, async () => {
    const agent = await sdk.ai.agents.getOrThrow(statePayload.agentId)
    const clientId = agent.slackIntegration?.clientId?.trim()
    const clientSecret = agent.slackIntegration?.clientSecret?.trim()
    if (!clientId || !clientSecret) {
      throw new Error("Slack OAuth client credentials are not configured")

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Restart the Slack OAuth flow to generate fresh state — the old entry is gone or consumed
  2. Do not refresh/replay the callback URL; each state works exactly once
  3. Check Redis availability and TTL configuration for the state cache key
  4. Ensure all server instances share the same Redis cache instance
  5. Confirm the initiate step stores { agentId, workspaceId } before redirecting to Slack
Defensive patterns

Strategy: try-catch

Validate before calling

const payload = await cache.get(getSlackOAuthStateCacheKey(state), { useTenancy: false })
if (!payload || !(payload as any).agentId || !(payload as any).workspaceId) {
  // state unknown/expired: re-initiate the OAuth flow
  return initiateSlackOAuth(agentId)
}

Type guard

function isSlackOAuthState(p: unknown): p is SlackOAuthState {
  return !!p && typeof p === "object" && typeof (p as SlackOAuthState).agentId === "string" && typeof (p as SlackOAuthState).workspaceId === "string"
}

Try / catch

try {
  await completeSlackOAuth(ctx)
} catch (err) {
  if (err.message.includes("invalid or expired")) {
    // state consumed/expired — instruct user to restart the install; do not retry the callback
  } else { throw err }
}

Prevention

When it happens

Trigger: Callback with a state that has no cache entry: state already used (double callback/retry), cache TTL expired, Redis restarted/flushed, multi-instance deployment pointing at different caches, or a forged/garbage state string.

Common situations: User refreshing the callback page (second hit consumes the single-use state); long delay between initiate and approve exceeding the cache TTL; Redis eviction under memory pressure; cache.useTenancy misconfiguration so lookup misses.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/da029693db4e632b. Report an issue: GitHub.