Simplified the login throttler, corrected races on throttler access, recreate the auth cookie with the remaining duration

This commit is contained in:
emeric
2019-07-28 16:09:44 +02:00
parent 336673770d
commit 9270d6814c
9 changed files with 179 additions and 100 deletions
+113 -16
View File
@@ -26,46 +26,66 @@
#include <Wt/WRandom.h> #include <Wt/WRandom.h>
#include "database/Session.hpp" #include "database/Session.hpp"
#include "utils/Exception.hpp"
#include "utils/Utils.hpp" #include "utils/Utils.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
namespace Auth { namespace Auth {
AuthService::AuthService(std::size_t maxThrottlerEntries) AuthService::AuthService(std::size_t maxThrottlerEntries)
: _loginThrottler {maxThrottlerEntries} : _passwordLoginThrottler {maxThrottlerEntries}
, _tokenLoginThrottler {maxThrottlerEntries}
{ {
} }
AuthService::PasswordCheckResult static
AuthService::checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) bool
checkUserPassword(Database::Session& session, const std::string& loginName, const std::string& password)
{ {
if (_loginThrottler.isClientThrottled(clientAddress))
return PasswordCheckResult::Throttled;
Database::User::PasswordHash passwordHash; Database::User::PasswordHash passwordHash;
{ {
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
if (!user) if (!user)
{ return false;
_loginThrottler.onBadClientAttempt(clientAddress);
return PasswordCheckResult::Mismatch;
}
passwordHash = user->getPasswordHash(); passwordHash = user->getPasswordHash();
} }
const Wt::Auth::BCryptHashFunction hashFunc {6}; const Wt::Auth::BCryptHashFunction hashFunc {6};
if (hashFunc.verify(password, passwordHash.salt, passwordHash.hash)) return hashFunc.verify(password, passwordHash.salt, passwordHash.hash);
}
AuthService::PasswordCheckResult
AuthService::checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password)
{
// Do not waste too much resource on brute force attacks (optim)
{ {
_loginThrottler.onGoodClientAttempt(clientAddress); std::shared_lock<std::shared_timed_mutex> lock {_passwordCheckMutex};
return PasswordCheckResult::Match;
if (_passwordLoginThrottler.isClientThrottled(clientAddress))
return PasswordCheckResult::Throttled;
} }
else
const bool match {Auth::checkUserPassword(session, loginName, password)};
{ {
_loginThrottler.onBadClientAttempt(clientAddress); std::unique_lock<std::shared_timed_mutex> lock {_passwordCheckMutex};
return PasswordCheckResult::Mismatch;
if (_passwordLoginThrottler.isClientThrottled(clientAddress))
return PasswordCheckResult::Throttled;
if (match)
{
_passwordLoginThrottler.onGoodClientAttempt(clientAddress);
return PasswordCheckResult::Match;
}
else
{
_passwordLoginThrottler.onBadClientAttempt(clientAddress);
return PasswordCheckResult::Mismatch;
}
} }
} }
@@ -93,5 +113,82 @@ AuthService::evaluatePasswordStrength(const std::string& loginName, const std::s
return validator.evaluateStrength(password, loginName, "").isValid(); return validator.evaluateStrength(password, loginName, "").isValid();
} }
std::string
AuthService::createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry)
{
const std::string secret {Wt::WRandom::generateId(64)};
auto transaction {session.createUniqueTransaction()};
Database::User::pointer user {Database::User::getById(session, userId)};
if (!user)
throw LmsException {"User deleted"};
Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secret, expiry, user)};
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
if (user->getAuthTokensCount() >= 50)
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
return secret;
}
static
boost::optional<AuthService::AuthTokenProcessResult::AuthTokenInfo>
processAuthToken(Database::Session& session, const std::string& tokenValue)
{
auto transaction {session.createUniqueTransaction()};
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, tokenValue)};
if (!authToken)
return boost::none;
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
{
authToken.remove();
return boost::none;
}
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
AuthService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser().id(), authToken->getExpiry()};
authToken.remove();
return res;
}
AuthService::AuthTokenProcessResult
AuthService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue)
{
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock<std::shared_timed_mutex> lock {_tokenCheckMutex};
if (_tokenLoginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
}
auto res {Auth::processAuthToken(session, tokenValue)};
{
std::unique_lock<std::shared_timed_mutex> lock {_tokenCheckMutex};
if (_tokenLoginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
if (!res)
{
_tokenLoginThrottler.onBadClientAttempt(clientAddress);
return AuthTokenProcessResult {AuthTokenProcessResult::State::NotFound};
}
_tokenLoginThrottler.onGoodClientAttempt(clientAddress);
return AuthTokenProcessResult {AuthTokenProcessResult::State::Found, std::move(*res)};
}
}
} // namespace Auth } // namespace Auth
+31 -1
View File
@@ -23,10 +23,12 @@
#include <string> #include <string>
#include <boost/optional.hpp>
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
#include "LoginThrottler.hpp" #include "LoginThrottler.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "database/Types.hpp"
namespace Database namespace Database
{ {
@@ -62,9 +64,37 @@ namespace Auth {
Database::User::PasswordHash hashPassword(const std::string& password) const; Database::User::PasswordHash hashPassword(const std::string& password) const;
bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const; bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const;
// Auth Token services
struct AuthTokenProcessResult
{
enum class State
{
Found,
Throttled,
NotFound,
};
struct AuthTokenInfo
{
Database::IdType userId;
Wt::WDateTime expiry;
};
State state;
boost::optional<AuthTokenInfo> authTokenInfo;
};
// Removed if found
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue);
std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry);
private: private:
LoginThrottler _loginThrottler; std::shared_timed_mutex _passwordCheckMutex;
std::shared_timed_mutex _tokenCheckMutex;
LoginThrottler _passwordLoginThrottler;
LoginThrottler _tokenLoginThrottler;
}; };
} }
+4 -26
View File
@@ -54,7 +54,7 @@ LoginThrottler::removeOutdatedEntries()
for (auto it {std::begin(_attemptsInfo)}; it != std::end(_attemptsInfo); ) for (auto it {std::begin(_attemptsInfo)}; it != std::end(_attemptsInfo); )
{ {
if (it->second.nextValidAttempt <= now) if (it->second <= now)
it = _attemptsInfo.erase(it); it = _attemptsInfo.erase(it);
else else
++it; ++it;
@@ -68,32 +68,14 @@ LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
std::unique_lock<std::shared_timed_mutex> lock {_mutex};
if (_attemptsInfo.size() >= _maxEntries) if (_attemptsInfo.size() >= _maxEntries)
removeOutdatedEntries(); removeOutdatedEntries();
// If still full, kill one random entry
if (_attemptsInfo.size() >= _maxEntries) if (_attemptsInfo.size() >= _maxEntries)
_attemptsInfo.erase(pickRandom(_attemptsInfo)); _attemptsInfo.erase(pickRandom(_attemptsInfo));
AttemptsInfo& attemptsInfo {_attemptsInfo[address]}; _attemptsInfo[address] = now.addSecs(3);
attemptsInfo.nbSuccessiveBadAttempts++; LMS_LOG(AUTH, INFO) << "Registering bad attempt for '" << clientAddress.to_string() << "'";
if (attemptsInfo.nbSuccessiveBadAttempts >= 50)
attemptsInfo.nextValidAttempt = now.addSecs(60);
if (attemptsInfo.nbSuccessiveBadAttempts >= 20)
attemptsInfo.nextValidAttempt = now.addSecs(10);
else if (attemptsInfo.nbSuccessiveBadAttempts >= 10)
attemptsInfo.nextValidAttempt = now.addSecs(5);
else if (attemptsInfo.nbSuccessiveBadAttempts >= 5)
attemptsInfo.nextValidAttempt = now.addSecs(2);
else if (attemptsInfo.nbSuccessiveBadAttempts >= 2)
attemptsInfo.nextValidAttempt = now.addSecs(1);
else
attemptsInfo.nextValidAttempt = now;
LMS_LOG(AUTH, INFO) << "Registering bad attempt for '" << clientAddress.to_string() << "' (" << attemptsInfo.nbSuccessiveBadAttempts << " successive bad attempts)";
} }
void void
@@ -101,8 +83,6 @@ LoginThrottler::onGoodClientAttempt(const boost::asio::ip::address& address)
{ {
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)}; const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
std::unique_lock<std::shared_timed_mutex> lock {_mutex};
_attemptsInfo.erase(address); _attemptsInfo.erase(address);
} }
@@ -111,13 +91,11 @@ LoginThrottler::isClientThrottled(const boost::asio::ip::address& address) const
{ {
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)}; const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
std::shared_lock<std::shared_timed_mutex> lock {_mutex};
auto it {_attemptsInfo.find(address)}; auto it {_attemptsInfo.find(address)};
if (it == _attemptsInfo.end()) if (it == _attemptsInfo.end())
return false; return false;
return it->second.nextValidAttempt > Wt::WDateTime::currentDateTime(); return it->second > Wt::WDateTime::currentDateTime();
} }
} // Auth } // Auth
+2 -8
View File
@@ -37,6 +37,7 @@ class LoginThrottler
public: public:
LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {} LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {}
// user must lock these calls to avoid races
bool isClientThrottled(const boost::asio::ip::address& address) const; bool isClientThrottled(const boost::asio::ip::address& address) const;
void onBadClientAttempt(const boost::asio::ip::address& address); void onBadClientAttempt(const boost::asio::ip::address& address);
void onGoodClientAttempt(const boost::asio::ip::address& address); void onGoodClientAttempt(const boost::asio::ip::address& address);
@@ -45,16 +46,9 @@ class LoginThrottler
void removeOutdatedEntries(); void removeOutdatedEntries();
struct AttemptsInfo
{
std::size_t nbSuccessiveBadAttempts {};
Wt::WDateTime nextValidAttempt;
};
const std::size_t _maxEntries; const std::size_t _maxEntries;
mutable std::shared_timed_mutex _mutex; std::unordered_map<boost::asio::ip::address, Wt::WDateTime> _attemptsInfo;
std::unordered_map<boost::asio::ip::address, AttemptsInfo> _attemptsInfo;
}; };
+1
View File
@@ -213,6 +213,7 @@ Session::prepareTables()
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
+4 -1
View File
@@ -61,10 +61,12 @@ class AuthToken
static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, Wt::Dbo::ptr<User> user); static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, Wt::Dbo::ptr<User> user);
static void removeExpiredTokens(Session& session, Wt::WDateTime now); static void removeExpiredTokens(Session& session, Wt::WDateTime now);
static pointer getByValue(Session& session, const std::string& value); static pointer getByValue(Session& session, const std::string& value);
static pointer getById(Session& session, IdType tokenId);
// Accessors // Accessors
Wt::Dbo::ptr<User> getUser() const { return _user; }
const Wt::WDateTime& getExpiry() const { return _expiry; } const Wt::WDateTime& getExpiry() const { return _expiry; }
Wt::Dbo::ptr<User> getUser() const { return _user; }
const std::string& getValue() const { return _value; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -121,6 +123,7 @@ class User : public Wt::Dbo::Dbo<User>
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 {_passwordSalt, _passwordHash}; }
Wt::WDateTime getLastLogin() const { return _lastLogin; } Wt::WDateTime getLastLogin() const { return _lastLogin; }
std::size_t getAuthTokensCount() const { return _authTokens.size(); }
// write // write
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
+21 -44
View File
@@ -40,31 +40,13 @@ static const std::string authCookieName {"LmsAuth"};
static static
void void
createAuthToken(Database::IdType userId) createAuthToken(Database::IdType userId, const Wt::WDateTime& expiry)
{ {
const std::string secret {getService<::Auth::AuthService>()->createAuthToken(LmsApp->getDbSession(), userId, expiry)};
const std::string secret {Wt::WRandom::generateId(48)};
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
Wt::WDateTime expiry;
{
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)};
if (!user)
return;
expiry = user->isDemo() ? now.addDays(7) : now.addYears(1);
Database::AuthToken::create(LmsApp->getDbSession(), secret, expiry, user);
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
Database::AuthToken::removeExpiredTokens(LmsApp->getDbSession(), now);
}
LmsApp->setCookie(authCookieName, LmsApp->setCookie(authCookieName,
secret, secret,
expiry.toTime_t() - now.toTime_t(), expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(),
"", "",
"", "",
LmsApp->environment().urlScheme() == "https"); LmsApp->environment().urlScheme() == "https");
@@ -78,33 +60,20 @@ processAuthToken(const Wt::WEnvironment& env)
if (!authCookie) if (!authCookie)
return boost::none; return boost::none;
Database::IdType userId {}; const auto res {getService<::Auth::AuthService>()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)};
switch (res.state)
{ {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; case ::Auth::AuthService::AuthTokenProcessResult::State::NotFound:
case ::Auth::AuthService::AuthTokenProcessResult::State::Throttled:
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(LmsApp->getDbSession(), *authCookie)}; LmsApp->setCookie(authCookieName, std::string {}, 0, "", "", env.urlScheme() == "https");
if (!authToken)
{
LMS_LOG(UI, INFO) << "Client '" << env.clientAddress() << "' presented a token that has not been found";
return boost::none; return boost::none;
}
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime()) case ::Auth::AuthService::AuthTokenProcessResult::State::Found:
{ createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry);
LMS_LOG(UI, INFO) << "Expired auth token for user '" << authToken->getUser()->getLoginName() << "'!"; break;
authToken.remove();
return boost::none;
}
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
userId = authToken->getUser().id();
authToken.remove();
} }
createAuthToken(userId); return res.authTokenInfo->userId;
return userId;
} }
class AuthModel : public Wt::WFormModel class AuthModel : public Wt::WFormModel
@@ -129,16 +98,24 @@ class AuthModel : public Wt::WFormModel
void saveData() void saveData()
{ {
bool isDemo;
{ {
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())};
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
_userId = user.id(); _userId = user.id();
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
isDemo = user->isDemo();
} }
if (Wt::asNumber(value(RememberMeField))) if (Wt::asNumber(value(RememberMeField)))
createAuthToken(*_userId); {
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
createAuthToken(*_userId, isDemo ? now.addDays(3) : now.addYears(1));
}
} }
bool validateField(Field field) bool validateField(Field field)
+2 -3
View File
@@ -27,9 +27,8 @@
namespace UserInterface { namespace UserInterface {
boost::optional<Database::IdType>
// If success, returns the authenticated user id processAuthToken(const Wt::WEnvironment& env);
boost::optional<Database::IdType> processAuthToken(const Wt::WEnvironment& env);
class Auth : public Wt::WTemplateFormView class Auth : public Wt::WTemplateFormView
{ {
+1 -1
View File
@@ -168,7 +168,7 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
return; return;
} }
auto userId {processAuthToken(env)}; const auto userId {processAuthToken(env)};
if (userId) if (userId)
{ {
handleUserLoggedIn(*userId); handleUserLoggedIn(*userId);