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

Added simple TOTP Two Factor Authentication #363

Merged
merged 12 commits into from
Sep 12, 2021
10 changes: 10 additions & 0 deletions db/patch-2fa.sql
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
1 change: 1 addition & 0 deletions server/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Database {
static patchList = {
"patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true,
"patch-2fa.sql": true,
}

/**
Expand Down
168 changes: 161 additions & 7 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -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);
});
Expand Down
10 changes: 10 additions & 0 deletions server/util-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
25 changes: 20 additions & 5 deletions src/components/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@
<form @submit.prevent="submit">
<h1 class="h3 mb-3 fw-normal" />

<div class="form-floating">
<div v-if="!tokenRequired" class="form-floating">
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username">
<label for="floatingInput">{{ $t("Username") }}</label>
</div>

<div class="form-floating mt-3">
<div v-if="!tokenRequired" class="form-floating mt-3">
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password">
<label for="floatingPassword">{{ $t("Password") }}</label>
</div>

<div v-if="tokenRequired">
<div class="form-floating mt-3">
<input id="floatingToken" v-model="token" type="text" maxlength="6" class="form-control" placeholder="123456">
<label for="floatingToken">{{ $t("Token") }}</label>
</div>
</div>

<div class="form-check mb-3 mt-3 d-flex justify-content-center pe-4">
<div class="form-check">
<input id="remember" v-model="$root.remember" type="checkbox" value="remember-me" class="form-check-input">
Expand Down Expand Up @@ -42,16 +49,24 @@ export default {
processing: false,
username: "",
password: "",

token: "",
res: null,
tokenRequired: false,
}
},
methods: {
submit() {
this.processing = true;
this.$root.login(this.username, this.password, (res) => {

this.$root.login(this.username, this.password, this.token, (res) => {
this.processing = false;
this.res = res;
console.log(res)

if (res.tokenRequired) {
this.tokenRequired = true;
} else {
this.res = res;
}
})
},
},
Expand Down
Loading