diff --git a/db/patch-2fa.sql b/db/patch-2fa.sql new file mode 100644 index 000000000..35069d854 --- /dev/null +++ b/db/patch-2fa.sql @@ -0,0 +1,10 @@ +-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. +BEGIN TRANSACTION; + +ALTER TABLE user + ADD twofa_secret VARCHAR(64); + +ALTER TABLE user + ADD twofa_status BOOLEAN default 0 NOT NULL; + +COMMIT; diff --git a/package.json b/package.json index e1c405cf3..e71fde810 100644 --- a/package.json +++ b/package.json @@ -56,20 +56,24 @@ "http-graceful-shutdown": "^3.1.4", "jsonwebtoken": "^8.5.1", "nodemailer": "^6.6.3", + "notp": "^2.0.3", "password-hash": "^1.2.2", "prom-client": "^13.2.0", "prometheus-api-metrics": "^3.2.0", + "qrcode": "^1.4.4", "redbean-node": "0.1.2", "socket.io": "^4.2.0", "socket.io-client": "^4.2.0", "sqlite3": "github:mapbox/node-sqlite3#593c9d", "tcp-ping": "^0.1.1", + "thirty-two": "^1.0.2", "v-pagination-3": "^0.1.6", "vue": "^3.2.8", "vue-chart-3": "^0.5.7", "vue-confirm-dialog": "^1.0.2", "vue-i18n": "^9.1.7", "vue-multiselect": "^3.0.0-alpha.2", + "vue-qrcode": "^1.0.0", "vue-router": "^4.0.11", "vue-toastification": "^2.0.0-rc.1" }, diff --git a/server/database.js b/server/database.js index e0bb0c9b8..4b3ad443e 100644 --- a/server/database.js +++ b/server/database.js @@ -30,6 +30,7 @@ class Database { static patchList = { "patch-setting-value-type.sql": true, "patch-improve-performance.sql": true, + "patch-2fa.sql": true, } /** diff --git a/server/server.js b/server/server.js index 2949c4be7..a0b9a2fbb 100644 --- a/server/server.js +++ b/server/server.js @@ -22,11 +22,15 @@ const gracefulShutdown = require("http-graceful-shutdown"); debug("Importing prometheus-api-metrics"); const prometheusAPIMetrics = require("prometheus-api-metrics"); +debug("Importing 2FA Modules"); +const notp = require("notp"); +const base32 = require("thirty-two"); + console.log("Importing this project modules"); debug("Importing Monitor"); const Monitor = require("./model/monitor"); debug("Importing Settings"); -const { getSettings, setSettings, setting, initJWTSecret } = require("./util-server"); +const { getSettings, setSettings, setting, initJWTSecret, genSecret } = require("./util-server"); debug("Importing Notification"); const { Notification } = require("./notification"); @@ -219,12 +223,38 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); if (user) { afterLogin(socket, user) - callback({ - ok: true, - token: jwt.sign({ - username: data.username, - }, jwtSecret), - }) + if (user.twofaStatus == 0) { + callback({ + ok: true, + token: jwt.sign({ + username: data.username, + }, jwtSecret), + }) + } + + if (user.twofaStatus == 1 && !data.token) { + callback({ + tokenRequired: true, + }) + } + + if (data.token) { + let verify = notp.totp.verify(data.token, user.twofa_secret); + + if (verify && verify.delta == 0) { + callback({ + ok: true, + token: jwt.sign({ + username: data.username, + }, jwtSecret), + }) + } else { + callback({ + ok: false, + msg: "Invalid Token!", + }) + } + } } else { callback({ ok: false, @@ -240,6 +270,130 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); callback(); }); + socket.on("prepare2FA", async (callback) => { + try { + checkLogin(socket) + + let user = await R.findOne("user", " id = ? AND active = 1 ", [ + socket.userID, + ]) + + if (user.twofa_status == 0) { + let newSecret = await genSecret() + let encodedSecret = base32.encode(newSecret); + let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`; + + await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [ + newSecret, + socket.userID, + ]); + + callback({ + ok: true, + uri: uri, + }) + } else { + callback({ + ok: false, + msg: "2FA is already enabled.", + }) + } + } catch (error) { + callback({ + ok: false, + msg: "Error while trying to prepare 2FA.", + }) + } + }); + + socket.on("save2FA", async (callback) => { + try { + checkLogin(socket) + + await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [ + socket.userID, + ]); + + callback({ + ok: true, + msg: "2FA Enabled.", + }) + } catch (error) { + callback({ + ok: false, + msg: "Error while trying to change 2FA.", + }) + } + }); + + socket.on("disable2FA", async (callback) => { + try { + checkLogin(socket) + + await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [ + socket.userID, + ]); + + callback({ + ok: true, + msg: "2FA Disabled.", + }) + } catch (error) { + callback({ + ok: false, + msg: "Error while trying to change 2FA.", + }) + } + }); + + socket.on("verifyToken", async (token, callback) => { + let user = await R.findOne("user", " id = ? AND active = 1 ", [ + socket.userID, + ]) + + let verify = notp.totp.verify(token, user.twofa_secret); + + if (verify && verify.delta == 0) { + callback({ + ok: true, + valid: true, + }) + } else { + callback({ + ok: false, + msg: "Invalid Token.", + valid: false, + }) + } + }); + + socket.on("twoFAStatus", async (callback) => { + checkLogin(socket) + + try { + let user = await R.findOne("user", " id = ? AND active = 1 ", [ + socket.userID, + ]) + + if (user.twofa_status == 1) { + callback({ + ok: true, + status: true, + }) + } else { + callback({ + ok: true, + status: false, + }) + } + } catch (error) { + callback({ + ok: false, + msg: "Error while trying to get 2FA status.", + }) + } + }); + socket.on("needSetup", async (callback) => { callback(needSetup); }); diff --git a/server/util-server.js b/server/util-server.js index a2fef0656..079bd82f3 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -271,3 +271,13 @@ exports.getTotalClientInRoom = (io, roomName) => { return 0; } } + +exports.genSecret = () => { + let secret = ""; + let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let charsLength = chars.length; + for ( let i = 0; i < 64; i++ ) { + secret += chars.charAt(Math.floor(Math.random() * charsLength)); + } + return secret; +} diff --git a/src/components/Login.vue b/src/components/Login.vue index bd51759c7..ca36fdb9f 100644 --- a/src/components/Login.vue +++ b/src/components/Login.vue @@ -4,16 +4,23 @@