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
) {
23 ignoreTrailingSlash: false,
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
110 * @param {http.IncomingMessage} req
111 * @param {http.ServerResponse} res
112 * @param {Object} ctx
114 async
handlerGetAdminLogin(req
, res
, ctx
) {
115 const _scope
= _fileScope('handlerGetAdminLogin');
116 this.logger
.debug(_scope
, 'called', { req
, ctx
});
118 Dingus
.setHeadHandler(req
, res
, ctx
);
120 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
122 await
this.sessionManager
.getAdminLogin(res
, ctx
);
127 * @param {http.IncomingMessage} req
128 * @param {http.ServerResponse} res
129 * @param {Object} ctx
131 async
handlerPostAdminLogin(req
, res
, ctx
) {
132 const _scope
= _fileScope('handlerPostAdminLogin');
133 this.logger
.debug(_scope
, 'called', { req
, ctx
});
137 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
139 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
141 await
this.ingestBody(req
, res
, ctx
);
143 await
this.sessionManager
.postAdminLogin(res
, ctx
);
148 * @param {http.IncomingMessage} req
149 * @param {http.ServerResponse} res
150 * @param {Object} ctx
152 async
handlerGetAdminLogout(req
, res
, ctx
) {
153 const _scope
= _fileScope('handlerGetAdminLogout');
154 this.logger
.debug(_scope
, 'called', { req
, ctx
});
158 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
160 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
162 await
this.sessionManager
.getAdminLogout(res
, ctx
);
167 * @param {http.IncomingMessage} req
168 * @param {http.ServerResponse} res
169 * @param {Object} ctx
171 async
handlerGetAdmin(req
, res
, ctx
) {
172 const _scope
= _fileScope('handlerGetAdmin');
173 this.logger
.debug(_scope
, 'called', { req
, ctx
});
177 Dingus
.setHeadHandler(req
, res
, ctx
);
179 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
181 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
182 await
this.manager
.getAdmin(res
, ctx
);
188 * @param {http.IncomingMessage} req
189 * @param {http.ServerResponse} res
190 * @param {Object} ctx
192 async
handlerPostAdmin(req
, res
, ctx
) {
193 const _scope
= _fileScope('handlerPostAdmin');
194 this.logger
.debug(_scope
, 'called', { req
, ctx
});
198 Dingus
.setHeadHandler(req
, res
, ctx
);
200 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
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
);
210 * @param {http.IncomingMessage} req
211 * @param {http.ServerResponse} res
212 * @param {Object} ctx
214 async
handlerGetAdminTicket(req
, res
, ctx
) {
215 const _scope
= _fileScope('handlerGetAdminTicket');
216 this.logger
.debug(_scope
, 'called', { req
, ctx
});
220 Dingus
.setHeadHandler(req
, res
, ctx
);
222 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
224 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
225 await
this.manager
.getAdminTicket(res
, ctx
);
231 * @param {http.IncomingMessage} req
232 * @param {http.ServerResponse} res
233 * @param {Object} ctx
235 async
handlerPostAdminTicket(req
, res
, ctx
) {
236 const _scope
= _fileScope('handlerPostAdminTicket');
237 this.logger
.debug(_scope
, 'called', { req
, ctx
});
241 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
243 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
244 await
this.ingestBody(req
, res
, ctx
);
245 await
this.manager
.postAdminTicket(res
, ctx
);
251 * @param {http.IncomingMessage} req
252 * @param {http.ServerResponse} res
253 * @param {Object} ctx
255 async
handlerGetMeta(req
, res
, ctx
) {
256 const _scope
= _fileScope('handlerGetMeta');
257 this.logger
.debug(_scope
, 'called', { req
, ctx
});
259 const responseTypes
= [
260 Enum
.ContentType
.ApplicationJson
,
261 Enum
.ContentType
.TextPlain
,
264 Dingus
.setHeadHandler(req
, res
, ctx
);
266 this.setResponseType(responseTypes
, req
, res
, ctx
);
268 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
270 await
this.manager
.getMeta(res
, ctx
);
275 * @param {http.IncomingMessage} req
276 * @param {http.ServerResponse} res
277 * @param {Object} ctx
279 async
handlerGetAuthorization(req
, res
, ctx
) {
280 const _scope
= _fileScope('handlerGetAuthorization');
281 this.logger
.debug(_scope
, 'called', { req
, ctx
});
285 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
287 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
288 await
this.manager
.getAuthorization(res
, ctx
);
294 * @param {http.IncomingMessage} req
295 * @param {http.ServerResponse} res
296 * @param {Object} ctx
298 async
handlerPostAuthorization(req
, res
, ctx
) {
299 const _scope
= _fileScope('handlerPostAuthorization');
300 this.logger
.debug(_scope
, 'called', { req
, ctx
});
302 const responseTypes
= [
303 Enum
.ContentType
.ApplicationJson
,
304 Enum
.ContentType
.TextPlain
,
307 this.setResponseType(responseTypes
, req
, res
, ctx
);
309 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
311 await
this.ingestBody(req
, res
, ctx
);
313 await
this.manager
.postAuthorization(res
, ctx
);
318 * @param {http.IncomingMessage} req
319 * @param {http.ServerResponse} res
320 * @param {Object} ctx
322 async
handlerPostConsent(req
, res
, ctx
) {
323 const _scope
= _fileScope('handlerPostConsent');
324 this.logger
.debug(_scope
, 'called', { req
, ctx
});
328 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
330 // This isn't specified as required as any valid payload carries intrinsic auth data.
331 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
333 await
this.ingestBody(req
, res
, ctx
);
335 await
this.manager
.postConsent(res
, ctx
);
340 * @param {http.IncomingMessage} req
341 * @param {http.ServerResponse} res
342 * @param {Object} ctx
344 async
handlerPostTicket(req
, res
, ctx
) {
345 const _scope
= _fileScope('handlerPostTicket');
346 this.logger
.debug(_scope
, 'called', { req
, ctx
});
348 const responseTypes
= [
349 Enum
.ContentType
.ApplicationJson
,
350 Enum
.ContentType
.TextPlain
,
353 this.setResponseType(responseTypes
, req
, res
, ctx
);
355 await
this.ingestBody(req
, res
, ctx
);
357 await
this.manager
.postTicket(req
, res
, ctx
);
362 * @param {http.IncomingMessage} req
363 * @param {http.ServerResponse} res
364 * @param {Object} ctx
366 async
handlerPostToken(req
, res
, ctx
) {
367 const _scope
= _fileScope('handlerPostToken');
368 this.logger
.debug(_scope
, 'called', { req
, ctx
});
370 const responseTypes
= [
371 Enum
.ContentType
.ApplicationJson
,
372 Enum
.ContentType
.TextPlain
,
375 this.setResponseType(responseTypes
, req
, res
, ctx
);
377 await
this.ingestBody(req
, res
, ctx
);
379 await
this.manager
.postToken(req
, res
, ctx
);
384 * @param {http.IncomingMessage} req
385 * @param {http.ServerResponse} res
386 * @param {Object} ctx
388 async
handlerPostRevocation(req
, res
, ctx
) {
389 const _scope
= _fileScope('handlerPostRevocation');
390 this.logger
.debug(_scope
, 'called', { req
, ctx
});
392 const responseTypes
= [
393 Enum
.ContentType
.ApplicationJson
,
394 Enum
.ContentType
.TextPlain
,
397 this.setResponseType(responseTypes
, req
, res
, ctx
);
399 await
this.ingestBody(req
, res
, ctx
);
401 await
this.manager
.postRevocation(req
, res
, ctx
);
406 * @param {http.IncomingMessage} req
407 * @param {http.ServerResponse} res
408 * @param {Object} ctx
410 async
handlerPostIntrospection(req
, res
, ctx
) {
411 const _scope
= _fileScope('handlerPostIntrospection');
412 this.logger
.debug(_scope
, 'called', { req
, ctx
});
414 const responseTypes
= [
415 Enum
.ContentType
.ApplicationJson
,
416 Enum
.ContentType
.TextPlain
,
419 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
421 this.setResponseType(responseTypes
, req
, res
, ctx
);
423 await
this.ingestBody(req
, res
, ctx
);
425 await
this.manager
.postIntrospection(res
, ctx
);
430 * @param {http.IncomingMessage} req
431 * @param {http.ServerResponse} res
432 * @param {Object} ctx
434 async
handlerPostUserInfo(req
, res
, ctx
) {
435 const _scope
= _fileScope('handlerPostUserInfo');
436 this.logger
.debug(_scope
, 'called', { req
, ctx
});
438 const responseTypes
= [
439 Enum
.ContentType
.ApplicationJson
,
440 Enum
.ContentType
.TextPlain
,
443 this.setResponseType(responseTypes
, req
, res
, ctx
);
445 await
this.ingestBody(req
, res
, ctx
);
447 await
this.manager
.postUserInfo(res
, ctx
);
452 * @param {http.IncomingMessage} req
453 * @param {http.ServerResponse} res
454 * @param {Object} ctx
456 async
handlerGetRoot(req
, res
, ctx
) {
457 const _scope
= _fileScope('handlerGetRoot');
458 const responseTypes
= [
459 Enum
.ContentType
.TextHTML
,
461 this.logger
.debug(_scope
, 'called', { req
, ctx
});
465 Dingus
.setHeadHandler(req
, res
, ctx
);
467 this.setResponseType(responseTypes
, req
, res
, ctx
);
469 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
471 await
this.manager
.getRoot(res
, ctx
);
476 * @param {http.IncomingMessage} req
477 * @param {http.ServerResponse} res
478 * @param {Object} ctx
480 async
handlerGetHealthcheck(req
, res
, ctx
) {
481 const _scope
= _fileScope('handlerGetHealthcheck');
482 this.logger
.debug(_scope
, 'called', { req
, ctx
});
484 Dingus
.setHeadHandler(req
, res
, ctx
);
486 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
488 await
this.manager
.getHealthcheck(res
, ctx
);
493 * @param {http.IncomingMessage} req
494 * @param {http.ServerResponse} res
495 * @param {Object} ctx
497 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
498 const _scope
= _fileScope('handlerGetAdminMaintenance');
499 this.logger
.debug(_scope
, 'called', { req
, ctx
});
503 Dingus
.setHeadHandler(req
, res
, ctx
);
505 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
507 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
508 await
this.manager
.getAdminMaintenance(res
, ctx
);
514 * FIXME: This doesn't seem to be working as envisioned. Maybe override render error method instead???
515 * Intercept this and redirect if we have enough information, otherwise default to framework.
516 * The redirect attempt should probably be contained in a Manager method, but here it is for now.
517 * @param {http.IncomingMessage} req
518 * @param {http.ServerResponse} res
519 * @param {Object} ctx
521 async
handlerInternalServerError(req
, res
, ctx
) {
522 const _scope
= _fileScope('handlerInternalServerError');
523 this.logger
.debug(_scope
, 'called', { req
, ctx
});
525 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
527 ...(ctx
.session
.state
&& { 'state': ctx
.session
.state
}),
528 'error': 'server_error',
529 'error_description': 'An internal server error occurred',
530 }).forEach(([name
, value
]) => ctx
.session
.redirectUri
.searchParams
.set(name
, value
));
531 res
.statusCode
= 302; // Found
532 res
.setHeader(Enum
.Header
.Location
, ctx
.session
.redirectUri
.href
);
537 super.handlerInternalServerError(req
, res
, ctx
);
543 module
.exports
= Service
;