Understanding OAuth scopes for Bots

By Steve Gill

Published: February 6, 2020

In this guide, we will explore Slack app permissioning and distribution using OAuth. Along the way, you will also learn how to identify which scopes your app needs and use OAuth to request them.

We're building an app that will send a direct message to users that join a specific channel. It's called the Welcome App! Once installed in a workspace, it will create a new channel named #the-welcome-channel if it doesn’t already exist. The channel will be used to thank them for joining the channel, but we're sure you'll bring your own use cases like describing channel etiquette.

This app is written in Python with our Python SDK. I will be sharing code snippets from the app throughout the guide, but you can view the full source code on GitHub. The code and implementation of OAuth is general enough that you should be able to follow, even if Python isn't your preferred language.

Before we get started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and create one. You also need to create a new app if you haven’t already. Alright, let’s get started.


Scopes

Scopes are used to grant your app permission to perform functionality in Slack, like call Web API methods and receive Events API events. As a user is taken through your app’s installation flow, they will need to permit access to the scopes your app is requesting.

Previously, Slack offered a bot scope which requested a wide variety of permissions and abilities. This meant your app was most likely requesting permissions it didn’t need. This leads to users not wanting to install your app for privacy or security concerns. Now, Slack allows apps to select only the smaller scopes the app requires to function. This makes it more likely that users will install your app.

To determine which scopes we need for our app, we should take a closer look at what our app does. Personally, instead of scouring the entire list of scopes that might make sense for my app, I look at what events or methods I need for my app and build out my scope list as I go.

  1. On installation, our app checks to see if a channel exists (private or public as we can’t create a new channel with the same name). A quick search through the list of methods leads me to conversations.list, which I can use to get the names of public & private channels. It also shows me what scopes are needed to use this method. In our case, we need channels:read and groups:read. (we don’t need im:read & mpim:read as we aren’t concerned about names of direct messages)

    # verifies if "the-welcome-channel" already exists
    def channel_exists():
    token = os.environ["SLACK_BOT_TOKEN"]
    client = slack.WebClient(token=token)
    
    # grab a list of all the channels in a workspace
    clist = client.conversations_list()
    exists = False
    for k in clist["channels"]:
        # look for the channel in the list of existing channels
        if k['name'] == 'the-welcome-channel':
        exists = True
        break
    if exists == False:
        # create the channel since it doesn't exist
        create_channel()
    
  2. If the channel doesn’t already exist, we need to create it. Looking through the list of methods leads me to conversations.create which needs the scope channels:manage.

    # creates a channel named "the-welcome-channel"
    def create_channel():
    token = os.environ["SLACK_BOT_TOKEN"]
    client = slack.WebClient(token=token)
    resp = client.conversations_create(name="the-welcome-channel")
    
  3. When a user joins our newly created channel, our app sends them a direct message. To see when a user joins our channel, we need to listen for an event. Looking at our list of events, we see that member_joined_channel is the event that we need (Note: events need to be added to your app’s configuration through api.slack.com/apps). The scopes required for this event are channels:read and groups:read (same ones from step one). Now to send a direct message, we need to use the chat.postMessage method which requires the chat:write scope.

    # Create an event listener for "member_joined_channel" events
    # Sends a DM to the user who joined the channel
    @slack_events_adapter.on("member_joined_channel")
    def member_joined_channel(event_data):
    user = event_data['event']['user']
    token = os.environ["SLACK_BOT_TOKEN"]
    client = slack.WebClient(token=token)
    msg = 'Welcome! Thanks for joining the-welcome-channel'
    client.chat_postMessage(channel=user, text=msg)
    

So our final list of scopes required are channels:read, groups:read, channels:manage, and chat:write.

Setting up OAuth & Requesting Scopes

If you are new to developing Slack apps, you may not have had to implement OAuth yet, and have instead been using our Basic App Setup. This works if you only intend to install the app on a single workspace. But if you want users to be able to install your app on additional workspaces or from the App Directory, you must implement OAuth.

I will be following the general flow of using OAuth with Slack which is covered on our API site and nicely illustrated in the image below:

