From 732d07829fad97ebd4ed8da07c3d03ee62bf9440 Mon Sep 17 00:00:00 2001
From: pritchey <pritchey@users.noreply.github.com>
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 <rambat1010@gmail.com>
---
 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 f5bf26aee..0c85b6d87 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 9f2b42aeb..8282070eb 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"
                   />
                 </div>
diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx
index c3b6a939d..6b25f42ac 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"
                   />
                 </div>
diff --git a/server/.env.example b/server/.env.example
index f83c5e72a..40bfed007 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 782a28887..269219fc8 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 6bdb90aa7..0c8962bdb 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 6e0b84970..02d9b01cb 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 3226f9f54..0f47a75b1 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"
-- 
GitLab