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.
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');
18 const _fileScope
= common
.fileScope(__filename
);
20 class Service
extends Dingus
{
21 constructor(logger
, db
, options
, asyncLocalStorage
) {
24 ignoreTrailingSlash: false,
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`;
35 // N.B. /admin routes not currently configurable
36 const route
= (r
) => `/${options.route[r]}`; // eslint-disable-line security/detect-object-injection
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));
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));
54 this.on('POST', route('introspection'), this.handlerPostIntrospection
.bind(this));
55 this.on('POST', route('userinfo'), this.handlerPostUserInfo
.bind(this));
57 // Information page about service
58 this.on(['GET'], '/', this.handlerGetRoot
.bind(this));
60 // Temmporary to see what rando payload someone is sending us unsolicited
61 this.on(['POST'], '/', this.handlerWhaGwan
.bind(this));
63 // Give load-balancers something to check
64 this.on(['GET'], route('healthcheck'), this.handlerGetHealthcheck
.bind(this));
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');
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));
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));
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));
90 // Page for upkeep info et cetera
91 this.on(['GET'], '/admin/maintenance', this.handlerGetAdminMaintenance
.bind(this));
97 * Perform any async startup tasks.
100 await
this.manager
.initialize();
105 * Do a little more on each request.
106 * @param {http.IncomingMessage} req
107 * @param {http.ServerResponse} res
108 * @param {Object} ctx
110 async
preHandler(req
, res
, ctx
) {
111 const _scope
= _fileScope('preHandler');
113 await
super.preHandler(req
, res
, ctx
);
114 ctx
.url
= req
.url
; // Persist this for logout redirect
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
;
122 this.logger
.debug(_scope
, 'no async local store');
128 * @param {http.IncomingMessage} req
129 * @param {http.ServerResponse} res
130 * @param {Object} ctx
132 async
handlerGetAdminLogin(req
, res
, ctx
) {
133 const _scope
= _fileScope('handlerGetAdminLogin');
134 this.logger
.debug(_scope
, 'called', { req
, ctx
});
136 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
138 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
140 await
this.sessionManager
.getAdminLogin(res
, ctx
, navLinks
);
145 * @param {http.IncomingMessage} req
146 * @param {http.ServerResponse} res
147 * @param {Object} ctx
149 async
handlerPostAdminLogin(req
, res
, ctx
) {
150 const _scope
= _fileScope('handlerPostAdminLogin');
151 this.logger
.debug(_scope
, 'called', { req
, ctx
});
155 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
157 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
159 await
this.ingestBody(req
, res
, ctx
);
161 await
this.sessionManager
.postAdminLogin(res
, ctx
, navLinks
);
166 * @param {http.IncomingMessage} req
167 * @param {http.ServerResponse} res
168 * @param {Object} ctx
170 async
handlerGetAdminSettings(req
, res
, ctx
) {
171 const _scope
= _fileScope('handlerGetAdminSettings');
172 this.logger
.debug(_scope
, 'called', { req
, ctx
});
176 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
178 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
)) {
179 await
this.sessionManager
.getAdminSettings(res
, ctx
, navLinks
);
185 * @param {http.IncomingMessage} req
186 * @param {http.ServerResponse} res
187 * @param {Object} ctx
189 async
handlerPostAdminSettings(req
, res
, ctx
) {
190 const _scope
= _fileScope('handlerPostAdminSettings');
191 this.logger
.debug(_scope
, 'called', { req
, ctx
});
195 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
197 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
)) {
198 await
this.ingestBody(req
, res
, ctx
);
199 await
this.sessionManager
.postAdminSettings(res
, ctx
, navLinks
);
205 * @param {http.IncomingMessage} req
206 * @param {http.ServerResponse} res
207 * @param {Object} ctx
209 async
handlerGetAdminLogout(req
, res
, ctx
) {
210 const _scope
= _fileScope('handlerGetAdminLogout');
211 this.logger
.debug(_scope
, 'called', { req
, ctx
});
215 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
217 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
219 await
this.sessionManager
.getAdminLogout(res
, ctx
);
224 * @param {http.IncomingMessage} req
225 * @param {http.ServerResponse} res
226 * @param {Object} ctx
228 async
handlerGetAdmin(req
, res
, ctx
) {
229 const _scope
= _fileScope('handlerGetAdmin');
230 this.logger
.debug(_scope
, 'called', { req
, ctx
});
234 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
236 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
237 await
this.manager
.getAdmin(res
, ctx
);
243 * @param {http.IncomingMessage} req
244 * @param {http.ServerResponse} res
245 * @param {Object} ctx
247 async
handlerPostAdmin(req
, res
, ctx
) {
248 const _scope
= _fileScope('handlerPostAdmin');
249 this.logger
.debug(_scope
, 'called', { req
, ctx
});
253 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
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
);
263 * @param {http.IncomingMessage} req
264 * @param {http.ServerResponse} res
265 * @param {Object} ctx
267 async
handlerGetAdminTicket(req
, res
, ctx
) {
268 const _scope
= _fileScope('handlerGetAdminTicket');
269 this.logger
.debug(_scope
, 'called', { req
, ctx
});
273 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
275 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
276 await
this.manager
.getAdminTicket(res
, ctx
);
282 * @param {http.IncomingMessage} req
283 * @param {http.ServerResponse} res
284 * @param {Object} ctx
286 async
handlerPostAdminTicket(req
, res
, ctx
) {
287 const _scope
= _fileScope('handlerPostAdminTicket');
288 this.logger
.debug(_scope
, 'called', { req
, ctx
});
292 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
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
);
302 * @param {http.IncomingMessage} req
303 * @param {http.ServerResponse} res
304 * @param {Object} ctx
306 async
handlerGetMeta(req
, res
, ctx
) {
307 const _scope
= _fileScope('handlerGetMeta');
308 this.logger
.debug(_scope
, 'called', { req
, ctx
});
310 const responseTypes
= [
311 Enum
.ContentType
.ApplicationJson
,
312 Enum
.ContentType
.TextPlain
,
315 this.setResponseType(responseTypes
, req
, res
, ctx
);
317 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
319 await
this.manager
.getMeta(res
, ctx
);
324 * @param {http.IncomingMessage} req
325 * @param {http.ServerResponse} res
326 * @param {Object} ctx
328 async
handlerGetAuthorization(req
, res
, ctx
) {
329 const _scope
= _fileScope('handlerGetAuthorization');
330 this.logger
.debug(_scope
, 'called', { req
, ctx
});
334 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
336 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
337 await
this.manager
.getAuthorization(res
, ctx
);
343 * @param {http.IncomingMessage} req
344 * @param {http.ServerResponse} res
345 * @param {Object} ctx
347 async
handlerPostAuthorization(req
, res
, ctx
) {
348 const _scope
= _fileScope('handlerPostAuthorization');
349 this.logger
.debug(_scope
, 'called', { req
, ctx
});
351 const responseTypes
= [
352 Enum
.ContentType
.ApplicationJson
,
353 Enum
.ContentType
.TextPlain
,
356 this.setResponseType(responseTypes
, req
, res
, ctx
);
358 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
360 await
this.ingestBody(req
, res
, ctx
);
362 await
this.manager
.postAuthorization(res
, ctx
);
367 * @param {http.IncomingMessage} req
368 * @param {http.ServerResponse} res
369 * @param {Object} ctx
371 async
handlerPostConsent(req
, res
, ctx
) {
372 const _scope
= _fileScope('handlerPostConsent');
373 this.logger
.debug(_scope
, 'called', { req
, ctx
});
377 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
379 // This isn't specified as required as any valid payload carries intrinsic auth data.
380 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
382 await
this.ingestBody(req
, res
, ctx
);
384 await
this.manager
.postConsent(res
, ctx
);
389 * @param {http.IncomingMessage} req
390 * @param {http.ServerResponse} res
391 * @param {Object} ctx
393 async
handlerPostTicket(req
, res
, ctx
) {
394 const _scope
= _fileScope('handlerPostTicket');
395 this.logger
.debug(_scope
, 'called', { req
, ctx
});
397 const responseTypes
= [
398 Enum
.ContentType
.ApplicationJson
,
399 Enum
.ContentType
.TextPlain
,
402 this.setResponseType(responseTypes
, req
, res
, ctx
);
404 await
this.ingestBody(req
, res
, ctx
);
406 await
this.manager
.postTicket(req
, res
, ctx
);
411 * @param {http.IncomingMessage} req
412 * @param {http.ServerResponse} res
413 * @param {Object} ctx
415 async
handlerPostToken(req
, res
, ctx
) {
416 const _scope
= _fileScope('handlerPostToken');
417 this.logger
.debug(_scope
, 'called', { req
, ctx
});
419 const responseTypes
= [
420 Enum
.ContentType
.ApplicationJson
,
421 Enum
.ContentType
.TextPlain
,
424 this.setResponseType(responseTypes
, req
, res
, ctx
);
426 await
this.ingestBody(req
, res
, ctx
);
428 await
this.manager
.postToken(req
, res
, ctx
);
433 * @param {http.IncomingMessage} req
434 * @param {http.ServerResponse} res
435 * @param {Object} ctx
437 async
handlerPostRevocation(req
, res
, ctx
) {
438 const _scope
= _fileScope('handlerPostRevocation');
439 this.logger
.debug(_scope
, 'called', { req
, ctx
});
441 const responseTypes
= [
442 Enum
.ContentType
.ApplicationJson
,
443 Enum
.ContentType
.TextPlain
,
446 this.setResponseType(responseTypes
, req
, res
, ctx
);
448 await
this.ingestBody(req
, res
, ctx
);
450 await
this.manager
.postRevocation(req
, res
, ctx
);
455 * @param {http.IncomingMessage} req
456 * @param {http.ServerResponse} res
457 * @param {Object} ctx
459 async
handlerPostIntrospection(req
, res
, ctx
) {
460 const _scope
= _fileScope('handlerPostIntrospection');
461 this.logger
.debug(_scope
, 'called', { req
, ctx
});
463 const responseTypes
= [
464 Enum
.ContentType
.ApplicationJson
,
465 Enum
.ContentType
.TextPlain
,
468 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
470 this.setResponseType(responseTypes
, req
, res
, ctx
);
472 await
this.ingestBody(req
, res
, ctx
);
474 await
this.manager
.postIntrospection(res
, ctx
);
479 * @param {http.IncomingMessage} req
480 * @param {http.ServerResponse} res
481 * @param {Object} ctx
483 async
handlerPostUserInfo(req
, res
, ctx
) {
484 const _scope
= _fileScope('handlerPostUserInfo');
485 this.logger
.debug(_scope
, 'called', { req
, ctx
});
487 const responseTypes
= [
488 Enum
.ContentType
.ApplicationJson
,
489 Enum
.ContentType
.TextPlain
,
492 this.setResponseType(responseTypes
, req
, res
, ctx
);
494 await
this.ingestBody(req
, res
, ctx
);
496 await
this.manager
.postUserInfo(res
, ctx
);
501 * @param {http.IncomingMessage} req
502 * @param {http.ServerResponse} res
503 * @param {Object} ctx
505 async
handlerGetRoot(req
, res
, ctx
) {
506 const _scope
= _fileScope('handlerGetRoot');
507 const responseTypes
= [
508 Enum
.ContentType
.TextHTML
,
510 this.logger
.debug(_scope
, 'called', { req
, ctx
});
514 this.setResponseType(responseTypes
, req
, res
, ctx
);
516 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
518 await
this.manager
.getRoot(res
, ctx
);
523 * Temporary to see what an unsolicited payload contains.
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
);
532 * @param {http.IncomingMessage} req
533 * @param {http.ServerResponse} res
534 * @param {Object} ctx
536 async
handlerGetHealthcheck(req
, res
, ctx
) {
537 const _scope
= _fileScope('handlerGetHealthcheck');
538 this.logger
.debug(_scope
, 'called', { req
, ctx
});
540 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
542 await
this.manager
.getHealthcheck(res
, ctx
);
547 * @param {http.IncomingMessage} req
548 * @param {http.ServerResponse} res
549 * @param {Object} ctx
551 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
552 const _scope
= _fileScope('handlerGetAdminMaintenance');
553 this.logger
.debug(_scope
, 'called', { req
, ctx
});
557 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
559 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
560 await
this.manager
.getAdminMaintenance(res
, ctx
);
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
574 async
handlerInternalServerError(req
, res
, ctx
) {
575 const _scope
= _fileScope('handlerInternalServerError');
576 this.logger
.debug(_scope
, 'called', { req
, ctx
});
578 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
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
);
590 await
super.handlerInternalServerError(req
, res
, ctx
);
596 module
.exports
= Service
;