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