+/* eslint-disable sonarjs/no-duplicate-string */
'use strict';
const common = require('./common');
const { MysteryBox } = require('@squeep/mystery-box');
const DBErrors = require('./db/errors');
const Chores = require('./chores');
-const { Publisher: QueuePublisher } = require('@squeep/amqp-helper');
+const { Publisher: QueuePublisher, Consumer: QueueConsumer } = require('@squeep/amqp-helper');
const _fileScope = common.fileScope(__filename);
const supportedCodeChallengeMethods = ['S256', 'SHA256'];
+/**
+ * @typedef {import('node:http')} http
+ */
+
class Manager {
constructor(logger, db, options) {
this.options = options;
this.logger = logger;
this.db = db;
- this.chores = new Chores(logger, db, options);
this.communication = new Communication(logger, options);
if (options.queues.amqp.url) {
this.queuePublisher = new QueuePublisher(logger, options.queues.amqp);
+ this.queueConsumer = new QueueConsumer(logger, options.queues.amqp);
}
+ this.chores = new Chores(logger, db, this.queuePublisher, options);
this.mysteryBox = new MysteryBox(options);
this.mysteryBox.on('statistics', common.mysteryBoxLogger(logger, _fileScope(this.constructor.name)));
async _connectQueues() {
await this.queuePublisher.connect();
await this.queuePublisher.establishAMQPPlumbing(this.options.queues.ticketPublishName);
+ await this.queuePublisher.establishAMQPPlumbing(this.options.queues.ticketRedeemedName);
+
+ await this.queueConsumer.connect();
+ await this.queueConsumer.establishAMQPPlumbing(this.options.queues.ticketPublishName);
+ const boundTicketProcessor = this.queuedTicketProcessor.bind(this);
+ await this.queueConsumer.consume(this.options.queues.ticketPublishName, boundTicketProcessor);
}
/**
* 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');
Manager._sensitiveResponse(res);
- ctx.session = Object.assign({}, ctx.session, {
+ ctx.session = {
+ ...ctx.session,
errorDescriptions: [],
- });
+ };
// Ingest and validate expected data, populating ctx.session.
await this._clientIdRequired(ctx);
/**
* 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');
const acceptedScopesSet = new Set();
const rejectedScopesSet = new Set();
- const submittedScopes = common.ensureArray(ctx.parsedBody['accepted_scopes'])
+ const submittedScopes = common.ensureArray(ctx.parsedBody['accepted_scopes[]'])
.concat((ctx.parsedBody['ad_hoc_scopes'] || '').split(scopeSplitRE));
submittedScopes.forEach((scope) => {
if (scope) {
/**
* 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 {Object}
+ * @param {object} ctx context
+ * @returns {Promise<object>} profile data
*/
async _fetchConsentProfileData(ctx) {
const _scope = _fileScope('_fetchConsentProfileData');
* Receives POST request from consent page, expecting these form fields:
* session - encrypted data collected from initial auth call
* accept - 'true' if consent was granted
- * accepted_scopes - list of scopes to grant
+ * accepted_scopes[] - list of scopes to grant
* ad_hoc_scopes - additional scopes specified by user
* me - selected profile to identify as
* expires - optional lifespan
* 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');
Manager._sensitiveResponse(res);
// Ensure session exists, persisting any login session data.
- ctx.session = Object.assign({}, ctx.session);
+ ctx.session = {
+ ...ctx.session,
+ };
try {
// Recover the session established on initial auth request.
const oldSession = await this.mysteryBox.unpack(ctx.parsedBody['session']);
/**
* 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');
- ctx.session = Object.assign({}, ctx.session, {
+ ctx.session = {
+ ...ctx.session,
errorDescriptions: [],
- });
+ };
if (!ctx.parsedBody) {
this.logger.debug(_scope, 'no body data', { ctx });
/**
* 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 {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 });
const nowEpoch = common.dateToEpoch();
return this.mysteryBox.pack({
c: common.requestId(),
- iss: nowEpoch,
+ iat: nowEpoch,
exp: nowEpoch + ticketLifespanSeconds,
sub: subject,
res: resource,
/**
* @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 {Ticket}
+ * @param {string} ticket ticket
+ * @returns {Promise<Ticket>} ticket object
*/
async _unpackTicket(ticket) {
const ticketObj = await this.mysteryBox.unpack(ticket);
return {
codeId: ticketObj.c,
- issued: new Date(ticketObj.iss * 1000),
+ issued: new Date(ticketObj.iat * 1000),
expires: new Date(ticketObj.exp * 1000),
subject: new URL(ticketObj.sub),
resource: new URL(ticketObj.res),
/**
* 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');
this.logger.debug(_scope, 'called', { ctx });
await this._restoreSessionFromCode(ctx);
- await this._checkSessionMatchingRedirectUri(ctx);
+ this._checkSessionMatchingRedirectUri(ctx);
if (ctx.session.error) {
throw new ResponseError(Enum.ErrorResponse.BadRequest);
/**
* 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');
this.logger.debug(_scope, 'called', { ctx });
- ctx.session = Object.assign({}, ctx.session);
+ ctx.session = {
+ ...ctx.session,
+ };
await this.db.context(async (dbCtx) => {
* 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');
}
const queueName = this.options.queues.ticketPublishName;
- const { ticket, resource, subject } = ctx.parsedBody;
+ const { ticket, resource, subject, iss } = ctx.parsedBody;
+ if (iss) {
+ try {
+ new URL(iss);
+ } 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);
}
}
try {
- const result = await this.queuePublisher.publish(queueName, { ticket, resource, subject, epochMs: Date.now() });
+ const result = await this.queuePublisher.publish(queueName, { ticket, resource, subject, iss, epochMs: Date.now() });
this.logger.debug(_scope, 'accepted ticket offer', { queueName, ticket, resource, subject, ctx, result });
} catch (e) {
this.logger.error(_scope, 'failed to publish ticket to queue', { error: e, queueName, ticket, resource, subject, ctx });
}
+ /**
+ * @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 channel
+ * @param {Buffer} message message
+ */
+ async queuedTicketProcessor(channel, message) {
+ const _scope = _fileScope('queuedTicketProcessor');
+
+ const queueName = this.options.queues.ticketRedeemedName;
+ let payload, ticket, resource, subject, iss;
+ try {
+ payload = message.content.toString();
+
+ this.logger.debug(_scope, 'processing ticket', { payload });
+ ({
+ ticket,
+ resource,
+ subject,
+ iss,
+ } = JSON.parse(payload));
+ } catch (e) {
+ this.logger.error(_scope, 'could not parse message, discarding', { error: e, message });
+ channel.ack(message);
+ return;
+ }
+
+ let issuerUrlObj;
+ try {
+ if (iss) {
+ issuerUrlObj = new URL(iss);
+ }
+ } catch (e) {
+ this.logger.debug(_scope, 'unparsable issuer, falling back to resource discovery', { error: e, payload });
+ }
+
+ let resourceUrlObj;
+ try {
+ resourceUrlObj = new URL(resource);
+ } catch (e) { // eslint-disable-line no-unused-vars
+ this.logger.error(_scope, 'unparsable resource, discarding', { payload });
+ channel.ack(message);
+ return;
+ }
+
+ let isNotRetryable = false;
+ try {
+ await this.db.context(async (dbCtx) => {
+
+ let token;
+ try {
+ token = await this.communication.redeemTicket(ticket, resourceUrlObj, issuerUrlObj);
+ isNotRetryable = true; // assume we cannot redeem a ticket more than once
+ this.logger.debug(_scope, 'successfully redeemed ticket', { token, payload });
+ channel.ack(message);
+ } catch (e) {
+ this.logger.error(_scope, 'failed to redeem ticket', { error: e, payload });
+ throw e;
+ }
+
+ // persist our redemption
+ const redeemedData = {
+ subject,
+ resource,
+ iss,
+ ticket,
+ token,
+ };
+ await this.db.ticketRedeemed(dbCtx, redeemedData);
+
+ try {
+ const result = await this.queuePublisher.publish(queueName, redeemedData);
+ this.logger.info(_scope, 'published ticket token', { queueName, ticket, resource, subject, iss, result });
+ } catch (e) {
+ this.logger.error(_scope, 'failed to publish token to queue', { error: e, queueName, ticket, token, resource, subject });
+ throw e; // return a 500
+ }
+
+ await this.db.ticketTokenPublished(dbCtx, redeemedData);
+
+ }); // dbCtx
+
+ } catch (e) {
+ isNotRetryable = isNotRetryable
+ || (e instanceof CommunicationErrors.ValidationError)
+ || (e?.response?.statusCode < 500)
+ ;
+ if (isNotRetryable) {
+ this.logger.error(_scope, 'failed to process ticket, not requeuing', { error: e, payload });
+ channel.ack(message);
+ return;
+ }
+ this.logger.error(_scope, 'failed to process ticket, requeuing', { error: e, payload });
+ throw e;
+ }
+ }
+
+
/**
* 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');
if (action === 'save-scopes') {
// Update the convenience scopes set for profiles.
- // Expect 'scopes-<profile>' with value of array of scopes
+ // Expect 'scopes-<profile>[]' with value of array of scopes
const profileKeys = ctx.parsedBody && Object.keys(ctx.parsedBody)
- .filter((k) => k.startsWith('scopes-'));
+ .filter((k) => k.startsWith('scopes-') && k.endsWith('[]'));
try {
await this.db.transaction(dbCtx, async (txCtx) => {
await Promise.all(
/* For each scopes-profile submitted, set those. */
profileKeys.map((profileKey) => {
- /* elide 'scope-' prefix to get the profile */
- const profile = profileKey.slice(7);
+ /* elide 'scope-' prefix and '[]' postfix to get the profile */
+ const profile = profileKey.slice(7, -2);
/* (should validate profile here) */
/* remove invalid scopes from submitted list */
/**
* 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);
}
const scopesSet = new Set();
const rawScopes = [
- ...(common.ensureArray(ctx.parsedBody['scopes'])),
+ ...(common.ensureArray(ctx.parsedBody['scopes[]'])),
...((ctx.parsedBody['adhoc'] || '').split(scopeSplitRE)),
].filter((scope) => scope);
rawScopes.forEach((scope) => {
this.logger.debug(_scope, 'ticket created', { ctx, ticketData, subjectData });
try {
- const result = await this.communication.deliverTicket(ctx.ticketEndpointUrl, ctx.ticketResourceUrl, ctx.ticketSubjectUrl, ticket);
+ const issuerUrl = new URL(this.options.dingus.selfBaseUrl);
+ const result = await this.communication.deliverTicket(ctx.ticketEndpointUrl, ctx.ticketResourceUrl, ctx.ticketSubjectUrl, ticket, issuerUrl);
ctx.notifications.push(`Success! Ticket was delivered. (${result?.statusMessage})`);
- this.logger.info(_scope, 'ticket delivered', { ctx, result });
+ this.logger.info(_scope, 'ticket delivered', { ctx });
} catch (e) {
this.logger.error(_scope, 'failed to deliver ticket', { ctx, error: e });
ctx.errors.push(`Failed to deliver ticket. (${e})`);
/**
* 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');
}
-module.exports = Manager;
\ No newline at end of file
+module.exports = Manager;