Added a new param internal-password-bcrypt-round, set to 12 by default
This commit is contained in:
@@ -60,6 +60,9 @@ acousticbrainz-api-base-url = "https://acousticbrainz.org";
|
|||||||
# Authentication
|
# Authentication
|
||||||
# Available backends: "internal", "PAM", "http-headers"
|
# Available backends: "internal", "PAM", "http-headers"
|
||||||
authentication-backend = "internal";
|
authentication-backend = "internal";
|
||||||
|
# The number of bcrypt rounds to be used when backend is set to "internal". The higher the more secure
|
||||||
|
internal-password-bcrypt-round = 12;
|
||||||
|
# The header to be used to read the authentication user when backend is set to "http-headers"
|
||||||
http-headers-login-field = "X-Forwarded-User";
|
http-headers-login-field = "X-Forwarded-User";
|
||||||
|
|
||||||
# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used)
|
# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 75 };
|
static constexpr Version LMS_DATABASE_VERSION{ 76 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -962,6 +962,12 @@ SELECT
|
|||||||
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_value_idx");
|
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_value_idx");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV75(Session& session)
|
||||||
|
{
|
||||||
|
// Added a new option to set the bcrypt count to be use to hash user's passwords
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD bcrypt_round_count INTEGER NOT NULL DEFAULT(7)");
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -1013,6 +1019,7 @@ SELECT
|
|||||||
{ 72, migrateFromV72 },
|
{ 72, migrateFromV72 },
|
||||||
{ 73, migrateFromV73 },
|
{ 73, migrateFromV73 },
|
||||||
{ 74, migrateFromV74 },
|
{ 74, migrateFromV74 },
|
||||||
|
{ 75, migrateFromV75 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
@@ -1044,7 +1051,9 @@ SELECT
|
|||||||
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
|
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
|
||||||
|
|
||||||
auto itMigrationFunc{ migrationFunctions.find(version) };
|
auto itMigrationFunc{ migrationFunctions.find(version) };
|
||||||
assert(itMigrationFunc != std::cend(migrationFunctions));
|
if (itMigrationFunc == std::cend(migrationFunctions))
|
||||||
|
throw core::LmsException{ "No code found to upgrade database!" };
|
||||||
|
|
||||||
itMigrationFunc->second(session);
|
itMigrationFunc->second(session);
|
||||||
|
|
||||||
VersionInfo::get(session).modify()->setVersion(++version);
|
VersionInfo::get(session).modify()->setVersion(++version);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ namespace lms::db
|
|||||||
public:
|
public:
|
||||||
struct PasswordHash
|
struct PasswordHash
|
||||||
{
|
{
|
||||||
|
std::size_t bcryptRoundCount;
|
||||||
std::string salt;
|
std::string salt;
|
||||||
std::string hash;
|
std::string hash;
|
||||||
};
|
};
|
||||||
@@ -91,7 +92,7 @@ namespace lms::db
|
|||||||
|
|
||||||
// accessors
|
// accessors
|
||||||
const std::string& getLoginName() const { return _loginName; }
|
const std::string& getLoginName() const { return _loginName; }
|
||||||
PasswordHash getPasswordHash() const { return PasswordHash{ _passwordSalt, _passwordHash }; }
|
PasswordHash getPasswordHash() const { return PasswordHash{ .bcryptRoundCount = static_cast<std::size_t>(_bcryptRoundCount), .salt = _passwordSalt, .hash = _passwordHash }; }
|
||||||
const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
|
const Wt::WDateTime& getLastLogin() const { return _lastLogin; }
|
||||||
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
|
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ namespace lms::db
|
|||||||
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
|
||||||
void setPasswordHash(const PasswordHash& passwordHash)
|
void setPasswordHash(const PasswordHash& passwordHash)
|
||||||
{
|
{
|
||||||
|
_bcryptRoundCount = passwordHash.bcryptRoundCount;
|
||||||
_passwordSalt = passwordHash.salt;
|
_passwordSalt = passwordHash.salt;
|
||||||
_passwordHash = passwordHash.hash;
|
_passwordHash = passwordHash.hash;
|
||||||
}
|
}
|
||||||
@@ -132,6 +134,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _type, "type");
|
Wt::Dbo::field(a, _type, "type");
|
||||||
Wt::Dbo::field(a, _loginName, "login_name");
|
Wt::Dbo::field(a, _loginName, "login_name");
|
||||||
|
Wt::Dbo::field(a, _bcryptRoundCount, "bcrypt_round_count");
|
||||||
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
Wt::Dbo::field(a, _passwordSalt, "password_salt");
|
||||||
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
Wt::Dbo::field(a, _passwordHash, "password_hash");
|
||||||
Wt::Dbo::field(a, _lastLogin, "last_login");
|
Wt::Dbo::field(a, _lastLogin, "last_login");
|
||||||
@@ -155,6 +158,7 @@ namespace lms::db
|
|||||||
static pointer create(Session& session, std::string_view loginName);
|
static pointer create(Session& session, std::string_view loginName);
|
||||||
|
|
||||||
std::string _loginName;
|
std::string _loginName;
|
||||||
|
int _bcryptRoundCount{};
|
||||||
std::string _passwordSalt;
|
std::string _passwordSalt;
|
||||||
std::string _passwordHash;
|
std::string _passwordHash;
|
||||||
Wt::WDateTime _lastLogin;
|
Wt::WDateTime _lastLogin;
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ namespace lms::auth
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordServiceBase::CheckResult
|
PasswordServiceBase::CheckResult PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
|
||||||
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
|
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
|
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
#include <Wt/WRandom.h>
|
#include <Wt/WRandom.h>
|
||||||
|
|
||||||
#include "core/Exception.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
@@ -31,7 +31,11 @@ namespace lms::auth
|
|||||||
{
|
{
|
||||||
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries)
|
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries)
|
||||||
: PasswordServiceBase{ db, maxThrottlerEntries }
|
: PasswordServiceBase{ db, maxThrottlerEntries }
|
||||||
|
, _bcryptRoundCount{ static_cast<unsigned>(core::Service<core::IConfig>::get()->getULong("internal-password-bcrypt-round", 12)) }
|
||||||
{
|
{
|
||||||
|
if (_bcryptRoundCount < 7 || _bcryptRoundCount > 31)
|
||||||
|
throw Exception{ "\"internal-password-bcrypt-round\" must be in range 7-31" };
|
||||||
|
|
||||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
|
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
|
||||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
|
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
|
||||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
|
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
|
||||||
@@ -54,7 +58,7 @@ namespace lms::auth
|
|||||||
if (!user)
|
if (!user)
|
||||||
{
|
{
|
||||||
LMS_LOG(AUTH, DEBUG, "hashing random stuff");
|
LMS_LOG(AUTH, DEBUG, "hashing random stuff");
|
||||||
// hash random stuff here to waste some time
|
// hash random stuff here to waste some time, don't give clue the user does not exist
|
||||||
hashRandomPassword();
|
hashRandomPassword();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -63,13 +67,29 @@ namespace lms::auth
|
|||||||
passwordHash = user->getPasswordHash();
|
passwordHash = user->getPasswordHash();
|
||||||
if (passwordHash.salt.empty() || passwordHash.hash.empty())
|
if (passwordHash.salt.empty() || passwordHash.hash.empty())
|
||||||
{
|
{
|
||||||
// hash random stuff here to waste some time
|
// hash random stuff here to waste some time, don't give clue the user has no password set
|
||||||
hashRandomPassword();
|
hashRandomPassword();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _hashFunc.verify(std::string{ password }, std::string{ passwordHash.salt }, std::string{ passwordHash.hash });
|
// Note: the round count set in the actual hash is used to verify, not the one used to construct _hashFunc
|
||||||
|
bool passwordMatched{ _hashFunc.verify(std::string{ password }, std::string{ passwordHash.salt }, std::string{ passwordHash.hash }) };
|
||||||
|
if (passwordMatched && passwordHash.bcryptRoundCount != _bcryptRoundCount)
|
||||||
|
{
|
||||||
|
LMS_LOG(AUTH, INFO, "Updating password hash for user '" << loginName << "' to match new bcrypt round count: previously " << passwordHash.bcryptRoundCount << " rounds, now " << _bcryptRoundCount << " rounds");
|
||||||
|
const db::User::PasswordHash updatedPasswordHash{ hashPassword(password) };
|
||||||
|
|
||||||
|
{
|
||||||
|
db::Session& session{ getDbSession() };
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
if (db::User::pointer user{ db::User::find(session, loginName) })
|
||||||
|
user.modify()->setPasswordHash(updatedPasswordHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return passwordMatched;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InternalPasswordService::canSetPasswords() const
|
bool InternalPasswordService::canSetPasswords() const
|
||||||
@@ -95,37 +115,37 @@ namespace lms::auth
|
|||||||
{
|
{
|
||||||
const db::User::PasswordHash passwordHash{ hashPassword(newPassword) };
|
const db::User::PasswordHash passwordHash{ hashPassword(newPassword) };
|
||||||
|
|
||||||
db::Session& session{ getDbSession() };
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
|
|
||||||
db::User::pointer user{ db::User::find(session, userId) };
|
|
||||||
if (!user)
|
|
||||||
throw Exception{ "User not found!" };
|
|
||||||
|
|
||||||
switch (checkPasswordAcceptability(newPassword, PasswordValidationContext{ user->getLoginName(), user->getType() }))
|
|
||||||
{
|
{
|
||||||
case PasswordAcceptabilityResult::OK:
|
db::Session& session{ getDbSession() };
|
||||||
break;
|
auto transaction{ session.createWriteTransaction() };
|
||||||
case PasswordAcceptabilityResult::TooWeak:
|
|
||||||
throw PasswordTooWeakException{};
|
|
||||||
case PasswordAcceptabilityResult::MustMatchLoginName:
|
|
||||||
throw PasswordMustMatchLoginNameException{};
|
|
||||||
}
|
|
||||||
|
|
||||||
user.modify()->setPasswordHash(passwordHash);
|
db::User::pointer user{ db::User::find(session, userId) };
|
||||||
|
if (!user)
|
||||||
|
throw Exception{ "User not found!" };
|
||||||
|
|
||||||
|
switch (checkPasswordAcceptability(newPassword, PasswordValidationContext{ user->getLoginName(), user->getType() }))
|
||||||
|
{
|
||||||
|
case PasswordAcceptabilityResult::OK:
|
||||||
|
break;
|
||||||
|
case PasswordAcceptabilityResult::TooWeak:
|
||||||
|
throw PasswordTooWeakException{};
|
||||||
|
case PasswordAcceptabilityResult::MustMatchLoginName:
|
||||||
|
throw PasswordMustMatchLoginNameException{};
|
||||||
|
}
|
||||||
|
|
||||||
|
user.modify()->setPasswordHash(passwordHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db::User::PasswordHash InternalPasswordService::hashPassword(std::string_view password) const
|
db::User::PasswordHash InternalPasswordService::hashPassword(std::string_view password) const
|
||||||
{
|
{
|
||||||
const std::string salt{ Wt::WRandom::generateId(32) };
|
const std::string salt{ Wt::WRandom::generateId(32) };
|
||||||
|
|
||||||
return db::User::PasswordHash{ .salt = salt, .hash = _hashFunc.compute(std::string{ password }, salt) };
|
return db::User::PasswordHash{ .bcryptRoundCount = _bcryptRoundCount, .salt = salt, .hash = _hashFunc.compute(std::string{ password }, salt) };
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void InternalPasswordService::hashRandomPassword() const
|
||||||
InternalPasswordService::hashRandomPassword() const
|
|
||||||
{
|
{
|
||||||
hashPassword(Wt::WRandom::generateId(32));
|
hashPassword(Wt::WRandom::generateId(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lms::auth
|
} // namespace lms::auth
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ namespace lms::auth
|
|||||||
db::User::PasswordHash hashPassword(std::string_view password) const;
|
db::User::PasswordHash hashPassword(std::string_view password) const;
|
||||||
void hashRandomPassword() const;
|
void hashRandomPassword() const;
|
||||||
|
|
||||||
const Wt::Auth::BCryptHashFunction _hashFunc{ 7 }; // TODO parametrize this
|
const unsigned _bcryptRoundCount;
|
||||||
|
const Wt::Auth::BCryptHashFunction _hashFunc{ static_cast<int>(_bcryptRoundCount) };
|
||||||
Wt::Auth::PasswordStrengthValidator _validator;
|
Wt::Auth::PasswordStrengthValidator _validator;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace lms::auth
|
} // namespace lms::auth
|
||||||
|
|||||||
Reference in New Issue
Block a user