update depedencies, changes to support updated authentication-module
[squeep-indie-auther] / src / service.js
1 'use strict';
2
3 /**
4 * Here we extend the base API server to define our routes and any route-specific
5 * behavior (middlewares) before handing off to the manager.
6 */
7
8 const path = require('path');
9 const { Dingus } = require('@squeep/api-dingus');
10 const common = require('./common');
11 const Manager = require('./manager');
12 const { Authenticator, SessionManager } = require('@squeep/authentication-module');
13 const { ResourceAuthenticator } = require('@squeep/resource-authentication-module');
14 const { initContext, navLinks } = require('./template/template-helper');
15 const Enum = require('./enum');
16 const { ResponseError } = require('./errors');
17
18 const _fileScope = common.fileScope(__filename);
19
20 class Service extends Dingus {
21 constructor(logger, db, options, asyncLocalStorage) {
22 super(logger, {
23 ...options.dingus,
24 ignoreTrailingSlash: false,
25 });
26 this.options = options;
27 this.asyncLocalStorage = asyncLocalStorage;
28 this.staticPath = path.normalize(path.join(__dirname, '..', 'static'));
29 this.manager = new Manager(logger, db, options);
30 this.authenticator = new Authenticator(logger, db, options);
31 this.sessionManager = new SessionManager(logger, this.authenticator, options);
32 this.resourceAuthenticator = new ResourceAuthenticator(logger, db, options);
33 this.loginPath = `${options.dingus.proxyPrefix}/admin/login`;
34
35 // N.B. /admin routes not currently configurable
36 const route = (r) => `/${options.route[r]}`; // eslint-disable-line security/detect-object-injection
37
38 // Service discovery
39 this.on(['GET'], route('metadata'), this.handlerGetMeta.bind(this));
40 // Also respond with metadata on well-known oauth2 endpoint if base has no prefix
41 if ((options?.dingus?.selfBaseUrl?.match(/\//g) || []).length === 3) {
42 this.on(['GET'], '/.well-known/oauth-authorization-server', this.handlerGetMeta.bind(this));
43 }
44
45 // Primary endpoints
46 this.on(['GET'], route('authorization'), this.handlerGetAuthorization.bind(this));
47 this.on(['POST'], route('authorization'), this.handlerPostAuthorization.bind(this));
48 this.on(['POST'], route('consent'), this.handlerPostConsent.bind(this));
49 this.on(['POST'], route('revocation'), this.handlerPostRevocation.bind(this));
50 this.on(['POST'], route('ticket'), this.handlerPostTicket.bind(this));
51 this.on(['POST'], route('token'), this.handlerPostToken.bind(this));
52
53 // Resource endpoints
54 this.on('POST', route('introspection'), this.handlerPostIntrospection.bind(this));
55 this.on('POST', route('userinfo'), this.handlerPostUserInfo.bind(this));
56
57 // Information page about service
58 this.on(['GET'], '/', this.handlerGetRoot.bind(this));
59
60 // Temmporary to see what rando payload someone is sending us unsolicited
61 this.on(['POST'], '/', this.handlerWhaGwan.bind(this));
62
63 // Give load-balancers something to check
64 this.on(['GET'], route('healthcheck'), this.handlerGetHealthcheck.bind(this));
65
66 // These routes are intended for accessing static content during development.
67 // In production, a proxy server would likely handle these first.
68 this.on(['GET'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
69 this.on(['GET'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
70 this.on(['GET'], '/static/:file', this.handlerGetStaticFile.bind(this));
71 this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
72 this.on(['GET'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
73
74 // Profile and token management for authenticated sessions
75 this.on(['GET'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
76 this.on(['GET'], '/admin/', this.handlerGetAdmin.bind(this));
77 this.on(['POST'], '/admin/', this.handlerPostAdmin.bind(this));
78
79 // Ticket-proffering interface for authenticated sessions
80 this.on(['GET'], '/admin/ticket', this.handlerGetAdminTicket.bind(this));
81 this.on(['POST'], '/admin/ticket', this.handlerPostAdminTicket.bind(this));
82
83 // User authentication and session establishment
84 this.on(['GET'], '/admin/login', this.handlerGetAdminLogin.bind(this));
85 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin.bind(this));
86 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout.bind(this));
87 this.on(['GET'], '/admin/settings', this.handlerGetAdminSettings.bind(this));
88 this.on(['POST'], '/admin/settings', this.handlerPostAdminSettings.bind(this));
89
90 // Page for upkeep info et cetera
91 this.on(['GET'], '/admin/maintenance', this.handlerGetAdminMaintenance.bind(this));
92
93 }
94
95
96 /**
97 * Perform any async startup tasks.
98 */
99 async initialize() {
100 await this.manager.initialize();
101 }
102
103
104 /**
105 * Do a little more on each request.
106 * @param {http.IncomingMessage} req
107 * @param {http.ServerResponse} res
108 * @param {Object} ctx
109 */
110 async preHandler(req, res, ctx) {
111 const _scope = _fileScope('preHandler');
112
113 await super.preHandler(req, res, ctx);
114 ctx.url = req.url; // Persist this for logout redirect
115
116 const logObject = this.asyncLocalStorage.getStore();
117 // istanbul ignore else
118 if (logObject) { // Debugging in vscode seems to kill ALS, work around
119 logObject.requestId = ctx.requestId;
120 delete ctx.requestId;
121 } else {
122 this.logger.debug(_scope, 'no async local store');
123 }
124 }
125
126
127 /**
128 * @param {http.IncomingMessage} req
129 * @param {http.ServerResponse} res
130 * @param {Object} ctx
131 */
132 async handlerGetAdminLogin(req, res, ctx) {
133 const _scope = _fileScope('handlerGetAdminLogin');
134 this.logger.debug(_scope, 'called', { req, ctx });
135
136 this.setResponseType(this.responseTypes, req, res, ctx);
137
138 await this.authenticator.sessionOptionalLocal(req, res, ctx);
139
140 await this.sessionManager.getAdminLogin(res, ctx, navLinks);
141 }
142
143
144 /**
145 * @param {http.IncomingMessage} req
146 * @param {http.ServerResponse} res
147 * @param {Object} ctx
148 */
149 async handlerPostAdminLogin(req, res, ctx) {
150 const _scope = _fileScope('handlerPostAdminLogin');
151 this.logger.debug(_scope, 'called', { req, ctx });
152
153 initContext(ctx);
154
155 this.setResponseType(this.responseTypes, req, res, ctx);
156
157 await this.authenticator.sessionOptionalLocal(req, res, ctx);
158
159 await this.ingestBody(req, res, ctx);
160
161 await this.sessionManager.postAdminLogin(res, ctx, navLinks);
162 }
163
164
165 /**
166 * @param {http.IncomingMessage} req
167 * @param {http.ServerResponse} res
168 * @param {Object} ctx
169 */
170 async handlerGetAdminSettings(req, res, ctx) {
171 const _scope = _fileScope('handlerGetAdminSettings');
172 this.logger.debug(_scope, 'called', { req, ctx });
173
174 initContext(ctx);
175
176 this.setResponseType(this.responseTypes, req, res, ctx);
177
178 if (await this.authenticator.sessionRequiredLocal(req, res, ctx)) {
179 await this.sessionManager.getAdminSettings(res, ctx, navLinks);
180 }
181 }
182
183
184 /**
185 * @param {http.IncomingMessage} req
186 * @param {http.ServerResponse} res
187 * @param {Object} ctx
188 */
189 async handlerPostAdminSettings(req, res, ctx) {
190 const _scope = _fileScope('handlerPostAdminSettings');
191 this.logger.debug(_scope, 'called', { req, ctx });
192
193 initContext(ctx);
194
195 this.setResponseType(this.responseTypes, req, res, ctx);
196
197 if (await this.authenticator.sessionRequiredLocal(req, res, ctx)) {
198 await this.ingestBody(req, res, ctx);
199 await this.sessionManager.postAdminSettings(res, ctx, navLinks);
200 }
201 }
202
203
204 /**
205 * @param {http.IncomingMessage} req
206 * @param {http.ServerResponse} res
207 * @param {Object} ctx
208 */
209 async handlerGetAdminLogout(req, res, ctx) {
210 const _scope = _fileScope('handlerGetAdminLogout');
211 this.logger.debug(_scope, 'called', { req, ctx });
212
213 initContext(ctx);
214
215 this.setResponseType(this.responseTypes, req, res, ctx);
216
217 await this.authenticator.sessionOptionalLocal(req, res, ctx);
218
219 await this.sessionManager.getAdminLogout(res, ctx);
220 }
221
222
223 /**
224 * @param {http.IncomingMessage} req
225 * @param {http.ServerResponse} res
226 * @param {Object} ctx
227 */
228 async handlerGetAdmin(req, res, ctx) {
229 const _scope = _fileScope('handlerGetAdmin');
230 this.logger.debug(_scope, 'called', { req, ctx });
231
232 initContext(ctx);
233
234 this.setResponseType(this.responseTypes, req, res, ctx);
235
236 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
237 await this.manager.getAdmin(res, ctx);
238 }
239 }
240
241
242 /**
243 * @param {http.IncomingMessage} req
244 * @param {http.ServerResponse} res
245 * @param {Object} ctx
246 */
247 async handlerPostAdmin(req, res, ctx) {
248 const _scope = _fileScope('handlerPostAdmin');
249 this.logger.debug(_scope, 'called', { req, ctx });
250
251 initContext(ctx);
252
253 this.setResponseType(this.responseTypes, req, res, ctx);
254
255 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
256 await this.ingestBody(req, res, ctx);
257 await this.manager.postAdmin(res, ctx);
258 }
259 }
260
261
262 /**
263 * @param {http.IncomingMessage} req
264 * @param {http.ServerResponse} res
265 * @param {Object} ctx
266 */
267 async handlerGetAdminTicket(req, res, ctx) {
268 const _scope = _fileScope('handlerGetAdminTicket');
269 this.logger.debug(_scope, 'called', { req, ctx });
270
271 initContext(ctx);
272
273 this.setResponseType(this.responseTypes, req, res, ctx);
274
275 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
276 await this.manager.getAdminTicket(res, ctx);
277 }
278 }
279
280
281 /**
282 * @param {http.IncomingMessage} req
283 * @param {http.ServerResponse} res
284 * @param {Object} ctx
285 */
286 async handlerPostAdminTicket(req, res, ctx) {
287 const _scope = _fileScope('handlerPostAdminTicket');
288 this.logger.debug(_scope, 'called', { req, ctx });
289
290 initContext(ctx);
291
292 this.setResponseType(this.responseTypes, req, res, ctx);
293
294 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
295 await this.ingestBody(req, res, ctx);
296 await this.manager.postAdminTicket(res, ctx);
297 }
298 }
299
300
301 /**
302 * @param {http.IncomingMessage} req
303 * @param {http.ServerResponse} res
304 * @param {Object} ctx
305 */
306 async handlerGetMeta(req, res, ctx) {
307 const _scope = _fileScope('handlerGetMeta');
308 this.logger.debug(_scope, 'called', { req, ctx });
309
310 const responseTypes = [
311 Enum.ContentType.ApplicationJson,
312 Enum.ContentType.TextPlain,
313 ];
314
315 this.setResponseType(responseTypes, req, res, ctx);
316
317 await this.authenticator.sessionOptionalLocal(req, res, ctx);
318
319 await this.manager.getMeta(res, ctx);
320 }
321
322
323 /**
324 * @param {http.IncomingMessage} req
325 * @param {http.ServerResponse} res
326 * @param {Object} ctx
327 */
328 async handlerGetAuthorization(req, res, ctx) {
329 const _scope = _fileScope('handlerGetAuthorization');
330 this.logger.debug(_scope, 'called', { req, ctx });
331
332 initContext(ctx);
333
334 this.setResponseType(this.responseTypes, req, res, ctx);
335
336 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
337 await this.manager.getAuthorization(res, ctx);
338 }
339 }
340
341
342 /**
343 * @param {http.IncomingMessage} req
344 * @param {http.ServerResponse} res
345 * @param {Object} ctx
346 */
347 async handlerPostAuthorization(req, res, ctx) {
348 const _scope = _fileScope('handlerPostAuthorization');
349 this.logger.debug(_scope, 'called', { req, ctx });
350
351 const responseTypes = [
352 Enum.ContentType.ApplicationJson,
353 Enum.ContentType.TextPlain,
354 ];
355
356 this.setResponseType(responseTypes, req, res, ctx);
357
358 await this.authenticator.sessionOptionalLocal(req, res, ctx);
359
360 await this.ingestBody(req, res, ctx);
361
362 await this.manager.postAuthorization(res, ctx);
363 }
364
365
366 /**
367 * @param {http.IncomingMessage} req
368 * @param {http.ServerResponse} res
369 * @param {Object} ctx
370 */
371 async handlerPostConsent(req, res, ctx) {
372 const _scope = _fileScope('handlerPostConsent');
373 this.logger.debug(_scope, 'called', { req, ctx });
374
375 initContext(ctx);
376
377 this.setResponseType(this.responseTypes, req, res, ctx);
378
379 // This isn't specified as required as any valid payload carries intrinsic auth data.
380 await this.authenticator.sessionOptionalLocal(req, res, ctx);
381
382 await this.ingestBody(req, res, ctx);
383
384 await this.manager.postConsent(res, ctx);
385 }
386
387
388 /**
389 * @param {http.IncomingMessage} req
390 * @param {http.ServerResponse} res
391 * @param {Object} ctx
392 */
393 async handlerPostTicket(req, res, ctx) {
394 const _scope = _fileScope('handlerPostTicket');
395 this.logger.debug(_scope, 'called', { req, ctx });
396
397 const responseTypes = [
398 Enum.ContentType.ApplicationJson,
399 Enum.ContentType.TextPlain,
400 ];
401
402 this.setResponseType(responseTypes, req, res, ctx);
403
404 await this.ingestBody(req, res, ctx);
405
406 await this.manager.postTicket(req, res, ctx);
407 }
408
409
410 /**
411 * @param {http.IncomingMessage} req
412 * @param {http.ServerResponse} res
413 * @param {Object} ctx
414 */
415 async handlerPostToken(req, res, ctx) {
416 const _scope = _fileScope('handlerPostToken');
417 this.logger.debug(_scope, 'called', { req, ctx });
418
419 const responseTypes = [
420 Enum.ContentType.ApplicationJson,
421 Enum.ContentType.TextPlain,
422 ];
423
424 this.setResponseType(responseTypes, req, res, ctx);
425
426 await this.ingestBody(req, res, ctx);
427
428 await this.manager.postToken(req, res, ctx);
429 }
430
431
432 /**
433 * @param {http.IncomingMessage} req
434 * @param {http.ServerResponse} res
435 * @param {Object} ctx
436 */
437 async handlerPostRevocation(req, res, ctx) {
438 const _scope = _fileScope('handlerPostRevocation');
439 this.logger.debug(_scope, 'called', { req, ctx });
440
441 const responseTypes = [
442 Enum.ContentType.ApplicationJson,
443 Enum.ContentType.TextPlain,
444 ];
445
446 this.setResponseType(responseTypes, req, res, ctx);
447
448 await this.ingestBody(req, res, ctx);
449
450 await this.manager.postRevocation(req, res, ctx);
451 }
452
453
454 /**
455 * @param {http.IncomingMessage} req
456 * @param {http.ServerResponse} res
457 * @param {Object} ctx
458 */
459 async handlerPostIntrospection(req, res, ctx) {
460 const _scope = _fileScope('handlerPostIntrospection');
461 this.logger.debug(_scope, 'called', { req, ctx });
462
463 const responseTypes = [
464 Enum.ContentType.ApplicationJson,
465 Enum.ContentType.TextPlain,
466 ];
467
468 await this.resourceAuthenticator.required(req, res, ctx);
469
470 this.setResponseType(responseTypes, req, res, ctx);
471
472 await this.ingestBody(req, res, ctx);
473
474 await this.manager.postIntrospection(res, ctx);
475 }
476
477
478 /**
479 * @param {http.IncomingMessage} req
480 * @param {http.ServerResponse} res
481 * @param {Object} ctx
482 */
483 async handlerPostUserInfo(req, res, ctx) {
484 const _scope = _fileScope('handlerPostUserInfo');
485 this.logger.debug(_scope, 'called', { req, ctx });
486
487 const responseTypes = [
488 Enum.ContentType.ApplicationJson,
489 Enum.ContentType.TextPlain,
490 ];
491
492 this.setResponseType(responseTypes, req, res, ctx);
493
494 await this.ingestBody(req, res, ctx);
495
496 await this.manager.postUserInfo(res, ctx);
497 }
498
499
500 /**
501 * @param {http.IncomingMessage} req
502 * @param {http.ServerResponse} res
503 * @param {Object} ctx
504 */
505 async handlerGetRoot(req, res, ctx) {
506 const _scope = _fileScope('handlerGetRoot');
507 const responseTypes = [
508 Enum.ContentType.TextHTML,
509 ];
510 this.logger.debug(_scope, 'called', { req, ctx });
511
512 initContext(ctx);
513
514 this.setResponseType(responseTypes, req, res, ctx);
515
516 await this.authenticator.sessionOptionalLocal(req, res, ctx);
517
518 await this.manager.getRoot(res, ctx);
519 }
520
521
522 /**
523 * Temporary to see what an unsolicited payload contains.
524 */
525 async handlerWhaGwan(req, res, ctx) {
526 this.setResponseType(this.responseTypes, req, res, ctx);
527 await this.ingestBody(req, res, ctx);
528 throw new ResponseError(Enum.ErrorResponse.MethodNotAllowed);
529 }
530
531 /**
532 * @param {http.IncomingMessage} req
533 * @param {http.ServerResponse} res
534 * @param {Object} ctx
535 */
536 async handlerGetHealthcheck(req, res, ctx) {
537 const _scope = _fileScope('handlerGetHealthcheck');
538 this.logger.debug(_scope, 'called', { req, ctx });
539
540 this.setResponseType(this.responseTypes, req, res, ctx);
541
542 await this.manager.getHealthcheck(res, ctx);
543 }
544
545
546 /**
547 * @param {http.IncomingMessage} req
548 * @param {http.ServerResponse} res
549 * @param {Object} ctx
550 */
551 async handlerGetAdminMaintenance(req, res, ctx) {
552 const _scope = _fileScope('handlerGetAdminMaintenance');
553 this.logger.debug(_scope, 'called', { req, ctx });
554
555 initContext(ctx);
556
557 this.setResponseType(this.responseTypes, req, res, ctx);
558
559 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
560 await this.manager.getAdminMaintenance(res, ctx);
561 }
562 }
563
564
565 /**
566 * FIXME: This doesn't seem to be working as envisioned. Maybe override render error method instead???
567 * Intercept this and redirect if we have enough information, otherwise default to framework.
568 * Fixing this will likely have to wait until an e2e test framework is in place.
569 * The redirect attempt should probably be contained in a Manager method, but here it is for now.
570 * @param {http.IncomingMessage} req
571 * @param {http.ServerResponse} res
572 * @param {Object} ctx
573 */
574 async handlerInternalServerError(req, res, ctx) {
575 const _scope = _fileScope('handlerInternalServerError');
576 this.logger.debug(_scope, 'called', { req, ctx });
577
578 if (ctx?.session?.redirectUri && ctx?.session?.clientIdentifier) {
579 Object.entries({
580 ...(ctx.session.state && { 'state': ctx.session.state }),
581 'error': 'server_error',
582 'error_description': 'An internal server error occurred',
583 }).forEach(([name, value]) => ctx.session.redirectUri.searchParams.set(name, value));
584 res.statusCode = 302; // Found
585 res.setHeader(Enum.Header.Location, ctx.session.redirectUri.href);
586 res.end();
587 return;
588 }
589
590 await super.handlerInternalServerError(req, res, ctx);
591 }
592
593
594 }
595
596 module.exports = Service;
597