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'], 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));
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'], '/', this.handlerGetRoot
.bind(this));
58 // Give load-balancers something to check
59 this.on(['GET'], 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'], '/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');
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));
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));
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));
83 // Page for upkeep info et cetera
84 this.on(['GET'], '/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 const _scope
= _fileScope('preHandler');
106 await
super.preHandler(req
, res
, ctx
);
107 ctx
.url
= req
.url
; // Persist this for logout redirect
109 const logObject
= this.asyncLocalStorage
.getStore();
110 // istanbul ignore else
111 if (logObject
) { // debugging in vscode seems to kill ALS, work around
112 logObject
.requestId
= ctx
.requestId
;
113 delete ctx
.requestId
;
115 this.logger
.debug(_scope
, 'no async local store');
121 * @param {http.IncomingMessage} req
122 * @param {http.ServerResponse} res
123 * @param {Object} ctx
125 async
handlerGetAdminLogin(req
, res
, ctx
) {
126 const _scope
= _fileScope('handlerGetAdminLogin');
127 this.logger
.debug(_scope
, 'called', { req
, ctx
});
129 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
131 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
133 await
this.sessionManager
.getAdminLogin(res
, ctx
);
138 * @param {http.IncomingMessage} req
139 * @param {http.ServerResponse} res
140 * @param {Object} ctx
142 async
handlerPostAdminLogin(req
, res
, ctx
) {
143 const _scope
= _fileScope('handlerPostAdminLogin');
144 this.logger
.debug(_scope
, 'called', { req
, ctx
});
148 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
150 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
152 await
this.ingestBody(req
, res
, ctx
);
154 await
this.sessionManager
.postAdminLogin(res
, ctx
);
159 * @param {http.IncomingMessage} req
160 * @param {http.ServerResponse} res
161 * @param {Object} ctx
163 async
handlerGetAdminLogout(req
, res
, ctx
) {
164 const _scope
= _fileScope('handlerGetAdminLogout');
165 this.logger
.debug(_scope
, 'called', { req
, ctx
});
169 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
171 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
173 await
this.sessionManager
.getAdminLogout(res
, ctx
);
178 * @param {http.IncomingMessage} req
179 * @param {http.ServerResponse} res
180 * @param {Object} ctx
182 async
handlerGetAdmin(req
, res
, ctx
) {
183 const _scope
= _fileScope('handlerGetAdmin');
184 this.logger
.debug(_scope
, 'called', { req
, ctx
});
188 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
190 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
191 await
this.manager
.getAdmin(res
, ctx
);
197 * @param {http.IncomingMessage} req
198 * @param {http.ServerResponse} res
199 * @param {Object} ctx
201 async
handlerPostAdmin(req
, res
, ctx
) {
202 const _scope
= _fileScope('handlerPostAdmin');
203 this.logger
.debug(_scope
, 'called', { req
, ctx
});
207 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
209 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
210 await
this.ingestBody(req
, res
, ctx
);
211 await
this.manager
.postAdmin(res
, ctx
);
217 * @param {http.IncomingMessage} req
218 * @param {http.ServerResponse} res
219 * @param {Object} ctx
221 async
handlerGetAdminTicket(req
, res
, ctx
) {
222 const _scope
= _fileScope('handlerGetAdminTicket');
223 this.logger
.debug(_scope
, 'called', { req
, ctx
});
227 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
229 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
230 await
this.manager
.getAdminTicket(res
, ctx
);
236 * @param {http.IncomingMessage} req
237 * @param {http.ServerResponse} res
238 * @param {Object} ctx
240 async
handlerPostAdminTicket(req
, res
, ctx
) {
241 const _scope
= _fileScope('handlerPostAdminTicket');
242 this.logger
.debug(_scope
, 'called', { req
, ctx
});
246 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
248 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
249 await
this.ingestBody(req
, res
, ctx
);
250 await
this.manager
.postAdminTicket(res
, ctx
);
256 * @param {http.IncomingMessage} req
257 * @param {http.ServerResponse} res
258 * @param {Object} ctx
260 async
handlerGetMeta(req
, res
, ctx
) {
261 const _scope
= _fileScope('handlerGetMeta');
262 this.logger
.debug(_scope
, 'called', { req
, ctx
});
264 const responseTypes
= [
265 Enum
.ContentType
.ApplicationJson
,
266 Enum
.ContentType
.TextPlain
,
269 this.setResponseType(responseTypes
, req
, res
, ctx
);
271 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
273 await
this.manager
.getMeta(res
, ctx
);
278 * @param {http.IncomingMessage} req
279 * @param {http.ServerResponse} res
280 * @param {Object} ctx
282 async
handlerGetAuthorization(req
, res
, ctx
) {
283 const _scope
= _fileScope('handlerGetAuthorization');
284 this.logger
.debug(_scope
, 'called', { req
, ctx
});
288 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
290 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
291 await
this.manager
.getAuthorization(res
, ctx
);
297 * @param {http.IncomingMessage} req
298 * @param {http.ServerResponse} res
299 * @param {Object} ctx
301 async
handlerPostAuthorization(req
, res
, ctx
) {
302 const _scope
= _fileScope('handlerPostAuthorization');
303 this.logger
.debug(_scope
, 'called', { req
, ctx
});
305 const responseTypes
= [
306 Enum
.ContentType
.ApplicationJson
,
307 Enum
.ContentType
.TextPlain
,
310 this.setResponseType(responseTypes
, req
, res
, ctx
);
312 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
314 await
this.ingestBody(req
, res
, ctx
);
316 await
this.manager
.postAuthorization(res
, ctx
);
321 * @param {http.IncomingMessage} req
322 * @param {http.ServerResponse} res
323 * @param {Object} ctx
325 async
handlerPostConsent(req
, res
, ctx
) {
326 const _scope
= _fileScope('handlerPostConsent');
327 this.logger
.debug(_scope
, 'called', { req
, ctx
});
331 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
333 // This isn't specified as required as any valid payload carries intrinsic auth data.
334 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
336 await
this.ingestBody(req
, res
, ctx
);
338 await
this.manager
.postConsent(res
, ctx
);
343 * @param {http.IncomingMessage} req
344 * @param {http.ServerResponse} res
345 * @param {Object} ctx
347 async
handlerPostTicket(req
, res
, ctx
) {
348 const _scope
= _fileScope('handlerPostTicket');
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.ingestBody(req
, res
, ctx
);
360 await
this.manager
.postTicket(req
, res
, ctx
);
365 * @param {http.IncomingMessage} req
366 * @param {http.ServerResponse} res
367 * @param {Object} ctx
369 async
handlerPostToken(req
, res
, ctx
) {
370 const _scope
= _fileScope('handlerPostToken');
371 this.logger
.debug(_scope
, 'called', { req
, ctx
});
373 const responseTypes
= [
374 Enum
.ContentType
.ApplicationJson
,
375 Enum
.ContentType
.TextPlain
,
378 this.setResponseType(responseTypes
, req
, res
, ctx
);
380 await
this.ingestBody(req
, res
, ctx
);
382 await
this.manager
.postToken(req
, res
, ctx
);
387 * @param {http.IncomingMessage} req
388 * @param {http.ServerResponse} res
389 * @param {Object} ctx
391 async
handlerPostRevocation(req
, res
, ctx
) {
392 const _scope
= _fileScope('handlerPostRevocation');
393 this.logger
.debug(_scope
, 'called', { req
, ctx
});
395 const responseTypes
= [
396 Enum
.ContentType
.ApplicationJson
,
397 Enum
.ContentType
.TextPlain
,
400 this.setResponseType(responseTypes
, req
, res
, ctx
);
402 await
this.ingestBody(req
, res
, ctx
);
404 await
this.manager
.postRevocation(req
, res
, ctx
);
409 * @param {http.IncomingMessage} req
410 * @param {http.ServerResponse} res
411 * @param {Object} ctx
413 async
handlerPostIntrospection(req
, res
, ctx
) {
414 const _scope
= _fileScope('handlerPostIntrospection');
415 this.logger
.debug(_scope
, 'called', { req
, ctx
});
417 const responseTypes
= [
418 Enum
.ContentType
.ApplicationJson
,
419 Enum
.ContentType
.TextPlain
,
422 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
424 this.setResponseType(responseTypes
, req
, res
, ctx
);
426 await
this.ingestBody(req
, res
, ctx
);
428 await
this.manager
.postIntrospection(res
, ctx
);
433 * @param {http.IncomingMessage} req
434 * @param {http.ServerResponse} res
435 * @param {Object} ctx
437 async
handlerPostUserInfo(req
, res
, ctx
) {
438 const _scope
= _fileScope('handlerPostUserInfo');
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
.postUserInfo(res
, ctx
);
455 * @param {http.IncomingMessage} req
456 * @param {http.ServerResponse} res
457 * @param {Object} ctx
459 async
handlerGetRoot(req
, res
, ctx
) {
460 const _scope
= _fileScope('handlerGetRoot');
461 const responseTypes
= [
462 Enum
.ContentType
.TextHTML
,
464 this.logger
.debug(_scope
, 'called', { req
, ctx
});
468 this.setResponseType(responseTypes
, req
, res
, ctx
);
470 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
472 await
this.manager
.getRoot(res
, ctx
);
477 * @param {http.IncomingMessage} req
478 * @param {http.ServerResponse} res
479 * @param {Object} ctx
481 async
handlerGetHealthcheck(req
, res
, ctx
) {
482 const _scope
= _fileScope('handlerGetHealthcheck');
483 this.logger
.debug(_scope
, 'called', { req
, ctx
});
485 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
487 await
this.manager
.getHealthcheck(res
, ctx
);
492 * @param {http.IncomingMessage} req
493 * @param {http.ServerResponse} res
494 * @param {Object} ctx
496 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
497 const _scope
= _fileScope('handlerGetAdminMaintenance');
498 this.logger
.debug(_scope
, 'called', { req
, ctx
});
502 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
504 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
505 await
this.manager
.getAdminMaintenance(res
, ctx
);
511 * FIXME: This doesn't seem to be working as envisioned. Maybe override render error method instead???
512 * Intercept this and redirect if we have enough information, otherwise default to framework.
513 * Fixing this will likely have to wait until an e2e test framework is in place.
514 * The redirect attempt should probably be contained in a Manager method, but here it is for now.
515 * @param {http.IncomingMessage} req
516 * @param {http.ServerResponse} res
517 * @param {Object} ctx
519 async
handlerInternalServerError(req
, res
, ctx
) {
520 const _scope
= _fileScope('handlerInternalServerError');
521 this.logger
.debug(_scope
, 'called', { req
, ctx
});
523 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
525 ...(ctx
.session
.state
&& { 'state': ctx
.session
.state
}),
526 'error': 'server_error',
527 'error_description': 'An internal server error occurred',
528 }).forEach(([name
, value
]) => ctx
.session
.redirectUri
.searchParams
.set(name
, value
));
529 res
.statusCode
= 302; // Found
530 res
.setHeader(Enum
.Header
.Location
, ctx
.session
.redirectUri
.href
);
535 await
super.handlerInternalServerError(req
, res
, ctx
);
541 module
.exports
= Service
;