slackapi / bolt-python

A framework to build Slack apps using Python
https://slack.dev/bolt-python/
MIT License
1.02k stars 236 forks source link

Bolt Slack API is very unclear on multiple workspace install #1076

Closed richarddli closed 3 weeks ago

richarddli commented 2 months ago

I'm creating a multiple workspace Slack bot using Bolt Python.

The documentation is very clear about the OAuth flow: https://slack.dev/bolt-python/concepts#authenticating-oauth. everything points to this. However, the documentation says nothing about what happens after you get the bot access token.

With a great amount of research, I found that the BoltJS (not Python) has more context. https://slack.dev/bolt-js/concepts#authenticating-oauth. In particular, it references an InstallationStore. So, I Google some more and end up finding the Slack SDK page for Token Lookup: https://slack.dev/python-slack-sdk/oauth/index.html#token-lookup.

Based on the design pattern on this page, it seems to suggest that I instantiate a new WebClient for each request with its own token. It's not clear to me if this is the appropriate design pattern for Bolt apps, either.

The page URLs

I'd love to see some sort of example (can't find it) and a link to how you build a multiple workspace app somewhere here: https://slack.dev/bolt-python/concepts#authenticating-oauth. At a minimum, parity with the JS docs would be helpful. But honestly I'm still confused about the appropriate design pattern and if I'm supposed to create a new AsyncApp with every request with its own access token, or if there's a better way.

hello-ashleyintech commented 2 months ago

Hi, @richarddli! Thank you for submitting this issue 😄

Bolt Python has built-in OAuth capabilities, as mentioned in the doc that you linked. Essentially, to enable OAuth on the app side, you'll need to initialize your app similar to this, taken from here:

app = App(
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
    installation_store=FileInstallationStore(),
    oauth_settings=OAuthSettings(
        client_id=os.environ.get("SLACK_CLIENT_ID"),
        client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
        scopes=["app_mentions:read", "channels:history", "im:history", "chat:write"],
        user_scopes=[],
        redirect_uri=None,
        install_path="/slack/install",
        redirect_uri_path="/slack/oauth_redirect",
        state_store=FileOAuthStateStore(expiration_seconds=600),
        callback_options=CallbackOptions(success=success, failure=failure),
    ),
)

As you mentioned, for Bolt Python OAuth, you do need some sort of data store solution to store all the credentials for each download of the app to a new workspace. Then, you need to point your app config to use the data store so it knows where to put the credentials for each new download.

In the above example, the code is using a local file as a store with one of our built-in InstallationStore options, but we also have [examples] (https://github.com/slackapi/bolt-python/tree/main/examples) in this repo for another type of built-in store we offer, sqlite3. I would recommend exploring one of our built-in InstallationStore options for you to be able to store the data you need. These docs provide more insight into built-in installation stores we offer.

The above is all you need to get started with OAuth - Bolt handles the rest. You can test installing to multiple workspaces once it's set up using the /slack/install path.

I hope this helps, let me know if you have additional questions! 🙌

richarddli commented 2 months ago

Hi @hello-ashleyintech! Thanks for the prompt response. Yes, I figured all the above out. The other thing that isn't mentioned is how to write your Slack event handler to handle multiple workspaces. This functionality does not seem to be supported directly by Bolt. The Token Lookup page has some hints. Here's where I ended up:

@app.message('hello')
async def message_hello(client, context):
    org_id = context['team_id']
    bot_token = get_access_token(org_id)
    await client.chat_postMessage(
       token=bot_token,
       channel=context['channel_id'],
       text=f'Hey there!'
    )

Is this how it's done? Or is there a better way? Example code is fine. I just couldn't find how to do this in the docs.

hello-ashleyintech commented 2 months ago

Hi, @richarddli!

I believe you can just do the following:

await client.chat_postMessage(
       channel=context['channel_id'],
       text=f'Hey there!'
)

Bolt should take a look at the incoming event, verify the installation instance passed into the request via the InstallationStore, and then if all looks good and it has access to the specified channel in the workspace, it'll post it in there.

You can view an example of this here with client.views_open. As you can see, they're not passing in any token, just the other needed params for the API call.

Let me know if you have additional questions!

richarddli commented 1 month ago

Hi @hello-ashleyintech. Thanks for all your help. I've gotten most (?) of this working, but I have one more error I can't figure out.

  1. I've created an OAuth settings that is identical to what you have above, with slightly different scopes:
oauth_settings = AsyncOAuthSettings(
    client_id=os.environ.get("SLACK_CLIENT_ID"),
    client_secret=os.environ.get("SLACK_CLIENT_SECRET"),
    scopes=["app_mentions:read", "channels:history", "im:history", "groups:history", "mpim:history", "chat:write"],
    user_scopes=[],
    redirect_uri=None,
    redirect_uri_path="/slack/oauth_redirect",
    install_path="/slack/install",
    state_store=FileOAuthStateStore(expiration_seconds=300),
    callback_options=CallbackOptions(success=success, failure=failure),
    )
  1. I then register handlers for all of this:
@fastapi_app.post("/slack/events")
async def slack_events(request: Request):
    return await slack_handler.handle(request)

@fastapi_app.get("/slack/install")
async def install(request: Request):
    return await slack_handler.handle(request)

@fastapi_app.get('/slack/oauth_redirect')
async def oauth_redirect(request: Request):
    return await slack_handler.handle(request)
  1. When I go to /slack/install in my browser, I get the install button, which tells me the Slack handler is working. I then initiate the OAuth flow and try to install into my test workspace.

  2. On the redirect to /slack/oauth_redirect, I see an "invalid browser" error message in my browser. My logs show:

INFO:     108.7.77.168:0 - "GET /slack/oauth_redirect?code=6052573369618.7107765928723.4943723e88b5ac78ed48f5e0a98d4346b85af9ec1ffb57e3dbd77f000962b88c&state=fa114bbc-36c4-43c2-acba-516a1a262eea HTTP/1.1" 400 Bad Request

I've verified that my client secret and client ID are correct, and read by the app itself. I also was able to get a successful access_token by taking the code value returned in the URL and sending it in via curl. I'm sure I'm missing something, but a bit mystified as to what?

hello-ashleyintech commented 1 month ago

Hi, @richarddli! 👋 Sorry to hear you're still running into some issues.

As a starting point for troubleshooting the Invalid browser error, I would recommend taking a look at these two potentially related issues: https://github.com/slackapi/bolt-python/issues/179 and https://github.com/slackapi/bolt-python/issues/492. Let me know if following the recommendations in these issues end up solving the error!

Kudratullah commented 1 month ago

Anyone has any idea how to solve the same issue with bolt-js sdk

richarddli commented 3 weeks ago

I'm going to close this issue. I do think the documentation for Python can be improved, because there's quite a bit of magic going on behind the scenes that is only obvious once you read the source (which is what I had to do).