-
-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathserver.js
256 lines (219 loc) · 7.96 KB
/
server.js
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
const fs = require('fs');
const path = require('path');
// Load configuation
const config = JSON.parse(fs.readFileSync(path.join(
__dirname,
'config.json'
)));
// Require depedancies
// express is used for handling incoming HTTP requests "like a webserver"
const express = require('express');
// bodyparser is for reading incoming data
const bodyParser = require('body-parser');
// cypto handles Crpytographic functions, sorta like passwords (for a bad example)
const crypto = require('crypto');
// fetch is used for HTTP/API requests
// and can be swapped out for inbuild fetch in node19+
//esm
//const fetch = require('node-fetch');
// cjs
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
// Express basics
const app = express();
const http = require('http').Server(app);
http.listen(config.port, function() {
console.log('Server raised on', config.port);
});
/* Session */
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
// For production see the node in the README.md
// ## Nginx and Cookie Security
// https://expressjs.com/en/advanced/best-practice-security.html#use-cookies-securely
// Setup a session manager
app.use(session({
secret: crypto.randomBytes(4).toString('base64'),
resave: true,
saveUninitialized: false,
cookie: {
secure: false,
maxAge: (30 * 60 * 1000)
},
rolling: true
}));
// Using Pug to make rendering easier
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.locals.basedir = path.join(__dirname, 'views');
app.set('view options', {
debug: false,
compileDebug: false
});
// need a script
app.use(express.static(path.join(__dirname, 'public')));
/* Flash Warnings/Error handler */
app.use(function(req,res,next) {
var flash = {
error: (req.session.error ? req.session.error : false),
warning: (req.session.warning ? req.session.warning : false),
success: (req.session.success ? req.session.success : false)
}
res.locals.flash = flash;
if (req.session.error) { req.session.error = ''; }
if (req.session.warning) { req.session.warning = ''; }
if (req.session.success) { req.session.success = ''; }
next();
});
// Routes
app
.route('/')
.get(async (req, res) => {
console.log('Incoming get request');
if (req.session.token) {
// probably logged in
// and will suffice for this example
console.log(`The server has a token: ${req.session.token.access_token}`);
// validate and return the token details
let validateResp = await fetch(
'https://id.twitch.tv/oauth2/validate',
{
headers: {
'Authorization': `Bearer ${req.session.token.access_token}`,
'Accept': 'application/json'
}
}
)
if (validateResp.status != 200) {
req.session.error = 'Token is invalid!';
res.redirect('/');
return;
}
let validateData = await validateResp.json();
console.log('Ok', validateData);
res.render(
'loggedin',
{
user: req.session.user,
token: validateData
}
);
return
}
// test for query string parameters
let { code, error, error_description, scope, state } = req.query;
if (code) {
// do the oAuth dance and exchange the token for a user token
// first validate the state is valid
state = decodeURIComponent(state);
if (req.session.state != state) {
req.session.error = 'State does not match. Please try again!';
res.redirect('/');
return;
}
// done with the state params
delete req.session.state;
// start the oAuth dance
let tokenResp = await fetch(
"https://id.twitch.tv/oauth2/token",
{
method: 'POST',
headers: {
'Accept': 'application/json'
},
body: new URLSearchParams([
[ 'client_id', config.client_id ],
[ 'client_secret', config.client_secret ],
[ 'code', code ],
[ 'grant_type', 'authorization_code' ],
[ 'redirect_uri', config.redirect_uri ]
])
}
);
if (tokenResp.status != 200) {
req.session.error = 'An Error occured: ' + await tokenResp.text();
res.redirect('/');
return;
}
// oAuth dance success!
req.session.token = await tokenResp.json();
// we'll go collect the user this token is for
let userResp = await fetch(
'https://api.twitch.tv/helix/users',
{
method: 'GET',
headers: {
'Accept': 'application/json',
'Client-ID': config.client_id,
'Authorization': `Bearer ${req.session.token.access_token}`
}
}
);
if (userResp.status != 200) {
req.session.error = 'An Error occured: ' + await tokenResp.text();
res.redirect('/');
return;
}
let userData = await userResp.json();
// malformed...
if (!userData.hasOwnProperty('data')) {
req.session.warning = 'We got a Token but failed to get your Twitch profile from Helix';
res.redirect('/');
return;
}
// not one user returned
if (userData.data.length != 1) {
req.session.warning = 'We got a Token but failed to get your Twitch profile from Helix';
res.redirect('/');
return;
}
req.session.user = userData.data[0];
res.redirect('/');
return;
}
var auth_error = '';
if ( error ) {
auth_error = 'oAuth Error ' + error_description;
}
// We use state to defend against CSRF attacks.
// We'll generate one and store it in the session
// twitch will return it to us later
req.session.state = crypto.randomBytes(16).toString('base64');
// this just passes a bunch of vairables to the view
// and the view handles the display logic
// it's a non exhaustive list of scopes tha exist
// how scopes are being fetched here is _bad_ but will suffice for this example
res.render('generator', {
client_id: config.client_id,
redirect_uri: config.redirect_uri,
auth_error,
scopes: JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'scopes.json'))),
state: req.session.state
});
})
.post((req, res) => {
console.log('Incoming post request');
res.redirect('/');
});
app
.route('/logout/')
.get((req, res) => {
console.log('Incoming logout request');
// as well as dumoing the session lets revoke the token
fetch(
'https://id.twitch.tv/oauth2/revoke'
+ '?client_id=' + config.client_id
+ '&token=' + req.session.token.access_token,
{
method: 'post'
}
)
.then(resp => {
console.log('KeyRevoke OK', resp.status);
})
.catch(err => {
console.error('KeyRevoke Fail', err);
});
// and dump
req.session.destroy();
res.redirect('/');
});