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');
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.asyncLocalStorage
= asyncLocalStorage
;
27 this.staticPath
= path
.normalize(path
.join(__dirname
, '..', 'static'));
28 this.manager
= new Manager(logger
, db
, options
);
29 this.authenticator
= new Authenticator(logger
, db
, options
);
30 this.sessionManager
= new SessionManager(logger
, this.authenticator
, options
);
31 this.resourceAuthenticator
= new ResourceAuthenticator(logger
, db
, options
);
32 this.loginPath
= `${options.dingus.proxyPrefix}/admin/login`;
34 // N.B. /admin routes not currently configurable
35 const route
= (r
) => `/${options.route[r]}`; // eslint-disable-line security/detect-object-injection
38 this.on(['GET'], route('metadata'), this.handlerGetMeta
.bind(this));
39 // Also respond with metadata on well-known oauth2 endpoint if base has no prefix
40 if ((options
?.dingus
?.selfBaseUrl
?.match(/\//g) || []).length
=== 3) {
41 this.on(['GET'], '/.well-known/oauth-authorization-server', this.handlerGetMeta
.bind(this));
45 this.on(['GET'], route('authorization'), this.handlerGetAuthorization
.bind(this));
46 this.on(['POST'], route('authorization'), this.handlerPostAuthorization
.bind(this));
47 this.on(['POST'], route('consent'), this.handlerPostConsent
.bind(this));
48 this.on(['POST'], route('revocation'), this.handlerPostRevocation
.bind(this));
49 this.on(['POST'], route('ticket'), this.handlerPostTicket
.bind(this));
50 this.on(['POST'], route('token'), this.handlerPostToken
.bind(this));
53 this.on('POST', route('introspection'), this.handlerPostIntrospection
.bind(this));
54 this.on('POST', route('userinfo'), this.handlerPostUserInfo
.bind(this));
56 // Information page about service
57 this.on(['GET'], '/', this.handlerGetRoot
.bind(this));
59 // Temmporary to see what rando payload someone is sending us unsolicited
60 this.on(['POST'], '/', this.handlerWhaGwan
.bind(this));
62 // Give load-balancers something to check
63 this.on(['GET'], route('healthcheck'), this.handlerGetHealthcheck
.bind(this));
65 // These routes are intended for accessing static content during development.
66 // In production, a proxy server would likely handle these first.
67 this.on(['GET'], '/static', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/static/`);
68 this.on(['GET'], '/static/', this.handlerGetStaticFile
.bind(this), 'index.html');
69 this.on(['GET'], '/static/:file', this.handlerGetStaticFile
.bind(this));
70 this.on(['GET'], '/favicon.ico', this.handlerGetStaticFile
.bind(this), 'favicon.ico');
71 this.on(['GET'], '/robots.txt', this.handlerGetStaticFile
.bind(this), 'robots.txt');
73 // Profile and token management for authenticated sessions
74 this.on(['GET'], '/admin', this.handlerRedirect
.bind(this), `${options.dingus.proxyPrefix}/admin/`);
75 this.on(['GET'], '/admin/', this.handlerGetAdmin
.bind(this));
76 this.on(['POST'], '/admin/', this.handlerPostAdmin
.bind(this));
78 // Ticket-proffering interface for authenticated sessions
79 this.on(['GET'], '/admin/ticket', this.handlerGetAdminTicket
.bind(this));
80 this.on(['POST'], '/admin/ticket', this.handlerPostAdminTicket
.bind(this));
82 // User authentication and session establishment
83 this.on(['GET'], '/admin/login', this.handlerGetAdminLogin
.bind(this));
84 this.on(['POST'], '/admin/login', this.handlerPostAdminLogin
.bind(this));
85 this.on(['GET'], '/admin/logout', this.handlerGetAdminLogout
.bind(this));
87 // Page for upkeep info et cetera
88 this.on(['GET'], '/admin/maintenance', this.handlerGetAdminMaintenance
.bind(this));
94 * Perform any async startup tasks.
97 await
this.manager
.initialize();
102 * Do a little more on each request.
103 * @param {http.IncomingMessage} req
104 * @param {http.ServerResponse} res
105 * @param {Object} ctx
107 async
preHandler(req
, res
, ctx
) {
108 const _scope
= _fileScope('preHandler');
110 await
super.preHandler(req
, res
, ctx
);
111 ctx
.url
= req
.url
; // Persist this for logout redirect
113 const logObject
= this.asyncLocalStorage
.getStore();
114 // istanbul ignore else
115 if (logObject
) { // Debugging in vscode seems to kill ALS, work around
116 logObject
.requestId
= ctx
.requestId
;
117 delete ctx
.requestId
;
119 this.logger
.debug(_scope
, 'no async local store');
125 * @param {http.IncomingMessage} req
126 * @param {http.ServerResponse} res
127 * @param {Object} ctx
129 async
handlerGetAdminLogin(req
, res
, ctx
) {
130 const _scope
= _fileScope('handlerGetAdminLogin');
131 this.logger
.debug(_scope
, 'called', { req
, ctx
});
133 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
135 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
137 await
this.sessionManager
.getAdminLogin(res
, ctx
);
142 * @param {http.IncomingMessage} req
143 * @param {http.ServerResponse} res
144 * @param {Object} ctx
146 async
handlerPostAdminLogin(req
, res
, ctx
) {
147 const _scope
= _fileScope('handlerPostAdminLogin');
148 this.logger
.debug(_scope
, 'called', { req
, ctx
});
152 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
154 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
156 await
this.ingestBody(req
, res
, ctx
);
158 await
this.sessionManager
.postAdminLogin(res
, ctx
);
163 * @param {http.IncomingMessage} req
164 * @param {http.ServerResponse} res
165 * @param {Object} ctx
167 async
handlerGetAdminLogout(req
, res
, ctx
) {
168 const _scope
= _fileScope('handlerGetAdminLogout');
169 this.logger
.debug(_scope
, 'called', { req
, ctx
});
173 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
175 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
177 await
this.sessionManager
.getAdminLogout(res
, ctx
);
182 * @param {http.IncomingMessage} req
183 * @param {http.ServerResponse} res
184 * @param {Object} ctx
186 async
handlerGetAdmin(req
, res
, ctx
) {
187 const _scope
= _fileScope('handlerGetAdmin');
188 this.logger
.debug(_scope
, 'called', { req
, ctx
});
192 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
194 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
195 await
this.manager
.getAdmin(res
, ctx
);
201 * @param {http.IncomingMessage} req
202 * @param {http.ServerResponse} res
203 * @param {Object} ctx
205 async
handlerPostAdmin(req
, res
, ctx
) {
206 const _scope
= _fileScope('handlerPostAdmin');
207 this.logger
.debug(_scope
, 'called', { req
, ctx
});
211 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
213 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
214 await
this.ingestBody(req
, res
, ctx
);
215 await
this.manager
.postAdmin(res
, ctx
);
221 * @param {http.IncomingMessage} req
222 * @param {http.ServerResponse} res
223 * @param {Object} ctx
225 async
handlerGetAdminTicket(req
, res
, ctx
) {
226 const _scope
= _fileScope('handlerGetAdminTicket');
227 this.logger
.debug(_scope
, 'called', { req
, ctx
});
231 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
233 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
234 await
this.manager
.getAdminTicket(res
, ctx
);
240 * @param {http.IncomingMessage} req
241 * @param {http.ServerResponse} res
242 * @param {Object} ctx
244 async
handlerPostAdminTicket(req
, res
, ctx
) {
245 const _scope
= _fileScope('handlerPostAdminTicket');
246 this.logger
.debug(_scope
, 'called', { req
, ctx
});
250 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
252 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
253 await
this.ingestBody(req
, res
, ctx
);
254 await
this.manager
.postAdminTicket(res
, ctx
);
260 * @param {http.IncomingMessage} req
261 * @param {http.ServerResponse} res
262 * @param {Object} ctx
264 async
handlerGetMeta(req
, res
, ctx
) {
265 const _scope
= _fileScope('handlerGetMeta');
266 this.logger
.debug(_scope
, 'called', { req
, ctx
});
268 const responseTypes
= [
269 Enum
.ContentType
.ApplicationJson
,
270 Enum
.ContentType
.TextPlain
,
273 this.setResponseType(responseTypes
, req
, res
, ctx
);
275 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
277 await
this.manager
.getMeta(res
, ctx
);
282 * @param {http.IncomingMessage} req
283 * @param {http.ServerResponse} res
284 * @param {Object} ctx
286 async
handlerGetAuthorization(req
, res
, ctx
) {
287 const _scope
= _fileScope('handlerGetAuthorization');
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.manager
.getAuthorization(res
, ctx
);
301 * @param {http.IncomingMessage} req
302 * @param {http.ServerResponse} res
303 * @param {Object} ctx
305 async
handlerPostAuthorization(req
, res
, ctx
) {
306 const _scope
= _fileScope('handlerPostAuthorization');
307 this.logger
.debug(_scope
, 'called', { req
, ctx
});
309 const responseTypes
= [
310 Enum
.ContentType
.ApplicationJson
,
311 Enum
.ContentType
.TextPlain
,
314 this.setResponseType(responseTypes
, req
, res
, ctx
);
316 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
318 await
this.ingestBody(req
, res
, ctx
);
320 await
this.manager
.postAuthorization(res
, ctx
);
325 * @param {http.IncomingMessage} req
326 * @param {http.ServerResponse} res
327 * @param {Object} ctx
329 async
handlerPostConsent(req
, res
, ctx
) {
330 const _scope
= _fileScope('handlerPostConsent');
331 this.logger
.debug(_scope
, 'called', { req
, ctx
});
335 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
337 // This isn't specified as required as any valid payload carries intrinsic auth data.
338 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
340 await
this.ingestBody(req
, res
, ctx
);
342 await
this.manager
.postConsent(res
, ctx
);
347 * @param {http.IncomingMessage} req
348 * @param {http.ServerResponse} res
349 * @param {Object} ctx
351 async
handlerPostTicket(req
, res
, ctx
) {
352 const _scope
= _fileScope('handlerPostTicket');
353 this.logger
.debug(_scope
, 'called', { req
, ctx
});
355 const responseTypes
= [
356 Enum
.ContentType
.ApplicationJson
,
357 Enum
.ContentType
.TextPlain
,
360 this.setResponseType(responseTypes
, req
, res
, ctx
);
362 await
this.ingestBody(req
, res
, ctx
);
364 await
this.manager
.postTicket(req
, res
, ctx
);
369 * @param {http.IncomingMessage} req
370 * @param {http.ServerResponse} res
371 * @param {Object} ctx
373 async
handlerPostToken(req
, res
, ctx
) {
374 const _scope
= _fileScope('handlerPostToken');
375 this.logger
.debug(_scope
, 'called', { req
, ctx
});
377 const responseTypes
= [
378 Enum
.ContentType
.ApplicationJson
,
379 Enum
.ContentType
.TextPlain
,
382 this.setResponseType(responseTypes
, req
, res
, ctx
);
384 await
this.ingestBody(req
, res
, ctx
);
386 await
this.manager
.postToken(req
, res
, ctx
);
391 * @param {http.IncomingMessage} req
392 * @param {http.ServerResponse} res
393 * @param {Object} ctx
395 async
handlerPostRevocation(req
, res
, ctx
) {
396 const _scope
= _fileScope('handlerPostRevocation');
397 this.logger
.debug(_scope
, 'called', { req
, ctx
});
399 const responseTypes
= [
400 Enum
.ContentType
.ApplicationJson
,
401 Enum
.ContentType
.TextPlain
,
404 this.setResponseType(responseTypes
, req
, res
, ctx
);
406 await
this.ingestBody(req
, res
, ctx
);
408 await
this.manager
.postRevocation(req
, res
, ctx
);
413 * @param {http.IncomingMessage} req
414 * @param {http.ServerResponse} res
415 * @param {Object} ctx
417 async
handlerPostIntrospection(req
, res
, ctx
) {
418 const _scope
= _fileScope('handlerPostIntrospection');
419 this.logger
.debug(_scope
, 'called', { req
, ctx
});
421 const responseTypes
= [
422 Enum
.ContentType
.ApplicationJson
,
423 Enum
.ContentType
.TextPlain
,
426 await
this.resourceAuthenticator
.required(req
, res
, ctx
);
428 this.setResponseType(responseTypes
, req
, res
, ctx
);
430 await
this.ingestBody(req
, res
, ctx
);
432 await
this.manager
.postIntrospection(res
, ctx
);
437 * @param {http.IncomingMessage} req
438 * @param {http.ServerResponse} res
439 * @param {Object} ctx
441 async
handlerPostUserInfo(req
, res
, ctx
) {
442 const _scope
= _fileScope('handlerPostUserInfo');
443 this.logger
.debug(_scope
, 'called', { req
, ctx
});
445 const responseTypes
= [
446 Enum
.ContentType
.ApplicationJson
,
447 Enum
.ContentType
.TextPlain
,
450 this.setResponseType(responseTypes
, req
, res
, ctx
);
452 await
this.ingestBody(req
, res
, ctx
);
454 await
this.manager
.postUserInfo(res
, ctx
);
459 * @param {http.IncomingMessage} req
460 * @param {http.ServerResponse} res
461 * @param {Object} ctx
463 async
handlerGetRoot(req
, res
, ctx
) {
464 const _scope
= _fileScope('handlerGetRoot');
465 const responseTypes
= [
466 Enum
.ContentType
.TextHTML
,
468 this.logger
.debug(_scope
, 'called', { req
, ctx
});
472 this.setResponseType(responseTypes
, req
, res
, ctx
);
474 await
this.authenticator
.sessionOptionalLocal(req
, res
, ctx
);
476 await
this.manager
.getRoot(res
, ctx
);
481 * Temporary to see what an unsolicited payload contains.
483 async
handlerWhaGwan(req
, res
, ctx
) {
484 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
485 await
this.ingestBody(req
, res
, ctx
);
486 throw new ResponseError(Enum
.ErrorResponse
.MethodNotAllowed
);
490 * @param {http.IncomingMessage} req
491 * @param {http.ServerResponse} res
492 * @param {Object} ctx
494 async
handlerGetHealthcheck(req
, res
, ctx
) {
495 const _scope
= _fileScope('handlerGetHealthcheck');
496 this.logger
.debug(_scope
, 'called', { req
, ctx
});
498 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
500 await
this.manager
.getHealthcheck(res
, ctx
);
505 * @param {http.IncomingMessage} req
506 * @param {http.ServerResponse} res
507 * @param {Object} ctx
509 async
handlerGetAdminMaintenance(req
, res
, ctx
) {
510 const _scope
= _fileScope('handlerGetAdminMaintenance');
511 this.logger
.debug(_scope
, 'called', { req
, ctx
});
515 this.setResponseType(this.responseTypes
, req
, res
, ctx
);
517 if (await
this.authenticator
.sessionRequiredLocal(req
, res
, ctx
, this.loginPath
)) {
518 await
this.manager
.getAdminMaintenance(res
, ctx
);
524 * FIXME: This doesn't seem to be working as envisioned. Maybe override render error method instead???
525 * Intercept this and redirect if we have enough information, otherwise default to framework.
526 * Fixing this will likely have to wait until an e2e test framework is in place.
527 * The redirect attempt should probably be contained in a Manager method, but here it is for now.
528 * @param {http.IncomingMessage} req
529 * @param {http.ServerResponse} res
530 * @param {Object} ctx
532 async
handlerInternalServerError(req
, res
, ctx
) {
533 const _scope
= _fileScope('handlerInternalServerError');
534 this.logger
.debug(_scope
, 'called', { req
, ctx
});
536 if (ctx
?.session
?.redirectUri
&& ctx
?.session
?.clientIdentifier
) {
538 ...(ctx
.session
.state
&& { 'state': ctx
.session
.state
}),
539 'error': 'server_error',
540 'error_description': 'An internal server error occurred',
541 }).forEach(([name
, value
]) => ctx
.session
.redirectUri
.searchParams
.set(name
, value
));
542 res
.statusCode
= 302; // Found
543 res
.setHeader(Enum
.Header
.Location
, ctx
.session
.redirectUri
.href
);
548 await
super.handlerInternalServerError(req
, res
, ctx
);
554 module
.exports
= Service
;