2a159a9181c7b4d327282035c014a4e87a194e55
[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'], 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'], '/.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'], '/', this.handlerGetRoot.bind(this));
57
58 // Give load-balancers something to check
59 this.on(['GET'], 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'], '/static', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/static/`);
64 this.on(['GET'], '/static/', this.handlerGetStaticFile.bind(this), 'index.html');
65 this.on(['GET'], '/static/:file', this.handlerGetStaticFile.bind(this));
66 this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile.bind(this), 'favicon.ico');
67 this.on(['GET'], '/robots.txt', this.handlerGetStaticFile.bind(this), 'robots.txt');
68
69 // Profile and token management for authenticated sessions
70 this.on(['GET'], '/admin', this.handlerRedirect.bind(this), `${options.dingus.proxyPrefix}/admin/`);
71 this.on(['GET'], '/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'], '/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'], '/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'], '/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 this.setResponseType(this.responseTypes, req, res, ctx);
123
124 await this.authenticator.sessionOptionalLocal(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 this.setResponseType(this.responseTypes, req, res, ctx);
182
183 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
184 await this.manager.getAdmin(res, ctx);
185 }
186 }
187
188
189 /**
190 * @param {http.IncomingMessage} req
191 * @param {http.ServerResponse} res
192 * @param {Object} ctx
193 */
194 async handlerPostAdmin(req, res, ctx) {
195 const _scope = _fileScope('handlerPostAdmin');
196 this.logger.debug(_scope, 'called', { req, ctx });
197
198 initContext(ctx);
199
200 this.setResponseType(this.responseTypes, req, res, ctx);
201
202 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
203 await this.ingestBody(req, res, ctx);
204 await this.manager.postAdmin(res, ctx);
205 }
206 }
207
208
209 /**
210 * @param {http.IncomingMessage} req
211 * @param {http.ServerResponse} res
212 * @param {Object} ctx
213 */
214 async handlerGetAdminTicket(req, res, ctx) {
215 const _scope = _fileScope('handlerGetAdminTicket');
216 this.logger.debug(_scope, 'called', { req, ctx });
217
218 initContext(ctx);
219
220 this.setResponseType(this.responseTypes, req, res, ctx);
221
222 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
223 await this.manager.getAdminTicket(res, ctx);
224 }
225 }
226
227
228 /**
229 * @param {http.IncomingMessage} req
230 * @param {http.ServerResponse} res
231 * @param {Object} ctx
232 */
233 async handlerPostAdminTicket(req, res, ctx) {
234 const _scope = _fileScope('handlerPostAdminTicket');
235 this.logger.debug(_scope, 'called', { req, ctx });
236
237 initContext(ctx);
238
239 this.setResponseType(this.responseTypes, req, res, ctx);
240
241 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
242 await this.ingestBody(req, res, ctx);
243 await this.manager.postAdminTicket(res, ctx);
244 }
245 }
246
247
248 /**
249 * @param {http.IncomingMessage} req
250 * @param {http.ServerResponse} res
251 * @param {Object} ctx
252 */
253 async handlerGetMeta(req, res, ctx) {
254 const _scope = _fileScope('handlerGetMeta');
255 this.logger.debug(_scope, 'called', { req, ctx });
256
257 const responseTypes = [
258 Enum.ContentType.ApplicationJson,
259 Enum.ContentType.TextPlain,
260 ];
261
262 this.setResponseType(responseTypes, req, res, ctx);
263
264 await this.authenticator.sessionOptionalLocal(req, res, ctx);
265
266 await this.manager.getMeta(res, ctx);
267 }
268
269
270 /**
271 * @param {http.IncomingMessage} req
272 * @param {http.ServerResponse} res
273 * @param {Object} ctx
274 */
275 async handlerGetAuthorization(req, res, ctx) {
276 const _scope = _fileScope('handlerGetAuthorization');
277 this.logger.debug(_scope, 'called', { req, ctx });
278
279 initContext(ctx);
280
281 this.setResponseType(this.responseTypes, req, res, ctx);
282
283 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
284 await this.manager.getAuthorization(res, ctx);
285 }
286 }
287
288
289 /**
290 * @param {http.IncomingMessage} req
291 * @param {http.ServerResponse} res
292 * @param {Object} ctx
293 */
294 async handlerPostAuthorization(req, res, ctx) {
295 const _scope = _fileScope('handlerPostAuthorization');
296 this.logger.debug(_scope, 'called', { req, ctx });
297
298 const responseTypes = [
299 Enum.ContentType.ApplicationJson,
300 Enum.ContentType.TextPlain,
301 ];
302
303 this.setResponseType(responseTypes, req, res, ctx);
304
305 await this.authenticator.sessionOptionalLocal(req, res, ctx);
306
307 await this.ingestBody(req, res, ctx);
308
309 await this.manager.postAuthorization(res, ctx);
310 }
311
312
313 /**
314 * @param {http.IncomingMessage} req
315 * @param {http.ServerResponse} res
316 * @param {Object} ctx
317 */
318 async handlerPostConsent(req, res, ctx) {
319 const _scope = _fileScope('handlerPostConsent');
320 this.logger.debug(_scope, 'called', { req, ctx });
321
322 initContext(ctx);
323
324 this.setResponseType(this.responseTypes, req, res, ctx);
325
326 // This isn't specified as required as any valid payload carries intrinsic auth data.
327 await this.authenticator.sessionOptionalLocal(req, res, ctx);
328
329 await this.ingestBody(req, res, ctx);
330
331 await this.manager.postConsent(res, ctx);
332 }
333
334
335 /**
336 * @param {http.IncomingMessage} req
337 * @param {http.ServerResponse} res
338 * @param {Object} ctx
339 */
340 async handlerPostTicket(req, res, ctx) {
341 const _scope = _fileScope('handlerPostTicket');
342 this.logger.debug(_scope, 'called', { req, ctx });
343
344 const responseTypes = [
345 Enum.ContentType.ApplicationJson,
346 Enum.ContentType.TextPlain,
347 ];
348
349 this.setResponseType(responseTypes, req, res, ctx);
350
351 await this.ingestBody(req, res, ctx);
352
353 await this.manager.postTicket(req, res, ctx);
354 }
355
356
357 /**
358 * @param {http.IncomingMessage} req
359 * @param {http.ServerResponse} res
360 * @param {Object} ctx
361 */
362 async handlerPostToken(req, res, ctx) {
363 const _scope = _fileScope('handlerPostToken');
364 this.logger.debug(_scope, 'called', { req, ctx });
365
366 const responseTypes = [
367 Enum.ContentType.ApplicationJson,
368 Enum.ContentType.TextPlain,
369 ];
370
371 this.setResponseType(responseTypes, req, res, ctx);
372
373 await this.ingestBody(req, res, ctx);
374
375 await this.manager.postToken(req, res, ctx);
376 }
377
378
379 /**
380 * @param {http.IncomingMessage} req
381 * @param {http.ServerResponse} res
382 * @param {Object} ctx
383 */
384 async handlerPostRevocation(req, res, ctx) {
385 const _scope = _fileScope('handlerPostRevocation');
386 this.logger.debug(_scope, 'called', { req, ctx });
387
388 const responseTypes = [
389 Enum.ContentType.ApplicationJson,
390 Enum.ContentType.TextPlain,
391 ];
392
393 this.setResponseType(responseTypes, req, res, ctx);
394
395 await this.ingestBody(req, res, ctx);
396
397 await this.manager.postRevocation(req, res, ctx);
398 }
399
400
401 /**
402 * @param {http.IncomingMessage} req
403 * @param {http.ServerResponse} res
404 * @param {Object} ctx
405 */
406 async handlerPostIntrospection(req, res, ctx) {
407 const _scope = _fileScope('handlerPostIntrospection');
408 this.logger.debug(_scope, 'called', { req, ctx });
409
410 const responseTypes = [
411 Enum.ContentType.ApplicationJson,
412 Enum.ContentType.TextPlain,
413 ];
414
415 await this.resourceAuthenticator.required(req, res, ctx);
416
417 this.setResponseType(responseTypes, req, res, ctx);
418
419 await this.ingestBody(req, res, ctx);
420
421 await this.manager.postIntrospection(res, ctx);
422 }
423
424
425 /**
426 * @param {http.IncomingMessage} req
427 * @param {http.ServerResponse} res
428 * @param {Object} ctx
429 */
430 async handlerPostUserInfo(req, res, ctx) {
431 const _scope = _fileScope('handlerPostUserInfo');
432 this.logger.debug(_scope, 'called', { req, ctx });
433
434 const responseTypes = [
435 Enum.ContentType.ApplicationJson,
436 Enum.ContentType.TextPlain,
437 ];
438
439 this.setResponseType(responseTypes, req, res, ctx);
440
441 await this.ingestBody(req, res, ctx);
442
443 await this.manager.postUserInfo(res, ctx);
444 }
445
446
447 /**
448 * @param {http.IncomingMessage} req
449 * @param {http.ServerResponse} res
450 * @param {Object} ctx
451 */
452 async handlerGetRoot(req, res, ctx) {
453 const _scope = _fileScope('handlerGetRoot');
454 const responseTypes = [
455 Enum.ContentType.TextHTML,
456 ];
457 this.logger.debug(_scope, 'called', { req, ctx });
458
459 initContext(ctx);
460
461 this.setResponseType(responseTypes, req, res, ctx);
462
463 await this.authenticator.sessionOptionalLocal(req, res, ctx);
464
465 await this.manager.getRoot(res, ctx);
466 }
467
468
469 /**
470 * @param {http.IncomingMessage} req
471 * @param {http.ServerResponse} res
472 * @param {Object} ctx
473 */
474 async handlerGetHealthcheck(req, res, ctx) {
475 const _scope = _fileScope('handlerGetHealthcheck');
476 this.logger.debug(_scope, 'called', { req, ctx });
477
478 this.setResponseType(this.responseTypes, req, res, ctx);
479
480 await this.manager.getHealthcheck(res, ctx);
481 }
482
483
484 /**
485 * @param {http.IncomingMessage} req
486 * @param {http.ServerResponse} res
487 * @param {Object} ctx
488 */
489 async handlerGetAdminMaintenance(req, res, ctx) {
490 const _scope = _fileScope('handlerGetAdminMaintenance');
491 this.logger.debug(_scope, 'called', { req, ctx });
492
493 initContext(ctx);
494
495 this.setResponseType(this.responseTypes, req, res, ctx);
496
497 if (await this.authenticator.sessionRequiredLocal(req, res, ctx, this.loginPath)) {
498 await this.manager.getAdminMaintenance(res, ctx);
499 }
500 }
501
502
503 /**
504 * FIXME: This doesn't seem to be working as envisioned. Maybe override render error method instead???
505 * Intercept this and redirect if we have enough information, otherwise default to framework.
506 * Fixing this will likely have to wait until an e2e test framework is in place.
507 * The redirect attempt should probably be contained in a Manager method, but here it is for now.
508 * @param {http.IncomingMessage} req
509 * @param {http.ServerResponse} res
510 * @param {Object} ctx
511 */
512 async handlerInternalServerError(req, res, ctx) {
513 const _scope = _fileScope('handlerInternalServerError');
514 this.logger.debug(_scope, 'called', { req, ctx });
515
516 if (ctx?.session?.redirectUri && ctx?.session?.clientIdentifier) {
517 Object.entries({
518 ...(ctx.session.state && { 'state': ctx.session.state }),
519 'error': 'server_error',
520 'error_description': 'An internal server error occurred',
521 }).forEach(([name, value]) => ctx.session.redirectUri.searchParams.set(name, value));
522 res.statusCode = 302; // Found
523 res.setHeader(Enum.Header.Location, ctx.session.redirectUri.href);
524 res.end();
525 return;
526 }
527
528 await super.handlerInternalServerError(req, res, ctx);
529 }
530
531
532 }
533
534 module.exports = Service;
535