[Slack App] Implement OAuth flow for public installations

updated on 08 October 2024

Prerequisites for This Post:

 Series on how to build and monetize app for Slack: Introduction

To follow along, you should already have a basic Slack bot built using Node.js and Bolt.js, deployed to AWS Lambda, and interacting with DynamoDB.

If you haven’t set that up yet, check out these official tutorials to get started:

In this tutorial, I'll show you how to enable any user to install your Slack bot to their organization.

You can achieve this by sharing a link like the following: https://api.reviewnudgebot.com/slack/install/

Alternatively, you can add an "Install" button to your website that directs users to this link.

image-e23l0

You can find the complete code example for this tutorial in my GitHub repository:

GitHub Repository - Step 8: Public Installation

This repository includes the code for each step of building a Slack bot, from the initial setup to developing paid features and integrating with the Paddle payment provider to accept subscriptions.

We’ll be implementing endpoints to complete the OAuth authentication process, allowing our Slack bot to obtain an authentication token to interact with new workspaces.

Fortunately, Bolt.js for Slack handles much of the heavy lifting for us, including:

  • Endpoint logic to initiate the OAuth installation process
  • Token management
  • Authentication of requests from different workspaces

This means we don’t need to manage tokens for posting messages or retrieving user information across various workspaces.

Our tasks are to:

  1. Configure a new receiver with ExpressReceiver:
    We’ll pass our secret tokens, Slack scopes (which users can review before installation), and other installation options.
  2. Implement a token management repository interface as specified by Bolt.js:
    We’ll create an interface to store tokens in DynamoDB.
  3. Expose two endpoints: One to accept installation requestsAnother to handle the authentication result event (success event with tokens)The ExpressReceiver will take care of the complex logic, and we’ll only need to define these endpoints and their handlers.

Get New Tokens

Since our bot will be installed in multiple workspaces, a single bot token is insufficient, as it’s only valid for one workspace.

To handle this, add the following two environment variables SLACK_CLIENT_ID and SLACK_CLIENT_SECRET to your .env files. You can find these values on the App Settings page under Basic Information and then App Credentials.

Next, import the new type of receiver for OAuth:

const { App, ExpressReceiver } = require("@slack/bolt");

Configure the ExpressReceiver and create your app instance.

You can find a complete example in the repository: https://github.com/maksouth/code-review-reminder-steps/blob/step-8-public-installation/app.js

const expressReceiver = new ExpressReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
    clientId: process.env.SLACK_CLIENT_ID,
    clientSecret: process.env.SLACK_CLIENT_SECRET,
    stateSecret: 'my-state-secret',
    scopes: [
        'chat:write',
        'channels:history',
        'channels:read',
        'links:read',
        'reactions:read',
        'users:read'
    ],
    processBeforeResponse: true,
    installationStore: createDynamoDBInstallationStore(),
    installerOptions: {
        directInstall: true,
        legacyStateVerification: true,
    }
});

const createOAuthApp = () => {
    const app = new App({
        receiver: expressReceiver,
        processBeforeResponse: true,
    });

    return app;
}

// Initializes your app with your bot token and app token
const app = createOAuthApp();

module.exports.handler = require('serverless-http')(expressReceiver.app);

Implement DynamoDB Installation Store

You may have noticed that createDynamoDBInstallationStore is used but not yet defined. This function is a simple interface that interacts with the actual storage, such as DynamoDB, since Bolt.js is not aware of our runtime environment.

Reference for the InstallationStore type we are going to implement: https://tools.slack.dev/node-slack-sdk/reference/oauth/interfaces/installationstore

Implement the Interface

First, we’ll create a thin abstraction over the database level to interact with DynamoDB.

const TABLE_NAME = 'codeReviewReminderBotSlackInstalls';
const INSTALLATION_TYPE_ENTERPRISE = 'enterprise';
const INSTALLATION_TYPE_TEAM = 'team';

const database = {
    async get(key, installationType) {
        console.log('[DYNAMODB] GET INSTALLATION REQUEST', key, installationType);
        return getItemByCompositeKey(
            TABLE_NAME,
            {
                team_or_enterprise_id: key,
                installation_type: installationType
            }).then((data) => {
                const result = JSON.parse(data.Item?.installation);
                return result;
            });
    },
    async delete(key, installationType) {
        console.log("[DYNAMODB] DELETE INSTALLATION", key, installationType);
        return deleteItemsByCompositeKey(
            TABLE_NAME,
            {
                team_or_enterprise_id: key,
                installation_type: installationType
            });
    },
    async set(key, installationType, value) {
        console.log("[DYNAMODB] ADD INSTALLATION", value);
        return putItem(
            TABLE_NAME,
            {
                team_or_enterprise_id: key,
                installation_type: installationType,
                installation: JSON.stringify(value)
            });
    }
};

The actual methods for interacting with DynamoDB are defined in this file: https://github.com/maksouth/code-review-reminder-steps/blob/step-8-public-installation/src/dynamoDB.js

Next, we’ll implement the InstallationStore interface, which includes three straightforward functions: store, retrieve, and delete. These functions handle installation details such as the auth token and workspace details.

