-
-
Notifications
You must be signed in to change notification settings - Fork 45
/
Copy pathserver.js
307 lines (261 loc) · 9.33 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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
// OIDC FUN STUFF
const jwt = require('jsonwebtoken');
let oidc_data = {};
let verifier_options;
let verifier_keys;
let verifier_client;
const jwksClient = require('jwks-rsa');
// Fetch OpenID data
// Twitch provides a endpoint that contains information about openID
// This includes the relevant endpoitns for authentatication
// And the available scopes
// And the keys for validation JWT's
async function getOpenIDConfig() {
let resp = await fetch(
'https://id.twitch.tv/oauth2/.well-known/openid-configuration',
{
method: 'GET',
headers: {
'Accept': 'application/json'
}
}
);
oidc_data = await resp.json();
console.log('BOOT: Got openID config');
verifier_options = {
algorithms: oidc_data.id_token_signing_alg_values_supported,
audience: config.client_id,
issuer: oidc_data.issuer
}
verifier_client = jwksClient({
jwksUri: oidc_data.jwks_uri
});
}
getOpenIDConfig();
// https://github.com/auth0/node-jsonwebtoken
function getKey(header, callback) {
verifier_client.getSigningKey(header.kid, function(err, key) {
var signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
// Express basics
const app = express();
const http = require('http').Server(app);
http.listen(config.port, function() {
console.log('Server raised on', config.port);
});
// 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
var session = require('express-session');
app.use(session({
secret: crypto.randomBytes(4).toString('base64'),
resave: true,
saveUninitialized: false,
cookie: {
secure: false,
maxAge: (15 * 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
// validate and return the token details
let resp = await fetch(
'https://id.twitch.tv/oauth2/validate',
{
headers: {
Authorization: `Bearer ${req.session.token.access_token}`
}
}
)
if (resp.status != 200) {
req.session.error = 'Token not valid!';
res.redirect('/');
return;
}
console.log(req.session.user);
console.log(req.session.payload);
res.render(
'loggedin',
{
user: req.session.user,
payload: req.session.payload,
token: await resp.json()
}
);
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 resp = await fetch(
oidc_data.token_endpoint,
{
"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 (resp.status != 200) {
// the oAuth dance failed
req.session.error = 'An Error occured: ' + await resp.text();
res.redirect('/');
return;
}
// oAuth dance success!
req.session.token = await resp.json();
// console.log(resp.body);
jwt.verify(req.session.token.id_token, getKey, verifier_options, async function(err, payload) {
if (err) {
if (err.Error) {
req.session.warning = 'Error: ' + err.Error;
} else if (err.body && err.body.message) {
req.session.warning = 'Error: ' + err.body.message;
} else {
console.log(err);
}
req.session.error = 'Twitch Hiccuped';
res.redirect('/');
} else {
console.log('Login From', payload.sub, payload);
req.session.payload = payload;
let userInfo = await fetch(
oidc_data.userinfo_endpoint,
{
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${req.session.token.access_token}`
}
}
);
if (userInfo.status != 200) {
req.session.error = 'Twitch Hiccuped b';
req.session.warning = 'Error: ' + await userInfo.text();
res.redirect('/');
return;
}
req.session.user = await userInfo.json();
res.redirect('/');
}
});
return;
}
var auth_error = '';
if ( error ) {
auth_error = 'oAuth Error ' + error_description;
}
// 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
res.render('login', {
auth_error,
state: req.session.state
});
})
.post((req, res) => {
console.log('Incoming post request');
res.redirect('/');
});
app
.route('/login/')
.get((req, res) => {
console.log('Incoming login request');
// 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');
// construct the linking URL
var login_url = oidc_data.authorization_endpoint
+ '?client_id=' + config.client_id
+ '&redirect_uri=' + encodeURIComponent(config.redirect_uri)
+ '&response_type=code'
+ '&force_verify=true'
+ '&scope=' + oidc_data.scopes_supported.join('+')
+ '&state=' + encodeURIComponent(req.session.state)
+ '&claims=' + JSON.stringify({
userinfo: {
email:null,
email_verified:null,
picture:null,
preferred_username:null
}
});
console.log('Redirect to', login_url);
res.redirect(login_url);
});
app
.route('/logout/')
.get((req, res) => {
console.log('Incoming logout request');
// and dump
req.session.destroy();
res.redirect('/');
});