From 6a0e0cc6ba90dc9ed7701fd2e576313137e31ac5 Mon Sep 17 00:00:00 2001 From: Paul Ritchey Date: Wed, 29 Nov 2023 08:17:07 -0500 Subject: [PATCH 1/3] Added improved password complexity checking capability. --- server/.env.example | 11 +++++++++++ server/models/user.js | 42 ++++++++++++++++++++++++++++++++++++++++++ server/package.json | 2 ++ 3 files changed, 55 insertions(+) diff --git a/server/.env.example b/server/.env.example index 03d1eb9bcef..2a775ceffe5 100644 --- a/server/.env.example +++ b/server/.env.example @@ -77,3 +77,14 @@ VECTOR_DB="lancedb" # AUTH_TOKEN="hunter2" # This is the password to your application if remote hosting. # STORAGE_DIR= # absolute filesystem path with no trailing slash # NO_DEBUG="true" + +########################################### +######## PASSWORD COMPLEXITY ############## +########################################### +#PASSWORDMINCHAR=16 +#PASSWORDMAXCHAR=250 +#PASSWORDLOWERCASE=1 +#PASSWORDUPPERCASE=1 +#PASSWORDNUMERIC=1 +#PASSWORDSYMBOL=1 +#PASSWORDREQUIREMENTS=4 diff --git a/server/models/user.js b/server/models/user.js index 782a2888768..1edcd66fdfb 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,8 +1,44 @@ const prisma = require("../utils/prisma"); const bcrypt = require("bcrypt"); +const Joi = require("joi"); +const passwordComplexity = require("joi-password-complexity"); + +function checkPasswordComplexity(password) { + // Set defaults for password complexity or use user defined settings. + const complexityOptions = { + min: process.env.PASSWORDMINCHAR || 8, + max: process.env.PASSWORDMAXCHAR || 250, + lowerCase: process.env.PASSWORDLOWERCASE || 1, + upperCase: process.env.PASSWORDUPPERCASE || 0, + numeric: process.env.PASSWORDNUMERIC || 0, + symbol: process.env.PASSWORDSYMBOL || 0, + requirementCount: process.env.PASSWORDREQUIREMENTS || 1, + }; + + let complexityCheck = passwordComplexity(complexityOptions, 'password').validate(password); + + // Check if password passed complexity check, if it did not + // gather all of the missed checks into one error string. + if (complexityCheck.hasOwnProperty('error')) { + let myError = ""; + let prepend = ""; + for (let i = 0; i < complexityCheck.error.details.length; i++) { + myError += prepend + complexityCheck.error.details[i].message; + prepend = ", "; + } + return { 'checkedOK': false, 'error': myError }; + } + return { 'checkedOK': true, 'error': 'No error.' }; +} const User = { create: async function ({ username, password, role = "default" }) { + // Ensure password meets complexity requirements. + passwordCheck = checkPasswordComplexity(password); + if (!passwordCheck.checkedOK) { + return { success: false, error: passwordCheck.error }; + } + try { const hashedPassword = bcrypt.hashSync(password, 10); const user = await prisma.users.create({ @@ -20,6 +56,12 @@ const User = { }, update: async function (userId, updates = {}) { + // Ensure password meets complexity requirements. + passwordCheck = checkPasswordComplexity(updates["password"]); + if (!passwordCheck.checkedOK) { + return { success: false, error: passwordCheck.error }; + } + try { // Rehash new password if it exists as update // will be given to us as plaintext. diff --git a/server/package.json b/server/package.json index 6bdb90aa772..0c8962bdb2b 100644 --- a/server/package.json +++ b/server/package.json @@ -36,6 +36,8 @@ "express": "^4.18.2", "extract-zip": "^2.0.1", "graphql": "^16.7.1", + "joi": "^17.11.0", + "joi-password-complexity": "^5.2.0", "js-tiktoken": "^1.0.7", "jsonwebtoken": "^8.5.1", "langchain": "^0.0.90", From 3338c5d6fdd158a692a6db4cb21a10872a5dc234 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 5 Dec 2023 08:59:41 -0800 Subject: [PATCH 2/3] Move password complexity checker as User.util dynamically import required libraries depending on code execution flow lint --- .../src/components/Modals/NewWorkspace.jsx | 10 +-- frontend/src/components/UserMenu/index.jsx | 24 +++--- frontend/src/index.css | 2 +- .../pages/Admin/Users/NewUserModal/index.jsx | 1 - .../Users/UserRow/EditUserModal/index.jsx | 1 - server/models/user.js | 86 +++++++++---------- server/yarn.lock | 45 ++++++++++ 7 files changed, 104 insertions(+), 65 deletions(-) diff --git a/frontend/src/components/Modals/NewWorkspace.jsx b/frontend/src/components/Modals/NewWorkspace.jsx index 5b8ecb8fc33..fb485040451 100644 --- a/frontend/src/components/Modals/NewWorkspace.jsx +++ b/frontend/src/components/Modals/NewWorkspace.jsx @@ -14,7 +14,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) { const form = new FormData(formEl.current); for (var [key, value] of form.entries()) data[key] = value; const { workspace, message } = await Workspace.new(data); - if (!!workspace){ + if (!!workspace) { window.location.href = paths.workspace.chat(workspace.slug); } setError(message); @@ -29,9 +29,7 @@ export default function NewWorkspaceModal({ hideModal = noop }) {
-

