Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin api #12

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/pluginApi.js
Original file line number Diff line number Diff line change
@@ -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;
17 changes: 17 additions & 0 deletions src/plugins/duo-backend/index.js
Original file line number Diff line number Diff line change
@@ -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,
};
164 changes: 164 additions & 0 deletions src/plugins/duo-backend/routes.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 5 additions & 1 deletion src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions src/routes/plugins.js
Original file line number Diff line number Diff line change
@@ -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;