From 6a46ca0947cc1c25fcab7e64ade8c7c2a8b82eef Mon Sep 17 00:00:00 2001 From: Andy Wright Date: Sat, 9 May 2020 09:13:43 -0400 Subject: [PATCH] docs: updates oauth docs with rfc-6819 examples Updates the examples and documentation to implmenent RFC-6819 Section 5.3.5: Link the "state" Parameter to User Agent Session. --- examples/oauth-v1/README.md | 13 +- examples/oauth-v1/app.js | 182 ++++++- examples/oauth-v1/package-lock.json | 109 ++++- examples/oauth-v1/package.json | 4 +- examples/oauth-v2/README.md | 15 +- examples/oauth-v2/app.js | 163 ++++++- examples/oauth-v2/package-lock.json | 713 +++++++++++++++++++++++++++- examples/oauth-v2/package.json | 2 + packages/oauth/README.md | 140 +++++- 9 files changed, 1255 insertions(+), 86 deletions(-) diff --git a/examples/oauth-v1/README.md b/examples/oauth-v1/README.md index 83bd46217..1adccda93 100644 --- a/examples/oauth-v1/README.md +++ b/examples/oauth-v1/README.md @@ -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 @@ -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 @@ -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 @@ -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!`. diff --git a/examples/oauth-v1/app.js b/examples/oauth-v1/app.js index 0bf8b8fd2..4cdce252b 100644 --- a/examples/oauth-v1/app.js +++ b/examples/oauth-v1/app.js @@ -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(``) +); -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(``); + redirectUri: `https://${req.get('host')}/slack/oauth_redirect`, + }); + + const htmlResponse = '' + + `\n` + + '\n' + + '\n

Success! Redirecting to the Slack App...

