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 { TemplateHelper: { initContext
} } = require('@squeep/html-template-helper');
15 const Enum
= require('./enum');
17 const _fileScope
= common
.fileScope(__filename
);
19 class Service
extends Dingus
{
20 constructor(logger
, db
, options
, asyncLocalStorage
) {
23 ignoreTrailingSlash: false,
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`;
33 // N.B. /admin routes not currently configurable
34 const route
= (r
) => `/${options.route[r]}`; // eslint-disable-line security/detect-object-injection
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));
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));
52 this.on('POST', route('introspection'), this.handlerPostIntrospection
.bind(this));
53 this.on('POST', route('userinfo'), this.handlerPostUserInfo
.bind(this));
55 // Information page about service
56 this.on(['GET', 'HEAD'], '/', this.handlerGetRoot
.bind(this));
58 // Give load-balancers something to check
59 this.on(['GET', 'HEAD'], route('healthcheck'), this.handlerGetHealthcheck
.bind(this));
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');
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));
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));
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));
83 // Page for upkeep info et cetera
84 this.on(['GET', 'HEAD'], '/admin/maintenance', this.handlerGetAdminMaintenance
.bind(this));
90 * Perform any async startup tasks.
93 await
this.manager
.initialize();
98 * Do a little more on each request.
99 * @param {http.IncomingMessage} req
100 * @param {http.ServerResponse} res
101 * @param {Object} ctx
103 async
preHandler(req
, res
, ctx
) {
104 await
super.preHandler(req
, res
, ctx
);
105 ctx
.url
= req
.url
; // Persist this for logout redirect
107 const logObject
= this.asyncLocalStorage
.getStore();
108 logObject
.requestId
= ctx
.requestId
;
109 delete ctx
.requestId
;
114 * @param {http.IncomingMessage} req
115 * @param {http.ServerResponse} res
116 * @param {Object} ctx
118 async
handlerGetAdminLogin(req
, res
, ctx
) {
119 const _scope
= _fileScope('handlerGetAdminLogin');
120 this.logger
.debug(_scope
, 'called', { req
, ctx
});
122 Dingus
.setHeadHandler(req
, res
, ctx
);
124 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
126 await
this.sessionManager
.getAdminLogin(res
, ctx
);
131 * @param {http.IncomingMessage} req
132 * @param {http.ServerResponse} res
133 * @param {Object} ctx
135 async
handlerPostAdminLogin(req
, res
, ctx
) {
136 const _scope
= _fileScope('handlerPostAdminLogin');
137 this.logger
.debug(_scope
, 'called', { req
, ctx
});
141 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
143 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
145 await
this.ingestBody(req
, res
, ctx
);
147 await
this.sessionManager
.postAdminLogin(res
, ctx
);
152 * @param {http.IncomingMessage} req
153 * @param {http.ServerResponse} res
154 * @param {Object} ctx
156 async
handlerGetAdminLogout(req
, res
, ctx
) {
157 const _scope
= _fileScope('handlerGetAdminLogout');
158 this.logger
.debug(_scope
, 'called', { req
, ctx
});
162 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
164 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
166 await
this.sessionManager
.getAdminLogout(res
, ctx
);
171 * @param {http.IncomingMessage} req
172 * @param {http.ServerResponse} res
173 * @param {Object} ctx
175 async
handlerGetAdmin(req
, res
, ctx
) {
176 const _scope
= _fileScope('handlerGetAdmin');
177 this.logger
.debug(_scope
, 'called', { req
, ctx
});
181 Dingus
.setHeadHandler(req
, res
, ctx
);
183 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
185 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
186 await
this.manager
.getAdmin(res
, ctx
);
192 * @param {http.IncomingMessage} req
193 * @param {http.ServerResponse} res
194 * @param {Object} ctx
196 async
handlerPostAdmin(req
, res
, ctx
) {
197 const _scope
= _fileScope('handlerPostAdmin');
198 this.logger
.debug(_scope
, 'called', { req
, ctx
});
202 Dingus
.setHeadHandler(req
, res
, ctx
);
204 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
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
);
214 * @param {http.IncomingMessage} req
215 * @param {http.ServerResponse} res
216 * @param {Object} ctx
218 async
handlerGetAdminTicket(req
, res
, ctx
) {
219 const _scope
= _fileScope('handlerGetAdminTicket');
220 this.logger
.debug(_scope
, 'called', { req
, ctx
});
224 Dingus
.setHeadHandler(req
, res
, ctx
);
226 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
228 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
229 await
this.manager
.getAdminTicket(res
, ctx
);
235 * @param {http.IncomingMessage} req
236 * @param {http.ServerResponse} res
237 * @param {Object} ctx
239 async
handlerPostAdminTicket(req
, res
, ctx
) {
240 const _scope
= _fileScope('handlerPostAdminTicket');
241 this.logger
.debug(_scope
, 'called', { req
, ctx
});
245 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
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
);
255 * @param {http.IncomingMessage} req
256 * @param {http.ServerResponse} res
257 * @param {Object} ctx
259 async
handlerGetMeta(req
, res
, ctx
) {
260 const _scope
= _fileScope('handlerGetMeta');
261 this.logger
.debug(_scope
, 'called', { req
, ctx
});
263 const responseTypes
= [
264 Enum
.ContentType
.ApplicationJson
,
265 Enum
.ContentType
.TextPlain
,
268 Dingus
.setHeadHandler(req
, res
, ctx
);
270 this.setResponseType(responseTypes
, req
, res
, ctx
);
272 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
274 await
this.manager
.getMeta(res
, ctx
);
279 * @param {http.IncomingMessage} req
280 * @param {http.ServerResponse} res
281 * @param {Object} ctx
283 async
handlerGetAuthorization(req
, res
, ctx
) {
284 const _scope
= _fileScope('handlerGetAuthorization');
285 this.logger
.debug(_scope
, 'called', { req
, ctx
});
289 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
291 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
292 await
this.manager
.getAuthorization(res
, ctx
);
298 * @param {http.IncomingMessage} req
299 * @param {http.ServerResponse} res
300 * @param {Object} ctx
302 async
handlerPostAuthorization(req
, res
, ctx
) {
303 const _scope
= _fileScope('handlerPostAuthorization');
304 this.logger
.debug(_scope
, 'called', { req
, ctx
});
306 const responseTypes
= [
307 Enum
.ContentType
.ApplicationJson
,
308 Enum
.ContentType
.TextPlain
,
311 this.setResponseType(responseTypes
, req
, res
, ctx
);
313 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
315 await
this.ingestBody(req
, res
, ctx
);
317 await
this.manager
.postAuthorization(res
, ctx
);
322 * @param {http.IncomingMessage} req
323 * @param {http.ServerResponse} res
324 * @param {Object} ctx
326 async
handlerPostConsent(req
, res
, ctx
) {
327 const _scope
= _fileScope('handlerPostConsent');
328 this.logger
.debug(_scope
, 'called', { req
, ctx
});
332 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
334 // This isn't specified as required as any valid payload carries intrinsic auth data.
335 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
337 await
this.ingestBody(req
, res
, ctx
);
339 await
this.manager
.postConsent(res
, ctx
);
344 * @param {http.IncomingMessage} req
345 * @param {http.ServerResponse} res
346 * @param {Object} ctx
348 async
handlerPostTicket(req
, res
, ctx
) {
349 const _scope
= _fileScope('handlerPostTicket');
350 this.logger
.debug(_scope
, 'called', { req
, ctx
});
352 const responseTypes
= [
353 Enum
.ContentType
.ApplicationJson
,
354 Enum
.ContentType
.TextPlain
,
357 this.setResponseType(responseTypes
, req
, res
, ctx
);
359 await
this.ingestBody(req
, res
, ctx
);
361 await
this.manager
.postTicket(req
, res
, ctx
);
366 * @param {http.IncomingMessage} req
367 * @param {http.ServerResponse} res
368 * @param {Object} ctx
370 async
handlerPostToken(req
, res
, ctx
) {
371 const _scope
= _fileScope('handlerPostToken');
372 this.logger
.debug(_scope
, 'called', { req
, ctx
});
374 const responseTypes
= [
375 Enum
.ContentType
.ApplicationJson
,
376 Enum
.ContentType
.TextPlain
,
379 this.setResponseType(responseTypes
, req
, res
, ctx
);
381 await
this.ingestBody(req
, res
, ctx
);
383 await
this.manager
.postToken(req
, res
, ctx
);
388 * @param {http.IncomingMessage} req
389 * @param {http.ServerResponse} res
390 * @param {Object} ctx
392 async
handlerPostRevocation(req
, res
, ctx
) {
393 const _scope
= _fileScope('handlerPostRevocation');
394 this.logger
.debug(_scope
, 'called', { req
, ctx
});
396 const responseTypes
= [
397 Enum
.ContentType
.ApplicationJson
,
398 Enum
.ContentType
.TextPlain
,
401 this.setResponseType(responseTypes
, req
, res
, ctx
);
403 await
this.ingestBody(req
, res
, ctx
);
405 await
this.manager
.postRevocation(req
, res
, ctx
);
410 * @param {http.IncomingMessage} req
411 * @param {http.ServerResponse} res
412 * @param {Object} ctx
414 async
handlerPostIntrospection(req
, res
, ctx
) {
415 const _scope
= _fileScope('handlerPostIntrospection');
416 this.logger
.debug(_scope
, 'called', { req
, ctx
});
418 const responseTypes
= [
419 Enum
.ContentType
.ApplicationJson
,
420 Enum
.ContentType
.TextPlain
,
423 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
425 this.setResponseType(responseTypes
, req
, res
, ctx
);
427 await
this.ingestBody(req
, res
, ctx
);
429 await
this.manager
.postIntrospection(res
, ctx
);
434 * @param {http.IncomingMessage} req
435 * @param {http.ServerResponse} res
436 * @param {Object} ctx
438 async
handlerPostUserInfo(req
, res
, ctx
) {
439 const _scope
= _fileScope('handlerPostUserInfo');
440 this.logger
.debug(_scope
, 'called', { req
, ctx
});
442 const responseTypes
= [
443 Enum
.ContentType
.ApplicationJson
,
444 Enum
.ContentType
.TextPlain
,
447 this.setResponseType(responseTypes
, req
, res
, ctx
);
449 await
this.ingestBody(req
, res
, ctx
);
451 await
this.manager
.postUserInfo(res
, ctx
);
456 * @param {http.IncomingMessage} req
457 * @param {http.ServerResponse} res
458 * @param {Object} ctx
460 async
handlerGetRoot(req
, res
, ctx
) {
461 const _scope
= _fileScope('handlerGetRoot');
462 const responseTypes
= [
463 Enum
.ContentType
.TextHTML
,
465 this.logger
.debug(_scope
, 'called', { req
, ctx
});
469 Dingus
.setHeadHandler(req
, res
, ctx
);
471 this.setResponseType(responseTypes
, req
, res
, ctx
);
473 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
475 await
this.manager
.getRoot(res
, ctx
);
480 * @param {http.IncomingMessage} req
481 * @param {http.ServerResponse} res
482 * @param {Object} ctx
484 async
handlerGetHealthcheck(req
, res
, ctx
) {
485 const _scope
= _fileScope('handlerGetHealthcheck');
486 this.logger
.debug(_scope
, 'called', { req
, ctx
});
488 Dingus
.setHeadHandler(req
, res
, ctx
);
490 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
492 await
this.manager
.getHealthcheck(res
, ctx
);
497 * @param {http.IncomingMessage} req
498 * @param {http.ServerResponse} res
499 * @param {Object} ctx
501 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
502 const _scope
= _fileScope('handlerGetAdminMaintenance');
503 this.logger
.debug(_scope
, 'called', { req
, ctx
});
507 Dingus
.setHeadHandler(req
, res
, ctx
);
509 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
511 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
512 await
this.manager
.getAdminMaintenance(res
, ctx
);
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
525 async
handlerInternalServerError(req
, res
, ctx
) {
526 const _scope
= _fileScope('handlerInternalServerError');
527 this.logger
.debug(_scope
, 'called', { req
, ctx
});
529 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
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
);
541 super.handlerInternalServerError(req
, res
, ctx
);
547 module
.exports
= Service
;