slack_oauth_flow_diagram.png

  1. Asking for Scopes

    This first step is sometimes also referred to as redirect to Slack or Add to Slack button. In this step, we redirect to Slack and pass along our list of required scopes, client ID and state as query parameters in the URL. You can get the client ID from the Basic Information section of your app. State is an optional value, but is recommended to prevent CSRF attacks.

    https://slack.com/oauth/v2/authorize?scope=channels:read,groups:read,channels:manage,chat:write&client_id=YOUR_CLIENT_ID&state=STATE_STRING
    

    Note: it is also possible to pass in a redirect_uri into your URL. A redirect_uri is used for Slack to know where to send the request after the user has granted permission to your app. In our example, instead of passing one in the URL, we request that you add a Redirect URL in your app’s configuration on api.slack.com/apps under the OAuth and Permissions section.

    Next, we are going to create a route in our app that contains an Add to Slack button using that url above.

    # Grab client ID from your environment variables
    client_id = os.environ["SLACK_CLIENT_ID"]
    # Generate random string to use as state to prevent CSRF attacks
    from uuid import uuid4
    state = str(uuid4())
    
    # Route to kick off Oauth flow
    @app.route("/begin_auth", methods=["GET"])
    def pre_install():
      return f'<a href="https://slack.com/oauth/v2/authorize?scope=channels:read,groups:read,channels:manage,chat:write&client_id={ client_id }&state={ state }"><img alt=""Add to Slack"" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>'
    

    Now when a user navigates to the route, they should see the Add to Slack Button.

    add_to_slack.png

    Clicking the button will trigger the next step.

  2. Waiting for the user to approve your request scopes

    The user will see the app installation UI (shown below) and have the option to accept the permissions and allow the app to install to the workspace:

    add_to_slack.png

  3. Exchanging a temporary authorization code for an access token

    After the user approves the app (step 2), Slack will redirect the user to your specified Redirect URL. As we mentioned in step 1, we did not include a redirect_uri in our Add to Slack Button. So our app will use our Redirect URL specified in the app’s OAuth and Permissions page.

    Our Redirect URL function will have to parse the HTTP request for the code and state query parameters. We need to check that the state parameter was created by our app. If it is, we can now exchange the code for an access token. To do this, we need to call the oauth.v2.access method with the code, client id and client secret. oauth.v2.access will return the access token which we can now save (preferably in a persistent database) and use for any of the Slack method calls we make. (Note: we need to use this access token for all of the Slack method calls we covered in the Scopes section above)

    # Grab client Secret from your environment variables
    client_secret = os.environ["SLACK_CLIENT_SECRET"]
    
    # Route for Oauth flow to redirect to after user accepts scopes
    @app.route("/finish_auth", methods=["GET", "POST"])
    def post_install():
    # Retrieve the auth code and state from the request params
    auth_code = request.args['code']
    received_state = request.args['state']
    
    # An empty string is a valid token for this request
    client = slack.WebClient(token="")
    
    # verify state received in params matches state we originally sent in auth request
    if received_state == state:
        # Request the auth tokens from Slack
        response = client.oauth_v2_access(
        client_id=client_id,
        client_secret=client_secret,
        code=auth_code
        )
    else:
        return "Invalid State"
    
    # Save the bot token to an environmental variable or to your data store
    os.environ["SLACK_BOT_TOKEN"] = response['access_token']
    
    # See if "the-welcome-channel" exists. Create it if it doesn't.
    channel_exists()
    
    # Don't forget to let the user know that auth has succeeded!
    return "Auth complete!"
    

Next Steps

At this point you should feel more comfortable learning what scopes your app needs and about using OAuth to request those scopes. A few resources you should check out next include:

  • Slack-Python-OAuth-Example - We used code snippets from this app in this tutorial. The README contains more detailed information about running the app locally using ngrok and setting up a redirect URL for OAuth as well as a request URL for events.
  • Learn more about OAuth with Slack. This also covers how you could use the user_scope parameter to get a user token for instances when you want your app to act on behalf of a user.
  • Review the list of scopes_ _available.
  • Review the list of events types available. Most events require specific scopes to work.

Questions? Comments? Contact our developer support at @SlackAPI or support@slack.com.

Related documentation