Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: updates oauth docs with rfc-6819 examples #1014

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions examples/oauth-v1/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# OAuth v1 Example

This repo contains a sample app for doing OAuth with Slack for [Classic Slack apps](https://api.slack.com/bot-users). Checkout `app.js`. The code includes a few different options which have been commented out. As you play around with the app, you can uncomment some of these options to get a deeper understanding of how to use this library.
This repo contains a sample app for doing OAuth with Slack for [Classic Slack apps](https://api.slack.com/bot-users). Checkout `app.js`. The code includes a few different options which have been commented out. As you play around with the app, you can uncomment some of these options to get a deeper understanding of how to use this library.

Local development requires a public URL where Slack can send requests. In this guide, we'll be using [`ngrok`](https://ngrok.com/download). Checkout [this guide](https://api.slack.com/tutorials/tunneling-with-ngrok) for setting it up.

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](https://slack.com/create). You also need to [create a new app](https://api.slack.com/apps?new_app=1) if you haven’t already.
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](https://slack.com/create). You also need to [create a new app](https://api.slack.com/apps?new_app=1) if you haven’t already.

## Install Dependencies

Expand All @@ -14,12 +14,13 @@ npm install

## Setup Environment Variables

This app requires you setup a few environment variables. You can get these values by navigating to your app's [**BASIC INFORMATION** Page](https://api.slack.com/apps).
This app requires you setup a few environment variables. You can get these values by navigating to your app's [**BASIC INFORMATION** Page](https://api.slack.com/apps).

```
```Shell
export SLACK_CLIENT_ID=YOUR_SLACK_CLIENT_ID
export SLACK_CLIENT_SECRET=YOUR_SLACK_CLIENT_SECRET
export SLACK_SIGNING_SECRET=YOUR_SLACK_SIGNING_SECRET
export SLACK_OAUTH_SECRET=AN_UNGUESSABLE_STRING_>=_32_CHAR
```

## Run the App
Expand All @@ -32,7 +33,7 @@ npm start

This will start the app on port `3000`.

Now lets start `ngrok` so we can access the app on an external network and create a `redirect url` for OAuth.
Now lets start `ngrok` so we can access the app on an external network and create a `redirect url` for OAuth.

```
ngrok http 3000
Expand All @@ -56,6 +57,6 @@ This app also listens to the `app_home_opened` event to illustrate fetching the
https://3cb89939.ngrok.io/slack/events
```

Lastly, in the **Events Subscription** page, click **Subscribe to bot events** and add `app_home_opened`.
Lastly, in the **Events Subscription** page, click **Subscribe to bot events** and add `app_home_opened`.

Everything is now setup. In your browser, navigate to http://localhost:3000/slack/install to initiate the oAuth flow. Once you install the app, it should redirect you back to your native slack app. Click on the home tab of your app in slack to see the message `Welcome to the App Home!`.
182 changes: 161 additions & 21 deletions examples/oauth-v1/app.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,186 @@
const { InstallProvider } = require('@slack/oauth');
const { createEventAdapter } = require('@slack/events-api');
const { WebClient } = require('@slack/web-api');
const { randomBytes, timingSafeEqual } = require('crypto');
const express = require('express');
const cookie = require('cookie');
const { sign, verify } = require('jsonwebtoken');
// Using Keyv as an interface to our database
// see https://github.com/lukechilds/keyv for more info
const Keyv = require('keyv');

/**
* These are all the environment variables that need to be available to the
* NodeJS process (i.e. `export SLACK_CLIENT_ID=abc123`)
*/
const ENVVARS = {
SLACK_CLIENT_ID: process.env.SLACK_CLIENT_ID,
SLACK_CLIENT_SECRET: process.env.SLACK_CLIENT_SECRET,
SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET,
DEVICE_SECRET: process.env.SLACK_OAUTH_SECRET, // 256+ bit CSRNG
};

const app = express();
const port = 3000;


// Initialize slack events adapter
const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET, {includeBody: true});
const slackEvents = createEventAdapter(ENVVARS.SLACK_SIGNING_SECRET);
// Set path to receive events
app.use('/slack/events', slackEvents.requestListener());

const installer = new InstallProvider({
clientId: process.env.SLACK_CLIENT_ID,
clientSecret: process.env.SLACK_CLIENT_SECRET,
// can use different keyv db adapters here
// ex: const keyv = new Keyv('redis://user:pass@localhost:6379');
// using the basic in-memory one below
const keyv = new Keyv();

keyv.on('error', err => console.log('Connection Error', err));

const makeInstaller = (req, res) => new InstallProvider({
clientId: ENVVARS.SLACK_CLIENT_ID,
clientSecret: ENVVARS.SLACK_CLIENT_SECRET,
authVersion: 'v1',
stateSecret: 'super-secret'
installationStore: {
storeInstallation: (installation) => {
return keyv.set(installation.team.id, installation);
},
fetchInstallation: (InstallQuery) => {
return keyv.get(InstallQuery.teamId);
},
},
stateStore: {
/**
* Generates a value that will be used to link the OAuth "state" parameter
* to User Agent (device) session.
* @see https://tools.ietf.org/html/rfc6819#section-5.3.5
* @param {InstallURLOptions} installUrlOptions - the object that was passed to `generateInstallUrl`
* @param {Date} timestamp - now, in milliseconds
* @return {String} - the value to be sent in the OAuth "state" parameter
*/
generateStateParam: async (installUrlOptions, timestamp) => {
/*
* generate an unguessable value that will be used in the OAuth "state"
* parameter, as well as in the User Agent
*/
const synchronizer = randomBytes(16).toString('hex');

/*
* Create, and sign the User Agent session state
*/
const token = await sign(
{ synchronizer, installUrlOptions },
process.env.SLACK_OAUTH_SECRET,
{ expiresIn: '3m' }
);

/*
* Add the User Agent session state to an http-only, secure, samesite cookie
*/
res.setHeader('Set-Cookie', cookie.serialize('slack_oauth', token, {
maxAge: 180, // will expire in 3 minutes
sameSite: 'lax', // limit the scope of the cookie to this site, but allow top level redirects
path: '/', // set the relative path that the cookie is scoped for
secure: true, // only support HTTPS connections
httpOnly: true, // dissallow client-side access to the cookie
overwrite: true, // overwrite the cookie every time, so nonce data is never re-used
}));

/**
* Return the value to be used in the OAuth "state" parameter
* NOTE that this should not be the same, as the signed session state.
* If you prefer the OAuth session state to also be a JWT, sign it with
* a separate secret
*/
return synchronizer;
},
/**
* Verifies that the OAuth "state" parameter, and the User Agent session
* are synchronized, and destroys the User Agent session, which should be a nonce
* @see https://tools.ietf.org/html/rfc6819#section-5.3.5
* @param {Date} timestamp - now, in milliseconds
* @param {String} state - the value that was returned in the OAuth "state" parameter
* @return {InstallURLOptions} - the object that was passed to `generateInstallUrl`
* @throws {Error} if the User Agent session state is invalid, or if the
* OAuth "state" parameter, and the state found in the User Agent session
* do not match
*/
verifyStateParam: async (timestamp, state) => {
/*
* Get the cookie header, if it exists
*/
const cookies = cookie.parse(req.get('cookie') || '');

/*
* Remove the User Agent session - it should be a nonce
*/
res.setHeader('Set-Cookie', cookie.serialize('slack_oauth', 'expired', {
maxAge: -99999999, // set the cookie to expire in the past
sameSite: 'lax', // limit the scope of the cookie to this site, but allow top level redirects
path: '/', // set the relative path that the cookie is scoped for
secure: true, // only support HTTPS connections
httpOnly: true, // dissallow client-side access to the cookie
overwrite: true, // overwrite the cookie every time, so nonce data is never re-used
}));

/*
* Verify that the User Agent session was signed by this server, and
* decode the session
*/
const {
synchronizer,
installUrlOptions
} = await verify(cookies.slack_oauth, process.env.SLACK_OAUTH_SECRET);

/*
* Verify that the value in the OAuth "state" parameter, and in the
* User Agent session are equal, and prevent timing attacks when
* comparing the values
*/
if (!timingSafeEqual(Buffer.from(synchronizer), Buffer.from(state))) {
throw new Error('The OAuth state, and device state are not synchronized. Try again.');
}

/**
* Return the object that was passed to `generateInstallUrl`
*/
return installUrlOptions
}
},
});

app.get('/', (req, res) => res.send('go to /slack/install'));
app.get('/', (req, res) =>
res.send(`<a href="/slack/install"><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>`)
);

app.get('/slack/install', async (req, res, next) => {
app.get('/slack/install', async (req, res) => {
try {
// feel free to modify the scopes
const url = await installer.generateInstallUrl({
const installer = makeInstaller(req, res);
const redirectUrl = await installer.generateInstallUrl({
scopes: ['channels:read', 'groups:read', 'incoming-webhook', 'bot' ],
metadata: 'some_metadata',
})

res.send(`<a href=${url}><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>`);
redirectUri: `https://${req.get('host')}/slack/oauth_redirect`,
});