' + + `\n ` + + '\n'; + 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!'); @@ -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":[ { @@ -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`)) diff --git a/examples/oauth-v1/package-lock.json b/examples/oauth-v1/package-lock.json index 5c60fbca6..63d48b095 100644 --- a/examples/oauth-v1/package-lock.json +++ b/examples/oauth-v1/package-lock.json @@ -35,6 +35,11 @@ "type-is": "~1.6.17" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -54,9 +59,9 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-signature": { "version": "1.0.6", @@ -81,6 +86,14 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -136,6 +149,13 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } } }, "finalhandler": { @@ -192,6 +212,84 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -293,6 +391,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", diff --git a/examples/oauth-v1/package.json b/examples/oauth-v1/package.json index f30d81de5..5eeb1f06a 100644 --- a/examples/oauth-v1/package.json +++ b/examples/oauth-v1/package.json @@ -17,6 +17,8 @@ "@slack/events-api": "^2.3.2", "@slack/oauth": "^1.0.0", "@slack/web-api": "^5.8.0", - "express": "^4.17.1" + "cookie": "~0.4.1", + "express": "^4.17.1", + "jsonwebtoken": "~8.5.1" } } diff --git a/examples/oauth-v2/README.md b/examples/oauth-v2/README.md index 99d67948f..2f3835c67 100644 --- a/examples/oauth-v2/README.md +++ b/examples/oauth-v2/README.md @@ -1,12 +1,12 @@ # OAuth v2 Example -This repo contains a sample app for implementing OAuth with Slack for new granular permission Slack apps. 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 implementing OAuth with Slack for new granular permission Slack apps. 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. -This example uses the [Keyv](https://github.com/lukechilds/keyv) library as a database solution. Keyv has adaptors for many popular database solutions. You can use whatever database or wrapper you wish to. +This example uses the [Keyv](https://github.com/lukechilds/keyv) library as a database solution. Keyv has adaptors for many popular database solutions. You can use whatever database or wrapper you wish to. ## Install Dependencies @@ -16,12 +16,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 @@ -34,7 +35,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 @@ -58,6 +59,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!`. diff --git a/examples/oauth-v2/app.js b/examples/oauth-v2/app.js index 9e83101c2..3618630c0 100644 --- a/examples/oauth-v2/app.js +++ b/examples/oauth-v2/app.js @@ -1,17 +1,31 @@ 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); +const slackEvents = createEventAdapter(ENVVARS.SLACK_SIGNING_SECRET); // Set path to receive events app.use('/slack/events', slackEvents.requestListener()); @@ -22,11 +36,10 @@ const keyv = new Keyv(); keyv.on('error', err => console.log('Connection Error', err)); -const installer = new InstallProvider({ - clientId: process.env.SLACK_CLIENT_ID, - clientSecret: process.env.SLACK_CLIENT_SECRET, +const makeInstaller = (req, res) => new InstallProvider({ + clientId: ENVVARS.SLACK_CLIENT_ID, + clientSecret: ENVVARS.SLACK_CLIENT_SECRET, authVersion: 'v2', - stateSecret: 'my-state-secret', installationStore: { storeInstallation: (installation) => { return keyv.set(installation.team.id, installation); @@ -35,32 +48,143 @@ const installer = new InstallProvider({ 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(``) +); -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', 'channels:manage', 'chat:write', 'incoming-webhook'], metadata: 'some_metadata', - }) - - res.send(``); + redirectUri: `https://${req.get('host')}/slack/oauth_redirect`, + }); + + /* + * NOTE that this redirects the client to Slack immediately because + * the OAuth flow is time sensitive (only valid for 3 minutes in this example) + */ + const htmlResponse = '' + + `\n` + + '\n' + + '\n

Success! Redirecting to the Slack App...

' + + `\n ` + + '\n'; + 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!'); @@ -69,8 +193,9 @@ 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); // }); @@ -82,7 +207,7 @@ slackEvents.on('app_home_opened', async (event) => { const web = new WebClient(DBInstallData.botToken); await web.views.publish({ user_id: event.user, - view: { + view: { "type":"home", "blocks":[ { @@ -103,4 +228,4 @@ slackEvents.on('app_home_opened', async (event) => { } }); -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`)) diff --git a/examples/oauth-v2/package-lock.json b/examples/oauth-v2/package-lock.json index cba0b99ea..fef3ba119 100644 --- a/examples/oauth-v2/package-lock.json +++ b/examples/oauth-v2/package-lock.json @@ -4,6 +4,192 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@slack/events-api": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@slack/events-api/-/events-api-2.3.2.tgz", + "integrity": "sha512-jIKLsvPn4Ute/WUSlVwTW3o58S0PRtZN+QO3El0MN10IFzZ6iEGXvvhjXGLovUydEvucO/Lu5cISm5+tKGAiBw==", + "requires": { + "@types/debug": "^4.1.4", + "@types/express": "^4.17.0", + "@types/lodash.isstring": "^4.0.6", + "@types/node": ">=4.2.0 < 13", + "@types/yargs": "^13.0.0", + "debug": "^2.6.1", + "express": "^4.0.0", + "lodash.isstring": "^4.0.1", + "raw-body": "^2.3.3", + "tsscmp": "^1.0.6", + "yargs": "^6.6.0" + } + }, + "@slack/logger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-2.0.0.tgz", + "integrity": "sha512-OkIJpiU2fz6HOJujhlhfIGrc8hB4ibqtf7nnbJQDerG0BqwZCfmgtK5sWzZ0TkXVRBKD5MpLrTmCYyMxoMCgPw==", + "requires": { + "@types/node": ">=8.9.0" + } + }, + "@slack/oauth": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-1.0.0.tgz", + "integrity": "sha512-M+AUQbFla4kbHphg9iOMlITLrnOU4ORsmtu98sHt+QJQpJTpIvaSmYFK35vkRHh6xIDDfojT2C997JtdofL4NA==", + "requires": { + "@slack/logger": "^2.0.0", + "@slack/web-api": "^5.7.0", + "@types/jsonwebtoken": "^8.3.7", + "@types/node": ">=6.0.0", + "jsonwebtoken": "^8.5.1", + "lodash.isstring": "^4.0.1" + } + }, + "@slack/types": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-1.6.0.tgz", + "integrity": "sha512-SrrAD/ZxDN4szQ35V/mY2TvKSyGsUWP8def1C8NMg9AvdYG0VyaL5f+Dd6jw8STosMFXd3zqjekMowT9LB9/IQ==" + }, + "@slack/web-api": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-5.8.1.tgz", + "integrity": "sha512-MONzkjWOXV39Dejo8B9WSl/F0dxcVh9wyeW6R0jf6T6BhwN4f24iErYtTh19g+MRhb0oiyeKfnFsJTSKQulfDA==", + "requires": { + "@slack/logger": ">=1.0.0 <3.0.0", + "@slack/types": "^1.2.1", + "@types/is-stream": "^1.1.0", + "@types/node": ">=8.9.0", + "@types/p-queue": "^2.3.2", + "axios": "^0.19.0", + "eventemitter3": "^3.1.0", + "form-data": "^2.5.0", + "is-stream": "^1.1.0", + "p-queue": "^2.4.2", + "p-retry": "^4.0.0" + } + }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.33", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", + "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, + "@types/express": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", + "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz", + "integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==", + "requires": { + "@types/node": "*" + } + }, + "@types/jsonwebtoken": { + "version": "8.3.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.3.9.tgz", + "integrity": "sha512-00rI8GbOKuRtoYxltFSRTVUXCRLbuYwln2/nUMPtFU9JGS7if+nnmLjeoFGmqsNCmblPLAaeQ/zMLVsHr6T5bg==", + "requires": { + "@types/node": "*" + } + }, + "@types/lodash": { + "version": "4.14.150", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz", + "integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w==" + }, + "@types/lodash.isstring": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/lodash.isstring/-/lodash.isstring-4.0.6.tgz", + "integrity": "sha512-uUGvF9G1G7jQ5H42Y38GA9rZmUoY8wI/OMSwnW0BZA+Ra0uxzpuQf4CixXl3yG3TvF6LjuduMyt1WvKl+je8QA==", + "requires": { + "@types/lodash": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, + "@types/node": { + "version": "12.12.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.38.tgz", + "integrity": "sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA==" + }, + "@types/p-queue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-2.3.2.tgz", + "integrity": "sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ==" + }, + "@types/qs": { + "version": "6.9.2", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.2.tgz", + "integrity": "sha512-a9bDi4Z3zCZf4Lv1X/vwnvbbDYSNz59h3i3KdyuYYN+YrLjSeJD0dnphdULDfySvUv6Exy/O0K6wX/kQpnPQ+A==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/yargs": { + "version": "13.0.8", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz", + "integrity": "sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -13,11 +199,29 @@ "negotiator": "0.6.2" } }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -35,11 +239,44 @@ "type-is": "~1.6.17" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -54,9 +291,9 @@ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, "cookie-signature": { "version": "1.0.6", @@ -71,6 +308,16 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -81,6 +328,14 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -91,6 +346,14 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -101,6 +364,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -136,6 +404,13 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } } }, "finalhandler": { @@ -152,6 +427,43 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -162,6 +474,21 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -187,16 +514,87 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.0.tgz", @@ -205,6 +603,61 @@ "json-buffer": "3.0.1" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -248,6 +701,22 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -256,16 +725,87 @@ "ee-first": "1.1.1" } }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "^1.0.0" + } + }, + "p-queue": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-2.4.2.tgz", + "integrity": "sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng==" + }, + "p-retry": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz", + "integrity": "sha512-jPH38/MRh263KKcq0wBNOGFJbm+U6784RilTmHjB/HM9kH9V8WlCpVUcdOmip9cjXOh6MxZ5yk1z2SjDUJfWmA==", + "requires": { + "@types/retry": "^0.12.0", + "retry": "^0.12.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -296,6 +836,48 @@ "unpipe": "1.0.0" } }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -306,6 +888,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -344,21 +931,85 @@ "send": "0.17.1" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -378,10 +1029,66 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "requires": { + "camelcase": "^3.0.0" + } } } } diff --git a/examples/oauth-v2/package.json b/examples/oauth-v2/package.json index a021293f6..0a3d23e65 100644 --- a/examples/oauth-v2/package.json +++ b/examples/oauth-v2/package.json @@ -13,7 +13,9 @@ "@slack/events-api": "^2.3.2", "@slack/oauth": "^1.0.0", "@slack/web-api": "^5.8.0", + "cookie": "~0.4.1", "express": "^4.17.1", + "jsonwebtoken": "~8.5.1", "keyv": "^4.0.0" } } diff --git a/packages/oauth/README.md b/packages/oauth/README.md index 631e56fa6..9625a3166 100644 --- a/packages/oauth/README.md +++ b/packages/oauth/README.md @@ -6,7 +6,7 @@ [![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/slackapi/node-slack-sdk) -The `@slack/oauth` package makes it simple to setup the OAuth flow for Slack apps. It supports [V2 OAuth](https://api.slack.com/authentication/oauth-v2) for Slack Apps as well as [V1 OAuth](https://api.slack.com/docs/oauth) for [Classic Slack apps](https://api.slack.com/authentication/quickstart). Slack apps that are installed in multiple workspaces, like in the App Directory or in an Enterprise Grid, will need to implement OAuth and store information about each of those installations (such as access tokens). +The `@slack/oauth` package makes it simple to setup the OAuth flow for Slack apps. It supports [V2 OAuth](https://api.slack.com/authentication/oauth-v2) for Slack Apps as well as [V1 OAuth](https://api.slack.com/docs/oauth) for [Classic Slack apps](https://api.slack.com/authentication/quickstart). Slack apps that are installed in multiple workspaces, like in the App Directory or in an Enterprise Grid, will need to implement OAuth and store information about each of those installations (such as access tokens). The package handles URL generation, state verification, and authorization code exchange for access tokens. It also provides an interface for easily plugging in your own database for saving and retrieving installation data. @@ -69,7 +69,7 @@ const installer = new InstallProvider({ You'll need an installation URL when you want to test your own installation, in order to submit your app to the App Directory, and if you need an additional authorizations (user tokens) from users inside a team when your app is already installed. These URLs are also commonly used on your own webpages as the link for an ["Add to Slack" button](https://api.slack.com/docs/slack-button). You may also need to generate an installation URL dynamically when an option's value is only known at runtime, and in this case you would redirect the user to the installation URL. -The `installProvider.generateInstallUrl()` method will create an installation URL for you. It takes in an options argument which at a minimum contains a `scopes` property. `installProvider.generateInstallUrl()` options argument also supports `metadata`, `teamId`, `redirectUri` and `userScopes` properties. +The `installProvider.generateInstallUrl()` method will create an installation URL for you. It takes in an options argument which at a minimum contains a `scopes` property. `installProvider.generateInstallUrl()` options argument also supports `metadata`, `teamId`, `redirectUri` and `userScopes` properties. ```javascript installer.generateInstallUrl({ @@ -82,7 +82,7 @@ installer.generateInstallUrl({ Adding custom metadata to the installation URL -You might want to present an "Add to Slack" button while the user is in the middle of some other tasks (e.g. linking their Slack account to your service). In these situations, you want to bring the user back to where they left off after the app installation is complete. Custom metadata can be used to capture partial (incomplete) information about the task (like which page they were on or inputs to form elements the user began to fill out) in progress. Then when the installation is complete, that custom metadata will be available for your app to recreate exactly where they left off. You must also use a [custom success handler when handling the OAuth redirect](#handling-the-oauth-redirect) to read the custom metadata after the installation is complete. +You might want to present an "Add to Slack" button while the user is in the middle of some other tasks (e.g. linking their Slack account to your service). In these situations, you want to bring the user back to where they left off after the app installation is complete. Custom metadata can be used to capture partial (incomplete) information about the task (like which page they were on or inputs to form elements the user began to fill out) in progress. Then when the installation is complete, that custom metadata will be available for your app to recreate exactly where they left off. You must also use a [custom success handler when handling the OAuth redirect](#handling-the-oauth-redirect) to read the custom metadata after the installation is complete. ```javascript installer.generateInstallUrl({ @@ -144,7 +144,7 @@ const callbackOptions = { const htmlResponse = `Success!` res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(htmlResponse); - }, + }, failure: (error, installOptions , req, res) => { // Do custom failure logic here res.writeHead(500, { 'Content-Type': 'text/html' }); @@ -162,7 +162,7 @@ app.get('/slack/oauth_redirect', (req, res) => { Although this package uses a default `MemoryInstallationStore`, it isn't recommended for production use since the access tokens it stores would be lost when the process terminates or restarts. Instead, `InstallProvider` has an option for supplying your own installation store, which is used to save and retrieve install information (like tokens) to your own database. -An installation store is an object that provides two methods: `storeInstallation` and `fetchInstallation`. `storeInstallation` takes an `installation` as an argument, which is an object that contains all installation related data (like tokens, teamIds, enterpriseIds, etc). `fetchInstallation` takes in a `installQuery`, which is used to query the database. The `installQuery` can contain `teamId`, `enterpriseId`, `userId`, and `conversationId`. +An installation store is an object that provides two methods: `storeInstallation` and `fetchInstallation`. `storeInstallation` takes an `installation` as an argument, which is an object that contains all installation related data (like tokens, teamIds, enterpriseIds, etc). `fetchInstallation` takes in a `installQuery`, which is used to query the database. The `installQuery` can contain `teamId`, `enterpriseId`, `userId`, and `conversationId`. In the following example, the `installationStore` option is used and the object is defined in line. The required methods are implemented by calling an example database library with simple get and set operations. @@ -214,7 +214,7 @@ result = { Reading extended installation data -The `installer.authorize()` method only returns a subset of the installation data returned by the installation store. To fetch the entire saved installation, use the `installer.installationStore.fetchInstallation()` method. +The `installer.authorize()` method only returns a subset of the installation data returned by the installation store. To fetch the entire saved installation, use the `installer.installationStore.fetchInstallation()` method. ```javascript // installer.installationStore.fetchInstallation takes in an installQuery as an argument @@ -228,38 +228,126 @@ const result = installer.installationStore.fetchInstallation({teamId:'my-Team-ID ### Using a custom state store -A state store handles generating the OAuth `state` parameter in the installation URL for a given set of options, and verifying the `state` in the OAuth callback and returning those same options. +The state store provides functions for generating the value used in the OAuth "state" parameter, `generateStateParam`, as well as verifying the value of the OAuth "state" parameter when the User Agent returns to your app, `verifyStateParam`. + +[Section 5.3.5 of RFC-6819](https://tools.ietf.org/html/rfc6819#section-5.3.5) explains how the OAuth "state" parameter is intended to be used to link the OAuth flow to the User Agent (device) that initiated the OAuth flow. The default state store does *not* link the User Agent, and is provided for demonstration purposes. -The default state store, `ClearStateStore`, does not use any storage. Instead, it signs the options (using the `stateSecret`) and encodes them along with a signature into `state`. Later during the OAuth callback, it verifies the signature. +To implement [Section 5.3.5 of RFC-6819](https://tools.ietf.org/html/rfc6819#section-5.3.5), you need to override the default state store, and synchronize the value sent in the OAuth "state" param with the User Agent (device). Following is an example for implementing Section 5.3.5 in an express app. -If you want to conceal the `metadata` used in the installation URL options you will need to store `state` on your server (in a database) by providing a custom state store. A custom state implements two methods: `generateStateParam()` and `verifyStateParam()`. When you instantiate the `InstallProvider` use the `stateStore` option to set your custom state store. And when using the custom state store, you no longer need to use the `stateSecret` option. +> Note that in this example, the InstallProvider has a per-request lifetime. +> +> Note that when overriding stateStore, you do not need to provide a `stateSecret` ```javascript -const installer = new InstallProvider({ +const { InstallProvider } = require('@slack/oauth'); +const { randomBytes, timingSafeEqual } = require('crypto'); +const cookie = require('cookie'); +const { sign, verify } = require('jsonwebtoken'); + +const makeInstaller = (req, res) => new InstallProvider({ clientId: process.env.SLACK_CLIENT_ID, clientSecret: process.env.SLACK_CLIENT_SECRET, stateStore: { - // generateStateParam's first argument is the entire InstallUrlOptions object which was passed into generateInstallUrl method - // the second argument is a date object - // the method is expected to return a string representing the state - generateStateParam: (installUrlOptions, date) => { - // generate a random string to use as state in the URL - const randomState = randomStringGenerator(); - // save installOptions to cache/db - myDB.set(randomState, installUrlOptions); - // return a state string that references saved options in DB - return randomState; + /** + * 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; }, - // verifyStateParam's first argument is a date object and the second argument is a string representing the state - // verifyStateParam is expected to return an object representing installUrlOptions - verifyStateParam: (date, state) => { - // fetch saved installOptions from DB using state reference - const installUrlOptions = myDB.get(randomState); - return installUrlOptions; + /** + * 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 } }, }); ``` + --- ### Setting the log level and using a custom logger