{"record":{"id":"2c36f53b1976cae1","repo":"Budibase/budibase","slug":"slack-app-configuration-token-has-expired-save-a-2c36f5","errorCode":null,"errorMessage":"Slack app configuration token has expired. Save a new config token and refresh token.","messagePattern":"Slack app configuration token has expired\\. Save a new config token and refresh token\\.","errorType":"http","errorClass":"HTTPError","httpStatus":400,"severity":"error","filePath":"packages/server/src/sdk/workspace/ai/slackAppConfig.ts","lineNumber":94,"sourceCode":"  }\n}\n\nexport const fetchConfigToken = async () => {\n  const config = await fetch()\n  if (!config?.configToken) {\n    throw new HTTPError(\n      \"Slack app configuration token is not configured for this workspace\",\n      400\n    )\n  }\n\n  if (!tokenNeedsRotation(config.expiresAt) && config.expiresAt) {\n    return decodeSecret(config.configToken)\n  }\n\n  if (!config.refreshToken) {\n    if (config.expiresAt) {\n      throw new HTTPError(\n        \"Slack app configuration token has expired. Save a new config token and refresh token.\",\n        400\n      )\n    }\n    return decodeSecret(config.configToken)\n  }\n\n  const rotated = await rotateSlackConfigToken({\n    refreshToken: decodeSecret(config.refreshToken),\n  })\n  return decodeSecret((await saveRotatedConfig(config, rotated)).configToken)\n}\n\nexport const save = async (configToken: string, refreshToken: string) => {\n  const db = getWorkspaceDB()\n  const existing = await fetch()\n  const now = new Date().toISOString()\n  const trimmedToken = configToken.trim()","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/sdk/workspace/ai/slackAppConfig.ts#L76-L112","documentation":"fetchConfigToken tries to auto-rotate an expired (or near-expiry) config token using the stored refresh token. If the token needs rotation, there is no refreshToken, and an expiresAt is present (so the token is known-expired), rotation is impossible and this 400 HTTPError is thrown, instructing the user to re-enter both tokens.","triggerScenarios":"Config token's expiresAt is within the 5-minute rotation buffer or past, config.refreshToken is absent, and configToken itself is expired — the no-refresh-token + expired-token combination.","commonSituations":"User saved only a config token without a refresh token; refresh token was cleared/removed from the config; token aged past expiry while refresh token was lost; legacy config saved before refresh tokens were required.","solutions":["Re-save both a new config token and a new refresh token via the Slack app config save endpoint","Generate fresh tokens from the Slack app configuration source and update the workspace config","If a refresh token exists elsewhere, save it so auto-rotation can work","Add validation at save time to require a refresh token so this state cannot occur"],"exampleFix":"// before\n{ configToken: \"bbai_enc::...\", expiresAt: \"2026-08-01T00:00:00Z\" } // no refreshToken\n// after\nawait save(newConfigToken, newRefreshToken)\n// results in\n{ configToken: \"bbai_enc::...\", refreshToken: \"bbai_enc::...\", expiresAt: \"2026-09-01T00:00:00Z\" }","handlingStrategy":"validation","validationCode":"const config = await slackAppConfig.fetch()\nconst expired = config?.expiresAt && new Date(config.expiresAt).getTime() <= Date.now() + 5 * 60 * 1000\nif (expired && !config?.refreshToken) {\n  throw new Error(\"Config token expired and no refresh token saved; re-save both tokens\")\n}","typeGuard":"const isRefreshable = (\n  config: SlackAppConfig | undefined\n): config is SlackAppConfig & { refreshToken: string } =>\n  typeof config?.refreshToken === \"string\" && config.refreshToken.length > 0","tryCatchPattern":"try {\n  const token = await fetchConfigToken()\n} catch (err) {\n  if (err instanceof HTTPError && err.status === 400 && err.message.includes(\"has expired\")) {\n    // prompt user to save a new config token and refresh token\n  } else { throw err }\n}","preventionTips":["Always save both config token and refresh token together","Never clear the refresh token when updating the config token","Monitor expiresAt and refresh proactively before the 5-minute buffer","Require refreshToken at save time to prevent unrecoverable states"],"tags":["slack","token-expired","configuration"],"backgroundTag":"jwt-token-expired","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}