const htmlResponse = '<html>'
+ `\n<meta http-equiv="refresh" content="0; URL=${redirectUrl}">`
+ '\n<body>'
+ '\n <h1>Success! Redirecting to the Slack App...</h1>'
+ `\n <button onClick="window.location = '${redirectUrl}'">Click here to redirect</button>`
+ '\n</body></html>';
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(htmlResponse);
} catch(error) {
console.log(error)
}
});

// example 1
// use default success and failure handlers
// example 1: use default success and failure handlers
app.get('/slack/oauth_redirect', async (req, res) => {
const installer = makeInstaller(req, res);
await installer.handleCallback(req, res);
});

// example 2
// using custom success and failure handlers
// example 2: using custom success and failure handlers
// const callbackOptions = {
// success: (installation, metadata, req, res) => {
// res.send('successful!');
Expand All @@ -50,20 +189,21 @@ app.get('/slack/oauth_redirect', async (req, res) => {
// res.send('failure');
// },
// }
//

// app.get('/slack/oauth_redirect', async (req, res) => {
// const installer = makeInstaller(req, res);
// await installer.handleCallback(req, res, callbackOptions);
// });

// When a user navigates to the app home, grab the token from our database and publish a view
slackEvents.on('app_home_opened', async (event, body) => {
slackEvents.on('app_home_opened', async (event) => {
try {
if (event.tab === 'home') {
const DBInstallData = await installer.authorize({teamId:body.team_id});
const DBInstallData = await installer.authorize({teamId:event.view.team_id});
const web = new WebClient(DBInstallData.botToken);
await web.views.publish({
user_id: event.user,
view: {
view: {
"type":"home",
"blocks":[
{
Expand All @@ -84,4 +224,4 @@ slackEvents.on('app_home_opened', async (event, body) => {
}
});

app.listen(port, () => console.log(`Example app listening on port ${port}! Go to http://localhost:3000/slack/install to initiate oauth flow`))
app.listen(port, () => console.log(`Example app listening on port ${port}! Go to http://localhost:3000 to initiate oauth flow`))
Loading