# Bot and user scopes with least privilege

*Slack Developer Study Guide · Design for Security · Last updated October 21, 2026 · https://www.certifyforce.com/study-guide/slack-developer/design-for-security/bot-and-user-scopes-with-least-privilege*

Picking the least permissive scope for each task: the granular scopes behind common methods (join, read history, post, look up emails, react, slash commands), which scopes only user tokens can hold, why scopes accumulate across reinstalls, Sign in with Slack's separate flow, and how Marketplace review judges a scope list.

- Certification: Salesforce Slack Developer
- Exam section: Design for Security
- Study guide: https://www.certifyforce.com/study-guide/slack-developer

**Official exam objective:**

> Request the appropriate scopes for your app's use case.

## What the exam expects

- Map a task to the one granular scope that unlocks its method: joining, reading public or private history, posting, looking up users by email, reacting, adding slash commands.
- Tell bot scopes from user scopes, and spot scopes that only a user token can hold.
- Explain why a reinstall never removes scopes and how to read a token's current scopes at runtime.
- Keep Sign in with Slack in its own OpenID flow.
- Apply Marketplace scope practice: least privilege, a usage reason per scope, nothing for future features.

Key terms: **scope** (a permission granted to a token at install); **bot scope** (held by the app's bot user, acting with its own identity); **user scope** (held by a user token, acting as the installer); **least privilege** (the smallest set of least permissive scopes that still works); **`x-oauth-scopes`** (response header listing the calling token's scopes).

## Key facts

| Fact | Detail | Source |
|---|---|---|
| `channels:join` | Bot (and legacy bot) only; unlocks `conversations.join` | [channels:join](https://docs.slack.dev/reference/scopes/channels.join) |
| `chat:write.public` | Post in public channels without joining; needs `chat:write` | [chat:write.public](https://docs.slack.dev/reference/scopes/chat.write.public) |
| Private channel posting | Only while the app is a member | [chat.postMessage](https://docs.slack.dev/reference/methods/chat.postMessage) |
| Public vs private history | `channels:history` vs `groups:history`, channels the app was added to | [channels:history](https://docs.slack.dev/reference/scopes/channels.history), [groups:history](https://docs.slack.dev/reference/scopes/groups.history) |
| Email lookup | `users:read.email`, requested together with `users:read` | [users:read.email](https://docs.slack.dev/reference/scopes/users.read.email) |
| Reactions | `reactions:write` for `reactions.add`/`reactions.remove` | [reactions:write](https://docs.slack.dev/reference/scopes/reactions.write) |
| Custom name and icon on posts | `chat:write.customize` (with `chat:write`) for `username`, `icon_url`, `icon_emoji` | [chat.postMessage](https://docs.slack.dev/reference/methods/chat.postMessage) |
| File upload | `files:write` for both upload methods | [files.getUploadURLExternal](https://docs.slack.dev/reference/methods/files.getUploadURLExternal) |
| Slash commands and shortcuts | `commands`, bot token | [commands](https://docs.slack.dev/reference/scopes/commands) |
| User-only scopes | `search:read`, `users.profile:write` | [search:read](https://docs.slack.dev/reference/scopes/search.read), [users.profile:write](https://docs.slack.dev/reference/scopes/users.profile.write) |
| Current scopes of a token | `x-oauth-scopes` header on every Web API response | [Installing with OAuth](https://docs.slack.dev/authentication/installing-with-oauth/) |
| Removing a scope | Impossible without revoking the token | [Installing with OAuth](https://docs.slack.dev/authentication/installing-with-oauth/) |
| Sign in with Slack | `/openid/connect/authorize`, scopes `openid`, `email`, `profile` | [Sign in with Slack](https://docs.slack.dev/authentication/sign-in-with-slack/) |

## Check yourself

- Why can't a reinstall with a shorter scope list shrink a token?
- When should an app request a user scope instead of a bot scope?
- Which two scopes does `users.lookupByEmail` need?
- Why does a combined sign-in and install request fail with `invalid_scope`?

## Sources

- [Scopes](https://docs.slack.dev/reference/scopes/)
- [Installing via OAuth authorization code flow](https://docs.slack.dev/authentication/installing-with-oauth/)
- [Submission guidelines and requirements](https://docs.slack.dev/slack-marketplace/slack-marketplace-app-guidelines-and-requirements/)
- [chat.postMessage method](https://docs.slack.dev/reference/methods/chat.postMessage)
- [channels:join scope](https://docs.slack.dev/reference/scopes/channels.join)
- [chat:write.public scope](https://docs.slack.dev/reference/scopes/chat.write.public)
- [chat:write.customize scope](https://docs.slack.dev/reference/scopes/chat.write.customize)
- [channels:history scope](https://docs.slack.dev/reference/scopes/channels.history)
- [groups:history scope](https://docs.slack.dev/reference/scopes/groups.history)
- [channels:read scope](https://docs.slack.dev/reference/scopes/channels.read)
- [users:read.email scope](https://docs.slack.dev/reference/scopes/users.read.email)
- [reactions:write scope](https://docs.slack.dev/reference/scopes/reactions.write)
- [commands scope](https://docs.slack.dev/reference/scopes/commands)
- [search:read scope](https://docs.slack.dev/reference/scopes/search.read)
- [users.profile:write scope](https://docs.slack.dev/reference/scopes/users.profile.write)
- [files.getUploadURLExternal method](https://docs.slack.dev/reference/methods/files.getUploadURLExternal)
- [files.completeUploadExternal method](https://docs.slack.dev/reference/methods/files.completeUploadExternal)
- [Using Sign in with Slack](https://docs.slack.dev/authentication/sign-in-with-slack/)

## About the full unit

The "Core concepts", "Exam traps" and "Worked scenario" sections of this unit, its 11 linked practice questions, and unlimited timed mock exams are included with the Slack Developer pack (USD $19): https://www.certifyforce.com/practice-exams
