+/* eslint-disable sonarjs/no-duplicate-string */
'use strict';
const common = require('./common');
const supportedCodeChallengeMethods = ['S256', 'SHA256'];
+/**
+ * @typedef {import('node:http')} http
+ */
+
class Manager {
constructor(logger, db, options) {
this.options = options;
/**
* Add an error to a session, keeping only the most-severe code, but all descriptions.
* This error is sent along on the redirection back to client endpoint.
- * @param {Object} ctx
- * @param {Object} ctx.session
- * @param {String[]=} ctx.session.errorDescriptions
- * @param {String=} ctx.session.error
- * @param {String} error
- * @param {String} errorDescription
+ * @param {object} ctx context
+ * @param {object} ctx.session session
+ * @param {string[]=} ctx.session.errorDescriptions errors
+ * @param {string=} ctx.session.error error
+ * @param {string} error error
+ * @param {string} errorDescription error
*/
static _setError(ctx, error, errorDescription) {
const errorPrecedence = [ // By increasing severity
* The authorization server MUST include the HTTP Cache-Control response
* header field with a value of no-store in any response
* containing tokens, credentials, or other sensitive information.
- * @param {http.ServerResponse} res
+ * @param {http.ServerResponse} res response
*/
static _sensitiveResponse(res) {
Object.entries({
/**
* Sets params entries as url search parameters.
- * @param {URL} url
- * @param {Object} params
+ * @param {URL} url url
+ * @param {object} params parameters
*/
static _setSearchParams(url, params) {
Object.entries(params).forEach((param) => url.searchParams.set(...param));
/**
* Serve the informational root page.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getRoot(res, ctx) {
const _scope = _fileScope('getRoot');
/**
* Serve the metadata for this service.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getMeta(res, ctx) {
const _scope = _fileScope('getMeta');
* Process an authorization request from a client.
* User has authenticated, check if user matches profile,
* present user with consent form.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getAuthorization(res, ctx) {
const _scope = _fileScope('getAuthorization');
/**
* Validates, fetches, and parses client_id url, populating clientIdentifier with client h-app data.
- * @param {Object} ctx
+ * @param {object} ctx context
*/
async _clientIdRequired(ctx) {
if (ctx.queryParams['client_id']) {
/**
* Ensure redirect_uri exists and is corroborated by clientIdentifier data.
- * @param {Object} ctx
+ * @param {object} ctx context
*/
static _redirectURIRequired(ctx) {
if (ctx.queryParams['redirect_uri']) {
}
}
}
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
Manager._setError(ctx, 'invalid_request', 'invalid value for parameter \'redirect_uri\'');
}
} else {
/**
* response_type must be valid
- * @param {Object} ctx
+ * @param {object} ctx context
*/
static _responseTypeRequired(ctx) {
ctx.session.responseType = ctx.queryParams['response_type'];
/**
* A state parameter must be present
- * @param {Object} ctx
+ * @param {object} ctx context
*/
static _stateRequired(ctx) {
ctx.session.state = ctx.queryParams['state'];
/**
* A code_challenge_method must be present and valid
- * @param {Object} ctx
+ * @param {object} ctx context
*/
_codeChallengeMethodRequired(ctx) {
ctx.session.codeChallengeMethod = ctx.queryParams['code_challenge_method'];
/**
* A code_challenge must be present
- * @param {Object} ctx
+ * @param {object} ctx context
*/
_codeChallengeRequired(ctx) {
ctx.session.codeChallenge = ctx.queryParams['code_challenge'];
/**
* Scopes may be present, with one known combination limitation
- * @param {Object} ctx
+ * @param {object} ctx context
*/
_scopeOptional(ctx) {
const _scope = _fileScope('_scopeOptional');
/**
* Parses me, if provided
- * @param {Object} ctx
+ * @param {object} ctx context
*/
async _meOptional(ctx) {
const me = ctx.queryParams['me'];
if (me) {
try {
ctx.session.me = await this.communication.validateProfile(me);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
ctx.session.me = undefined;
}
}
/**
* Ensure authenticated identifier matches profile.
- * @param {Object} ctx
- * @returns {Boolean}
+ * @param {object} ctx context
+ * @returns {boolean} is valid
*/
_profileValidForIdentifier(ctx) {
const _scope = _fileScope('_profileValidForIdentifier');
/**
* Get numeric value from form field data.
- * @param {*} ctx
- * @param {String} field
- * @param {String} customField
- * @returns {Number=}
+ * @param {*} ctx context
+ * @param {string} field field
+ * @param {string} customField custom field
+ * @returns {number=} lifespan
*/
_parseLifespan(ctx, field, customField) {
const _scope = _fileScope('_parseLifespan');
/**
* Validate any accepted scopes, ensure uniqueness, return as array.
- * @param {Object} ctx
- * @returns {String=}
+ * @param {object} ctx context
+ * @returns {string[]} scopes
*/
_parseConsentScopes(ctx) {
const _scope = _fileScope('_ingestConsentScopes');
/**
* Parse and validate selected me is a valid profile option.
- * @param {Object} ctx
- * @returns {URL}
+ * @param {object} ctx context
+ * @returns {URL} url
*/
_parseConsentMe(ctx) {
const _scope = _fileScope('_parseConsentMe');
/**
* Get up-to-date profile data from selected profile endpoint.
- * @param {Object} ctx
- * @returns {Promise<Object>}
+ * @param {object} ctx context
+ * @returns {Promise<object>} profile data
*/
async _fetchConsentProfileData(ctx) {
const _scope = _fileScope('_fetchConsentProfileData');
* expires-seconds - optional custom lifespan
* refresh - optional refresh lifespan
* refresh-seconds - optional custom refresh lifespan
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postConsent(res, ctx) {
const _scope = _fileScope('postConsent');
/**
* Redeem a code for a profile url, and maybe more profile info.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postAuthorization(res, ctx) {
const _scope = _fileScope('postAuthorization');
/**
* Ingest an incoming authorization redemption request, parsing fields
* onto a new session object on the context.
- * @param {*} dbCtx
- * @param {Object} ctx
+ * @param {object} ctx context
+ * @returns {Promise<void>}
*/
async _ingestPostAuthorizationRequest(ctx) {
const _scope = _fileScope('_ingestPostAuthorizationRequest');
/**
* Unpack the session data from provided code overtop of context session ..
- * @param {Object} ctx
+ * @param {object} ctx context
*/
async _restoreSessionFromCode(ctx) {
const _scope = _fileScope('_restoreSessionFromCode');
/**
* Ensure provided client_id matches session clientId.
- * @param {Object} ctx
+ * @param {object} ctx context
*/
_checkSessionMatchingClientId(ctx) {
const _scope = _fileScope('_checkSessionMatchingClientId');
try {
clientId = new URL(clientId);
ctx.session.clientId = new URL(ctx.session.clientId);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.debug(_scope, 'un-parsable client_id url', { ctx });
delete ctx.session.clientId;
Manager._setError(ctx, 'invalid_request', 'malformed client_id');
/**
- * @param {Object} ctx
+ * @param {object} ctx context
*/
_checkSessionMatchingRedirectUri(ctx) {
const _scope = _fileScope('_checkSessionMatchingClientId');
try {
redirectUri = new URL(redirectUri);
ctx.session.redirectUri = new URL(ctx.session.redirectUri);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.debug(_scope, 'un-parsable redirect_uri url', { ctx });
delete ctx.session.redirectUri;
Manager._setError(ctx, 'invalid_request', 'malformed redirect_url');
/**
* Validate grant_type, either persist on session or set error.
- * @param {Object} ctx
- * @param {String[]} validGrantTypes
- * @param {Boolean} treatEmptyAs
+ * @param {object} ctx context
+ * @param {string[]} validGrantTypes grant types
+ * @param {string=} treatEmptyAs grant type
*/
_checkGrantType(ctx, validGrantTypes = ['authorization_code'], treatEmptyAs = 'authorization_code') {
const _scope = _fileScope('_checkGrantType');
/**
- * @param {Object} ctx
+ * @param {object} ctx context
*/
_checkSessionMatchingCodeVerifier(ctx) {
const _scope = _fileScope('_checkSessionMatchingCodeVerifier');
/**
* Attempt to revoke a token.
- * @param {*} dbCtx
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {*} dbCtx db context
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async _revokeToken(dbCtx, res, ctx) {
const _scope = _fileScope('_revokeToken');
/**
* Legacy token validation flow.
- * @param {*} dbCtx
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {*} dbCtx db context
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async _validateToken(dbCtx, req, res, ctx) {
const _scope = _fileScope('_validateToken');
/**
* Given a list of newly-requested scopes, return a list of scopes
* from previousScopes which are not in requestedScopes.
- * @param {String[]} previousScopes
- * @param {String[]} requestedScopes
- * @returns {String[]}
+ * @param {string[]} previousScopes scopes
+ * @param {string[]} requestedScopes scopes
+ * @returns {string[]} scopes
*/
static _scopeDifference(previousScopes, requestedScopes) {
const scopesToRemove = [];
/**
* Redeem a refresh token for a new token.
- * @param {*} dbCtx
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {*} dbCtx db context
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async _refreshToken(dbCtx, req, res, ctx) {
const _scope = _fileScope('_refreshToken');
/**
* Generate a new ticket for later redemption.
- * @param {Object} payload
- * @param {} payload.subject deliver ticket to this endpoint
- * @param {} payload.resource url the redeemed ticket is valid for accessing
- * @param {String[]} payload.scopes list of scopes assigned to ticket
- * @param {String} payload.identifier user generating ticket
- * @param {} payload.profile profile of user generating ticket
- * @param {Number} payload.ticketLifespanSeconds ticket redeemable for this long
- * @returns {Promise<String>}
- */
+ * @param {object} payload payload
+ * @param {string} payload.subject deliver ticket to this endpoint
+ * @param {string} payload.resource url the redeemed ticket is valid for accessing
+ * @param {string[]} payload.scopes list of scopes assigned to ticket
+ * @param {string} payload.identifier user generating ticket
+ * @param {string} payload.profile profile of user generating ticket
+ * @param {number} payload.ticketLifespanSeconds ticket redeemable for this long
+ * @returns {Promise<string>} ticket
+ */
async _mintTicket({ subject, resource, scopes, identifier, profile, ticketLifespanSeconds }) {
const _scope = _fileScope('_mintTicket');
this.logger.debug(_scope, 'called', { subject, resource, scopes, identifier, profile, ticketLifespanSeconds });
/**
* @typedef Ticket
- * @property {String} codeId
- * @property {Date} issued
- * @property {Date} expires
- * @property {URL} subject
- * @property {URL} resource
- * @property {String[]} scopes
- * @property {String} identifier
- * @property {URL} profile
+ * @property {string} codeId code id
+ * @property {Date} issued issued at
+ * @property {Date} expires expires at
+ * @property {URL} subject subject
+ * @property {URL} resource resource
+ * @property {string[]} scopes scopes
+ * @property {string} identifier identifier
+ * @property {URL} profile profile
*/
/**
*
- * @param {String} ticket
- * @returns {Promise<Ticket>}
+ * @param {string} ticket ticket
+ * @returns {Promise<Ticket>} ticket object
*/
async _unpackTicket(ticket) {
const ticketObj = await this.mysteryBox.unpack(ticket);
/**
* Redeem a ticket for a token.
- * @param {*} dbCtx
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {*} dbCtx db context
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
+ * @returns {Promise<void>}
*/
async _ticketAuthToken(dbCtx, req, res, ctx) {
const _scope = _fileScope('_ticketAuthToken');
/**
* Redeem a code for a token.
- * @param {*} dbCtx
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {*} dbCtx db context
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async _codeToken(dbCtx, req, res, ctx) {
const _scope = _fileScope('_codeToken');
/**
* Issue, refresh, or validate a token.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postToken(req, res, ctx) {
const _scope = _fileScope('postToken');
* Ingest token from authorization header, setting ctx.bearer.isValid appropriately.
* ctx.bearer not set if auth method not recognized.
* This is for legacy validation on token endpoint.
- * @param {*} dbCtx
- * @param {http.ClientRequest} req
- * @param {Object} ctx
+ * @param {*} dbCtx db context
+ * @param {http.ClientRequest} req request
+ * @param {object} ctx context
+ * @returns {Promise<void>}
*/
async _checkTokenValidationRequest(dbCtx, req, ctx) {
const _scope = _fileScope('_checkTokenValidationRequest');
};
try {
Object.assign(ctx.bearer, await this.mysteryBox.unpack(authString));
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.debug(_scope, 'failed to unpack token', { ctx });
Manager._setError(ctx, 'invalid_request', 'invalid token');
return;
/**
* Accept an unsolicited ticket proffering.
- * @param {http.ClientRequest} req
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ClientRequest} req request
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postTicket(req, res, ctx) {
const _scope = _fileScope('postTicket');
if (iss) {
try {
new URL(iss);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.debug(_scope, 'unparsable issuer', { ticket, resource, subject, iss, ctx });
// continue, will try resource for metadata
}
}
try {
new URL(resource);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.debug(_scope, 'unparsable resource', { ticket, resource, subject, ctx });
throw new ResponseError(Enum.ErrorResponse.BadRequest);
}
}
+ /**
+ * @typedef {object} AMQPChannel
+ * @property {Function} ack ack
+ */
/**
* Process messages from proffered ticket queue.
* Attempt to redeem ticket and publish to redeemed token queue.
- * @param {AMQPChannel} channel
- * @param {Buffer} message
+ * @param {AMQPChannel} channel channel
+ * @param {Buffer} message message
*/
async queuedTicketProcessor(channel, message) {
const _scope = _fileScope('queuedTicketProcessor');
let resourceUrlObj;
try {
resourceUrlObj = new URL(resource);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.error(_scope, 'unparsable resource, discarding', { payload });
channel.ack(message);
return;
/**
* Validate a token and return data about it.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postIntrospection(res, ctx) {
const _scope = _fileScope('postIntrospection');
/**
* Revoke a token or refresh token.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postRevocation(res, ctx) {
const _scope = _fileScope('postRevocation');
/**
* Profile information for a token.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postUserInfo(res, ctx) {
const _scope = _fileScope('postUserInfo');
/**
* Show admin interface, allowing manipulation of profiles and scopes.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getAdmin(res, ctx) {
const _scope = _fileScope('getAdmin');
/**
* Process admin interface events.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postAdmin(res, ctx) {
const _scope = _fileScope('postAdmin');
/**
* Show ticket proffer interface.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getAdminTicket(res, ctx) {
const _scope = _fileScope('getAdminTicket');
/**
* Handle ticket proffer interface submission.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async postAdminTicket(res, ctx) {
const _scope = _fileScope('postAdminTicket');
].forEach((param) => {
try {
ctx[param.ctxProp] = new URL(ctx.parsedBody[param.bodyParam]);
- } catch (e) {
+ } catch (e) { // eslint-disable-line no-unused-vars
this.logger.debug(_scope, `invalid ${param.bodyParam}`, { ctx });
ctx.errors.push(param.err);
}
/**
* Report on generally uninteresting backend information.
* Also allow a few event invocations.
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getAdminMaintenance(res, ctx) {
const _scope = _fileScope('getAdminMaintenance');
/**
*
- * @param {http.ServerResponse} res
- * @param {Object} ctx
+ * @param {http.ServerResponse} res response
+ * @param {object} ctx context
*/
async getHealthcheck(res, ctx) {
const _scope = _fileScope('getHealthcheck');