From 732d07829fad97ebd4ed8da07c3d03ee62bf9440 Mon Sep 17 00:00:00 2001 From: pritchey Date: Tue, 5 Dec 2023 12:13:06 -0500 Subject: [PATCH] 401-Password Complexity Check Capability (#402) * Added improved password complexity checking capability. * Move password complexity checker as User.util dynamically import required libraries depending on code execution flow lint * Ensure persistence of password requirements on restarts via env-dump Copy example schema to docker env as well --------- Co-authored-by: timothycarambat --- docker/.env.example | 14 ++++++ .../pages/Admin/Users/NewUserModal/index.jsx | 1 - .../Users/UserRow/EditUserModal/index.jsx | 1 - server/.env.example | 13 +++++ server/models/user.js | 50 +++++++++++++++++-- server/package.json | 2 + server/utils/helpers/updateENV.js | 8 +++ server/yarn.lock | 45 +++++++++++++++++ 8 files changed, 128 insertions(+), 6 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index f5bf26aee3..0c85b6d876 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -79,3 +79,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/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index 9f2b42aeb8..8282070eb6 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 c3b6a939d6..6b25f42ac6 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/.env.example b/server/.env.example index f83c5e72a6..40bfed0075 100644 --- a/server/.env.example +++ b/server/.env.example @@ -78,3 +78,16 @@ 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 ############## +########################################### +# 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 +#PASSWORDNUMERIC=1 +#PASSWORDSYMBOL=1 +#PASSWORDREQUIREMENTS=4 diff --git a/server/models/user.js b/server/models/user.js index 782a288876..269219fc89 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,9 +1,14 @@ const prisma = require("../utils/prisma"); -const bcrypt = require("bcrypt"); const User = { create: async function ({ username, password, role = "default" }) { + const passwordCheck = this.checkPasswordComplexity(password); + if (!passwordCheck.checkedOK) { + return { user: null, error: passwordCheck.error }; + } + try { + const bcrypt = require("bcrypt"); const hashedPassword = bcrypt.hashSync(password, 10); const user = await prisma.users.create({ data: { @@ -21,9 +26,14 @@ const User = { update: async function (userId, updates = {}) { 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; @@ -82,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/package.json b/server/package.json index 6bdb90aa77..0c8962bdb2 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", diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index 6e0b84970b..02d9b01cbb 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -286,6 +286,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) { diff --git a/server/yarn.lock b/server/yarn.lock index 3226f9f540..0f47a75b1c 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"