- New Workspace -

+

New Workspace

{error && ( -

- Error: {error} -

+

Error: {error}

)}
diff --git a/frontend/src/components/UserMenu/index.jsx b/frontend/src/components/UserMenu/index.jsx index deb303fbd54..6a549f82029 100644 --- a/frontend/src/components/UserMenu/index.jsx +++ b/frontend/src/components/UserMenu/index.jsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { isMobile } from 'react-device-detect'; -import paths from '../../utils/paths'; -import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from '../../utils/constants'; -import { Person, SignOut } from '@phosphor-icons/react'; -import { userFromStorage } from '../../utils/request'; +import React, { useState, useEffect, useRef } from "react"; +import { isMobile } from "react-device-detect"; +import paths from "../../utils/paths"; +import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "../../utils/constants"; +import { Person, SignOut } from "@phosphor-icons/react"; +import { userFromStorage } from "../../utils/request"; export default function UserMenu({ children }) { if (isMobile) return <>{children}; @@ -20,14 +20,14 @@ function useLoginMode() { const user = !!window.localStorage.getItem(AUTH_USER); const token = !!window.localStorage.getItem(AUTH_TOKEN); - if (user && token) return 'multi'; - if (!user && token) return 'single'; + if (user && token) return "multi"; + if (!user && token) return "single"; return null; } function userDisplay() { const user = userFromStorage(); - return user?.username?.slice(0, 2) || 'AA'; + return user?.username?.slice(0, 2) || "AA"; } function UserButton() { @@ -47,9 +47,9 @@ function UserButton() { useEffect(() => { if (showMenu) { - document.addEventListener('mousedown', handleClose); + document.addEventListener("mousedown", handleClose); } - return () => document.removeEventListener('mousedown', handleClose); + return () => document.removeEventListener("mousedown", handleClose); }, [showMenu]); if (mode === null) return null; @@ -62,7 +62,7 @@ function UserButton() { type="button" className="uppercase transition-all duration-300 w-[35px] h-[35px] text-base font-semibold rounded-full flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient justify-center text-white p-2 hover:border-slate-100 hover:border-opacity-50 border-transparent border" > - {mode === 'multi' ? userDisplay() : } + {mode === "multi" ? userDisplay() : } {showMenu && ( diff --git a/frontend/src/index.css b/frontend/src/index.css index 22affd2026e..937631beba2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -378,4 +378,4 @@ dialog::backdrop { 100% { opacity: 0; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index 9f2b42aeb8e..8282070eb6b 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -77,7 +77,6 @@ export default function NewUserModal() { className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder="User's initial password" required={true} - minLength={8} autoComplete="off" /> diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index c3b6a939d66..6b25f42ac69 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -77,7 +77,6 @@ export default function EditUserModal({ currentUser, user }) { type="text" className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder={`${user.username}'s new password`} - minLength={8} autoComplete="off" /> diff --git a/server/models/user.js b/server/models/user.js index 1edcd66fdfb..269219fc897 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,45 +1,14 @@ const prisma = require("../utils/prisma"); -const bcrypt = require("bcrypt"); -const Joi = require("joi"); -const passwordComplexity = require("joi-password-complexity"); - -function checkPasswordComplexity(password) { - // Set defaults for password complexity or use user defined settings. - const complexityOptions = { - min: process.env.PASSWORDMINCHAR || 8, - max: process.env.PASSWORDMAXCHAR || 250, - lowerCase: process.env.PASSWORDLOWERCASE || 1, - upperCase: process.env.PASSWORDUPPERCASE || 0, - numeric: process.env.PASSWORDNUMERIC || 0, - symbol: process.env.PASSWORDSYMBOL || 0, - requirementCount: process.env.PASSWORDREQUIREMENTS || 1, - }; - - let complexityCheck = passwordComplexity(complexityOptions, 'password').validate(password); - - // Check if password passed complexity check, if it did not - // gather all of the missed checks into one error string. - if (complexityCheck.hasOwnProperty('error')) { - let myError = ""; - let prepend = ""; - for (let i = 0; i < complexityCheck.error.details.length; i++) { - myError += prepend + complexityCheck.error.details[i].message; - prepend = ", "; - } - return { 'checkedOK': false, 'error': myError }; - } - return { 'checkedOK': true, 'error': 'No error.' }; -} const User = { create: async function ({ username, password, role = "default" }) { - // Ensure password meets complexity requirements. - passwordCheck = checkPasswordComplexity(password); + const passwordCheck = this.checkPasswordComplexity(password); if (!passwordCheck.checkedOK) { - return { success: false, error: passwordCheck.error }; + return { user: null, error: passwordCheck.error }; } try { + const bcrypt = require("bcrypt"); const hashedPassword = bcrypt.hashSync(password, 10); const user = await prisma.users.create({ data: { @@ -56,16 +25,15 @@ const User = { }, update: async function (userId, updates = {}) { - // Ensure password meets complexity requirements. - passwordCheck = checkPasswordComplexity(updates["password"]); - if (!passwordCheck.checkedOK) { - return { success: false, error: passwordCheck.error }; - } - try { - // Rehash new password if it exists as update - // will be given to us as plaintext. - if (updates.hasOwnProperty("password") && updates.password.length >= 8) { + // Rehash new password if it exists as update field + if (updates.hasOwnProperty("password")) { + const passwordCheck = this.checkPasswordComplexity(updates.password); + if (!passwordCheck.checkedOK) { + return { success: false, error: passwordCheck.error }; + } + + const bcrypt = require("bcrypt"); updates.password = bcrypt.hashSync(updates.password, 10); } else { delete updates.password; @@ -124,6 +92,38 @@ const User = { return []; } }, + + checkPasswordComplexity: function (passwordInput = "") { + const passwordComplexity = require("joi-password-complexity"); + // Can be set via ENV variable on boot. No frontend config at this time. + // Docs: https://www.npmjs.com/package/joi-password-complexity + const complexityOptions = { + min: process.env.PASSWORDMINCHAR || 8, + max: process.env.PASSWORDMAXCHAR || 250, + lowerCase: process.env.PASSWORDLOWERCASE || 0, + upperCase: process.env.PASSWORDUPPERCASE || 0, + numeric: process.env.PASSWORDNUMERIC || 0, + symbol: process.env.PASSWORDSYMBOL || 0, + // reqCount should be equal to how many conditions you are testing for (1-4) + requirementCount: process.env.PASSWORDREQUIREMENTS || 0, + }; + + const complexityCheck = passwordComplexity( + complexityOptions, + "password" + ).validate(passwordInput); + if (complexityCheck.hasOwnProperty("error")) { + let myError = ""; + let prepend = ""; + for (let i = 0; i < complexityCheck.error.details.length; i++) { + myError += prepend + complexityCheck.error.details[i].message; + prepend = ", "; + } + return { checkedOK: false, error: myError }; + } + + return { checkedOK: true, error: "No error." }; + }, }; module.exports = { User }; diff --git a/server/yarn.lock b/server/yarn.lock index 3226f9f5406..0f47a75b1ce 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -150,6 +150,18 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + "@mapbox/node-pre-gyp@^1.0.0", "@mapbox/node-pre-gyp@^1.0.10": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" @@ -224,6 +236,23 @@ resolved "https://registry.yarnpkg.com/@sevinf/maybe/-/maybe-0.5.0.tgz#e59fcea028df615fe87d708bb30e1f338e46bb44" integrity sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg== +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -1556,6 +1585,22 @@ isomorphic-fetch@^3.0.0: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" +joi-password-complexity@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/joi-password-complexity/-/joi-password-complexity-5.2.0.tgz#5308f4e7c6c39ce0a6a050597883d5fd7f2800b4" + integrity sha512-exQOcaKC4EuZwwNVQ/5/FcnCzdwdzjA2RPIrRgZXTjzkFhY5NUtP83SlcNSUK3OvbRFpjUq1FCzhHg/uqPg90g== + +joi@^17.11.0: + version "17.11.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" + integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + js-tiktoken@^1.0.6, js-tiktoken@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.7.tgz#56933fcd2093e8304060dfde3071bda91812e6f5" From 8ee10487bb25e16b88a4d4d4ea61cef513a4cbcc Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 5 Dec 2023 09:07:24 -0800 Subject: [PATCH 3/3] Ensure persistence of password requirements on restarts via env-dump Copy example schema to docker env as well --- docker/.env.example | 14 ++++++++++++++ server/.env.example | 4 +++- server/utils/helpers/updateENV.js | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index 74ab3ef62c8..5e85ba68977 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -78,3 +78,17 @@ VECTOR_DB="lancedb" STORAGE_DIR="/app/server/storage" UID='1000' GID='1000' + +########################################### +######## PASSWORD COMPLEXITY ############## +########################################### +# Enforce a password schema for your organization users. +# Documentation on how to use https://github.com/kamronbatman/joi-password-complexity +# Default is only 8 char minimum +# PASSWORDMINCHAR=8 +# PASSWORDMAXCHAR=250 +# PASSWORDLOWERCASE=1 +# PASSWORDUPPERCASE=1 +# PASSWORDNUMERIC=1 +# PASSWORDSYMBOL=1 +# PASSWORDREQUIREMENTS=4 \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index 2a775ceffe5..d4501fbd674 100644 --- a/server/.env.example +++ b/server/.env.example @@ -81,7 +81,9 @@ VECTOR_DB="lancedb" ########################################### ######## PASSWORD COMPLEXITY ############## ########################################### -#PASSWORDMINCHAR=16 +# Enforce a password schema for your organization users. +# Documentation on how to use https://github.com/kamronbatman/joi-password-complexity +#PASSWORDMINCHAR=8 #PASSWORDMAXCHAR=250 #PASSWORDLOWERCASE=1 #PASSWORDUPPERCASE=1 diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index b7ecffa1406..f78de7dd057 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -282,6 +282,14 @@ async function dumpENV() { "CACHE_VECTORS", "STORAGE_DIR", "SERVER_PORT", + // Password Schema Keys if present. + "PASSWORDMINCHAR", + "PASSWORDMAXCHAR", + "PASSWORDLOWERCASE", + "PASSWORDUPPERCASE", + "PASSWORDNUMERIC", + "PASSWORDSYMBOL", + "PASSWORDREQUIREMENTS", ]; for (const key of protectedKeys) {