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