Authenticate a Slack Agent
You are building a Slack bot that calls Willow tools. A mention arrives, and before you can do anything you have to decide which Willow identity the tool calls should run as. This page walks that decision with real requests.
If instead you want Willow's own Slack app to trigger background agents for you, that is already built — see Connect the Willow Agent Slack app. This page is for a Slack app you write yourself.
Step 1: Resolve the initiator to an email
A Slack event tells you who triggered it, but only as an opaque workspace ID:
{
"type": "app_mention",
"user": "U08DSCDTF5Z",
"channel": "C0123456789",
"text": "<@U0BOTBOT> what's the status of ticket 4182?"
}
Willow identifies people by email, so exchange that ID with Slack's users.info:
curl -s "https://slack.com/api/users.info?user=U08DSCDTF5Z" \
-H "Authorization: Bearer xoxb-your-bot-token"
{
"ok": true,
"user": {
"id": "U08DSCDTF5Z",
"team_id": "T0123456789",
"is_bot": false,
"is_restricted": false,
"profile": {
"real_name": "Dana Cohen",
"email": "dana@example.com"
}
}
}
The profile.email field requires the users:read.email bot scope. Without a valid token the call returns {"ok":false,"error":"not_authed"}, so if you are testing by opening the URL in a browser, that response is expected.
Now you have dana@example.com and a decision to make.
Step 2: Choose a route
| Route | The user experiences | Tool calls run as | Choose it when |
|---|---|---|---|
| Impersonate | Nothing — the bot just answers | Dana | You trust your own app enough for Willow to treat its word as an admin's. |
| OTT | A one-time DM: "sign in to continue" | Dana | You would rather Dana prove it herself, and you are fine asking once per session. |
| Background agent | Nothing — the bot just answers | The agent | The work is not really on Dana's behalf. |
Once you know it is dana@example.com, a backend that can obtain an Okta or Keycloak JWT for her has a strictly better option than impersonation: send that JWT and let Willow verify it. Same zero-friction experience, but the identity is proven rather than asserted. See Auth Exchange.
Before either user-facing route: verify the event
Both impersonation and OTT turn a Slack event into a claim about a person, so the event itself has to be trustworthy first.
- Verify Slack's request signature on every event, using the
x-slack-signatureandx-slack-request-timestampheaders. Without this, anyone who learns your endpoint URL can POST a fakeapp_mentionnaming any user ID they like — and with impersonation, that is a direct path to acting as your CEO. - Reject guests and outsiders. Check
is_restrictedandis_ultra_restrictedfor guest accounts,is_botfor other bots, and compareteam_idagainst your own workspace so Slack Connect users from another organization cannot trigger the bot. - Ignore your own bot's messages so a reply cannot loop back in as a new trigger.
Route 1: Impersonate
Send an API token with the run:impersonate scope and name the user in a header. Willow acts as Dana with no sign-in and no interaction:
curl -X POST https://your-org.mcp-s.com/mcp \
-H "Authorization: Bearer wxt_xxxxx" \
-H "x-user-email: dana@example.com" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Dana sees only an answer, and the audit log attributes the call to her. This is the best experience available and the largest amount of trust you can hand a piece of your own code.
What you are accepting: your bot has become an authentication system. The token can act as any user in the organization, so the code path from Slack event to x-user-email is now the thing standing between an attacker and every account you have.
Two Slack-specific risks are worth naming:
- Slack email is only as trustworthy as the workspace. If email is not managed by your IdP through SSO or SCIM, a user can change their own Slack account email. Confirm your workspace is IdP-managed, or keep your own mapping from Slack user ID to Willow user and do not trust
profile.emailat request time. - Willow cannot see any of this. From the gateway's side there is no difference between your carefully verified event and a forged one. Every control lives in your code.
Keep the token server-side, grant it only run:impersonate, and use a token dedicated to this bot so revoking it does not take down your other integrations.
Route 2: OTT
If you would rather not hold that much trust, have Dana sign in — once — and act as her for a bounded window afterward. Your bot mints a token, gets a sign-in URL for it, DMs her the link, and polls until she completes SSO.
# 1. Mint a token and ask for a sign-in URL
OTT=$(openssl rand -hex 32)
curl -X POST https://your-org.mcp-s.com/mcp/generate-auth-url \
-H "Authorization: $OTT" \
-H "Content-Type: application/json" \
-d '{"email":"dana@example.com"}'
# { "data": { "url": "https://your-org.connect.example.com/org/auth?token=..." } }
# 2. DM that URL to Dana, then poll. Send the bare token, not accessKey:token.
curl https://your-org.mcp-s.com/mcp/get-access-token \
-H "Authorization: $OTT"
# 401 until she signs in, then { "data": { "userAccessKey": "..." } }
# 3. Call tools as Dana
curl -X POST https://your-org.mcp-s.com/mcp \
-H "Authorization: $USER_ACCESS_KEY:$OTT" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Store the userAccessKey and token against Dana's Slack user ID and reuse them until they expire — 24 hours by default. After that the next mention starts with another DM. That expiry is the point: access is time-boxed, and a bot compromised later holds credentials that die on their own.
The email in step 1 is the one you resolved from Slack, and it pins the link to Dana: Connect requires the person who completes SSO to be that user, so the sign-in fails for anyone else. Unlike impersonation, naming her email proves nothing on its own — she still has to sign in, and your bot never sees her access key until she does.
Deliver the link as a direct message anyway, never into the channel. And if you omit the email, the token binds to whoever completes the sign-in: the first person to click a link that leaked into a channel becomes the identity your bot uses for Dana's Slack account, and the poll response tells you only that somebody signed in.
Because your code proposes the token itself, there is nothing server-issued to persist: no client registration, no refresh token, no redirect endpoint to host. A bot with no database can hold the pair in memory and re-run the DM flow after a restart.
Route 3: Background agent
Sometimes the human who typed the message is not the right identity at all. A nightly digest, a channel-wide triage bot, or a bot reacting to a GitHub webhook is doing its own work — attributing it to whoever happened to mention it is misleading in the audit log and gives the bot whatever access that person has.
In that case give the bot its own identity:
curl -X POST https://your-org.mcp-s.com/mcp \
-H "Authorization: Bearer <access_key>:<secret>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
A background agent is the right shape when you want Willow to hold the definition — the system prompt, an explicit set of tools, triggers, and session history. A machine user is the leaner option if you only want a credential and would rather grant tools through group membership.
Either way, every Slack user gets identical access, so scope the agent to the least any of them should have. Nothing in a public channel should be able to reach a tool you would not expose to the whole channel.
If this is the route you want, check whether you need to build it at all: Willow's Slack app already listens in channels and starts background agents from triggers.
Putting it together
A reasonable default for a bot serving employees: verify the Slack signature, resolve the email, and use auth exchange if your backend can produce a JWT. If it cannot, use OTT — the one-time DM is a small price for not making your Slack app an identity provider. Reserve impersonation for a bot running on infrastructure you would trust with an admin credential, and use a background agent whenever the work is not really on anyone's behalf.
Related
- How Agents Authenticate to Willow — the full set of options and how to choose
- User Session Authentication — the OTT flow in detail
- Auth Exchange
- API Tokens — creating the
run:impersonatetoken - Background Agents
- Set up Slack for background agents — the prebuilt alternative