mirror of https://github.com/TriliumNext/Notes
Ported from branch OIDC
parent
1c8cc36402
commit
9c748f326a
@ -0,0 +1,9 @@
|
||||
OAUTH_ENABLED="false"
|
||||
BASE_URL="http://localhost:8080"
|
||||
CLIENT_ID="1234"
|
||||
ISSUER_BASE_URL="https://example.com/xyz/.well-known/openid-configuration"
|
||||
SECRET="I-Like-Trilium-Notes"
|
||||
AUTH_0_LOGOUT="false"
|
||||
|
||||
TOTP_ENABLED="false"
|
||||
TOTP_SECRET="Trilium-Notes-is-the-best"
|
||||
@ -0,0 +1,9 @@
|
||||
class OpenIDError {
|
||||
message: string;
|
||||
|
||||
constructor(message: string) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenIDError;
|
||||
@ -0,0 +1,297 @@
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsWidget from "./options_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h2 class=""><b>What is Multi-Factor Authentication?</b></h2>
|
||||
<div class="">
|
||||
<i>
|
||||
Multi-Factor Authentication (MFA) adds an extra layer of security to your account. Instead
|
||||
of just entering a password to log in, MFA requires you to provide one or more additional
|
||||
pieces of evidence to verify your identity. This way, even if someone gets hold of your
|
||||
password, they still can't access your account without the second piece of information.
|
||||
It's like adding an extra lock to your door, making it much harder for anyone else to
|
||||
break in.</i>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<h3><b>OAuth/OpenID</b></h3>
|
||||
<span><i>OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity.</i></span>
|
||||
<div>
|
||||
<label>
|
||||
<b>Enable OAuth/OpenID</b>
|
||||
</label>
|
||||
<input type="checkbox" class="oauth-enabled-checkbox" disabled="true" />
|
||||
<span class="env-oauth-enabled" "alert alert-warning" role="alert" style="font-weight: bold; color: red !important;" > </span>
|
||||
</div>
|
||||
<div>
|
||||
<span> <b>Token status: </b></span><span class="token-status"> Needs login! </span><span><b> User status: </b></span><span class="user-status"> No user saved!</span>
|
||||
<br>
|
||||
<button class="oauth-login-button" onclick="location.href='/authenticate'" > Login to configured OAuth/OpenID service </button>
|
||||
<button class="save-user-button" > Save User </button>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<h3><b>Time-based One-Time Password</b></h3>
|
||||
<div>
|
||||
<label>
|
||||
<b>Enable TOTP</b>
|
||||
</label>
|
||||
<input type="checkbox" class="totp-enabled" />
|
||||
<span class="env-totp-enabled" "alert alert-warning" role="alert" style="font-weight: bold; color: red !important;" > </span>
|
||||
</div>
|
||||
<div>
|
||||
<span><i>TOTP (Time-Based One-Time Password) is a security feature that generates a unique, temporary
|
||||
code which changes every 30 seconds. You use this code, along with your password to log into your
|
||||
account, making it much harder for anyone else to access it.</i></span>
|
||||
</div>
|
||||
<br>
|
||||
<h4> Generate TOTP Secret </h4>
|
||||
<div>
|
||||
<span class="totp-secret" > TOTP Secret Key </span>
|
||||
<br>
|
||||
<button class="regenerate-totp" disabled="true"> Regenerate TOTP Secret </button>
|
||||
</div>
|
||||
<br>
|
||||
<h4> Single Sign-on Recovery Keys </h4>
|
||||
<div>
|
||||
<span ><i>Single sign-on recovery keys are used to login in the event you cannot access your Authenticator codes. Keep them somewhere safe and secure. </i></span>
|
||||
<br><br>
|
||||
<span class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">After a recovery key is used it cannot be used again.</span>
|
||||
<br><br>
|
||||
<table style="border: 0px solid white">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="key_0">Recover Key 1</td>
|
||||
<td style="width: 20px" />
|
||||
<td class="key_1">Recover Key 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key_2">Recover Key 3</td>
|
||||
<td />
|
||||
<td class="key_3">Recover Key 4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key_4">Recover Key 5</td>
|
||||
<td />
|
||||
<td class="key_5">Recover Key 6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key_6">Recover Key 7</td>
|
||||
<td />
|
||||
<td class="key_7">Recover Key 8</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<button class="generate-recovery-code" disabled="true"> Generate Recovery Keys </button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class MultiFactorAuthenticationOptions extends OptionsWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$regenerateTotpButton = this.$widget.find(".regenerate-totp");
|
||||
this.$totpDetails = this.$widget.find(".totp-details");
|
||||
this.$totpEnabled = this.$widget.find(".totp-enabled");
|
||||
this.$totpSecret = this.$widget.find(".totp-secret");
|
||||
this.$totpSecretInput = this.$widget.find(".totp-secret-input");
|
||||
this.$authenticatorCode = this.$widget.find(".authenticator-code");
|
||||
this.$generateRecoveryCodeButton = this.$widget.find(
|
||||
".generate-recovery-code"
|
||||
);
|
||||
this.$oAuthEnabledCheckbox = this.$widget.find(".oauth-enabled-checkbox");
|
||||
this.$saveUserButton = this.$widget.find(".save-user-button");
|
||||
this.$oauthLoginButton = this.$widget.find(".oauth-login-button");
|
||||
this.$tokenStatus = this.$widget.find(".token-status");
|
||||
this.$userStatus = this.$widget.find(".user-status");
|
||||
this.$envEnabledTOTP = this.$widget.find(".env-totp-enabled");
|
||||
this.$envEnabledOAuth = this.$widget.find(".env-oauth-enabled");
|
||||
|
||||
|
||||
this.$recoveryKeys = [];
|
||||
|
||||
for (let i = 0; i < 8; i++)
|
||||
this.$recoveryKeys.push(this.$widget.find(".key_" + i));
|
||||
|
||||
this.$totpEnabled.on("change", async () => {
|
||||
this.updateSecret();
|
||||
});
|
||||
|
||||
this.$oAuthEnabledCheckbox.on("change", async () => {
|
||||
this.updateOAuthStatus();
|
||||
});
|
||||
|
||||
this.$generateRecoveryCodeButton.on("click", async () => {
|
||||
this.setRecoveryKeys();
|
||||
});
|
||||
|
||||
this.$regenerateTotpButton.on("click", async () => {
|
||||
this.generateKey();
|
||||
});
|
||||
|
||||
this.$saveUserButton.on("click", (async) => {
|
||||
server
|
||||
.get("oauth/authenticate")
|
||||
.then((result) => {
|
||||
console.log(result.message);
|
||||
toastService.showMessage(result.message);
|
||||
})
|
||||
.catch((result) => {
|
||||
console.error(result.message);
|
||||
toastService.showError(result.message);
|
||||
});
|
||||
});
|
||||
|
||||
this.$protectedSessionTimeout = this.$widget.find(
|
||||
".protected-session-timeout-in-seconds"
|
||||
);
|
||||
this.$protectedSessionTimeout.on("change", () =>
|
||||
this.updateOption(
|
||||
"protectedSessionTimeout",
|
||||
this.$protectedSessionTimeout.val()
|
||||
)
|
||||
);
|
||||
|
||||
this.displayRecoveryKeys();
|
||||
}
|
||||
|
||||
async updateSecret() {
|
||||
if (this.$totpEnabled.prop("checked")) {
|
||||
server.post("totp/enable");
|
||||
|
||||
}
|
||||
else {
|
||||
server.post("totp/disable");
|
||||
}
|
||||
}
|
||||
|
||||
async updateOAuthStatus() {
|
||||
if (this.$oAuthEnabledCheckbox.prop("checked")){
|
||||
server.post("oauth/enable");
|
||||
}
|
||||
else{
|
||||
server.post("oauth/disable");
|
||||
}
|
||||
}
|
||||
|
||||
async setRecoveryKeys() {
|
||||
server.get("totp_recovery/generate").then((result) => {
|
||||
if (!result.success) {
|
||||
toastService.showError("Error in revevery code generation!");
|
||||
return;
|
||||
}
|
||||
this.keyFiller(result.recoveryCodes);
|
||||
server.post("totp_recovery/set", {
|
||||
recoveryCodes: result.recoveryCodes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async keyFiller(values) {
|
||||
// Forces values to be a string so it doesn't error out when I split.
|
||||
// Will be a non-issue when I update everything to typescript.
|
||||
const keys = (values + "").split(",");
|
||||
for (let i = 0; i < keys.length; i++) this.$recoveryKeys[i].text(keys[i]);
|
||||
}
|
||||
|
||||
async generateKey() {
|
||||
server.get("totp/generate").then((result) => {
|
||||
if (result.success) {
|
||||
this.$totpSecret.text(result.message);
|
||||
} else {
|
||||
toastService.showError(result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
// TODO: Rework the logic since I've changed how OAuth works
|
||||
|
||||
// server.get("oauth/status").then((result) => {
|
||||
// if (result.enabled) {
|
||||
// if (result.success)
|
||||
// this.$oAuthEnabledCheckbox.prop("checked", result.message);
|
||||
|
||||
// this.$oauthLoginButton.prop("disabled", !result.message);
|
||||
// this.$saveUserButton.prop("disabled", !result.message);
|
||||
|
||||
// if (result.message) {
|
||||
// this.$oauthLoginButton.prop("disabled", false);
|
||||
// this.$saveUserButton.prop("disabled", false);
|
||||
// server.get("oauth/validate").then((result) => {
|
||||
// if (result.success) {
|
||||
// this.$tokenStatus.text("Logged in!");
|
||||
|
||||
// if (result.user) {
|
||||
// this.$userStatus.text("User saved!");
|
||||
// } else {
|
||||
// this.$saveUserButton.prop("disabled", false);
|
||||
// this.$userStatus.text("User not saved");
|
||||
// }
|
||||
// } else this.$tokenStatus.text("Not logged in!");
|
||||
// });
|
||||
// }
|
||||
// } else {
|
||||
// this.$oAuthEnabledCheckbox.prop("checked", false);
|
||||
// this.$oauthLoginButton.prop("disabled", true);
|
||||
// this.$saveUserButton.prop("disabled", true);
|
||||
// this.$oAuthEnabledCheckbox.prop("disabled", true);
|
||||
|
||||
// this.$envEnabledOAuth.text(
|
||||
// "OAuth can only be enabled with environment variables. REQUIRES RESTART"
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
|
||||
server.get("totp/status").then((result) => {
|
||||
if (result.enabled)
|
||||
if (result.success) {
|
||||
this.$totpEnabled.prop("checked", result.message);
|
||||
this.$totpSecretInput.prop("disabled", !result.message);
|
||||
this.$totpSecret.prop("disapbled", !result.message);
|
||||
this.$regenerateTotpButton.prop("disabled", !result.message);
|
||||
this.$authenticatorCode.prop("disabled", !result.message);
|
||||
this.$generateRecoveryCodeButton.prop("disabled", !result.message);
|
||||
} else {
|
||||
toastService.showError(result.message);
|
||||
}
|
||||
else {
|
||||
this.$totpEnabled.prop("checked", false);
|
||||
this.$totpEnabled.prop("disabled", true);
|
||||
this.$totpSecretInput.prop("disabled", true);
|
||||
this.$totpSecret.prop("disapbled", true);
|
||||
this.$regenerateTotpButton.prop("disabled", true);
|
||||
this.$authenticatorCode.prop("disabled", true);
|
||||
this.$generateRecoveryCodeButton.prop("disabled", true);
|
||||
|
||||
this.$envEnabledTOTP.text(
|
||||
"TOTP_ENABLED is not set in environment variable. Requires restart."
|
||||
);
|
||||
}
|
||||
});
|
||||
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
|
||||
}
|
||||
|
||||
displayRecoveryKeys() {
|
||||
server.get("totp_recovery/enabled").then((result) => {
|
||||
if (!result.success) {
|
||||
this.keyFiller(Array(8).fill("Error generating recovery keys!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.keysExist) {
|
||||
this.keyFiller(Array(8).fill("No key set"));
|
||||
this.$generateRecoveryCodeButton.text("Generate Recovery Codes");
|
||||
return;
|
||||
}
|
||||
});
|
||||
server.get("totp_recovery/used").then((result) => {
|
||||
this.keyFiller((result.usedRecoveryCodes + "").split(","));
|
||||
this.$generateRecoveryCodeButton.text("Regenerate Recovery Codes");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import recovery_codes from'../../services/encryption/recovery_codes.js';
|
||||
import {Request} from 'express';
|
||||
import {randomBytes} from 'crypto';
|
||||
|
||||
function setRecoveryCodes(req: Request) {
|
||||
const success = recovery_codes.setRecoveryCodes(req.body.recoveryCodes);
|
||||
return {success: success, message: 'Recovery codes set!'};
|
||||
}
|
||||
|
||||
function veryifyRecoveryCode(req: Request) {
|
||||
const success = recovery_codes.verifyRecoveryCode(req.body.recovery_code_guess);
|
||||
|
||||
return {success: success};
|
||||
}
|
||||
|
||||
function checkForRecoveryKeys() {
|
||||
return {success
|
||||
: true, keysExist: recovery_codes.isRecoveryCodeSet()};
|
||||
}
|
||||
|
||||
function generateRecoveryCodes() {
|
||||
const recoveryKeys = [
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64'),
|
||||
randomBytes(16).toString('base64')
|
||||
];
|
||||
|
||||
recovery_codes.setRecoveryCodes(recoveryKeys.toString());
|
||||
|
||||
return {success: true, recoveryCodes: recoveryKeys.toString()};
|
||||
}
|
||||
|
||||
function getUsedRecoveryCodes() {
|
||||
return {
|
||||
success: true,
|
||||
usedRecoveryCodes: recovery_codes.getUsedRecoveryCodes().toString()
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
setRecoveryCodes,
|
||||
generateRecoveryCodes,
|
||||
veryifyRecoveryCode,
|
||||
checkForRecoveryKeys,
|
||||
getUsedRecoveryCodes
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import options from '../../services/options.js';
|
||||
import {generateSecret} from 'time2fa';
|
||||
|
||||
function generateTOTPSecret() {
|
||||
return {success: 'true', message: generateSecret()};
|
||||
}
|
||||
|
||||
function getTotpEnabled() {
|
||||
if (process.env.TOTP_ENABLED === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (process.env.TOTP_ENABLED.toLocaleLowerCase() !== 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTOTPStatus() {
|
||||
const totpEnabled = options.getOptionBool('totpEnabled');
|
||||
return {success: 'true', message: totpEnabled, enabled: getTotpEnabled()};
|
||||
}
|
||||
|
||||
function enableTOTP() {
|
||||
if (!getTotpEnabled()) {
|
||||
return {success: 'false'};
|
||||
}
|
||||
|
||||
options.setOption('totpEnabled', true);
|
||||
options.setOption('oAuthEnabled', false);
|
||||
return {success: 'true'};
|
||||
}
|
||||
|
||||
function disableTOTP() {
|
||||
options.setOption('totpEnabled', false);
|
||||
return {success: true};
|
||||
}
|
||||
|
||||
function getSecret() {
|
||||
return process.env.TOTP_SECRET;
|
||||
}
|
||||
|
||||
export default {
|
||||
generateSecret: generateTOTPSecret,
|
||||
getTOTPStatus,
|
||||
enableTOTP,
|
||||
disableTOTP,
|
||||
getSecret
|
||||
};
|
||||
@ -0,0 +1,163 @@
|
||||
import optionService from "../options.js";
|
||||
import myScryptService from "./my_scrypt.js";
|
||||
import utils from "../utils.js";
|
||||
import dataEncryptionService from "./data_encryption.js";
|
||||
import sql from "../sql.js";
|
||||
import sqlInit from "../sql_init.js";
|
||||
|
||||
function saveSubjectIdentifier(subjectIdentifier: string) {
|
||||
if (isUserSaved()) return false;
|
||||
|
||||
// Allows setup with existing instances of trilium
|
||||
sql.execute(`
|
||||
CREATE TABLE IF NOT EXISTS "user_data"
|
||||
(
|
||||
tmpID INT,
|
||||
userIDEcnryptedDataKey TEXT,
|
||||
userIDVerificationHash TEXT,
|
||||
salt TEXT,
|
||||
derivedKey TEXT,
|
||||
isSetup TEXT DEFAULT "false",
|
||||
UNIQUE (tmpID),
|
||||
PRIMARY KEY (tmpID)
|
||||
);`);
|
||||
|
||||
const verificationSalt = utils.randomSecureToken(32);
|
||||
const derivedKeySalt = utils.randomSecureToken(32);
|
||||
|
||||
const verificationHash = myScryptService.getSubjectIdentifierVerificationHash(
|
||||
subjectIdentifier,
|
||||
verificationSalt
|
||||
);
|
||||
if (verificationHash === undefined) {
|
||||
console.log("Verification hash undefined!");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const userIDEncryptedDataKey = setDataKey(
|
||||
subjectIdentifier,
|
||||
utils.randomSecureToken(16),
|
||||
verificationSalt
|
||||
);
|
||||
|
||||
if (userIDEncryptedDataKey === undefined || userIDEncryptedDataKey === null) {
|
||||
console.log("USERID ENCRYPTED DATA KEY NULL");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = {
|
||||
tmpID: 0,
|
||||
userIDVerificationHash: utils.toBase64(verificationHash),
|
||||
salt: verificationSalt,
|
||||
derivedKey: derivedKeySalt,
|
||||
userIDEcnryptedDataKey: userIDEncryptedDataKey,
|
||||
isSetup: "true",
|
||||
};
|
||||
|
||||
console.log("Saved data: " + data);
|
||||
sql.upsert("user_data", "tmpID", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
function isSubjectIdentifierSaved() {
|
||||
const value = sql.getValue("SELECT userIDEcnryptedDataKey FROM user_data;");
|
||||
if (value === undefined || value === null || value === "") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function isUserSaved() {
|
||||
const isSaved = sql.getValue<string>("SELECT isSetup FROM user_data;");
|
||||
return isSaved === "true" ? true : false;
|
||||
}
|
||||
|
||||
function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
console.log("Database not initialized!");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!isUserSaved()) {
|
||||
console.log("DATABASE NOT SETUP");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const salt = sql.getValue("SELECT salt FROM user_data;");
|
||||
if (salt == undefined) {
|
||||
console.log("Salt undefined");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const givenHash = myScryptService
|
||||
.getSubjectIdentifierVerificationHash(subjectIdentifier)
|
||||
?.toString("base64");
|
||||
if (givenHash === undefined) {
|
||||
console.log("Sub id hash undefined!");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const savedHash = sql.getValue(
|
||||
"SELECT userIDVerificationHash FROM user_data"
|
||||
);
|
||||
if (savedHash === undefined) {
|
||||
console.log("verification hash undefined");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log("Matches: " + givenHash === savedHash);
|
||||
return givenHash === savedHash;
|
||||
}
|
||||
|
||||
function setDataKey(
|
||||
subjectIdentifier: string,
|
||||
plainTextDataKey: string | Buffer,
|
||||
salt: string
|
||||
) {
|
||||
console.log("Subject Identifier: " + subjectIdentifier);
|
||||
const subjectIdentifierDerivedKey =
|
||||
myScryptService.getSubjectIdentifierDerivedKey(subjectIdentifier, salt);
|
||||
|
||||
if (subjectIdentifierDerivedKey === undefined) {
|
||||
console.log("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY");
|
||||
return undefined;
|
||||
}
|
||||
const newEncryptedDataKey = dataEncryptionService.encrypt(
|
||||
subjectIdentifierDerivedKey,
|
||||
plainTextDataKey
|
||||
);
|
||||
|
||||
return newEncryptedDataKey;
|
||||
}
|
||||
|
||||
function getDataKey(subjectIdentifier: string) {
|
||||
console.log("Subject Identifier: " + subjectIdentifier);
|
||||
const subjectIdentifierDerivedKey =
|
||||
myScryptService.getSubjectIdentifierDerivedKey(subjectIdentifier);
|
||||
|
||||
const encryptedDataKey = sql.getValue(
|
||||
"SELECT userIDEcnryptedDataKey FROM user_data"
|
||||
);
|
||||
|
||||
if (encryptedDataKey === undefined || encryptedDataKey === null) {
|
||||
console.log("Encrypted data key empty!");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (subjectIdentifierDerivedKey === undefined) {
|
||||
console.log("SOMETHING WENT WRONG SAVING USER ID DERIVED KEY");
|
||||
return undefined;
|
||||
}
|
||||
const decryptedDataKey = dataEncryptionService.decrypt(
|
||||
subjectIdentifierDerivedKey,
|
||||
encryptedDataKey.toString()
|
||||
);
|
||||
|
||||
return decryptedDataKey;
|
||||
}
|
||||
|
||||
export default {
|
||||
verifyOpenIDSubjectIdentifier,
|
||||
getDataKey,
|
||||
setDataKey,
|
||||
saveSubjectIdentifier,
|
||||
isSubjectIdentifierSaved,
|
||||
};
|
||||
@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
import sql from '../sql.js';
|
||||
import optionService from '../options.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
function isRecoveryCodeSet() {
|
||||
return optionService.getOptionBool('encryptedRecoveryCodes');
|
||||
}
|
||||
|
||||
function setRecoveryCodes(recoveryCodes: string) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const securityKey = crypto.randomBytes(32);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', securityKey, iv);
|
||||
let encryptedRecoveryCodes = cipher.update(recoveryCodes, 'utf-8', 'hex');
|
||||
|
||||
sql.transactional(() => {
|
||||
optionService.setOption('recoveryCodeInitialVector', iv.toString('hex'));
|
||||
optionService.setOption('recoveryCodeSecurityKey', securityKey.toString('hex'));
|
||||
optionService.setOption('recoveryCodesEncrypted', encryptedRecoveryCodes + cipher.final('hex'));
|
||||
optionService.setOption('encryptedRecoveryCodes', 'true');
|
||||
return true;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
function getRecoveryCodes() {
|
||||
if (!isRecoveryCodeSet()) {
|
||||
return Array(8).fill("Keys not set")
|
||||
}
|
||||
|
||||
return sql.transactional(() => {
|
||||
const iv = Buffer.from(optionService.getOption('recoveryCodeInitialVector'), 'hex');
|
||||
const securityKey = Buffer.from(optionService.getOption('recoveryCodeSecurityKey'), 'hex');
|
||||
const encryptedRecoveryCodes = optionService.getOption('recoveryCodesEncrypted');
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', securityKey, iv);
|
||||
const decryptedData = decipher.update(encryptedRecoveryCodes, 'hex', 'utf-8');
|
||||
|
||||
const decryptedString = decryptedData + decipher.final('utf-8');
|
||||
return decryptedString.split(',');
|
||||
});
|
||||
}
|
||||
|
||||
function removeRecoveryCode(usedCode: string) {
|
||||
const oldCodes: string[] = getRecoveryCodes();
|
||||
const today = new Date();
|
||||
oldCodes[oldCodes.indexOf(usedCode)] = today.toJSON().replace(/-/g, '/');
|
||||
setRecoveryCodes(oldCodes.toString());
|
||||
}
|
||||
|
||||
function verifyRecoveryCode(recoveryCodeGuess: string) {
|
||||
const recoveryCodeRegex = RegExp(/^.{22}==$/gm);
|
||||
if (!recoveryCodeRegex.test(recoveryCodeGuess)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const recoveryCodes = getRecoveryCodes();
|
||||
var loginSuccess = false;
|
||||
recoveryCodes.forEach((recoveryCode: string) => {
|
||||
if (recoveryCodeGuess === recoveryCode) {
|
||||
removeRecoveryCode(recoveryCode);
|
||||
loginSuccess = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
return loginSuccess;
|
||||
}
|
||||
|
||||
function getUsedRecoveryCodes() {
|
||||
if (!isRecoveryCodeSet()){
|
||||
return Array(8).fill("Recovery code not set")
|
||||
}
|
||||
|
||||
const dateRegex = RegExp(/^\d{4}\/\d{2}\/\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/gm);
|
||||
const recoveryCodes = getRecoveryCodes();
|
||||
const usedStatus: string[] = [];
|
||||
|
||||
recoveryCodes.forEach((recoveryKey: string) => {
|
||||
if (dateRegex.test(recoveryKey)) usedStatus.push('Used: ' + recoveryKey);
|
||||
else usedStatus.push('Recovery code ' + recoveryCodes.indexOf(recoveryKey) + ' is unused');
|
||||
});
|
||||
return usedStatus;
|
||||
}
|
||||
|
||||
export default {
|
||||
setRecoveryCodes,
|
||||
verifyRecoveryCode,
|
||||
getUsedRecoveryCodes,
|
||||
isRecoveryCodeSet
|
||||
};
|
||||
@ -0,0 +1,136 @@
|
||||
import OpenIDError from "../errors/open_id_error.js";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import openIDEncryption from "./encryption/open_id_encryption.js";
|
||||
import sqlInit from "./sql_init.js";
|
||||
import options from "./options.js";
|
||||
import { Session, auth } from "express-openid-connect";
|
||||
import sql from "./sql.js";
|
||||
|
||||
function isOpenIDEnabled() {
|
||||
return checkOpenIDRequirements();
|
||||
}
|
||||
|
||||
function isUserSaved() {
|
||||
const dbf = sql.getValue<string>("SELECT isSetup FROM user_data;");
|
||||
return dbf === "true" ? true : false;
|
||||
}
|
||||
|
||||
function checkOpenIDRequirements() {
|
||||
if (process.env.OAUTH_ENABLED === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (process.env.OAUTH_ENABLED.toLocaleLowerCase() !== "true") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (process.env.BASE_URL === undefined) {
|
||||
throw new OpenIDError("BASE_URL is undefined in .env!");
|
||||
}
|
||||
if (process.env.CLIENT_ID === undefined) {
|
||||
throw new OpenIDError("CLIENT_ID is undefined in .env!");
|
||||
}
|
||||
if (process.env.ISSUER_BASE_URL === undefined) {
|
||||
throw new OpenIDError("ISSUER_BASE_URL is undefined in .env!");
|
||||
}
|
||||
if (process.env.SECRET === undefined) {
|
||||
throw new OpenIDError("SECRET is undefined in .env!");
|
||||
}
|
||||
if (process.env.AUTH_0_LOGOUT === undefined) {
|
||||
throw new OpenIDError("AUTH_0_LOGOUT is undefined in .env!");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getOAuthStatus() {
|
||||
return {
|
||||
success: true,
|
||||
message: checkOpenIDRequirements(),
|
||||
};
|
||||
}
|
||||
|
||||
function isTokenValid(req: Request, res: Response, next: NextFunction) {
|
||||
const userStatus = openIDEncryption.isSubjectIdentifierSaved();
|
||||
|
||||
if (req.oidc !== undefined) {
|
||||
const result = req.oidc
|
||||
.fetchUserInfo()
|
||||
.then((result) => {
|
||||
return {
|
||||
success: true,
|
||||
message: "Token is valid",
|
||||
user: userStatus,
|
||||
};
|
||||
})
|
||||
.catch((result) => {
|
||||
return {
|
||||
success: false,
|
||||
message: "Token is not valid",
|
||||
user: userStatus,
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: "Token not set up",
|
||||
user: userStatus,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function checkAuth0Logout() {
|
||||
if (process.env.AUTH_0_LOGOUT === undefined) return false;
|
||||
if (process.env.AUTH_0_LOGOUT.toLocaleLowerCase() === "true") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function generateOAuthConfig() {
|
||||
const authRoutes = {
|
||||
callback: "/callback",
|
||||
login: "/authenticate",
|
||||
postLogoutRedirect: "/login",
|
||||
logout: "/logout",
|
||||
};
|
||||
|
||||
const logoutParams = {
|
||||
// end_session_endpoint: "/end-session/",
|
||||
};
|
||||
|
||||
const authConfig = {
|
||||
authRequired: true,
|
||||
auth0Logout: checkAuth0Logout(),
|
||||
baseURL: process.env.BASE_URL,
|
||||
clientID: process.env.CLIENT_ID,
|
||||
issuerBaseURL: process.env.ISSUER_BASE_URL,
|
||||
secret: process.env.SECRET,
|
||||
clientSecret: process.env.SECRET,
|
||||
authorizationParams: {
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
},
|
||||
routes: authRoutes,
|
||||
idpLogout: false,
|
||||
logoutParams: logoutParams,
|
||||
afterCallback: async (req: Request, res: Response, session: Session) => {
|
||||
if (!sqlInit.isDbInitialized()) return session;
|
||||
|
||||
if (isUserSaved()) return session;
|
||||
|
||||
if (req.oidc.user === undefined) console.log("user invalid!");
|
||||
else openIDEncryption.saveSubjectIdentifier(req.oidc.user.sub.toString());
|
||||
|
||||
return session;
|
||||
},
|
||||
};
|
||||
return authConfig;
|
||||
}
|
||||
|
||||
export default {
|
||||
generateOAuthConfig,
|
||||
getOAuthStatus,
|
||||
isOpenIDEnabled,
|
||||
checkOpenIDRequirements,
|
||||
isTokenValid,
|
||||
isUserSaved,
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
import {Totp} from 'time2fa';
|
||||
|
||||
function getTotpSecret() {
|
||||
return process.env.TOTP_SECRET;
|
||||
}
|
||||
|
||||
function checkForTotSecret() {
|
||||
if (process.env.TOTP_SECRET !== undefined) return true;
|
||||
else return false;
|
||||
}
|
||||
|
||||
function validateTOTP(guessedPasscode: string) {
|
||||
if (process.env.TOTP_SECRET === undefined) return false;
|
||||
|
||||
try {
|
||||
const valid = Totp.validate({
|
||||
passcode: guessedPasscode,
|
||||
secret: process.env.TOTP_SECRET.trim()
|
||||
});
|
||||
return valid;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getTotpSecret,
|
||||
checkForTotSecret,
|
||||
validateTOTP
|
||||
};
|
||||
Loading…
Reference in New Issue