mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
TOTP fully working MVP
This commit is contained in:
parent
fad51409a9
commit
4574b81caf
2
package-lock.json
generated
2
package-lock.json
generated
@ -19447,7 +19447,7 @@
|
|||||||
},
|
},
|
||||||
"isexe": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ie/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||||
},
|
},
|
||||||
"isobject": {
|
"isobject": {
|
||||||
|
@ -57,7 +57,7 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
|||||||
this.generateKey();
|
this.generateKey();
|
||||||
|
|
||||||
this.$totpEnabled.on("change", async () => {
|
this.$totpEnabled.on("change", async () => {
|
||||||
this.updateCheckboxOption("totpEnabled", this.$totpEnabled);
|
this.updateSecret();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$regenerateTotpButton.on("click", async () => {
|
this.$regenerateTotpButton.on("click", async () => {
|
||||||
@ -79,6 +79,11 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSecret() {
|
||||||
|
if (this.$totpEnabled.prop("checked")) server.post("totp/enable");
|
||||||
|
else server.post("totp/disable");
|
||||||
|
}
|
||||||
|
|
||||||
async generateKey() {
|
async generateKey() {
|
||||||
server.get("totp/generate").then((result) => {
|
server.get("totp/generate").then((result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -89,17 +94,6 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// async toggleTOTP() {
|
|
||||||
// if (this.$totpEnabled)
|
|
||||||
// server.post("totp/enable").then((result) => {
|
|
||||||
// if (!result.success) toastService.showError(result.message);
|
|
||||||
// });
|
|
||||||
// else
|
|
||||||
// server.post("totp/disable").then((result) => {
|
|
||||||
// if (!result.success) toastService.showError(result.message);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
optionsLoaded(options) {
|
optionsLoaded(options) {
|
||||||
server.get("totp/enabled").then((result) => {
|
server.get("totp/enabled").then((result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@ -112,10 +106,6 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.get("totp/get").then((result) => {
|
|
||||||
this.$totpSecretInput.text(result.secret);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
|
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import options = require("../../services/options");
|
import options = require("../../services/options");
|
||||||
import totp_secret = require("../../services/encryption/totp_secret");
|
import totp_secret = require("../../services/encryption/totp_secret");
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
import totp_fs = require("../../services/totp_secret")
|
||||||
const speakeasy = require("speakeasy");
|
const speakeasy = require("speakeasy");
|
||||||
|
|
||||||
function verifyOTPToken(guessedToken: any) {
|
function verifyOTPToken(guessedToken: any) {
|
||||||
@ -27,22 +28,23 @@ function checkForTOTP() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enableTOTP() {
|
function enableTOTP() {
|
||||||
options.setOption("totpEnab| voidled", true);
|
options.setOption("totpEnabled", true);
|
||||||
return { success: "true" };
|
return { success: "true" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableTOTP() {
|
function disableTOTP() {
|
||||||
options.setOption("totpEnabled", false);
|
options.setOption("totpEnabled", false);
|
||||||
return { success: "true" };
|
|
||||||
|
return { success: totp_fs.removeTotpSecret() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTotpSecret(req: Request) {
|
function setTotpSecret(req: Request) {
|
||||||
console.log("TODO: Save Secret");
|
options.setOption
|
||||||
// totp_secret.setTotpSecret(req.body.secret);
|
totp_fs.saveTotpSecret(req.body.secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSecret() {
|
function getSecret() {
|
||||||
return "TODO: Get Secret";
|
return totp_fs.getTotpSecret()
|
||||||
}
|
}
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
|
@ -8,14 +8,17 @@ import passwordService = require('../services/encryption/password');
|
|||||||
import assetPath = require('../services/asset_path');
|
import assetPath = require('../services/asset_path');
|
||||||
import appPath = require('../services/app_path');
|
import appPath = require('../services/app_path');
|
||||||
import ValidationError = require('../errors/validation_error');
|
import ValidationError = require('../errors/validation_error');
|
||||||
|
import totp_secret = require('../services/totp_secret');
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AppRequest } from './route-interface';
|
import { AppRequest } from './route-interface';
|
||||||
|
|
||||||
|
const speakeasy = require("speakeasy")
|
||||||
|
|
||||||
function loginPage(req: Request, res: Response) {
|
function loginPage(req: Request, res: Response) {
|
||||||
res.render('login', {
|
res.render('login', {
|
||||||
failedAuth: false,
|
failedAuth: false,
|
||||||
// totpEnabled: optionService.getOption("hasOTPEnabled"),
|
// totpEnabled: optionService.getOption("hasOTPEnabled"),
|
||||||
totpEnabled: false,
|
totpEnabled: optionService.getOption("totpEnabled") && totp_secret.checkForTotSecret(),
|
||||||
assetPath: assetPath,
|
assetPath: assetPath,
|
||||||
appPath: appPath
|
appPath: appPath
|
||||||
});
|
});
|
||||||
@ -63,29 +66,54 @@ function login(req: AppRequest, res: Response) {
|
|||||||
const guessedPassword = req.body.password;
|
const guessedPassword = req.body.password;
|
||||||
const guessedTotp = req.body.token;
|
const guessedTotp = req.body.token;
|
||||||
|
|
||||||
if (verifyPassword(guessedPassword)) {
|
if (!verifyPassword(guessedPassword))
|
||||||
const rememberMe = req.body.rememberMe;
|
{
|
||||||
|
sendLoginError( req, res)
|
||||||
req.session.regenerate(() => {
|
return
|
||||||
if (rememberMe) {
|
|
||||||
req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks
|
|
||||||
} else {
|
|
||||||
req.session.cookie.expires = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.session.loggedIn = true;
|
|
||||||
res.redirect('.');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
|
if (optionService.getOption("totpEnabled") && totp_secret.checkForTotSecret())
|
||||||
|
if (!verifyTOTP(guessedTotp))
|
||||||
|
{
|
||||||
|
sendLoginError( req, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const rememberMe = req.body.rememberMe;
|
||||||
|
|
||||||
|
req.session.regenerate(() => {
|
||||||
|
if (rememberMe) {
|
||||||
|
req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks
|
||||||
|
} else {
|
||||||
|
req.session.cookie.expires = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.loggedIn = true;
|
||||||
|
res.redirect('.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendLoginError(req: AppRequest, res: Response) {
|
||||||
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
|
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
|
||||||
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
|
log.info(`WARNING: Wrong password or TOTP from ${req.ip}, rejecting.`);
|
||||||
|
|
||||||
res.status(401).render('login', {
|
res.status(401).render('login', {
|
||||||
failedAuth: true,
|
failedAuth: true,
|
||||||
|
totpEnabled: optionService.getOption("totpEnabled") && totp_secret.checkForTotSecret(),
|
||||||
assetPath: assetPath
|
assetPath: assetPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function verifyTOTP( guessedToken: string) {
|
||||||
|
const tokenValidates = speakeasy.totp.verify({
|
||||||
|
secret: totp_secret.getTotpSecret(),
|
||||||
|
encoding: "base32",
|
||||||
|
token: guessedToken,
|
||||||
|
window: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return tokenValidates
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyPassword(guessedPassword: string) {
|
function verifyPassword(guessedPassword: string) {
|
||||||
|
@ -97,7 +97,7 @@ const defaultOptions = [
|
|||||||
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
|
{ name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true },
|
||||||
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
|
{ name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true },
|
||||||
{ name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true },
|
{ name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true },
|
||||||
// { name: 'totpEnabled', value: 'false', isSynced: false }
|
{ name: 'totpEnabled', value: 'false', isSynced: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
function initStartupOptions() {
|
function initStartupOptions() {
|
||||||
|
37
src/services/totp_secret.ts
Normal file
37
src/services/totp_secret.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import fs = require('fs');
|
||||||
|
import crypto = require('crypto');
|
||||||
|
import dataDir = require('./data_dir');
|
||||||
|
import log = require('./log');
|
||||||
|
|
||||||
|
const totpSecretPath = `${dataDir.TRILIUM_DATA_DIR}/totp_secret.txt`;
|
||||||
|
|
||||||
|
function saveTotpSecret(secret: string)
|
||||||
|
{
|
||||||
|
if (!fs.existsSync(totpSecretPath))
|
||||||
|
log.info("Generated totp secret file");
|
||||||
|
|
||||||
|
fs.writeFileSync(totpSecretPath, secret, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotpSecret(){
|
||||||
|
|
||||||
|
return fs.readFileSync(totpSecretPath, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForTotSecret() {
|
||||||
|
return fs.existsSync(totpSecretPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTotpSecret() {
|
||||||
|
console.log("Attempting to remove secret")
|
||||||
|
fs.unlink( totpSecretPath, a => {
|
||||||
|
console.log("Unable to remove totp secret")
|
||||||
|
console.log(a)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export = {saveTotpSecret, getTotpSecret, checkForTotSecret, removeTotpSecret};
|
Loading…
x
Reference in New Issue
Block a user