slackapi / bolt-python

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

OAuth multiple workspaces restrictions #1019

Closed RScicomp closed 8 months ago

RScicomp commented 8 months ago

Hello! First time user of slack bolt here! I have a question about restricting the app to certain workspaces - especially the ability to install a slack app to a workspace.

In the following example I see that the flask oauth_app: https://github.com/slackapi/bolt-python/blob/main/examples/flask/oauth_app.py

exposes an endpoint slack/install. I was wondering - does this mean that ANYONE can install the slack app to their workspace? Even if they are outside of the organization? Note i have not distributed my app.

Is there a way to ensure that only certain workspaces within the organization can use the slack app and is there a process/settings to do so?

seratch commented 8 months ago

Hi @RScicomp, thanks for asking the questions!

does this mean that ANYONE can install the slack app to their workspace?

When you enable the OAuth flow to run your own custom installation flow for multiple workspaces, anyone who knows the URL can install the app into a workspace.

Is there a way to ensure that only certain workspaces within the organization can use the slack app and is there a process/settings to do so?

To prevent people outside of your organization from installing your app, there are several approaches to go with:

  1. Require Sign in with Slack first and then allow the user to go through the app installation flow within the same web browser session; you can save the sign in info in the browser cookies and check the state before both redirecting the user and accepting the callback with code param; you can inherit the methods and do the valition before calling super.handle_xxx method
  2. Validate the installation result's enterprise_id or team_id and if it's an unknown workspace, immediately revoke the installation using auth.revoke API call with the issued OAuth access token; You can inherit this method and check the installation data returned by super.run_installation method
  3. Add some secret to the URL query string and/or allow a specfic range of client IP addresses (say, your corporate network only)

The second/third approach should be much easier to implement, but the first one could be the most robust.

I hope this helps.

RScicomp commented 8 months ago

Got it thanks! I'm just curious because i haven't distributed my slack app when I look at the app directory in the slack web browser and the description of the app i see:

This app is just for your workspace
Only people on <workspace> can view, install and use this app. This app was created by richard.​gao ([richard.​gao](https://<workspace>.slack.com/team/<teamid>)). Ask them for installation instructions or to add you as an app collaborator to install this app for yourself.

I just want to clarify: unless i hit the distribute button for my slack app no one besides those in my workspace can download it correct?

Also! Is there any point to having the oauth set up in my code if i'm just going to restrict it to a single workspace anyways?

seratch commented 8 months ago

I just want to clarify: unless i hit the distribute button for my slack app no one besides those in my workspace can download it correct?

Ah, sorry I overlooked the part in your description. When you don't make your app publicly distributed, indeed the app can be installed into your develop workspace only.

Also! Is there any point to having the oauth set up in my code if i'm just going to restrict it to a single workspace anyways?

Perhaps, this question no longer needs to be answered but if you want to do so after enabling the distribution, the three approaches I mentioned above are the options for you.

It seems that everything is clear now. Can we close this issue now?

RScicomp commented 8 months ago

got it thanks for clarifying - one final question: You mentioned that you would use the data from run_installation and revoke access I've written some code that seems to work is this what you meant?


def success(args: SuccessArgs) -> BoltResponse:
    # You can access the installation data from the args
    installation = args.installation
    if(args.installation.team_id != <team>):
        app.client.auth_revoke(token=installation.bot_token,test=false)
        return BoltResponse(status=200, body="Installation failed: Team ID does not match")
    else:
        # Then you can modify the response as needed
        return BoltResponse(status=200, body="Installation successful")

def failure(args: FailureArgs) -> BoltResponse:
    # Handle failure here
    return BoltResponse(status=200, body=f"Installation failed: {args.reason}")

# Create CallbackOptions instance
callback_options = CallbackOptions(success=success, failure=failure)

 app = App(
    ...
    oauth_settings=OAuthSettings(
        ...
        callback_options=callback_options
    )
RScicomp commented 8 months ago

Another question will this slack/install page still work if I choose to add a firewall with specific IPs? I've heard that slack IP addresses tend to vary so wonder if this might block installation from happening

seratch commented 8 months ago

Using callback_options works too but my primary suggestion is to inherit OAuthFlow class and override run_installation method to add an additional team_id validation. As I mentioned above, you can call super.run_installation and then check the team_id in the installation data. If it does not match, you can revoke the issued token and return None. Also, if you want to make the error page better, you can add your own callback_options.failure too.

As for the IP address topic, indeed Slack's server IP address is not static, but this is the case where your app receives a payload request such as Events API delivery from the Slack server. In this topic, we are discussing about the OAuth flow, right? The OAuth flow is a communication between your end-users (who are going to install your app) and your app. Thus, Slack's IP address does not matter in the case.

seratch commented 8 months ago

Technically, the configuration for IP address validation could be complicated because perhaps you want to serve both /slack/events and the URLs for OAuth under the same domain. You need to adjust the frontend settings to apply IP address restriction only to the OAuth URLs or serve the OAuth flow with a different load balancer.

RScicomp commented 8 months ago

Using callback_options works too but my primary suggestion is to inherit OAuthFlow class and override run_installation method to add an additional team_id validation. As I mentioned above, you can call super.run_installation and then check the team_id in the installation data. If it does not match, you can revoke the issued token and return None. Also, if you want to make the error page better, you can add your own callback_options.failure too.

Ahhh ok thanks I'm guessing this is more what you meant?

from typing import Optional 
from slack_sdk.oauth.installation_store import Installation

class OAuthFlowCustomRestricted(OAuthFlow):
    def __init__(self, *args, allowed_team_id, **kwargs):
        super().__init__(*args, **kwargs)
        self.allowed_team_id = allowed_team_id

    def run_installation(self, code: str) -> Optional[Installation]:
        installation = super().run_installation(code)
        if installation is not None and installation.team_id != self.allowed_team_id:
            self.client.auth_revoke(token=installation.bot_token,test=False)
            self.logger.warning(f"Installation attempted for unauthorized team")
            return None
        return installation
oauth_flow = OAuthFlowCustomRestricted(
    settings= OAuthSettings(
       ....
    ),
    allowed_team_id=<teamid>
)
app = App(
signing_secret=secret_client.get_secret('WorkleapSlackBotSlackSigningSecretDev').value,
oauth_flow=oauth_flow
)

As for the IP address topic, indeed Slack's server IP address is not static, but this is the case where your app receives a payload request such as Events API delivery from the Slack server. In this topic, we are discussing about the OAuth flow, right? The OAuth flow is a communication between your end-users (who are going to install your app) and your app. Thus, Slack's IP address does not matter in the case.

Awesome thanks for the clarification! Yes was talking about the oauthflow

seratch commented 8 months ago

Yes, you are right on both!

seratch commented 8 months ago

But if the allowed_team_id is a single workspace, you can simply hold off enabling the public distribution. With that, you don't need to customize your code in the way. Let me close this issue now, but please feel free to post a new issue whenever you get something unclear about bolt-python.