Findings
Slack OAuth: user tokens need `user_scope`, nest under `authed_user`, and errors hide in 200s
Date: 2026-06-11
Finding#
Wiring the Slack plugin to act as the authorizing user (not as a bot) tripped three Slack-specific deviations from vanilla OAuth 2.0:
Scope param split. Slack reads the standard
scopequery param as bot scopes and issues anxoxb-bot token. User scopes must go in a separateuser_scopeparam to get anxoxp-user token. Generic OAuth code that always setsscopesilently builds a bot integration.Token nesting.
oauth.v2.accessreturns the bot token at the top-levelaccess_token; the user token is nested atauthed_user.access_token(with its ownrefresh_token/expires_inwhen token rotation is on). Generic exchange code that reads top-levelaccess_tokenstores the wrong token — orundefinedwhen no bot scopes were requested.HTTP 200 error envelope. Slack returns errors as
200 {ok:false, error}— aresponse.okcheck passes and the failure surfaces later as a stored garbage token.
Also: files.upload returns method_deprecated for Slack apps created after May 2025. Upload requires the 3-step external flow (files.getUploadURLExternal → POST bytes to pre-signed URL → files.completeUploadExternal). And search.all is user-token-only — it can never work on a bot token.
Resolution#
buildPluginAuthUrlsends scopes asuser_scopefor slack (packages/server/src/auth/plugin-oauth.ts)handlePluginCallbackextractsauthed_user.*and rejectsok:falsebodies for slack- Slack app config must declare the same scopes under
scopes.user(notscopes.bot) slack_upload_filemigrated to the external upload flow (packages/plugins/slack/tools/index.ts)