WIP
parent
4cce6bab09
commit
e7a6e22593
@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function (_state) {
|
||||
return function createDummyAuthenticator(_sessionId) {
|
||||
return {
|
||||
handler: () => {
|
||||
return { result: "completed" };
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const defaultValue = require("default-value");
|
||||
|
||||
const errors = require("../errors");
|
||||
|
||||
module.exports = function ({ uiaTracker }) {
|
||||
return function (options) {
|
||||
if (options.flows == null || options.flows.length === 0) {
|
||||
throw new Error("At least one authentication flow must be defined");
|
||||
} else {
|
||||
let required = defaultValue(options.required, true);
|
||||
|
||||
return function userInteractiveAuthenticationMiddleware(req, res, next) {
|
||||
if (req.body.auth != null && req.body.auth.session != null) {
|
||||
let authenticationData = req.body.auth;
|
||||
/* FIXME: Riot currently doesn't pass back the session ID it was handed: https://github.com/vector-im/riot-web/issues/8458 - need to work around this*/
|
||||
let sessionId = authenticationData.session;
|
||||
let authenticationType = authenticationData.type;
|
||||
|
||||
return Promise.try(() => {
|
||||
if (authenticationType != null) {
|
||||
return uiaTracker.performAuthentication({
|
||||
sessionId: sessionId,
|
||||
type: authenticationType,
|
||||
data: authenticationData
|
||||
});
|
||||
} else {
|
||||
/* Client re-requested authentication session state without providing credentials; they most likely completed an out-of-band stage. */
|
||||
return uiaTracker.getStatus({ sessionId });
|
||||
}
|
||||
}).then((status) => {
|
||||
if (status.completed) {
|
||||
req.uiaSessionId = sessionId;
|
||||
return next();
|
||||
} else {
|
||||
throw new errors.UIARequired("More authentication steps are required", {
|
||||
noErrorCode: true,
|
||||
errorMeta: status.sessionData
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
/* Workaround for Riot violating spec: https://github.com/vector-im/riot-web/issues/8458#issuecomment-488839052 */
|
||||
let riotAttempt = (req.body.auth != null);
|
||||
|
||||
if (required || riotAttempt) {
|
||||
return Promise.try(() => {
|
||||
return uiaTracker.createSession({
|
||||
flows: options.flows,
|
||||
});
|
||||
}).then((sessionData) => {
|
||||
throw new errors.UIARequired("User-Interactive Authentication is required for this endpoint", {
|
||||
noErrorCode: true,
|
||||
errorMeta: sessionData
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/* MARKER:
|
||||
If successful: either let through request (if all stages completed), or send back a 401 with updated "completed" list
|
||||
Authentication method handlers are defined on the uiaTracker itself
|
||||
Also need to remove the `tx = knex` from db modules
|
||||
|
||||
errorMeta: {
|
||||
flows: [],
|
||||
params: {},
|
||||
session: sessionId
|
||||
}
|
||||
*/
|
||||
/* MARKER: Store flows/params in UIA tracker, send flows/params to user, write logic for subsequent UIA requests that interacts with the UIA tracker... also add code for UIA initialization where it is *optional* to do so */
|
@ -0,0 +1,160 @@
|
||||
"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));
|
||||
}
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue