diff --git a/src/pluginApi.js b/src/pluginApi.js new file mode 100644 index 0000000..84e0f68 --- /dev/null +++ b/src/pluginApi.js @@ -0,0 +1,24 @@ +const fs = require("fs").promises; + +async function loadPlugins(router) { + // get all plugins + const pluginDirList = await fs.readdir("./controllers/"); + console.error(`Found plugins: ${pluginDirList}`); + // load all plugin routes + const pluginsApi = {}; + + for (const plugin of pluginDirList) { + var isDir = fs.statSync(plugin).isDirectory(); + console.error(`Found plugin: ${plugin} isDir: ${isDir}`); + + const pluginPath = `./controllers/${plugin}`; + + const pluginRoutes = require(pluginPath); + + pluginsApi[plugin] = pluginRoutes; + } + + router.use(pluginsRouter); +} + +module.exports = loadPlugins; diff --git a/src/plugins/duo-backend/index.js b/src/plugins/duo-backend/index.js new file mode 100644 index 0000000..e17f55d --- /dev/null +++ b/src/plugins/duo-backend/index.js @@ -0,0 +1,17 @@ +const routes = require("./routes.js"); + +const name = "duo-backend"; + +const version = "1.0.0"; + +const addPluginRoutes = async (options) => { + const router = express.Router(); + router.use("/auth/v2", routes); + return router; +}; + +module.exports = { + name, + version, + routes: addPluginRoutes, +}; diff --git a/src/plugins/duo-backend/routes.js b/src/plugins/duo-backend/routes.js new file mode 100644 index 0000000..43250de --- /dev/null +++ b/src/plugins/duo-backend/routes.js @@ -0,0 +1,164 @@ +const express = require("express"); +const { createHmac } = require("crypto"); + +const { getTopicById, getTopicByKey } = require("../../pluginApi").topic; + +const { validateDuoSignature } = require("../middleware/validate-duo"); + +const { + createPushToTopic, + pushToTopicDevices, + getPushByIdent, + getPushResponse, +} = require("../service/push"); + +const router = express.Router(); + +// The /ping endpoint acts as a "liveness check" that can be called to verify that Duo is up before trying to call other Auth API endpoints. +router.get("/ping", (request, response) => { + return response.json({ + stat: "OK", + response: { + time: Math.round(Date.now() / 1000), + validation: process.env.NO_DUO_AUTH ? "skipped" : "enabled", // added to indicate if we're currently validating cert + }, + }); +}); + +// The /preauth endpoint determines whether a user is authorized to log in, and (if so) returns the user's available authentication factors. +router.post("/preauth", validateDuoSignature, async (request, response) => { + console.log("preauth", request.body, request.topic); + const { username } = request.body; + + // no devices in topic, deny + if (!request?.topic?.devices || request.topic.devices.length === 0) { + return response.json({ + stat: "OK", + response: { + devices: [], + result: "deny", + status_msg: "No devices in topic", + }, + }); + } + + // loop devices, create array + const devices = request.topic.devices.map((device) => { + return { + capabilities: ["auto", "push"], + device: `${device.deviceKey}`, + display_name: `${device.name}`, + name: `${device.name}`, + number: "", + type: "phone", + }; + }); + + return response.json({ + stat: "OK", + response: { + devices: devices, + result: "auth", // TODO this should take into account lockouts, etc. + status_msg: "Account is active", + }, + }); +}); + +const WAIT_TIME = 30; // seconds + +// The /auth endpoint performs second-factor authentication for a user by sending a push notification to the user's smartphone app, verifying a passcode, or placing a phone call. +router.post("/auth", validateDuoSignature, async (request, response) => { + console.log("auth", request.body); + const { device, username, factor, ipaddr, async } = request.body; + + // non-async + if (!async) { + const pushPayload = { + categoryId: "button.approve_deny", + title: `Login Approval Request`, + subtitle: `Request from ${ipaddr}`, + body: `Approve login request for ${username}?`, + priority: "high", + ttl: WAIT_TIME, + }; + + const createdPush = await createPushToTopic(request.topic, pushPayload); + console.log("createdPush", pushPayload, createdPush); + + await pushToTopicDevices(request.topic, createdPush, pushPayload); + + // **wait** for push response here + + const waitForResponse = async (pushId) => { + var totalLoops = 0; // set your counter to 1 + + function timeout(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async function checkForResponse() { + const response = await getPushResponse(pushId); + + if (response) { + console.log("checkForResponse", response); + return response; + } + + await timeout(1000); + totalLoops++; // increment the counter (seconds) + if (totalLoops < WAIT_TIME) { + return await checkForResponse(); + } else { + return false; + } + } + + return checkForResponse(); + }; + + const result = await waitForResponse(createdPush.dataValues.id); + console.log("result", result); + + if (JSON.parse(result.serviceResponse).actionIdentifier === "approve") { + return response.json({ + stat: "OK", + response: { + result: "allow", + status: "allow", + }, + serviceData: JSON.parse(result.serviceResponse), + }); + } else { + return response.json({ + stat: "OK", + response: { + result: "deny", + status: "deny", + }, + serviceData: JSON.parse(result.serviceResponse), + }); + } + } else { + return response.json({ + stat: "OK", + response: { + txid: "45f7c92b-f45f-4862-8545-e0f58e78075a", + }, + }); + } +}); + +// unused for async +router.get("/auth_status", validateDuoSignature, (request, response) => { + console.log("auth_status", request.query); + + return response.json({ + stat: "OK", + response: { + result: "allow", + status: "allow", + }, + }); +}); + +module.exports = router; diff --git a/src/routes.js b/src/routes.js index 41f98e1..5b2cfe7 100644 --- a/src/routes.js +++ b/src/routes.js @@ -8,6 +8,7 @@ const TopicRouter = require("./routes/topic.js"); const PushRouter = require("./routes/push.js"); const DuoAPIV2 = require("./routes/duo.js"); +const PluginRouters = require("./routes/plugins.js"); const GoogleAuthRouter = require("./routes/auth/google.js"); const EmailAuthRouter = require("./routes/auth/email.js"); @@ -18,7 +19,10 @@ router.use("/topic", TopicRouter); router.use("/push", PushRouter); -router.use("/auth/v2", DuoAPIV2); +// router.use("/auth/v2", DuoAPIV2); +// router.use(PluginRouters); + +PluginRouters(router); router.use("/auth/google", GoogleAuthRouter.router); router.use("/auth/email", EmailAuthRouter.router); diff --git a/src/routes/plugins.js b/src/routes/plugins.js new file mode 100644 index 0000000..9534f17 --- /dev/null +++ b/src/routes/plugins.js @@ -0,0 +1,30 @@ +const express = require("express"); +const fs = require("fs").promises; + +const { + createPushRequest, + recordPushResponse, + getPushStatus, + getPushStatusPoll, +} = require("../controllers/push"); + +async function loadPlugins(router) { + // get all plugins + const pluginDirList = await fs.readdir("./src/plugins"); + console.error(`Found plugins: ${pluginDirList}`); + // load all plugin routes + const pluginsRouter = express.Router(); + + for (const plugin of pluginDirList) { + const pluginPath = `../plugins/${plugin}/index.js`; + + console.error(`Loading plugin: ${pluginPath}`); + const pluginRoutes = require(pluginPath); + + pluginsRouter.use(pluginRoutes.routes()); + } + + router.use(pluginsRouter); +} + +module.exports = loadPlugins;