From 97b1e67d80cc555cc6d4ee2e30a54ebf1c6b8558 Mon Sep 17 00:00:00 2001
From: Gareth Andrew Lloyd <gareth.lloyd@memgraph.io>
Date: Tue, 30 Jan 2024 18:17:05 +0000
Subject: [PATCH] Fix auth durability (#1644)

* Change auth durability to store hash algorithm
* Add Salt to SHA256
---
 src/auth/auth.cpp           |  61 ++++++++++-
 src/auth/crypto.cpp         | 212 ++++++++++++++++++++++++++++--------
 src/auth/crypto.hpp         |  43 +++++++-
 src/auth/models.cpp         | 124 +++++++++++++--------
 src/auth/models.hpp         |  19 +++-
 src/utils/enum.hpp          |  12 ++
 tests/unit/auth.cpp         |  80 ++++++++------
 tests/unit/auth_handler.cpp |  48 ++++----
 8 files changed, 437 insertions(+), 162 deletions(-)

diff --git a/src/auth/auth.cpp b/src/auth/auth.cpp
index 33d8b2cac..88f0c4410 100644
--- a/src/auth/auth.cpp
+++ b/src/auth/auth.cpp
@@ -13,6 +13,7 @@
 
 #include <fmt/format.h>
 
+#include "auth/crypto.hpp"
 #include "auth/exceptions.hpp"
 #include "license/license.hpp"
 #include "utils/flag_validation.hpp"
@@ -43,6 +44,9 @@ namespace memgraph::auth {
 const std::string kUserPrefix = "user:";
 const std::string kRolePrefix = "role:";
 const std::string kLinkPrefix = "link:";
+const std::string kVersion = "version";
+
+static constexpr auto kVersionV1 = "V1";
 
 /**
  * All data stored in the `Auth` storage is stored in an underlying
@@ -61,8 +65,59 @@ const std::string kLinkPrefix = "link:";
  * key="link:<username>", value="<rolename>"
  */
 
+namespace {
+void MigrateVersions(kvstore::KVStore &store) {
+  static constexpr auto kPasswordHashV0V1 = "password_hash";
+  auto version_str = store.Get(kVersion);
+
+  if (!version_str) {
+    using namespace std::string_literals;
+
+    // pre versioning, add version to the store
+    auto puts = std::map<std::string, std::string>{{kVersion, kVersionV1}};
+
+    // also add hash kind into durability
+
+    auto it = store.begin(kUserPrefix);
+    auto const e = store.end(kUserPrefix);
+
+    if (it != e) {
+      const auto hash_algo = CurrentHashAlgorithm();
+      spdlog::info("Updating auth durability, assuming previously stored as {}", AsString(hash_algo));
+
+      for (; it != e; ++it) {
+        auto const &[key, value] = *it;
+        try {
+          auto user_data = nlohmann::json::parse(value);
+
+          auto password_hash = user_data[kPasswordHashV0V1];
+          if (!password_hash.is_string()) {
+            throw AuthException("Couldn't load user data!");
+          }
+          // upgrade the password_hash to include the hash algortihm
+          if (password_hash.empty()) {
+            user_data[kPasswordHashV0V1] = nullptr;
+          } else {
+            user_data[kPasswordHashV0V1] = HashedPassword{hash_algo, password_hash};
+          }
+          puts.emplace(key, user_data.dump());
+        } catch (const nlohmann::json::parse_error &e) {
+          throw AuthException("Couldn't load user data!");
+        }
+      }
+    }
+
+    // Perform migration to V1
+    store.PutMultiple(puts);
+    version_str = kVersionV1;
+  }
+}
+};  // namespace
+
 Auth::Auth(std::string storage_directory, Config config)
-    : storage_(std::move(storage_directory)), module_(FLAGS_auth_module_executable), config_{std::move(config)} {}
+    : storage_(std::move(storage_directory)), module_(FLAGS_auth_module_executable), config_{std::move(config)} {
+  MigrateVersions(storage_);
+}
 
 std::optional<User> Auth::Authenticate(const std::string &username, const std::string &password) {
   if (module_.IsUsed()) {
@@ -153,6 +208,10 @@ std::optional<User> Auth::Authenticate(const std::string &username, const std::s
                                           username, "https://memgr.ph/auth"));
       return std::nullopt;
     }
+    if (user->UpgradeHash(password)) {
+      SaveUser(*user);
+    }
+
     return user;
   }
 }
diff --git a/src/auth/crypto.cpp b/src/auth/crypto.cpp
index c433eaf62..a8351635a 100644
--- a/src/auth/crypto.cpp
+++ b/src/auth/crypto.cpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Licensed as a Memgraph Enterprise file under the Memgraph Enterprise
 // License (the "License"); by using this file, you agree to be bound by the terms of the License, and you may not use
