Integrating Slack into your Laravel API

Ben Stones
Aug 19, 2019 · 8 min read

While there are many guides — and indeed, packages — that help you to integrate Slack into a standard Laravel application, there are very few when it comes to Laravel APIs.

Many packages expect you to store a state key in session storage to protect against forgery attacks before redirecting users to begin the Slack authorisation flow. Clearly, this is not designed for stateless applications.

This article will help you to integrate Slack support into your Laravel API using the adam-paterson/oauth2-slack package.

Using OAuth 2.0 with Slack

There are three parts to the authorisation flow for apps that use OAuth 2.0:

  • Step 1: You send the user to a Slack page, with a predefined set of scopes that your app needs along with a random state key as defined by your application. The user is then asked to authorise your app.
  • Step 2: The user is redirected back to your website with a verification code provided as a query string parameter. Slack will also return the state key you provided in step 1 so that you can verify the request came from Slack.
  • Step 3: You use the verification code to request an access token from the Slack API. You can then store this access token in your database for future requests.

What’s a scope?

A scope is the permitted capabilities of your app: when a user is asked to authorise your app, Slack will make the user aware of the permissions your app will have.

For example, if you want to be able to retrieve the list of channels the user has access to, you would need to include the channels:read scope when you initially redirect the user to begin the authorisation flow.

You can learn more about scopes and permissions in the Slack documentation. If you want to learn more about scopes as part of the OAuth 2.0 specification, start here.

What’s the state?

Slack allows you to send a state key as part of the redirect URL when you initially take the user to the Slack website to authorise your app. Slack returns the value of the state key as another parameter in the query string.

You would then compare the returned value against the one you have stored for the user. If they don’t match, your app should assume the request is fraudulent and bail.

I know what you’re thinking: how can I do that when my API is stateless? You’ll be using the cache() helper to store the state, but we’ll get to that later.

Registering your app with Slack

You first need to register your app with Slack so that you can get a set of client credentials you need to communicate with the API.

I would recommend you set the scopes your app needs in the OAuth & Permissions tab on the Slack website in case you want to submit your app to the Slack App Directory.

This does not actually replace the need to attach the scopes that your app needs when you redirect the user to authorise your app, but Slack will not allow you to submit your app for inclusion in the App Directory if you don’t declare the scopes your app needs and why.

If you decide to submit your app for inclusion in the Slack App Directory with the scopes you declared, Slack will not allow you to attach any other scopes during the authorisation flow once your app has been published.

The benefit of being included in the Slack App Directory is that your users won’t be warned during the authorisation flow that your app hasn’t been reviewed or approved by Slack.

Storing your Slack API credentials

Create a configuration file to store the OAuth credentials provided by Slack:

config/slack.php:<?php

return [
/*
|----------------------------------------------------------------
| Client ID
|----------------------------------------------------------------
| This value represents your client ID. We send this each time so
| that Slack knows which client application is making the given
| request.
|
*/
'client_id' => env('SLACK_CLIENT_ID'),
/*
|----------------------------------------------------------------
| Client Secret
|----------------------------------------------------------------
|
| This value represents your secret key. You must not store this
| in any public-facing environment file, as it is used by the
| Slack API to verify requests that use your client ID as the
| request author.
|
*/
'client_secret' => env('SLACK_CLIENT_SECRET'),
/*
|----------------------------------------------------------------
| Redirect URI
|----------------------------------------------------------------
|
| This value represents the callback URL that Slack will
| redirect your user onto after authorising or cancelling the
| authorisation flow. Slack will use the default redirect URI if
| this is left empty.
|
*/
'redirect_uri' => env('SLACK_REDIRECT_URI'),

];

Note: Don’t store your client secret in version control.

Add the variables to your environment file:

.env:SLACK_CLIENT_ID=<id>
SLACK_CLIENT_SECRET=<secret>
SLACK_REDIRECT_URI=<uri>

Starting the Authorisation Flow

We first need to return the authorisation URL to the end user.

Fortunately, there is a helper method in the adam-paterson/oauth2-slack package that can help us with this, aptly named ->getAuthorizationUrl().
We need to pass the scope and state as an array of options.

As we will need to exchange the returned verification code for an access token, it would be best if we create a class to do this:

app/Services/Slack/Slack.php:<?php

namespace App\Services\Slack;

use AdamPaterson\OAuth2\Client\Provider\Slack as Provider;
use Illuminate\Support\Str;

class Slack
{
/**
*
@var Provider
*/
protected $provider;

/**
* Slack constructor.
*
*
@param string|null $clientId
*
@param string|null $clientSecret
*
@param string|null $redirectUri
*/
public function __construct(string $clientId = null, string $clientSecret = null, string $redirectUri = null)
{
$redirectUri = $redirectUri ?? config('slack.redirect_uri');

if ($redirectUri) {
$options['redirectUri'] = $redirectUri;
}

$options['clientId'] = $clientId ?? config('slack.client_id');
$options['clientSecret'] = $clientSecret ?? config('slack.client_secret'); $this->provider = new Provider($options);
}
/**
* Get the authorisation URL.
*
*
@param string|null $state
*
@return array
*/
public function getAuthorisationUrl(string $state = null)
{
$state = $state ?: Str::random(32);

return [
'url' => $this->provider->getAuthorizationUrl([
'state' => $state,
'scope' => 'channels:read chat:write:bot',
]),

'state' => $state, // We need to store this for later
];
}

/**
* Get an access token from the provider.
*
*
@param string $code
*
@return string
*/
public function getAccessToken(string $code)
{
return $this->provider->getAccessToken('authorization_code', [
'code' => $code,
])->getToken();
}
}