The implementation requires checking whether the app is installed in an organization (enterprise installation) or a single workspace. However, this distinction won’t impact any other parts of your bot.

const createDynamoDBInstallationStore = () => {
    return {
        storeInstallation: async (installation) => {
            // Bolt will pass your handler an installation object
            // Change the lines below so they save to your database
            if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
                // handle storing org-wide app installation
                return await database.set(installation.enterprise.id, INSTALLATION_TYPE_ENTERPRISE, installation);
            }
            if (installation.team !== undefined) {
                // single team app installation
                return await database.set(installation.team.id, INSTALLATION_TYPE_TEAM, installation);
            }
            throw new Error('Failed saving installation data to installationStore');
        },
        fetchInstallation: async (installQuery) => {
            // Bolt will pass your handler an installQuery object
            // Change the lines below so they fetch from your database
            if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
                // handle org wide app installation lookup
                return await database.get(installQuery.enterpriseId, INSTALLATION_TYPE_ENTERPRISE);
            }
            if (installQuery.teamId !== undefined) {
                // single team app installation lookup
                return await database.get(installQuery.teamId, INSTALLATION_TYPE_TEAM);
            }
            throw new Error('Failed fetching installation');
        },
        deleteInstallation: async (installQuery) => {
            // Bolt will pass your handler  an installQuery object
            // Change the lines below so they delete from your database
            if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
                // org wide app installation deletion
                return await database.delete(installQuery.enterpriseId, INSTALLATION_TYPE_ENTERPRISE);
            }
            if (installQuery.teamId !== undefined) {
                // single team app installation deletion
                return await database.delete(installQuery.teamId, INSTALLATION_TYPE_TEAM);
            }
            throw new Error('Failed to delete installation');
        },
    };
}

Now, we can import the implementation of InstallationStore and use it with ExpressReceiver.

Define the Table

It’s a good practice to store installation details in a separate DynamoDB table, as this is a distinct concern that isn’t directly related to our bot logic (unless you prefer a single-table design for NoSQL storage).

Let’s update the serverless.yml file to define this new table.

codeReviewReminderBotSlackInstallsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: codeReviewReminderBotSlackInstalls
        AttributeDefinitions:
          - AttributeName: team_or_enterprise_id
            AttributeType: S
          - AttributeName: installation_type
            AttributeType: S
        KeySchema:
          - AttributeName: installation_type
            KeyType: HASH
          - AttributeName: team_or_enterprise_id
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 2
          WriteCapacityUnits: 2

Additionally, update the resources section under iamRoleStatements in the serverless.yml file to ensure that our Lambda function can interact with the new DynamoDB table.

Make sure to grant the necessary permissions for actions like GetItem, PutItem, and DeleteItem on the table you defined.

iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - { "Fn::GetAtt": [ "codeReviewReminderBotSlackInstallsTable", "Arn" ] }

Expose Two OAuth Endpoints

Define in serverless.yml

Update the serverless.yml file to expose two new endpoints that Bolt.js will use for the OAuth process. These endpoints will handle installation requests and the authentication result.

Make sure to specify the correct HTTP methods and path for each endpoint, so they can be properly utilized during the OAuth flow.

functions:
  slack:
    handler: app.handler
    events:
      - http:
          path: slack/events
          method: post
      - http:
          method: get
          path: /slack/install
      - http:
          method: get
          path: /slack/oauth_redirect

Deploy to AWS and Test the Real Version

We’re finished with the code changes and can now deploy to AWS to test the public installation.

Run the following command: serverless deploy

Update Slack App Settings

We’ve declared and configured the endpoints in our Lambda app to handle installation and authentication requests. Now, we need to inform Slack where to find these endpoints to complete the OAuth handshake.

  1. Go to Slack Settings -> OAuth & Permissions.
  2. Under Redirect URLs, add the AWS API Gateway URL that corresponds to your OAuth endpoints.

Make sure the URL is correct to ensure a smooth authentication process.

https://<your-api-id>.execute-api.<your-region>.amazonaws.com/dev/slack/oauth_redirect

image-0m7ls

Test: Try to Install the App in Different Workspaces!

First, let’s test the public installation in the workspace we’ve been using for testing.

  1. Delete the App from Your WorkspaceGo to Configuration.Click on Remove App.
image-evwwd
image-17xfl

2. Install the App Using OAuth

Navigate to the install endpoint that we declared in the serverless.yml file.

The URL should look like this, matching the base URL from your API Gateway for the Lambda function:

https://<your-api-id>.execute-api.<your-region>.amazonaws.com/dev/slack/install

You’ll see a prompt to install the app to your organization. Follow the on-screen instructions to complete the installation process

image-xszi6

Success screen, click “Open Slack”

As a bonus, you can check that a new entry has been added to the DynamoDB table for installations. This confirms that the installation process was successful and that your bot is now registered in the new workspace.

image-lvmbo

In the same way, you can now install the app in any workspace. Feel free to share the installation link with your friends and invite them to use the bot in their organizations!

Read more

Built on Unicorn Platform