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 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 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
124 await
this.authenticator
.sessionOptionalLocal(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 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
183 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
184 await
this.manager
.getAdmin(res
, ctx
);
190 * @param {http.IncomingMessage} req
191 * @param {http.ServerResponse} res
192 * @param {Object} ctx
194 async
handlerPostAdmin(req
, res
, ctx
) {
195 const _scope
= _fileScope('handlerPostAdmin');
196 this.logger
.debug(_scope
, 'called', { req
, 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 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
222 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
223 await
this.manager
.getAdminTicket(res
, ctx
);
229 * @param {http.IncomingMessage} req
230 * @param {http.ServerResponse} res
231 * @param {Object} ctx
233 async
handlerPostAdminTicket(req
, res
, ctx
) {
234 const _scope
= _fileScope('handlerPostAdminTicket');
235 this.logger
.debug(_scope
, 'called', { req
, ctx
});
239 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
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
);
249 * @param {http.IncomingMessage} req
250 * @param {http.ServerResponse} res
251 * @param {Object} ctx
253 async
handlerGetMeta(req
, res
, ctx
) {
254 const _scope
= _fileScope('handlerGetMeta');
255 this.logger
.debug(_scope
, 'called', { req
, ctx
});
257 const responseTypes
= [
258 Enum
.ContentType
.ApplicationJson
,
259 Enum
.ContentType
.TextPlain
,
262 this.setResponseType(responseTypes
, req
, res
, ctx
);
264 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
266 await
this.manager
.getMeta(res
, ctx
);
271 * @param {http.IncomingMessage} req
272 * @param {http.ServerResponse} res
273 * @param {Object} ctx
275 async
handlerGetAuthorization(req
, res
, ctx
) {
276 const _scope
= _fileScope('handlerGetAuthorization');
277 this.logger
.debug(_scope
, 'called', { req
, ctx
});
281 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
283 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
284 await
this.manager
.getAuthorization(res
, ctx
);
290 * @param {http.IncomingMessage} req
291 * @param {http.ServerResponse} res
292 * @param {Object} ctx
294 async
handlerPostAuthorization(req
, res
, ctx
) {
295 const _scope
= _fileScope('handlerPostAuthorization');
296 this.logger
.debug(_scope
, 'called', { req
, ctx
});
298 const responseTypes
= [
299 Enum
.ContentType
.ApplicationJson
,
300 Enum
.ContentType
.TextPlain
,
303 this.setResponseType(responseTypes
, req
, res
, ctx
);
305 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
307 await
this.ingestBody(req
, res
, ctx
);
309 await
this.manager
.postAuthorization(res
, ctx
);
314 * @param {http.IncomingMessage} req
315 * @param {http.ServerResponse} res
316 * @param {Object} ctx
318 async
handlerPostConsent(req
, res
, ctx
) {
319 const _scope
= _fileScope('handlerPostConsent');
320 this.logger
.debug(_scope
, 'called', { req
, ctx
});
324 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
326 // This isn't specified as required as any valid payload carries intrinsic auth data.
327 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
329 await
this.ingestBody(req
, res
, ctx
);
331 await
this.manager
.postConsent(res
, ctx
);
336 * @param {http.IncomingMessage} req
337 * @param {http.ServerResponse} res
338 * @param {Object} ctx
340 async
handlerPostTicket(req
, res
, ctx
) {
341 const _scope
= _fileScope('handlerPostTicket');
342 this.logger
.debug(_scope
, 'called', { req
, ctx
});
344 const responseTypes
= [
345 Enum
.ContentType
.ApplicationJson
,
346 Enum
.ContentType
.TextPlain
,
349 this.setResponseType(responseTypes
, req
, res
, ctx
);
351 await
this.ingestBody(req
, res
, ctx
);
353 await
this.manager
.postTicket(req
, res
, ctx
);
358 * @param {http.IncomingMessage} req
359 * @param {http.ServerResponse} res
360 * @param {Object} ctx
362 async
handlerPostToken(req
, res
, ctx
) {
363 const _scope
= _fileScope('handlerPostToken');
364 this.logger
.debug(_scope
, 'called', { req
, ctx
});
366 const responseTypes
= [
367 Enum
.ContentType
.ApplicationJson
,
368 Enum
.ContentType
.TextPlain
,
371 this.setResponseType(responseTypes
, req
, res
, ctx
);
373 await
this.ingestBody(req
, res
, ctx
);
375 await
this.manager
.postToken(req
, res
, ctx
);
380 * @param {http.IncomingMessage} req
381 * @param {http.ServerResponse} res
382 * @param {Object} ctx
384 async
handlerPostRevocation(req
, res
, ctx
) {
385 const _scope
= _fileScope('handlerPostRevocation');
386 this.logger
.debug(_scope
, 'called', { req
, ctx
});
388 const responseTypes
= [
389 Enum
.ContentType
.ApplicationJson
,
390 Enum
.ContentType
.TextPlain
,
393 this.setResponseType(responseTypes
, req
, res
, ctx
);
395 await
this.ingestBody(req
, res
, ctx
);
397 await
this.manager
.postRevocation(req
, res
, ctx
);
402 * @param {http.IncomingMessage} req
403 * @param {http.ServerResponse} res
404 * @param {Object} ctx
406 async
handlerPostIntrospection(req
, res
, ctx
) {
407 const _scope
= _fileScope('handlerPostIntrospection');
408 this.logger
.debug(_scope
, 'called', { req
, ctx
});
410 const responseTypes
= [
411 Enum
.ContentType
.ApplicationJson
,
412 Enum
.ContentType
.TextPlain
,
415 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
417 this.setResponseType(responseTypes
, req
, res
, ctx
);
419 await
this.ingestBody(req
, res
, ctx
);
421 await
this.manager
.postIntrospection(res
, ctx
);
426 * @param {http.IncomingMessage} req
427 * @param {http.ServerResponse} res
428 * @param {Object} ctx
430 async
handlerPostUserInfo(req
, res
, ctx
) {
431 const _scope
= _fileScope('handlerPostUserInfo');
432 this.logger
.debug(_scope
, 'called', { req
, ctx
});
434 const responseTypes
= [
435 Enum
.ContentType
.ApplicationJson
,
436 Enum
.ContentType
.TextPlain
,
439 this.setResponseType(responseTypes
, req
, res
, ctx
);
441 await
this.ingestBody(req
, res
, ctx
);
443 await
this.manager
.postUserInfo(res
, ctx
);
448 * @param {http.IncomingMessage} req
449 * @param {http.ServerResponse} res
450 * @param {Object} ctx
452 async
handlerGetRoot(req
, res
, ctx
) {
453 const _scope
= _fileScope('handlerGetRoot');
454 const responseTypes
= [
455 Enum
.ContentType
.TextHTML
,
457 this.logger
.debug(_scope
, 'called', { req
, ctx
});
461 this.setResponseType(responseTypes
, req
, res
, ctx
);
463 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
465 await
this.manager
.getRoot(res
, ctx
);
470 * @param {http.IncomingMessage} req
471 * @param {http.ServerResponse} res
472 * @param {Object} ctx
474 async
handlerGetHealthcheck(req
, res
, ctx
) {
475 const _scope
= _fileScope('handlerGetHealthcheck');
476 this.logger
.debug(_scope
, 'called', { req
, ctx
});
478 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
480 await
this.manager
.getHealthcheck(res
, ctx
);
485 * @param {http.IncomingMessage} req
486 * @param {http.ServerResponse} res
487 * @param {Object} ctx
489 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
490 const _scope
= _fileScope('handlerGetAdminMaintenance');
491 this.logger
.debug(_scope
, 'called', { req
, ctx
});
495 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
497 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
498 await
this.manager
.getAdminMaintenance(res
, ctx
);
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
512 async
handlerInternalServerError(req
, res
, ctx
) {
513 const _scope
= _fileScope('handlerInternalServerError');
514 this.logger
.debug(_scope
, 'called', { req
, ctx
});
516 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
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
);
528 await
super.handlerInternalServerError(req
, res
, ctx
);
534 module
.exports
= Service
;