-
Notifications
You must be signed in to change notification settings - Fork 676
Expand file tree
/
Copy pathapp.js
More file actions
227 lines (205 loc) · 8.36 KB
/
app.js
File metadata and controls
227 lines (205 loc) · 8.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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(ENVVARS.SLACK_SIGNING_SECRET);
// Set path to receive events
app.use('/slack/events', slackEvents.requestListener());
// 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',
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(`<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) => {
try {
const installer = makeInstaller(req, res);
const redirectUrl = await installer.generateInstallUrl({
scopes: ['channels:read', 'groups:read', 'incoming-webhook', 'bot' ],
metadata: 'some_metadata',
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
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
// const callbackOptions = {
// success: (installation, metadata, req, res) => {
// res.send('successful!');
// },
// failure: (error, installOptions , 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) => {
try {
if (event.tab === 'home') {
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: {
"type":"home",
"blocks":[
{
"type": "section",
"block_id": "section678",
"text": {
"type": "mrkdwn",
"text": "Welcome to the App Home!"
},
}
]
},
});
}
}
catch (error) {
console.error(error);
}
});
app.listen(port, () => console.log(`Example app listening on port ${port}! Go to http://localhost:3000 to initiate oauth flow`))