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
- Restart the Slack OAuth flow to generate fresh state — the old entry is gone or consumed
- Do not refresh/replay the callback URL; each state works exactly once
- Check Redis availability and TTL configuration for the state cache key
- Ensure all server instances share the same Redis cache instance
- 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
- Treat state as strictly single-use; never replay callback URLs
- Set a generous but bounded cache TTL for state entries (covering slow approvals)
- Use one shared Redis instance across all server replicas
- Persist state before redirecting the user to Slack
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
- Slack app creation response was incomplete
- Slack OAuth callback is missing state
- Slack OAuth authorization failed
- Slack OAuth callback is missing the authorization code
- Slack OAuth client credentials are not configured
AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29).
Data as JSON: /api/errors/da029693db4e632b.
Report an issue: GitHub.