Building a Multi-Workspace Slack App: OAuth Install Flow and Token Storage
Ship a Slack app multiple workspaces can install: the OAuth 2.0 flow, state validation, per-team token storage, and rotation. AI scaffolds it; you secure it.
- #slack
- #chatops
- #oauth
- #security
My first Slack app had exactly one token, pasted into an environment variable, serving exactly one workspace. That’s fine until someone on another team wants to install it, and then the whole architecture falls apart. A single hardcoded token can’t represent “this bot, in their workspace, with their permissions.” To serve more than one workspace you have to implement the OAuth install flow properly: each workspace installs your app, you receive a bot token for that team, and you store and look up tokens per installation. It’s not hard, but it’s full of security-critical details that are easy to fumble.
I built our install flow with AI assistance, and OAuth is one of the places I most firmly treat AI as a fast junior engineer. It writes a flow that completes an install — and routinely skips state validation (hello, CSRF), stores tokens in plaintext, or forgets per-team isolation. A human reviews the security of every step before this app accepts a single real installation. The model gets you a working flow fast; you make it one you’d actually trust with other people’s workspaces.
The OAuth flow in three legs
- User hits your “Add to Slack” button → you redirect to Slack’s
oauth/v2/authorizewith your scopes and astatevalue. - User approves → Slack redirects back to your
redirect_uriwith acodeand yourstate. - You exchange the
codeatoauth.v2.accessfor a per-team bot token.
// Leg 1: start install
app.get('/install', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
saveState(state, { ttl: 600 }); // remember it
const url = `https://slack.com/oauth/v2/authorize?client_id=${CLIENT_ID}`
+ `&scope=chat:write,commands&state=${state}&redirect_uri=${REDIRECT}`;
res.redirect(url);
});
Validate state — this is CSRF protection
The state parameter is not optional decoration. It’s how you prove the callback corresponds to an install you initiated, not a forged request. Validate it on the way back and reject if it’s missing or unknown:
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query;
if (!state || !(await consumeState(state))) { // one-time use
return res.status(400).send('Invalid state');
}
const result = await app.client.oauth.v2.access({
client_id: CLIENT_ID, client_secret: CLIENT_SECRET,
code, redirect_uri: REDIRECT,
});
await storeInstallation(result);
res.send('Installed! You can close this tab.');
});
Consume the state (delete it) so it can’t be replayed. The model loves to skip this — it’s the single most important line to review.
Store tokens per team, encrypted
The oauth.v2.access response gives you a bot token scoped to one team. Store it keyed by team.id, encrypted at rest, never in plaintext or a log:
async function storeInstallation(res) {
await installs.put(res.team.id, {
teamId: res.team.id,
botToken: encrypt(res.access_token), // never store raw
botUserId: res.bot_user_id,
scopes: res.scope,
installedAt: Date.now(),
});
}
Then look up the right token per incoming event by team:
async function tokenForTeam(teamId) {
const i = await installs.get(teamId);
if (!i) throw new Error(`App not installed for team ${teamId}`);
return decrypt(i.botToken);
}
Pro Tip: Bolt has a built-in InstallationStore interface — implement storeInstallation and fetchInstallation and the framework handles per-team token lookup for you on every event. Use it rather than threading tokens through your handlers by hand; fewer places to leak a token.
Handle uninstalls and token revocation
When a workspace removes your app, Slack sends app_uninstalled (and tokens_revoked). Delete the stored token promptly — a stale token for an uninstalled workspace is both useless and a liability:
app.event('app_uninstalled', async ({ context }) => {
await installs.delete(context.teamId);
});
Failing to clean up means your store accumulates dead tokens, which is exactly the kind of thing a security review flags. Treat uninstall as a hard delete.
Scope minimally and re-consent deliberately
Every scope you request appears on the consent screen and expands what a leaked token can do. Request the minimum, and know that adding scopes later requires users to re-install (re-consent). I draft the scope list with AI and then cut it down, the same discipline I apply to manifests — the model always over-asks “to be safe.”
This install flow is the foundation for distributing an internal app across many teams, and it pairs with version-controlled app config so your manifest and your OAuth setup stay in sync.
Where AI helps and where you own it
The model writes the redirect plumbing, the token-store CRUD, and the uninstall cleanup quickly — genuinely useful, and I scaffold it with Cursor or Claude, refining the prompt in the prompt workspace. What stays human is the security spine: state validation, encryption at rest, per-team isolation, and prompt token cleanup on uninstall. Never paste a real client secret or bot token into the model, give it placeholders, and review the diff. Verify request signatures on every endpoint, including the events that drive uninstall cleanup — this app now lives in workspaces you don’t administer.
Conclusion
Serving more than one workspace means doing OAuth properly: a state-validated install flow, per-team encrypted token storage, and prompt cleanup on uninstall. Request minimal scopes, use Bolt’s InstallationStore so you’re not threading tokens by hand, and treat every security step as non-negotiable. Let AI scaffold the flow fast while a human secures it before the first real install. More in the Slack category, and starter prompts for the OAuth boilerplate.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.