"use strict"; const Promise = require("bluebird"); const nanoid = require("nanoid"); const mapObj = require("map-obj"); const defaultValue = require("default-value"); const errors = require("./errors"); function createResponse(session) { return { session: session.id, completed: session.completedStageKeys, params: mapObj(session.stages, ([stageKey, stageData]) => { return [stageKey, stageData.parameters]; }), flows: session.flows.map((flow) => { return { stages: flow.stages }; }) }; } function createResult(session) { return Promise.try(() => { return createResponse(session); }).then((response) => { return { completed: session.flows.some((flow) => { return flow.stages.every((stage) => session.completedStageKeys.includes(stage)); }), sessionData: response }; }); } // let stages = { // "m.login.dummy": (_sessionId) => { // return { // parameters: { // foo: "bar" // }, // handler: () => { // return { result: "completed" }; // } // }; // } // }; /* FIXME: - Restrict to valid stages / stage combinations (flows) */ function mapObjAsync(object, handler) { return Promise.map(Object.entries(object), ([key, value]) => { return handler(key, value); }).reduce((mappedObject, [key, value]) => { mappedObject[key] = value; return mappedObject; }, {}); } module.exports = function createUIATracker({ stages }) { let sessions = new Map(); function getSession(sessionId) { if (sessions.has(sessionId)) { return sessions.get(sessionId); } else { throw new errors.BadRequest("Authentication session does not exist"); } } return { createSession: function ({ flows }) { return Promise.try(() => { let sessionId = nanoid(); return mapObjAsync(stages, (stageKey, stageInitializer) => { return Promise.try(() => { return stageInitializer(); }).then((data) => { return [stageKey, { handler: data.handler, parameters: defaultValue(data.parameters, {}) }]; }); }).then((initializedStages) => { let sessionObject = { id: sessionId, completedStageKeys: [], stages: initializedStages, flows: flows }; sessions.set(sessionId, sessionObject); return createResponse(sessionObject); }); }); }, performAuthentication: function ({ sessionId, type, data }) { return Promise.try(() => { let session = getSession(sessionId); let stage = session.stages[type]; return Promise.try(() => { if (stage != null) { if (session.completedStageKeys.includes(type)) { /* Stage was already completed */ /* FIXME: For now, we throw a 400 error; we may need to reevaluate this later: https://github.com/matrix-org/matrix-doc/issues/1987 */ throw new errors.BadRequest("Attempted to complete an already-completed authentication stage"); } else { return Promise.try(() => { let handler = stage.handler; if (handler != null) { return Promise.try(() => { return handler(data); }).catch(errors.AuthenticationError, (error) => { /* This attaches information about the current state of the authentication process, to any authentication error that occurs, so that it ends up in the response - as is required by the specification, for retryable authentication errors. */ return Promise.try(() => { return createResponse(session); }).then((response) => { error.errorMeta = response; throw error; }); }); } else { throw new Error("Authentication stage is missing a handler; this is a bug"); } }).then((handlerResult) => { if (handlerResult.newParameters != null) { Object.assign(stage.parameters, handlerResult.newParameters); } if (handlerResult.result === "completed") { session.completedStageKeys.push(type); } else if (handlerResult.result === "nextStepRequired") { /* Do nothing, in this case new parameters will usually have been assigned */ } else if (handlerResult.result === "failed") { /* FIXME: Send along metadata? */ throw new errors.Unauthorized(handlerResult.reason); } else { throw new Error(`Unrecognized authentication handler result '${handlerResult.result}'; this is a bug`); } return createResult(session); }); } } else { throw new errors.BadRequest("Attempted to complete a non-existent authentication stage"); } }); }); }, getStatus: function ({ sessionId }) { return createResult(getSession(sessionId)); } }; };