Our helper class can be instantiated with a different client ID and client secret if required; and provides us with separate methods to retrieve the authorisation URL and to convert an authorisation code for an access token.

Generating the authorisation URL

When you call the ->getAuthorisationUrl() method, a state key will be generated that will be appended to the redirect URI that your API will pass down to your front-end application to redirect the user onto.

Before your API returns the redirect URL to your front-end, you need to store the state key first so you can do a comparison check after the user returns to your front-end application from the Slack authorisation page.

Presumably, you have a controller from which the ->getAuthorisationUrl() method is being called. You need to extract the state key from the response and cache it:

app/Http/Controllers/SlackController.php:<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\Slack\Slack;
class SlackController extends Controller
{
/**
* Send the grant token authorisation URL.
*
*
@param Request $request
*
@return \Illuminate\Http\JsonResponse
*/
public function grant(Request $request)
{
$auth = (new Slack)->getAuthorisationUrl();
cache([
"user.{$request->user()->id}.state" => $auth['token'],
], now()->addHour());
// Or with the helper class, if preferred: // (new SecurityToken('slack'))->store($auth['token']); return response()->json([
'url' => $auth['url'],
]);
}
}

If you want a much more scalable solution, I would recommend creating helper classes to store and verify these tokens, because the logic can be repurposed for other providers with public APIs such as Trello and Jira.

For example, the Trello API uses OAuth 1, which requires the creation of ‘temporary credentials’ that have to be stored temporarily while the user goes through the authorisation flow before returning to your front-end application.

After the user has authorised your application, Slack will append and return the state key when they redirect the user back.

Remember, when a user authorises your app, your front-end will also receive an authorisation code as part of the redirect from the Slack website which your API will need in order to generate an access token.

Handling a successful authorisation

Verifying the state key

When a state key is included in the authorisation URL, Slack will return it when they redirect the user back. Your front-end will need to send a request to your API with the returned state key and authorisation code.

If you are using the example helper classes, you can verify the state key by instantiating the VerifySlackSecurityToken rule class in a validation class:

app/Http/Requests/Slack/CreateAccessToken.php:/**
* Get the validation rules that apply to the request.
*
*
@return array
*/
public function rules()
{
return [
'code' => 'required', // We'll swap this for an Access Token
'state' => ['required', new VerifySlackSecurityToken],
];
}

You can then type-hint the validation class as a dependency on the controller method handling the request.

Otherwise, just verify the provided state key directly in the controller method handling the request, or in a validation class which is type-hinted as a dependency on the controller method handling the request:

$state = cache("user.{$request->user()->id}.state")if ($request->state !== $state) {
return abort(400, 'The security token is invalid.');
}

Requesting an Access Token

To request an access token, call the ->getAccessToken() method and pass the authorisation code provided by your front-end application:

app/Http/Controllers/SlackController.php:<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Services\Slack\Slack;
class SlackController extends Controller
{
/**
* Authorise access with Slack.
*
*
@param CreateAccessToken $request
*
@return \Illuminate\Http\JsonResponse
*/
public function authorise(Request $request)
{
// Handle state key validation if not done in validation class
$token = (new Slack)->getAccessToken($request->code); // Store the access token for the user
}
}

Storing the Access Token

Now we’ve got the access token, let’s store it:

$request->user()->slackIntegration()->create([
'access_token' => $token,
]);

In the case above, we are storing it in a table using this schema:

Schema::create('slack_integrations', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->unique();
$table->string('access_token');
$table->timestamps();

$table->foreign('user_id')->references('id')->on('users');
});

Next, we need to create a model for the table:

app/SlackIntegration.php:<?phpnamespace App;

use Illuminate\Database\Eloquent\Model;

class SlackIntegration extends Model
{
/**
* The attributes that aren't mass assignable.
*
*
@var array
*/
protected $guarded = [
'id',
'created_at',
'updated_at',
];
/**
* The attributes that should be hidden for arrays.
*
*
@var array
*/
protected $hidden = [
'access_token',
];
/**
* Get the owning user model.
*
*
@return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

And likewise, a way to retrieve the integration record from the User model:

app/User.php:<?phpnamespace App;

use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
/**
* Get the Slack integration belonging to this user.
*
*
@return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function slackIntegration()
{
return $this->hasOne(SlackIntegration::class);
}
}

Using the access token to make requests

You will now be able to perform actions against a user’s account on Slack, but remember you are always limited to the scopes your application has been granted by each user.

For example, with the channels:read scope, we can retrieve the list of channels the user has access to:

$user = User::findOrFail($id)->slackIntegration;$client = new Client([
'headers' => [
'Authorization' => 'Bearer ' . $this->model->access_token,
],

'base_uri' => 'https://slack.com/api/',
]);
$request = $client->post('conversations.list', [
'form_params' => [
'token' => $user->slackIntegration->access_token,
],
]);

About me

My name is Ben Stones. I lead Sprint Boards®, an online retrospective board for Agile developers, providing distributed teams with the tools they need to coordinate, discuss and collaborate in real-time.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade