slackapi / bolt-python

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

Question regarding implementation of Socket Mode while being installed to multiple workspaces without hosting OAuth in the App #834

Closed ryanandonian closed 1 year ago

ryanandonian commented 1 year ago

I'm developing an App that will be deployed to several workspaces (via the "Add to Slack" button OAuth flow), and is not planned to be distributed and listed publicly via the public app directory. Our OAuth is handled in a separate service, which writes the OAuth tokens+some extra data to a storage layer that is accessible to this App. The App ideally will not have any publicly accessible ingress as it will be deployed inside a VPC for security reasons (but it can reach out to Slack's APIs as well as this storage layer).

I'm trying to figure out the best way to leverage the Slack Bolt SDK for Python since it handles a lot of the complexities for us, but it seems like a bot token is required for SocketModeHandler initialization (by virtue of requiring a slack_bolt.App instance).

# from https://api.slack.com/apis/connections/socket#sdks
# Install the Slack app and get xoxb- token in advance
app = App(token=os.environ["SLACK_BOT_TOKEN"])

if __name__ == "__main__":
    SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start()

Since this app will be installed to multiple workspaces, it doesn't seem right to use a Bot Token here to initialize the App which is passed to the SocketModeHandler, since it seems that a Bot Token is generated on a per-workspace level (eg: a bot token installed to WorkspaceA will not work with WorkspaceB, and visa versa). It seems like the authorize and installation_store parameters are built for this use case

# for multi-workspace apps
authorize: Optional[Callable[..., AuthorizeResult]] = None,
installation_store: Optional[InstallationStore] = None,

Right now I'm able to forego the BotToken use by building my App with the OAuthSettings, but that seems to assume the app will service OAuth (and uses the default InstallationStores), but I get what seems like an expected error when receiving new events:

ERROR:slack_bolt.MultiTeamsAuthorization:Although the app should be installed into this workspace, the AuthorizeResult (returned value from authorize) for it was not found.

If I'm reading this correctly, this error pops up because there are no existing Installations for the given users as it uses the default installation_store, which is always empty since nothing I'm writing is populating it.

It seems like I'll need to do one of two things:

  1. Use the lower level SDK (slack_sdk, or a custom websocket using the response from apps.connections.open )
  2. Provide the OAuthSettings a custom InstallationStore which it can read/write Token updates to for the connected token storage

Option 2 seems like the ideal path forward, but I am a little unsure given most of the documentation seems to be built around "If you run OAuth inside this app, there's a lot of automatic stuff you get" and I can't seem to find much in terms of "if OAuth is handled elsewhere, here's how you integrate with it".

The slack_bolt version

slack-bolt==1.16.2
slack-sdk==3.19.5

Python runtime version

Python 3.11.1
hello-ashleyintech commented 1 year ago

Hi, @ryanandonian! Thank you for your question! 🙌

With regards to using OAuth and Socket Mode, we have an example of that which can be seen here.

In terms of making sure your app can correctly track and recognize installations, the second option you provided is a good starting point. You can try to create a custom InstallationStore and StateStore that you can pass into your initialized OAuth Settings (example here).

If you find that the above still isn't working for your use case, let me know and feel free to provide more details and we can dive into this deeper.

ryanandonian commented 1 year ago

Thanks for your help so far @hello-ashleyintech . I've got a start on the custom InstallationStore, but I just want to confirm something before getting further-

If I'm reading this correctly, the first check here is just "has anyone from this Team done an Installation?" https://github.com/slackapi/bolt-python/blob/e580fda97e2ed3d9905f4e10f3821bf05049035b/slack_bolt/authorization/authorize.py#L166-L170

and then slightly down the line here - https://github.com/slackapi/bolt-python/blob/e580fda97e2ed3d9905f4e10f3821bf05049035b/slack_bolt/authorization/authorize.py#L183, this is then continuing to fetch the user-specific token from the InstallationStore if it exists?

Translating this into a more generic InstallationStore implementer logic, there may be several different valid cases for find_installation:

And just to clarify one thing so I'm understanding things clearly - since Bot Tokens are not tied to a user, is that why there's the "find the team and grab a bot token" check first before fetching a user's token?

seratch commented 1 year ago

@ryanandonian Yes, your understanding seems to be correct 👍

ryanandonian commented 1 year ago

Thank you for all your quick help @hello-ashleyintech and @seratch ! I have my custom InstallationStore now hooked up to our "off app" database and it's consuming user_status_changed events and using the proper tokens when responding to the app_home_opened events. There's a sufficient gap between these services to where there's no requirement for HTTP/s Ingress, which is one of the components of our security reqs here (since this app can read and write Slack users, we wanted to have a large gap between the wild west of the internet and this service).

In case I did a bad job explaining how exactly this is set up (or for anyone in the future who's finding this page), here's a rough diagram of how the "separate services" will be deployed, where that "OAuth Flow Control" piece down at the bottom is a sort of "generalized OAuth handler system" that handles OAuth flows for multiple 3rd party providers, one of which is now Slack.

image