@@ -22,10 +22,14 @@
 
 namespace {
 using namespace std::literals;
-inline constexpr std::array password_encryption_mappings{
-    std::pair{"bcrypt"sv, memgraph::auth::PasswordEncryptionAlgorithm::BCRYPT},
-    std::pair{"sha256"sv, memgraph::auth::PasswordEncryptionAlgorithm::SHA256},
-    std::pair{"sha256-multiple"sv, memgraph::auth::PasswordEncryptionAlgorithm::SHA256_MULTIPLE}};
+
+constexpr auto kHashAlgo = "hash_algo";
+constexpr auto kPasswordHash = "password_hash";
+
+inline constexpr std::array password_hash_mappings{
+    std::pair{"bcrypt"sv, memgraph::auth::PasswordHashAlgorithm::BCRYPT},
+    std::pair{"sha256"sv, memgraph::auth::PasswordHashAlgorithm::SHA256},
+    std::pair{"sha256-multiple"sv, memgraph::auth::PasswordHashAlgorithm::SHA256_MULTIPLE}};
 
 inline constexpr uint64_t ONE_SHA_ITERATION = 1;
 inline constexpr uint64_t MULTIPLE_SHA_ITERATIONS = 1024;
@@ -35,7 +39,7 @@ inline constexpr uint64_t MULTIPLE_SHA_ITERATIONS = 1024;
 DEFINE_VALIDATED_string(password_encryption_algorithm, "bcrypt",
                         "The password encryption algorithm used for authentication.", {
                           if (const auto result =
-                                  memgraph::utils::IsValidEnumValueString(value, password_encryption_mappings);
+                                  memgraph::utils::IsValidEnumValueString(value, password_hash_mappings);
                               result.HasError()) {
                             const auto error = result.GetError();
                             switch (error) {
@@ -45,7 +49,7 @@ DEFINE_VALIDATED_string(password_encryption_algorithm, "bcrypt",
                               }
                               case memgraph::utils::ValidationError::InvalidValue: {
                                 std::cout << "Invalid value for password encryption algorithm. Allowed values: "
-                                          << memgraph::utils::GetAllowedEnumValuesString(password_encryption_mappings)
+                                          << memgraph::utils::GetAllowedEnumValuesString(password_hash_mappings)
                                           << std::endl;
                                 break;
                               }
@@ -58,7 +62,7 @@ DEFINE_VALIDATED_string(password_encryption_algorithm, "bcrypt",
 
 namespace memgraph::auth {
 namespace BCrypt {
-std::string EncryptPassword(const std::string &password) {
+std::string HashPassword(const std::string &password) {
   char salt[BCRYPT_HASHSIZE];
   char hash[BCRYPT_HASHSIZE];
 
@@ -86,16 +90,30 @@ bool VerifyPassword(const std::string &password, const std::string &hash) {
 }  // namespace BCrypt
 
 namespace SHA {
+
+namespace {
+
+constexpr auto SHA_LENGTH = 64U;
+constexpr auto SALT_SIZE = 16U;
+constexpr auto SALT_SIZE_DURABLE = SALT_SIZE * 2;
+
 #if OPENSSL_VERSION_MAJOR >= 3
-std::string EncryptPasswordOpenSSL3(const std::string &password, const uint64_t number_of_iterations) {
+std::string HashPasswordOpenSSL3(std::string_view password, const uint64_t number_of_iterations,
+                                 std::string_view salt) {
   unsigned char hash[SHA256_DIGEST_LENGTH];
 
   EVP_MD_CTX *ctx = EVP_MD_CTX_new();
   EVP_MD *md = EVP_MD_fetch(nullptr, "SHA2-256", nullptr);
 
   EVP_DigestInit_ex(ctx, md, nullptr);
+
+  if (!salt.empty()) {
+    DMG_ASSERT(salt.size() == SALT_SIZE);
+    EVP_DigestUpdate(ctx, salt.data(), salt.size());
+  }
+
   for (auto i = 0; i < number_of_iterations; i++) {
-    EVP_DigestUpdate(ctx, password.c_str(), password.size());
+    EVP_DigestUpdate(ctx, password.data(), password.size());
   }
   EVP_DigestFinal_ex(ctx, hash, nullptr);
 
@@ -103,6 +121,11 @@ std::string EncryptPasswordOpenSSL3(const std::string &password, const uint64_t
   EVP_MD_CTX_free(ctx);
 
   std::stringstream result_stream;
+
+  for (unsigned char salt_char : salt) {
+    result_stream << std::hex << std::setw(2) << std::setfill('0') << (((unsigned int)salt_char) & 0xFFU);
+  }
+
   for (auto hash_char : hash) {
     result_stream << std::hex << std::setw(2) << std::setfill('0') << (int)hash_char;
   }
@@ -110,17 +133,27 @@ std::string EncryptPasswordOpenSSL3(const std::string &password, const uint64_t
   return result_stream.str();
 }
 #else
-std::string EncryptPasswordOpenSSL1_1(const std::string &password, const uint64_t number_of_iterations) {
+std::string HashPasswordOpenSSL1_1(std::string_view password, const uint64_t number_of_iterations,
+                                   std::string_view salt) {
   unsigned char hash[SHA256_DIGEST_LENGTH];
 
   SHA256_CTX sha256;
   SHA256_Init(&sha256);
+
+  if (!salt.empty()) {
+    DMG_ASSERT(salt.size() == SALT_SIZE);
+    SHA256_Update(&sha256, salt.data(), salt.size());
+  }
+
   for (auto i = 0; i < number_of_iterations; i++) {
-    SHA256_Update(&sha256, password.c_str(), password.size());
+    SHA256_Update(&sha256, password.data(), password.size());
   }
   SHA256_Final(hash, &sha256);
 
   std::stringstream ss;
+  for (unsigned char salt_char : salt) {
+    ss << std::hex << std::setw(2) << std::setfill('0') << (((unsigned int)salt_char) & 0xFFU);
+  }
   for (auto hash_char : hash) {
     ss << std::hex << std::setw(2) << std::setfill('0') << (int)hash_char;
   }
@@ -129,55 +162,144 @@ std::string EncryptPasswordOpenSSL1_1(const std::string &password, const uint64_
 }
 #endif
 
-std::string EncryptPassword(const std::string &password, const uint64_t number_of_iterations) {
+std::string HashPassword(std::string_view password, const uint64_t number_of_iterations, std::string_view salt) {
 #if OPENSSL_VERSION_MAJOR >= 3
-  return EncryptPasswordOpenSSL3(password, number_of_iterations);
+  return HashPasswordOpenSSL3(password, number_of_iterations, salt);
 #else
-  return EncryptPasswordOpenSSL1_1(password, number_of_iterations);
+  return HashPasswordOpenSSL1_1(password, number_of_iterations, salt);
 #endif
 }
 
-bool VerifyPassword(const std::string &password, const std::string &hash, const uint64_t number_of_iterations) {
-  auto password_hash = EncryptPassword(password, number_of_iterations);
+auto ExtractSalt(std::string_view salt_durable) -> std::array<char, SALT_SIZE> {
+  static_assert(SALT_SIZE_DURABLE % 2 == 0);
+  static_assert(SALT_SIZE_DURABLE / 2 == SALT_SIZE);
+
+  MG_ASSERT(salt_durable.size() == SALT_SIZE_DURABLE);
+  auto const *b = salt_durable.cbegin();
+  auto const *const e = salt_durable.cend();
+
+  auto salt = std::array<char, SALT_SIZE>{};
+  auto *inserter = salt.begin();
+
+  auto const toval = [](char a) -> uint8_t {
+    if ('0' <= a && a <= '9') {
+      return a - '0';
+    }
+    if ('a' <= a && a <= 'f') {
+      return 10 + (a - 'a');
+    }
+    MG_ASSERT(false, "Currupt hash, can't extract salt");
+    __builtin_unreachable();
+  };
+
+  for (; b != e; b += 2, ++inserter) {
+    *inserter = static_cast<char>(static_cast<uint8_t>(toval(b[0]) << 4U) | toval(b[1]));
+  }
+  return salt;
+}
+
+bool IsSalted(std::string_view hash) { return hash.size() == SHA_LENGTH + SALT_SIZE_DURABLE; }
+
+bool VerifyPassword(std::string_view password, std::string_view hash, const uint64_t number_of_iterations) {
+  auto password_hash = std::invoke([&] {
+    if (hash.size() == SHA_LENGTH) [[unlikely]] {
+      // Just SHA256
+      return HashPassword(password, number_of_iterations, {});
+    } else {
+      // SHA256 + SALT
+      MG_ASSERT(IsSalted(hash));
+      auto const salt_durable = std::string_view{hash.data(), SALT_SIZE_DURABLE};
+      std::array<char, SALT_SIZE> salt = ExtractSalt(salt_durable);
+      return HashPassword(password, number_of_iterations, {salt.data(), salt.size()});
+    }
+  });
   return password_hash == hash;
 }
+
+}  // namespace
+
 }  // namespace SHA
 
-bool VerifyPassword(const std::string &password, const std::string &hash) {
-  const auto password_encryption_algorithm = utils::StringToEnum<PasswordEncryptionAlgorithm>(
-      FLAGS_password_encryption_algorithm, password_encryption_mappings);
+HashedPassword HashPassword(const std::string &password, std::optional<PasswordHashAlgorithm> override_algo) {
+  auto const hash_algo = override_algo.value_or(CurrentHashAlgorithm());
+  auto password_hash = std::invoke([&] {
+    switch (hash_algo) {
+      case PasswordHashAlgorithm::BCRYPT: {
+        return BCrypt::HashPassword(password);
+      }
+      case PasswordHashAlgorithm::SHA256:
+      case PasswordHashAlgorithm::SHA256_MULTIPLE: {
+        auto gen = std::mt19937(std::random_device{}());
+        auto salt = std::array<char, SHA::SALT_SIZE>{};
+        auto dis = std::uniform_int_distribution<unsigned char>(0, 255);
+        std::generate(salt.begin(), salt.end(), [&]() { return dis(gen); });
+        auto iterations = (hash_algo == PasswordHashAlgorithm::SHA256) ? ONE_SHA_ITERATION : MULTIPLE_SHA_ITERATIONS;
+        return SHA::HashPassword(password, iterations, {salt.data(), salt.size()});
+      }
+    }
+  });
+  return HashedPassword{hash_algo, std::move(password_hash)};
+};
 
-  if (!password_encryption_algorithm.has_value()) {
-    throw AuthException("Invalid password encryption flag '{}'!", FLAGS_password_encryption_algorithm);
+namespace {
+
+auto InternalParseHashAlgorithm(std::string_view algo) -> PasswordHashAlgorithm {
+  auto maybe_parsed = utils::StringToEnum<PasswordHashAlgorithm>(algo, password_hash_mappings);
+  if (!maybe_parsed) {
+    throw AuthException("Invalid password encryption '{}'!", algo);
   }
-
-  switch (password_encryption_algorithm.value()) {
-    case PasswordEncryptionAlgorithm::BCRYPT:
-      return BCrypt::VerifyPassword(password, hash);
-    case PasswordEncryptionAlgorithm::SHA256:
-      return SHA::VerifyPassword(password, hash, ONE_SHA_ITERATION);
-    case PasswordEncryptionAlgorithm::SHA256_MULTIPLE:
-      return SHA::VerifyPassword(password, hash, MULTIPLE_SHA_ITERATIONS);
-  }
-
-  throw AuthException("Invalid password encryption flag '{}'!", FLAGS_password_encryption_algorithm);
+  return *maybe_parsed;
 }
 
-std::string EncryptPassword(const std::string &password) {
-  const auto password_encryption_algorithm = utils::StringToEnum<PasswordEncryptionAlgorithm>(
-      FLAGS_password_encryption_algorithm, password_encryption_mappings);
+PasswordHashAlgorithm &InternalCurrentHashAlgorithm() {
+  static auto current = PasswordHashAlgorithm::BCRYPT;
+  static std::once_flag flag;
+  std::call_once(flag, [] { current = InternalParseHashAlgorithm(FLAGS_password_encryption_algorithm); });
+  return current;
+}
+}  // namespace
 
-  if (!password_encryption_algorithm.has_value()) {
-    throw AuthException("Invalid password encryption flag '{}'!", FLAGS_password_encryption_algorithm);
+auto CurrentHashAlgorithm() -> PasswordHashAlgorithm { return InternalCurrentHashAlgorithm(); }
+
+void SetHashAlgorithm(std::string_view algo) {
+  auto &current = InternalCurrentHashAlgorithm();
+  current = InternalParseHashAlgorithm(algo);
+}
+
+auto AsString(PasswordHashAlgorithm hash_algo) -> std::string_view {
+  return *utils::EnumToString<PasswordHashAlgorithm>(hash_algo, password_hash_mappings);
+}
+
+bool HashedPassword::VerifyPassword(const std::string &password) {
+  switch (hash_algo) {
+    case PasswordHashAlgorithm::BCRYPT:
+      return BCrypt::VerifyPassword(password, password_hash);
+    case PasswordHashAlgorithm::SHA256:
+      return SHA::VerifyPassword(password, password_hash, ONE_SHA_ITERATION);
+    case PasswordHashAlgorithm::SHA256_MULTIPLE:
+      return SHA::VerifyPassword(password, password_hash, MULTIPLE_SHA_ITERATIONS);
   }
+}
 
-  switch (password_encryption_algorithm.value()) {
-    case PasswordEncryptionAlgorithm::BCRYPT:
-      return BCrypt::EncryptPassword(password);
-    case PasswordEncryptionAlgorithm::SHA256:
-      return SHA::EncryptPassword(password, ONE_SHA_ITERATION);
-    case PasswordEncryptionAlgorithm::SHA256_MULTIPLE:
-      return SHA::EncryptPassword(password, MULTIPLE_SHA_ITERATIONS);
+void to_json(nlohmann::json &j, const HashedPassword &p) {
+  j = nlohmann::json{{kHashAlgo, p.hash_algo}, {kPasswordHash, p.password_hash}};
+}
+
+void from_json(const nlohmann::json &j, HashedPassword &p) {
+  // NOLINTNEXTLINE(cppcoreguidelines-init-variables)
+  PasswordHashAlgorithm hash_algo;
+  j.at(kHashAlgo).get_to(hash_algo);
+  auto password_hash = j.value(kPasswordHash, std::string());
+  p = HashedPassword{hash_algo, std::move(password_hash)};
+}
+
+bool HashedPassword::IsSalted() const {
+  switch (hash_algo) {
+    case PasswordHashAlgorithm::BCRYPT:
+      return true;
+    case PasswordHashAlgorithm::SHA256:
+    case PasswordHashAlgorithm::SHA256_MULTIPLE:
+      return SHA::IsSalted(password_hash);
   }
 }
 
diff --git a/src/auth/crypto.hpp b/src/auth/crypto.hpp
index dbceb128b..c5dfc1c05 100644
--- a/src/auth/crypto.hpp
+++ b/src/auth/crypto.hpp
@@ -1,4 +1,4 @@
-// Copyright 2023 Memgraph Ltd.
+// Copyright 2024 Memgraph Ltd.
 //
 // Licensed as a Memgraph Enterprise file under the Memgraph Enterprise
 // License (the "License"); by using this file, you agree to be bound by the terms of the License, and you may not use
@@ -8,14 +8,45 @@
 
 #pragma once
 
+#include <json/json.hpp>
+#include <optional>
 #include <string>
 
 namespace memgraph::auth {
-enum class PasswordEncryptionAlgorithm : uint8_t { BCRYPT, SHA256, SHA256_MULTIPLE };
+/// Need to be stable, auth durability depends on this
+enum class PasswordHashAlgorithm : uint8_t { BCRYPT = 0, SHA256 = 1, SHA256_MULTIPLE = 2 };
 
-/// @throw AuthException if unable to encrypt the password.
-std::string EncryptPassword(const std::string &password);
+void SetHashAlgorithm(std::string_view algo);
 
-/// @throw AuthException if unable to verify the password.
-bool VerifyPassword(const std::string &password, const std::string &hash);
+auto CurrentHashAlgorithm() -> PasswordHashAlgorithm;
+
+auto AsString(PasswordHashAlgorithm hash_algo) -> std::string_view;
+
+struct HashedPassword {
+  HashedPassword() = default;
+  HashedPassword(PasswordHashAlgorithm hash_algo, std::string password_hash)
+      : hash_algo{hash_algo}, password_hash{std::move(password_hash)} {}
+  HashedPassword(HashedPassword const &) = default;
+  HashedPassword(HashedPassword &&) = default;
+  HashedPassword &operator=(HashedPassword const &) = default;
+  HashedPassword &operator=(HashedPassword &&) = default;
+
+  friend bool operator==(HashedPassword const &, HashedPassword const &) = default;
+
+  bool VerifyPassword(const std::string &password);
+
+  bool IsSalted() const;
+
+  auto HashAlgo() const -> PasswordHashAlgorithm { return hash_algo; }
+
+  friend void to_json(nlohmann::json &j, const HashedPassword &p);
+  friend void from_json(const nlohmann::json &j, HashedPassword &p);
+
+ private:
+  PasswordHashAlgorithm hash_algo{PasswordHashAlgorithm::BCRYPT};
+  std::string password_hash{};
+};
+
+/// @throw AuthException if unable to hash the password.
+HashedPassword HashPassword(const std::string &password, std::optional<PasswordHashAlgorithm> override_algo = {});
 }  // namespace memgraph::auth
diff --git a/src/auth/models.cpp b/src/auth/models.cpp
index 7ded6410d..a59a73c7b 100644
--- a/src/auth/models.cpp
+++ b/src/auth/models.cpp
@@ -25,6 +25,21 @@
 namespace memgraph::auth {
 namespace {
 
+constexpr auto kRoleName = "rolename";
+constexpr auto kPermissions = "permissions";
+constexpr auto kGrants = "grants";
+constexpr auto kDenies = "denies";
+constexpr auto kUsername = "username";
+constexpr auto kPasswordHash = "password_hash";
+
+#ifdef MG_ENTERPRISE
+constexpr auto kGlobalPermission = "global_permission";
+constexpr auto kFineGrainedAccessHandler = "fine_grained_access_handler";
+constexpr auto kAllowAll = "allow_all";
+constexpr auto kDefault = "default";
+constexpr auto kDatabases = "databases";
+#endif
+
 // Constant list of all available permissions.
 const std::vector<Permission> kPermissionsAll = {Permission::MATCH,
                                                  Permission::CREATE,
@@ -233,8 +248,9 @@ std::vector<Permission> Permissions::GetDenies() const {
 
 nlohmann::json Permissions::Serialize() const {
   nlohmann::json data = nlohmann::json::object();
-  data["grants"] = grants_;
-  data["denies"] = denies_;
+
+  data[kGrants] = grants_;
+  data[kDenies] = denies_;
   return data;
 }
 
@@ -242,10 +258,10 @@ Permissions Permissions::Deserialize(const nlohmann::json &data) {
   if (!data.is_object()) {
     throw AuthException("Couldn't load permissions data!");
   }
-  if (!data["grants"].is_number_unsigned() || !data["denies"].is_number_unsigned()) {
+  if (!data[kGrants].is_number_unsigned() || !data[kDenies].is_number_unsigned()) {
     throw AuthException("Couldn't load permissions data!");
   }
-  return Permissions{data["grants"], data["denies"]};
+  return Permissions{data[kGrants], data[kDenies]};
 }
 
 uint64_t Permissions::grants() const { return grants_; }
@@ -307,8 +323,8 @@ nlohmann::json FineGrainedAccessPermissions::Serialize() const {
     return {};
   }
   nlohmann::json data = nlohmann::json::object();
-  data["permissions"] = permissions_;
-  data["global_permission"] = global_permission_.has_value() ? global_permission_.value() : -1;
+  data[kPermissions] = permissions_;
+  data[kGlobalPermission] = global_permission_.has_value() ? global_permission_.value() : -1;
   return data;
 }
 
@@ -321,13 +337,13 @@ FineGrainedAccessPermissions FineGrainedAccessPermissions::Deserialize(const nlo
   }
   std::optional<uint64_t> global_permission;
 
-  if (data["global_permission"].empty() || data["global_permission"] == -1) {
+  if (data[kGlobalPermission].empty() || data[kGlobalPermission] == -1) {
     global_permission = std::nullopt;
   } else {
-    global_permission = data["global_permission"];
+    global_permission = data[kGlobalPermission];
   }
 
-  return FineGrainedAccessPermissions(data["permissions"], global_permission);
+  return FineGrainedAccessPermissions(data[kPermissions], global_permission);
 }
 
 const std::unordered_map<std::string, uint64_t> &FineGrainedAccessPermissions::GetPermissions() const {
@@ -433,13 +449,13 @@ const FineGrainedAccessPermissions &Role::GetFineGrainedAccessEdgeTypePermission
 
 nlohmann::json Role::Serialize() const {
   nlohmann::json data = nlohmann::json::object();
-  data["rolename"] = rolename_;
-  data["permissions"] = permissions_.Serialize();
+  data[kRoleName] = rolename_;
+  data[kPermissions] = permissions_.Serialize();
 #ifdef MG_ENTERPRISE
   if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
-    data["fine_grained_access_handler"] = fine_grained_access_handler_.Serialize();
+    data[kFineGrainedAccessHandler] = fine_grained_access_handler_.Serialize();
   } else {
-    data["fine_grained_access_handler"] = {};
+    data[kFineGrainedAccessHandler] = {};
   }
 #endif
   return data;
@@ -449,21 +465,21 @@ Role Role::Deserialize(const nlohmann::json &data) {
   if (!data.is_object()) {
     throw AuthException("Couldn't load role data!");
   }
-  if (!data["rolename"].is_string() || !data["permissions"].is_object()) {
+  if (!data[kRoleName].is_string() || !data[kPermissions].is_object()) {
     throw AuthException("Couldn't load role data!");
   }
-  auto permissions = Permissions::Deserialize(data["permissions"]);
+  auto permissions = Permissions::Deserialize(data[kPermissions]);
 #ifdef MG_ENTERPRISE
   if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
     FineGrainedAccessHandler fine_grained_access_handler;
     // We can have an empty fine_grained if the user was created without a valid license
-    if (data["fine_grained_access_handler"].is_object()) {
-      fine_grained_access_handler = FineGrainedAccessHandler::Deserialize(data["fine_grained_access_handler"]);
+    if (data[kFineGrainedAccessHandler].is_object()) {
+      fine_grained_access_handler = FineGrainedAccessHandler::Deserialize(data[kFineGrainedAccessHandler]);
     }
-    return {data["rolename"], permissions, std::move(fine_grained_access_handler)};
+    return {data[kRoleName], permissions, std::move(fine_grained_access_handler)};
   }
 #endif
-  return {data["rolename"], permissions};
+  return {data[kRoleName], permissions};
 }
 
 bool operator==(const Role &first, const Role &second) {
@@ -533,10 +549,10 @@ const std::string &Databases::GetDefault() const {
 
 nlohmann::json Databases::Serialize() const {
   nlohmann::json data = nlohmann::json::object();
-  data["grants"] = grants_dbs_;
-  data["denies"] = denies_dbs_;
-  data["allow_all"] = allow_all_;
-  data["default"] = default_db_;
+  data[kGrants] = grants_dbs_;
+  data[kDenies] = denies_dbs_;
+  data[kAllowAll] = allow_all_;
+  data[kDefault] = default_db_;
   return data;
 }
 
@@ -544,22 +560,22 @@ Databases Databases::Deserialize(const nlohmann::json &data) {
   if (!data.is_object()) {
     throw AuthException("Couldn't load database data!");
   }
-  if (!data["grants"].is_structured() || !data["denies"].is_structured() || !data["allow_all"].is_boolean() ||
-      !data["default"].is_string()) {
+  if (!data[kGrants].is_structured() || !data[kDenies].is_structured() || !data[kAllowAll].is_boolean() ||
+      !data[kDefault].is_string()) {
     throw AuthException("Couldn't load database data!");
   }
-  return {data["allow_all"], data["grants"], data["denies"], data["default"]};
+  return {data[kAllowAll], data[kGrants], data[kDenies], data[kDefault]};
 }
 #endif
 
 User::User() = default;
 
 User::User(const std::string &username) : username_(utils::ToLowerCase(username)) {}
-User::User(const std::string &username, std::string password_hash, const Permissions &permissions)
+User::User(const std::string &username, std::optional<HashedPassword> password_hash, const Permissions &permissions)
     : username_(utils::ToLowerCase(username)), password_hash_(std::move(password_hash)), permissions_(permissions) {}
 
 #ifdef MG_ENTERPRISE
-User::User(const std::string &username, std::string password_hash, const Permissions &permissions,
+User::User(const std::string &username, std::optional<HashedPassword> password_hash, const Permissions &permissions,
            FineGrainedAccessHandler fine_grained_access_handler, Databases db_access)
     : username_(utils::ToLowerCase(username)),
       password_hash_(std::move(password_hash)),
@@ -569,16 +585,16 @@ User::User(const std::string &username, std::string password_hash, const Permiss
 #endif
 
 bool User::CheckPassword(const std::string &password) {
-  if (password_hash_.empty()) return true;
-  return VerifyPassword(password, password_hash_);
+  return password_hash_ ? password_hash_->VerifyPassword(password) : true;
 }
 
-void User::UpdatePassword(const std::optional<std::string> &password) {
+void User::UpdatePassword(const std::optional<std::string> &password,
+                          std::optional<PasswordHashAlgorithm> algo_override) {
   if (!password) {
-    password_hash_ = "";
+    password_hash_.reset();
     return;
   }
-  password_hash_ = EncryptPassword(*password);
+  password_hash_ = HashPassword(*password, algo_override);
 }
 
 void User::SetRole(const Role &role) { role_.emplace(role); }
@@ -637,16 +653,20 @@ const Role *User::role() const {
 
 nlohmann::json User::Serialize() const {
   nlohmann::json data = nlohmann::json::object();
-  data["username"] = username_;
-  data["password_hash"] = password_hash_;
-  data["permissions"] = permissions_.Serialize();
+  data[kUsername] = username_;
+  if (password_hash_.has_value()) {
+    data[kPasswordHash] = *password_hash_;
+  } else {
+    data[kPasswordHash] = nullptr;
+  }
+  data[kPermissions] = permissions_.Serialize();
 #ifdef MG_ENTERPRISE
   if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
-    data["fine_grained_access_handler"] = fine_grained_access_handler_.Serialize();
-    data["databases"] = database_access_.Serialize();
+    data[kFineGrainedAccessHandler] = fine_grained_access_handler_.Serialize();
+    data[kDatabases] = database_access_.Serialize();
   } else {
-    data["fine_grained_access_handler"] = {};
-    data["databases"] = {};
+    data[kFineGrainedAccessHandler] = {};
+    data[kDatabases] = {};
   }
 #endif
   // The role shouldn't be serialized here, it is stored as a foreign key.
@@ -657,15 +677,23 @@ User User::Deserialize(const nlohmann::json &data) {
   if (!data.is_object()) {
     throw AuthException("Couldn't load user data!");
   }
-  if (!data["username"].is_string() || !data["password_hash"].is_string() || !data["permissions"].is_object()) {
+  auto password_hash_json = data[kPasswordHash];
+  if (!data[kUsername].is_string() || !(password_hash_json.is_object() || password_hash_json.is_null()) ||
+      !data[kPermissions].is_object()) {
     throw AuthException("Couldn't load user data!");
   }
-  auto permissions = Permissions::Deserialize(data["permissions"]);
+
+  std::optional<HashedPassword> password_hash{};
+  if (password_hash_json.is_object()) {
+    password_hash = password_hash_json.get<HashedPassword>();
+  }
+
+  auto permissions = Permissions::Deserialize(data[kPermissions]);
 #ifdef MG_ENTERPRISE
   if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
     Databases db_access;
-    if (data["databases"].is_structured()) {
-      db_access = Databases::Deserialize(data["databases"]);
+    if (data[kDatabases].is_structured()) {
+      db_access = Databases::Deserialize(data[kDatabases]);
     } else {
       // Back-compatibility
       spdlog::warn("User without specified database access. Given access to the default database.");
@@ -674,13 +702,13 @@ User User::Deserialize(const nlohmann::json &data) {
     }
     FineGrainedAccessHandler fine_grained_access_handler;
     // We can have an empty fine_grained if the user was created without a valid license
-    if (data["fine_grained_access_handler"].is_object()) {
-      fine_grained_access_handler = FineGrainedAccessHandler::Deserialize(data["fine_grained_access_handler"]);
+    if (data[kFineGrainedAccessHandler].is_object()) {
+      fine_grained_access_handler = FineGrainedAccessHandler::Deserialize(data[kFineGrainedAccessHandler]);
     }
-    return {data["username"], data["password_hash"], permissions, std::move(fine_grained_access_handler), db_access};
+    return {data[kUsername], std::move(password_hash), permissions, std::move(fine_grained_access_handler), db_access};
   }
 #endif
-  return {data["username"], data["password_hash"], permissions};
+  return {data[kUsername], std::move(password_hash), permissions};
 }
 
 bool operator==(const User &first, const User &second) {
diff --git a/src/auth/models.hpp b/src/auth/models.hpp
index 4af635e3c..bb6dd2a7a 100644
--- a/src/auth/models.hpp
+++ b/src/auth/models.hpp
@@ -15,6 +15,7 @@
 
 #include <json/json.hpp>
 #include <utility>
+#include "crypto.hpp"
 #include "dbms/constants.hpp"
 #include "utils/logging.hpp"
 
@@ -332,9 +333,9 @@ class User final {
   User();
 
   explicit User(const std::string &username);
-  User(const std::string &username, std::string password_hash, const Permissions &permissions);
+  User(const std::string &username, std::optional<HashedPassword> password_hash, const Permissions &permissions);
 #ifdef MG_ENTERPRISE
-  User(const std::string &username, std::string password_hash, const Permissions &permissions,
+  User(const std::string &username, std::optional<HashedPassword> password_hash, const Permissions &permissions,
        FineGrainedAccessHandler fine_grained_access_handler, Databases db_access = {});
 #endif
   User(const User &) = default;
@@ -346,8 +347,18 @@ class User final {
   /// @throw AuthException if unable to verify the password.
   bool CheckPassword(const std::string &password);
 
+  bool UpgradeHash(const std::string password) {
+    if (!password_hash_) return false;
+    if (password_hash_->IsSalted()) return false;
+
+    auto const algo = password_hash_->HashAlgo();
+    UpdatePassword(password, algo);
+    return true;
+  }
+
   /// @throw AuthException if unable to set the password.
-  void UpdatePassword(const std::optional<std::string> &password = std::nullopt);
+  void UpdatePassword(const std::optional<std::string> &password = {},
+                      std::optional<PasswordHashAlgorithm> algo_override = std::nullopt);
 
   void SetRole(const Role &role);
 
@@ -382,7 +393,7 @@ class User final {
 
  private:
   std::string username_;
-  std::string password_hash_;
+  std::optional<HashedPassword> password_hash_;
   Permissions permissions_;
 #ifdef MG_ENTERPRISE
   FineGrainedAccessHandler fine_grained_access_handler_;
diff --git a/src/utils/enum.hpp b/src/utils/enum.hpp
index 4f1ffbd0d..50a6d4621 100644
--- a/src/utils/enum.hpp
+++ b/src/utils/enum.hpp
@@ -55,6 +55,18 @@ std::optional<Enum> StringToEnum(const auto &value, const auto &mappings) {
   return mapping_iter->second;
 }
 
+// Tries to convert a enum into string, which would then contain a value if the conversion
+// has been successful.
+template <typename Enum>
+auto EnumToString(const auto &value, const auto &mappings) -> std::optional<std::string_view> {
+  const auto mapping_iter =
+      std::find_if(mappings.begin(), mappings.end(), [&](const auto &mapping) { return mapping.second == value; });
+  if (mapping_iter == mappings.cend()) {
+    return std::nullopt;
+  }
+  return mapping_iter->first;
+}
+
 template <typename T, typename Enum>
 requires std::integral<T>
 inline T EnumToNum(Enum res) {
diff --git a/tests/unit/auth.cpp b/tests/unit/auth.cpp
index 3c2931a77..bc2947a12 100644
--- a/tests/unit/auth.cpp
+++ b/tests/unit/auth.cpp
@@ -666,6 +666,17 @@ TEST(AuthWithoutStorage, UserSerializeDeserialize) {
   ASSERT_EQ(user, output);
 }
 
+TEST(AuthWithoutStorage, UserSerializeDeserializeWithOutPassword) {
+  auto user = User("test");
+  user.permissions().Grant(Permission::MATCH);
+  user.permissions().Deny(Permission::MERGE);
+
+  auto data = user.Serialize();
+
+  auto output = User::Deserialize(data);
+  ASSERT_EQ(user, output);
+}
+
 TEST(AuthWithoutStorage, RoleSerializeDeserialize) {
   auto role = Role("test");
   role.permissions().Grant(Permission::MATCH);
@@ -716,8 +727,9 @@ TEST(AuthWithoutStorage, CaseInsensitivity) {
   {
     auto perms = Permissions();
     auto fine_grained_access_handler = FineGrainedAccessHandler();
-    auto user1 = User("test", "pw", perms, fine_grained_access_handler);
-    auto user2 = User("Test", "pw", perms, fine_grained_access_handler);
+    auto passwordHash = HashPassword("pw");
+    auto user1 = User("test", passwordHash, perms, fine_grained_access_handler);
+    auto user2 = User("Test", passwordHash, perms, fine_grained_access_handler);
     ASSERT_EQ(user1, user2);
     ASSERT_EQ(user1.username(), user2.username());
     ASSERT_EQ(user1.username(), "test");
@@ -920,51 +932,50 @@ TEST_F(AuthWithStorage, CaseInsensitivity) {
 }
 
 TEST(AuthWithoutStorage, Crypto) {
-  auto hash = EncryptPassword("hello");
-  ASSERT_TRUE(VerifyPassword("hello", hash));
-  ASSERT_FALSE(VerifyPassword("hello1", hash));
+  auto hash = HashPassword("hello");
+  ASSERT_TRUE(hash.VerifyPassword("hello"));
+  ASSERT_FALSE(hash.VerifyPassword("hello1"));
 }
 
 class AuthWithVariousEncryptionAlgorithms : public ::testing::Test {
  protected:
-  void SetUp() override { FLAGS_password_encryption_algorithm = "bcrypt"; }
+  void SetUp() override { SetHashAlgorithm("bcrypt"); }
 };
 
 TEST_F(AuthWithVariousEncryptionAlgorithms, VerifyPasswordDefault) {
-  auto hash = EncryptPassword("hello");
-  ASSERT_TRUE(VerifyPassword("hello", hash));
-  ASSERT_FALSE(VerifyPassword("hello1", hash));
+  auto hash = HashPassword("hello");
+  ASSERT_TRUE(hash.VerifyPassword("hello"));
+  ASSERT_FALSE(hash.VerifyPassword("hello1"));
 }
 
 TEST_F(AuthWithVariousEncryptionAlgorithms, VerifyPasswordSHA256) {
-  FLAGS_password_encryption_algorithm = "sha256";
-  auto hash = EncryptPassword("hello");
-  ASSERT_TRUE(VerifyPassword("hello", hash));
-  ASSERT_FALSE(VerifyPassword("hello1", hash));
+  SetHashAlgorithm("sha256");
+  auto hash = HashPassword("hello");
+  ASSERT_TRUE(hash.VerifyPassword("hello"));
+  ASSERT_FALSE(hash.VerifyPassword("hello1"));
 }
 
 TEST_F(AuthWithVariousEncryptionAlgorithms, VerifyPasswordSHA256_1024) {
-  FLAGS_password_encryption_algorithm = "sha256-multiple";
-  auto hash = EncryptPassword("hello");
-  ASSERT_TRUE(VerifyPassword("hello", hash));
-  ASSERT_FALSE(VerifyPassword("hello1", hash));
+  SetHashAlgorithm("sha256-multiple");
+  auto hash = HashPassword("hello");
+  ASSERT_TRUE(hash.VerifyPassword("hello"));
+  ASSERT_FALSE(hash.VerifyPassword("hello1"));
 }
 
-TEST_F(AuthWithVariousEncryptionAlgorithms, VerifyPasswordThrow) {
-  FLAGS_password_encryption_algorithm = "abcd";
-  ASSERT_THROW(EncryptPassword("hello"), AuthException);
+TEST_F(AuthWithVariousEncryptionAlgorithms, SetEncryptionAlgorithmNonsenseThrow) {
+  ASSERT_THROW(SetHashAlgorithm("abcd"), AuthException);
 }
 
-TEST_F(AuthWithVariousEncryptionAlgorithms, VerifyPasswordEmptyEncryptionThrow) {
-  FLAGS_password_encryption_algorithm = "";
-  ASSERT_THROW(EncryptPassword("hello"), AuthException);
+TEST_F(AuthWithVariousEncryptionAlgorithms, SetEncryptionAlgorithmEmptyThrow) {
+  ASSERT_THROW(SetHashAlgorithm(""), AuthException);
 }
 
 class AuthWithStorageWithVariousEncryptionAlgorithms : public ::testing::Test {
  protected:
   void SetUp() override {
     memgraph::utils::EnsureDir(test_folder_);
-    FLAGS_password_encryption_algorithm = "bcrypt";
+    SetHashAlgorithm("bcrypt");
+
     memgraph::license::global_license_checker.EnableTesting();
   }
 
@@ -982,25 +993,26 @@ TEST_F(AuthWithStorageWithVariousEncryptionAlgorithms, AddUserDefault) {
 }
 
 TEST_F(AuthWithStorageWithVariousEncryptionAlgorithms, AddUserSha256) {
-  FLAGS_password_encryption_algorithm = "sha256";
+  SetHashAlgorithm("sha256");
   auto user = auth.AddUser("Alice", "alice");
   ASSERT_TRUE(user);
   ASSERT_EQ(user->username(), "alice");
 }
 
 TEST_F(AuthWithStorageWithVariousEncryptionAlgorithms, AddUserSha256_1024) {
-  FLAGS_password_encryption_algorithm = "sha256-multiple";
+  SetHashAlgorithm("sha256-multiple");
   auto user = auth.AddUser("Alice", "alice");
   ASSERT_TRUE(user);
   ASSERT_EQ(user->username(), "alice");
 }
 
-TEST_F(AuthWithStorageWithVariousEncryptionAlgorithms, AddUserThrow) {
-  FLAGS_password_encryption_algorithm = "abcd";
-  ASSERT_THROW(auth.AddUser("Alice", "alice"), AuthException);
-}
-
-TEST_F(AuthWithStorageWithVariousEncryptionAlgorithms, AddUserEmptyPasswordEncryptionThrow) {
-  FLAGS_password_encryption_algorithm = "";
-  ASSERT_THROW(auth.AddUser("Alice", "alice"), AuthException);
+TEST(Serialize, HashedPassword) {
+  for (auto algo :
+       {PasswordHashAlgorithm::BCRYPT, PasswordHashAlgorithm::SHA256, PasswordHashAlgorithm::SHA256_MULTIPLE}) {
+    auto sut = HashPassword("password", algo);
+    nlohmann::json j = sut;
+    auto ret = j.get<HashedPassword>();
+    ASSERT_EQ(sut, ret);
+    ASSERT_TRUE(ret.VerifyPassword("password"));
+  }
 }
diff --git a/tests/unit/auth_handler.cpp b/tests/unit/auth_handler.cpp
index 1230fe4ba..a162d1838 100644
--- a/tests/unit/auth_handler.cpp
+++ b/tests/unit/auth_handler.cpp
@@ -57,7 +57,7 @@ TEST_F(AuthQueryHandlerFixture, GivenAuthQueryHandlerWhenInitializedHaveNoUserna
 }
 
 TEST_F(AuthQueryHandlerFixture, GivenUserWhenNoDeniesOrGrantsThenNothingIsReturned) {
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   auth->SaveUser(user);
 
   { ASSERT_EQ(auth_handler.GetUsernames().size(), 1); }
@@ -71,7 +71,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenNoDeniesOrGrantsThenNothingIsReturn
 
 TEST_F(AuthQueryHandlerFixture, GivenUserWhenAddedGrantPermissionThenItIsReturned) {
   perms.Grant(memgraph::auth::Permission::MATCH);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -92,7 +92,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenAddedGrantPermissionThenItIsReturne
 
 TEST_F(AuthQueryHandlerFixture, GivenUserWhenAddedDenyPermissionThenItIsReturned) {
   perms.Deny(memgraph::auth::Permission::MATCH);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -114,7 +114,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenAddedDenyPermissionThenItIsReturned
 TEST_F(AuthQueryHandlerFixture, GivenUserWhenPrivilegeRevokedThenNothingIsReturned) {
   perms.Deny(memgraph::auth::Permission::MATCH);
   perms.Revoke(memgraph::auth::Permission::MATCH);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -180,7 +180,7 @@ TEST_F(AuthQueryHandlerFixture, GivenRoleWhenPrivilegeRevokedThenNothingIsReturn
 TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedTwoPrivilegesThenBothAreReturned) {
   perms.Grant(memgraph::auth::Permission::MATCH);
   perms.Grant(memgraph::auth::Permission::CREATE);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -191,7 +191,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserAndRoleWhenOneGrantedAndOtherGrantedThe
   perms.Grant(memgraph::auth::Permission::MATCH);
   memgraph::auth::Role role = memgraph::auth::Role{"Mates_role", perms};
   auth->SaveRole(role);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   user.SetRole(role);
   auth->SaveUser(user);
 
@@ -215,7 +215,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserAndRoleWhenOneDeniedAndOtherDeniedThenB
   perms.Deny(memgraph::auth::Permission::MATCH);
   memgraph::auth::Role role = memgraph::auth::Role{"Mates_role", perms};
   auth->SaveRole(role);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms};
   user.SetRole(role);
   auth->SaveUser(user);
 
@@ -245,7 +245,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserAndRoleWhenOneGrantedAndOtherDeniedThen
   user_perms.Grant(memgraph::auth::Permission::MATCH);
   memgraph::auth::User user = memgraph::auth::User{
       user_name,
-      "",
+      std::nullopt,
       user_perms,
   };
   user.SetRole(role);
@@ -275,7 +275,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserAndRoleWhenOneDeniedAndOtherGrantedThen
 
   memgraph::auth::Permissions user_perms{};
   user_perms.Deny(memgraph::auth::Permission::MATCH);
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", user_perms};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, user_perms};
   user.SetRole(role);
   auth->SaveUser(user);
 
@@ -305,7 +305,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedPrivilegeOnLabelThenIsDispla
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -334,7 +334,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedMultiplePrivilegesOnLabelThe
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -364,7 +364,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedAllPrivilegesOnLabelThenTopO
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -392,7 +392,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedGlobalPrivilegeOnLabelThenIs
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -421,7 +421,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedGlobalMultiplePrivilegesOnLa
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -451,7 +451,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedGlobalAllPrivilegesOnLabelTh
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -480,7 +480,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedPrivilegeOnEdgeTypeThenIsDis
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -509,7 +509,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedMultiplePrivilegesOnEdgeType
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -539,7 +539,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedAllPrivilegesOnEdgeTypeThenT
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -567,7 +567,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedGlobalPrivilegeOnEdgeTypeThe
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -597,7 +597,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedGlobalMultiplePrivilegesOnEd
 
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -627,7 +627,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedGlobalAllPrivilegesOnEdgeTyp
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -656,7 +656,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedAndDeniedOnLabelThenNoPermis
       memgraph::auth::FineGrainedAccessPermissions{},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -685,7 +685,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedAndDeniedOnEdgeTypeThenNoPer
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);
@@ -714,7 +714,7 @@ TEST_F(AuthQueryHandlerFixture, GivenUserWhenGrantedReadAndDeniedUpdateThenOneIs
       memgraph::auth::FineGrainedAccessPermissions{read_permission},
   };
 
-  memgraph::auth::User user = memgraph::auth::User{user_name, "", perms, handler};
+  memgraph::auth::User user = memgraph::auth::User{user_name, std::nullopt, perms, handler};
   auth->SaveUser(user);
 
   auto privileges = auth_handler.GetPrivileges(user_name);