How to Build a Multi-Workspace Slack Application in Go
Learn to create a multi-workspace Slack application using Go. This guide covers setting up, coding, and distributing your app across multiple Slack workspaces.
David Abramov
May 31, 2022
Ā ā¢Ā
10
Ā min read
Share this post
Recently, I started working on a Slack application that connects Slack and our own Blink platform. If youāre not familiar, Blink is a no-code automation platform for cloud engineering teams, and it enables users to build automations within managed workspaces for different teams or projects. In order to support this use case, I needed a way to build a multi-workspace application in Slack.
When I started reading about and playing with the Slack platform, it suddenly became clear to me that Slack is so much bigger and more complex than Iād ever realized.
The Slack API Docs seem very large and complex at first glance, but they are actually quite detailed and user-friendly. Unfortunately, the official Bolt family of SDKs doesnāt include Go, my recent language of choice, and, although I did find some useful guides on the web, none of them really targeted my use case of building an app which can be distributed across multiple workspaces and eventually listed on the Slack app store, the App Directory.
After my research, I ended up using the Slack API in Go to build the needed Slack application, and Iām writing this hands-on article to share my experience. While I built this application to support Blinkās particular use case, Iāve written the rest of this blog post as a general how-to guide for anyone building a similar Slack application.
Give your app a name and select the workspace you will be developing your app in.
NOTE: As our app will be publicly distributed, the workspace selection here is not very important, and weāll build our app in a way which will allows us to install it on any workspace, regardless of the choice you make in this phase.
The Scenario
Letās imagine that you are developing a really awesome REST application that accepts as input 2 integers and returns their sum.
Weāll use the Gin web framework to write our app:
go get -u github.com/gin-gonic/gin
Hereās the code:
package main
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
)
type Input struct {
Num1 int `json:"num1"`
Num2 int `json:"num2"`
}
type Output struct {
Sum int `json:"sum"`
}
func handle(c *gin.Context) {
input := &Input{}
if err := json.NewDecoder(c.Request.Body).Decode(input); err != nil {
c.String(http.StatusBadRequest, "error reading request body: %s", err.Error())
return
}
c.JSON(http.StatusOK, &Output{
Sum: input.Num1 + input.Num2,
})
}
func main() {
app := gin.Default()
app.POST("/", handle)
_ = app.Run()
}
By default, if we donāt specify an argument for the āRunā method, the app will listen on port 8080.
Letās try it:
go run main.go
And then:
curl -X POST http://localhost:8080 --data '{"num1":1,"num2":2}' -i
And hereās the result:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 21 May 2022 17:46:26 GMT
Content-Length: 9
{"sum":3}
Letās call this app our āCoreā service that we want to integrate with Slack, and create a standalone Slack dedicated service that will connect the users of our Slack app to the āCoreā app.
Letās visualize this:
The above diagram shows the data flow. Each time a user will interact with our Slack app, the Slack platform will access our Slack service which will in turn access our āCoreā service to process the input and return a response which will be returned to the Slack platform (and then to the user).
Supporting Multiple Slack Workspaces
Every access to the Slack API requires the caller to provide an access token.
There are several types of tokens. Our app will work only with bot tokens.
Every time our app will be installed on some workspace, weāll get such a bot token from the Slack platform.
The installation process on some workspace is actually an OAuth authorization flow, in which we send the user to Slack to authorize our app on his workspace, and once he approves our app, weāll need to exchange our temporary access code for a permanent access token which weāll use in every operation which requires access to the Slack API.
Letās start with a small snippet of our Slack service code.
To store these tokens, weāll use the popular and quite simple Bolt DB framework:
go get github.com/boltdb/bolt/...
Letās initialize the DB and create a bucket of tokens, in which tokens are mapped to their workspaces (by ID):
NOTE: When developing a real app, consider storing your access tokens in a more secure way!
Developing Locally
As weāre developing locally and the Slack platform requires us to provide publicly accessible URLs, we need to create some connection between our local device and the wider network. For this we can use ngrok:
ngrok http 5400
Running the above command should print a URL which exposes port 5400 on your local device to the outer world.
Configuring Our App
To keep things simple, weāll register a Slash Command for our app:
Navigate to Features ā Slash Commands and click the āCreate New Commandā button:
Fill out the following form:
Provide the name of the command (e.g. āplusā), the ngrok URL from the previous section suffixed with ā/cmd/ā and the command name and a description (e.g. āSum 2 Integersā). The rest of the fields are optional.
Setting Up App Installation
As weāre developing a multi workspace app, we need to enable the OAuth installation flow.
Navigate to Features ā OAuth & Permissions:
And click the āAdd New Redirect URLā button in the āRedirect URLsā section:
Enter your ngrok URL with a ā/installā suffix attached to it (e.g. āhttps://03e2-109-66-212-180.ngrok.io/installā), click āAddā and then click āSave URLsā.
Scopes
The scopes requested by our app are those that our app will request upon each installation to some workspace. You can see in the āScopesā section that we already have the ācommandsā bot scope, as weāve added a Slash Command to our app.
Enabling Public Distribution
Navigate to Settings ā Manage Distribution:
And under the āShare Your App with Other Workspacesā section, make sure that the hard coded information removal checkbox is checked:
Finally, click the āActivate Public Distributionā button.
Connecting It All Together
Now that we have our awesome āCoreā Service and weāve configured our Slack app, we can go ahead and continue writing the code of our Slack service.
Weāve already configured our storage mechanism, and youāve probably noticed that we are already committed to implementing some endpoints we shared with the slack platform:
/install - to install our app using the OAuth flow.
/cmd/plus - to forward the request to sum 2 integers to our āCoreā service, using the input from the user on Slack.
Implementing the OAuth Installation Flow
As with every standard OAuth flow, we need a pair of client ID and secret. You can view both under the āApp Credentialsā section after navigating to Settings ā Basic Information:
We need to access the Slack platform, so letās import the Go SDK:
Letās break it down. First, we check for the presence of the āerrorā query parameter. If this parameter is present, then it means the installation failed for some reason (e.g. the user declined to authorize our app). In that case weāll abort the rest of the flow and just notify of an error.
Next, as in every standard OAuth flow, weāre checking for the temporary code by looking for the ācodeā query parameter, and if it is present we use the Slack Go SDK to exchange that code for an access token, and store that token in our storage, mapped to the workspace ID.Finally, we redirect the user to our app page in his Slack client using Deep linking.
Notice how weāre reading the āCLIENT_IDā and āCLIENT_SECRETā env variables to perform the exchange operation.
IMPORTANT: When developing a real app, consider adding a āstateā parameter to your OAuth flow for extra security!
Implementing the Slash Command
Before we implement our slash command endpoint, letās implement some middleware that will make sure that requests to this endpoint are really coming from Slack.
There are several methods provided by the Slack platform to achieve such verification, but weāll use the recommended and straightforward way of using the Signing Secret present under the āApp Credentialsā section in Settings ā Basic Information:
Hereās the signature verification middleware code:
First, weāre reading the signature verification secret from the āSIGNATURE_SECRETā env variable.
Then, weāre reading the request body, while copying it and preserving it for later reads which may be invoked by other handlers in the chain, and finally we verify the signature of the request body. If all is well, weāre calling the āNextā method to invoke any other handlers in the chain.
Letās have a look at the code of the actual handler:
Letās break it down. Weāre parsing the request body to get the slash command input.
Then, weāre parsing the parameters passed in with the command to verify we have 2 integers (e.g. ā/plus 1 2ā is valid, but ā/plus 1 aā is not).
Finally, weāre using the 2 provided parameters to invoke the endpoint on our āCoreā service to get the sum of the 2 given integers, sending the user a direct message with the calculated sum returned by the āCoreā service.
In order to send a direct message to the user we are reading the token mapped to the relevant workspace from the DB.
Also, we need to add the āchat:writeā bot scope for our app, otherwise our app will not be able to send direct messages to users.
Navigate to Settings ā OAuth & Permissions, and under the āScopesā section add the required scope to the list of bot scopes required by your app:
Finally, letās register our slash command handler under a new ācmdā API group. Hereās the current code of our Slack service:
For convenience, we copied the definitions of the āInputā and āOutputā structs from our āCoreā service.
Notice that when weāre sending the direct message to the user weāre using the access token of the workspace the user invoked the command from.
Playing With Our App
Run your app using the following command, providing values for the required env variables:
PORT=5400 CLIENT_ID=... CLIENT_SECRET=... SIGNATURE_SECRET=... go run main.go
The value of the āPORTā variable is used by the gin framework if no argument is passed to the āRunā method.
Letās first install our app on some workspace. To do that, we need to initiate the OAuth installation flow. To do that, Navigate to Settings ā Manage Distribution:
Press the āCopyā button in the āSharable URLā section and paste the copied URL into a new tab in your browser.
This should open a page which resembles the following page, allowing you to choose a workspace and install the app to it:
Press āAllowā and in the following window:
Press the āOpen Slackā button, which should take you to the āAboutā page of your app in your Slack client:
Now, navigate to some public channel in your workspace and type in ā/plusā:
Press enter to choose the ā/plusā command of your app, suffix the ā/plusā command with 2 integers of your choice and send the message, e.g.:
Once you send the message, you should receive a direct message from your app, notifying you of the result:
Listening to Events
The Slack Events API is an integral part of any Slack application, especially those that are intended to be distributed across multiple workspaces.
There are 3 kinds of events:
URL Verification - The Slack platform sends a challenge string, and our app should respond with that string and with a 200 OK status code. In this way the Slack platform checks that our app is alive.
Rate Limiting - The Slack platform informs our app of some rate limit being reached on some API endpoint. We can just acknowledge this event with a 200 OK status code.
Callback - The āinterestingā events. These events have an inner event which can have various types.
Letās register our app to a basic app deletion event.
Hereās our Slack service code, including our event handler:
When an event is sent, we check the type, and respond accordingly. The only callback event weāre going to handle is app uninstallation. We handle it by deleting the access token we used to access the Slack platform when we needed to interact with the workspace the app was deleted from.
Run the updated version of the app which includes the event handler and then navigate to Features ā Event Subscriptions:
Enable events and under the āRequest URLā section, enter your ngrok URL suffixed with ā/event/handleā (e.g. āhttps://03e2-109-66-212-180.ngrok.io/event/handleā) and wait for the Slack platform to verify your events URL:
Then, under the āSubscribe to bot eventsā section, add the āapp_uninstalledā event.
Deleting Our App
From the direct message channel your app opened with your user to inform you of the result of summing 2 integers, click on the app name:
Press āConfigurationā, which should open a browser window with the details of the app installation on your workspace:
And press the āRemove Appā button under the section with the same name.
Open your workspace from your Slack client. You should notice that the direct message channel with your app is not there anymore:
Also, you can see in the output of your Slack service, that a request was made to the events endpoint and that it was responded with a 200 OK status code: