The server
slack_server.py
OAuth2Server.discover("https://slack.com") is the wrong move here. Sign in with
Slack is a separate OpenID Connect surface with its own token endpoint
(openid.connect.token), and it issues an identity token — not the workspace
token the packs’ tools authenticate with. Declare the constant.
Two shapes of Slack app
Token rotation is opt-in per app, and it changes which credential provider you want:
With rotation off there is nothing to refresh, and
OAuth2Client will refuse to
be built without a refresh token — correctly, since a client that can never
refresh is a footgun:
slack_bot_token.py
slack_client.py
$SLACK_BOT_TOKEN is also the pack’s fallback: with no configure() call at
all, the tools read it directly. That is the shape for a script, not for a
product.
Scopes
The pack declares eleven bot scopes —chat:write, the four *:read and four
*:history scopes conversations need, users:read and reactions:write:
slack_scopes.py
search_messages needs a user token. Slack’s search.messages cannot be
called with a bot token at all; it wants a user token carrying search:read.
That scope is not in slack.SCOPES, and scopes_for(slack.TOOLS) reports the
pack’s bot scopes for every tool including that one. Ask for it explicitly, or
accept that one of the eighteen tools will fail with not_allowed_token_type.
Slack splits scopes across two parameters. scope asks for bot scopes;
user-token scopes go in a separate user_scope. authorize() puts your scopes
in scope, so the user half travels as a vendor parameter:
slack_connect.py
user_scope can live in the server’s authorization_params instead,
beside the rest of the vendor lore.
Getting the first grant
The route, the session andstate are yours. What is
Slack-specific is reading the response, because the token you want may not be
the one at the top level:
slack_callback.py
grant.access_token is the bot token. The user token — the one
search_messages needs — is nested under authed_user, along with its own
scopes and, under rotation, its own refresh token. The two refresh
independently; whichever refresh token you store is the one you get back.
On an app without rotation, exchange() raises: no refresh token came back and
the default expect_refresh_token=True says that is the silent-death case. Here
it is not, so say so — flow.exchange(code, expect_refresh_token=False) — and
hold the result in a StaticTokenProvider.
Refresh behaviour
Rotation invalidates the token you just used. Slack returns a new refresh token on every refresh and kills the old one, so a refresh token you failed to persist is a dead integration at the next refresh.on_refresh is where that
write goes, and it is not optional on a rotating app.
Concurrency is the trap rotation brings. Twelve parallel tool calls each
firing their own refresh would poison eleven of them. One OAuth2Client
serialises refreshes across those calls; what it cannot see is your other
process holding the same refresh token. Across processes, your store is the
shared cache — one writer through on_refresh, readers rebuilding the client
from the stored value.
Slack reports token failures inside a 200. oauth.v2.access answers a bad
code with HTTP 200 and {"ok": false, "error": "invalid_code"}. Charter checks
the error field regardless of status, so it raises CredentialError like any
other server — the same rule the pack’s envelope applies to
the tools themselves.
Reinstalling replaces the grant. A workspace reinstall issues fresh tokens;
the previous refresh token stops working, and the symptom is invalid_grant
against a user who just clicked “Allow”. Re-run the callback path that stores
the grant on every install, not only the first.
One thing this page has not verified against the live server: whether Slack acts
on PKCE. Charter sends code_challenge by default, and Slack does not document
oauth.v2.access as verifying a code_verifier. If your install flow rejects
the extra parameters, flow.authorize(..., pkce=False) exists for that; state
is your CSRF defense either way.
Where the rest is
- Getting the grant — the route, the session,
state, PKCE - Authorization servers — discovery, and what varies between servers
- Packs — the ten Slack tools and what they trim