Migrated scrobbling stuff

This commit is contained in:
emeric
2021-10-18 20:39:47 +02:00
parent fe298e10d9
commit a0489b2d94
106 changed files with 54 additions and 57 deletions
+40
View File
@@ -0,0 +1,40 @@
add_library(lmsauth SHARED
impl/AuthTokenService.cpp
impl/AuthServiceBase.cpp
impl/EnvService.cpp
impl/LoginThrottler.cpp
impl/PasswordServiceBase.cpp
impl/http-headers/HttpHeadersEnvService.cpp
impl/internal/InternalPasswordService.cpp
)
target_include_directories(lmsauth INTERFACE
include
)
target_include_directories(lmsauth PRIVATE
include
impl
)
target_link_libraries(lmsauth PRIVATE
lmsutils
lmsdatabase
)
target_link_libraries(lmsauth PUBLIC
pthread
Boost::system
Wt::Wt
)
if (USE_PAM)
target_compile_options(lmsauth PRIVATE "-DLMS_SUPPORT_PAM")
target_sources(lmsauth PRIVATE impl/pam/PAMPasswordService.cpp)
target_include_directories(lmsauth PRIVATE ${PAM_INCLUDE_DIR})
target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES})
endif (USE_PAM)
install(TARGETS lmsauth DESTINATION lib)
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AuthServiceBase.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
using namespace Database;
AuthServiceBase::AuthServiceBase(Db& db)
: _db {db}
{}
UserId
AuthServiceBase::getOrCreateUser(std::string_view loginName)
{
Session& session {getDbSession()};
auto transaction {session.createUniqueTransaction()};
User::pointer user {User::getByLoginName(session, loginName)};
if (!user)
{
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN);
user = User::create(session, loginName);
user.modify()->setType(type);
}
return user->getId();
}
void
AuthServiceBase::onUserAuthenticated(UserId userId)
{
Session& session {getDbSession()};
auto transaction {session.createUniqueTransaction()};
User::pointer user {User::getById(session, userId)};
if (user)
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
}
Session&
AuthServiceBase::getDbSession()
{
return _db.getTLSSession();
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#include "database/Types.hpp"
namespace Database
{
class Db;
class Session;
}
namespace Auth
{
class AuthServiceBase
{
protected:
AuthServiceBase(Database::Db& db);
Database::UserId getOrCreateUser(std::string_view loginName);
void onUserAuthenticated(Database::UserId userId);
Database::Session& getDbSession();
private:
Database::Db& _db;
};
}
@@ -0,0 +1,142 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AuthTokenService.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h>
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
std::unique_ptr<IAuthTokenService> createAuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries)
{
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries);
}
static const Wt::Auth::SHA1HashFunction sha1Function;
AuthTokenService::AuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries)
: AuthServiceBase {db}
, _loginThrottler {maxThrottlerEntries}
{
}
std::string
AuthTokenService::createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry)
{
const std::string secret {Wt::WRandom::generateId(32)};
const std::string secretHash {sha1Function.compute(secret, {})};
Database::Session& session {getDbSession()};
auto transaction {session.createUniqueTransaction()};
Database::User::pointer user {Database::User::getById(session, userId)};
if (!user)
throw Exception {"User deleted"};
Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, 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;
}
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
AuthTokenService::processAuthToken(std::string_view secret)
{
const std::string secretHash {sha1Function.compute(std::string {secret}, {})};
Database::Session& session {getDbSession()};
auto transaction {session.createUniqueTransaction()};
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)};
if (!authToken)
return std::nullopt;
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
{
authToken.remove();
return std::nullopt;
}
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
authToken.remove();
return res;
}
AuthTokenService::AuthTokenProcessResult
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
{
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
}
auto res {processAuthToken(tokenValue)};
{
std::unique_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
if (!res)
{
_loginThrottler.onBadClientAttempt(clientAddress);
return AuthTokenProcessResult {AuthTokenProcessResult::State::Denied};
}
_loginThrottler.onGoodClientAttempt(clientAddress);
onUserAuthenticated(res->userId);
return AuthTokenProcessResult {AuthTokenProcessResult::State::Granted, std::move(*res)};
}
}
void
AuthTokenService::clearAuthTokens(Database::UserId userId)
{
Database::Session& session {getDbSession()};
auto transaction {session.createUniqueTransaction()};
Database::User::pointer user {Database::User::getById(session, userId)};
if (!user)
throw Exception {"User deleted"};
user.modify()->clearAuthTokens();
}
} // namespace Auth
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include "auth/IAuthTokenService.hpp"
#include "AuthServiceBase.hpp"
#include "LoginThrottler.hpp"
namespace Database
{
class Session;
}
namespace Auth
{
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
{
public:
AuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries);
AuthTokenService(const AuthTokenService&) = delete;
AuthTokenService& operator=(const AuthTokenService&) = delete;
AuthTokenService(AuthTokenService&&) = delete;
AuthTokenService& operator=(AuthTokenService&&) = delete;
private:
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
std::string createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry) override;
void clearAuthTokens(Database::UserId userId) override;
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret);
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
};
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "auth/IEnvService.hpp"
#include "auth/Types.hpp"
#include "http-headers/HttpHeadersEnvService.hpp"
namespace Auth
{
std::unique_ptr<IEnvService>
createEnvService(std::string_view backendName, Database::Db& db)
{
if (backendName == "http-headers")
return std::make_unique<HttpHeadersEnvService>(db);
throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"};
}
}
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
/* This file contains some classes in order to get info from file using the libavconv */
#include "LoginThrottler.hpp"
#include "utils/Logger.hpp"
#include "utils/Random.hpp"
namespace Auth {
static
boost::asio::ip::address_v6
getAddressWithMask(const boost::asio::ip::address_v6& address, std::size_t prefix)
{
assert(prefix % 8 == 0);
std::array<uint8_t, 16> truncatedBytes;
auto bytes {address.to_bytes()};
std::copy(std::cbegin(bytes), std::next(std::cbegin(bytes), prefix / 8), truncatedBytes.begin());
return boost::asio::ip::address_v6 {truncatedBytes};
}
static
boost::asio::ip::address
getAddressToThrottle(const boost::asio::ip::address& address)
{
return address.is_v6() ? getAddressWithMask(address.to_v6(), 64) : address;
}
void
LoginThrottler::removeOutdatedEntries()
{
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
for (auto it {std::begin(_attemptsInfo)}; it != std::end(_attemptsInfo); )
{
if (it->second <= now)
it = _attemptsInfo.erase(it);
else
++it;
}
}
void
LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
{
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
if (_attemptsInfo.size() >= _maxEntries)
removeOutdatedEntries();
if (_attemptsInfo.size() >= _maxEntries)
_attemptsInfo.erase(Random::pickRandom(_attemptsInfo));
_attemptsInfo[address] = now.addSecs(3);
LMS_LOG(AUTH, DEBUG) << "Registering bad attempt for '" << clientAddress.to_string() << "'";
}
void
LoginThrottler::onGoodClientAttempt(const boost::asio::ip::address& address)
{
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
_attemptsInfo.erase(clientAddress);
}
bool
LoginThrottler::isClientThrottled(const boost::asio::ip::address& address) const
{
const boost::asio::ip::address clientAddress {getAddressToThrottle(address)};
auto it {_attemptsInfo.find(clientAddress)};
if (it == _attemptsInfo.end())
return false;
return it->second > Wt::WDateTime::currentDateTime();
}
} // Auth
@@ -0,0 +1,54 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <Wt/WDateTime.h>
#include "utils/NetAddress.hpp"
#include "utils/Exception.hpp"
namespace Auth {
class LoginThrottler
{
public:
LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {}
// user must lock these calls to avoid races
bool isClientThrottled(const boost::asio::ip::address& address) const;
void onBadClientAttempt(const boost::asio::ip::address& address);
void onGoodClientAttempt(const boost::asio::ip::address& address);
private:
void removeOutdatedEntries();
const std::size_t _maxEntries;
std::unordered_map<boost::asio::ip::address, Wt::WDateTime> _attemptsInfo;
};
} // Auth
@@ -0,0 +1,97 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PasswordServiceBase.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/WRandom.h>
#include "internal/InternalPasswordService.hpp"
#ifdef LMS_SUPPORT_PAM
#include "pam/PAMPasswordService.hpp"
#endif // LMS_SUPPORT_PAM
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
static const Wt::Auth::SHA1HashFunction sha1Function;
std::unique_ptr<IPasswordService>
createPasswordService(std::string_view passwordAuthenticationBackend, Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
{
if (passwordAuthenticationBackend == "internal")
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService);
#ifdef LMS_SUPPORT_PAM
else if (passwordAuthenticationBackend == "pam")
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService);
#endif // LMS_SUPPORT_PAM
throw Exception {"Authentication backend '" + std::string {passwordAuthenticationBackend} + "' is not supported!"};
}
PasswordServiceBase::PasswordServiceBase(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: AuthServiceBase {db}
, _loginThrottler {maxThrottlerEntries}
, _authTokenService {authTokenService}
{
}
PasswordServiceBase::CheckResult
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 << "'";
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return {CheckResult::State::Throttled};
}
const bool match {checkUserPassword(loginName, password)};
{
std::unique_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return {CheckResult::State::Throttled};
if (match)
{
_loginThrottler.onGoodClientAttempt(clientAddress);
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
else
{
_loginThrottler.onBadClientAttempt(clientAddress);
return {CheckResult::State::Denied};
}
}
}
} // namespace Auth
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include "auth/IPasswordService.hpp"
#include "AuthServiceBase.hpp"
#include "LoginThrottler.hpp"
namespace Database
{
class Db;
class Session;
}
namespace Auth
{
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
{
public:
PasswordServiceBase(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
PasswordServiceBase(const PasswordServiceBase&) = delete;
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
PasswordServiceBase(PasswordServiceBase&&) = delete;
PasswordServiceBase& operator=(PasswordServiceBase&&) = delete;
protected:
IAuthTokenService& getAuthTokenService() { return _authTokenService; }
private:
virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0;
CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password) override;
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
IAuthTokenService& _authTokenService;
};
} // namespace Auth
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "HttpHeadersEnvService.hpp"
#include <Wt/WEnvironment.h>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
namespace Auth
{
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
: AuthServiceBase {db}
, _fieldName {Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User")}
{
LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'";
}
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
{
const std::string loginName {env.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{
const std::string loginName {request.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::UserId userId {getOrCreateUser(loginName)};
onUserAuthenticated(userId);
return {CheckResult::State::Granted, userId};
}
} // namespace Auth
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "auth/IEnvService.hpp"
#include "AuthServiceBase.hpp"
namespace Auth
{
class HttpHeadersEnvService : public IEnvService, public AuthServiceBase
{
public:
HttpHeadersEnvService(Database::Db& db);
private:
CheckResult processEnv(const Wt::WEnvironment& env) override;
CheckResult processRequest(const Wt::Http::Request& request) override;
std::string _fieldName;
};
} // namespace Auth
@@ -0,0 +1,138 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "InternalPasswordService.hpp"
#include <Wt/WRandom.h>
#include "auth/IAuthTokenService.hpp"
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
InternalPasswordService::InternalPasswordService(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: PasswordServiceBase {db, maxThrottlerEntries, authTokenService}
{
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4);
_validator.setMinimumPassPhraseWords(1);
_validator.setMinimumMatchLength(3);
}
bool
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
Database::User::PasswordHash passwordHash;
{
Database::Session& session {getDbSession()};
auto transaction {session.createSharedTransaction()};
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
if (!user)
{
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
// hash random stuff here to waste some time
hashRandomPassword();
return false;
}
// Don't allow users being created or coming from other backends
passwordHash = user->getPasswordHash();
if (passwordHash.salt.empty() || passwordHash.hash.empty())
{
// hash random stuff here to waste some time
hashRandomPassword();
return false;
}
}
return _hashFunc.verify(std::string {password}, std::string {passwordHash.salt}, std::string {passwordHash.hash});
}
bool
InternalPasswordService::canSetPasswords() const
{
return true;
}
IPasswordService::PasswordAcceptabilityResult
InternalPasswordService::checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const
{
switch (context.userType)
{
case Database::UserType::ADMIN:
case Database::UserType::REGULAR:
return _validator.evaluateStrength(std::string {password}, context.loginName, "").isValid() ? PasswordAcceptabilityResult::OK : PasswordAcceptabilityResult::TooWeak;
case Database::UserType::DEMO:
return password == context.loginName ? PasswordAcceptabilityResult::OK : PasswordAcceptabilityResult::MustMatchLoginName;
}
throw NotImplementedException {};
}
void
InternalPasswordService::setPassword(Database::UserId userId, std::string_view newPassword)
{
const Database::User::PasswordHash passwordHash {hashPassword(newPassword)};
Database::Session& session {getDbSession()};
auto transaction {session.createUniqueTransaction()};
Database::User::pointer user {Database::User::getById(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);
getAuthTokenService().clearAuthTokens(userId);
}
Database::User::PasswordHash
InternalPasswordService::hashPassword(std::string_view password) const
{
const std::string salt {Wt::WRandom::generateId(32)};
return {salt, _hashFunc.compute(std::string {password}, salt)};
}
void
InternalPasswordService::hashRandomPassword() const
{
hashPassword(Wt::WRandom::generateId(32));
}
} // namespace Auth
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include "database/User.hpp"
#include "PasswordServiceBase.hpp"
#include "LoginThrottler.hpp"
namespace Auth
{
class IAuthTokenService;
class InternalPasswordService : public PasswordServiceBase
{
public:
InternalPasswordService(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
private:
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
bool canSetPasswords() const override;
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
void setPassword(Database::UserId userId, std::string_view newPassword) override;
Database::User::PasswordHash hashPassword(std::string_view password) const;
void hashRandomPassword() const;
const Wt::Auth::BCryptHashFunction _hashFunc {7}; // TODO parametrize this
Wt::Auth::PasswordStrengthValidator _validator;
};
}
@@ -0,0 +1,202 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PAMPasswordService.hpp"
#ifndef LMS_SUPPORT_PAM
#error "Should not compile this"
#endif
#include <cstring>
#include <security/pam_appl.h>
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
{
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
}
std::string_view message() const { return _errorMsg; }
private:
std::string _errorMsg;
};
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
if (err != PAM_SUCCESS)
throw PAMError {"start failed", _pamh, err};
}
~PAMContext()
{
int err {pam_end(_pamh, 0)};
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
}
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext {password};
ScopedConvContextSetter scopedContext {*this, authContext};
int err {pam_authenticate(_pamh, 0)};
if (err != PAM_SUCCESS)
throw PAMError {"authenticate failed", _pamh, err};
}
void validateAccount()
{
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
if (err != PAM_SUCCESS)
throw PAMError {"acct_mgmt failed", _pamh, err};
}
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password {password} {}
std::string_view getPassword() const { return _password; }
private:
std::string_view _password;
};
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext {pamContext}
{
_pamContext._convContext = &convContext;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
private:
PAMContext& _pamContext;
};
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
PAMContext& context {*static_cast<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
return PAM_CONV_ERR;
}
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
if (!response)
return PAM_CONV_ERR;
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
*resps = response;
return PAM_SUCCESS;
}
ConvContext* _convContext {};
pam_conv _conv {&PAMContext::conv, this};
pam_handle_t *_pamh {};
};
bool
PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
{
try
{
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
PAMContext pamContext {loginName};
pamContext.authenticate(password);
pamContext.validateAccount();
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
return false;
}
}
bool
PAMPasswordService::canSetPasswords() const
{
return false;
}
IPasswordService::PasswordAcceptabilityResult
PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
{
throw NotImplementedException {};
}
void
PAMPasswordService::setPassword(Database::UserId, std::string_view)
{
throw NotImplementedException {};
}
} // namespace Auth
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include "PasswordServiceBase.hpp"
namespace Auth
{
class PAMPasswordService: public PasswordServiceBase
{
public:
using PasswordServiceBase::PasswordServiceBase;
private:
bool checkUserPassword(std::string_view loginName,std::string_view password) override;
bool canSetPasswords() const override;
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
void setPassword(Database::UserId userId, std::string_view newPassword) override;
};
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
/* This file contains some classes in order to get info from file using the libavconv */
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <boost/asio/ip/address.hpp>
#include <Wt/WDateTime.h>
#include "database/Types.hpp"
namespace Database
{
class Db;
class User;
}
namespace Auth
{
class IAuthTokenService
{
public:
virtual ~IAuthTokenService() = default;
// Auth Token services
struct AuthTokenProcessResult
{
enum class State
{
Granted,
Throttled,
Denied,
};
struct AuthTokenInfo
{
Database::UserId userId;
Wt::WDateTime expiry;
};
State state {State::Denied};
std::optional<AuthTokenInfo> authTokenInfo {};
};
// Provided token is only accepted once
virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
// Returns a one time token
virtual std::string createAuthToken(Database::UserId userid, const Wt::WDateTime& expiry) = 0;
virtual void clearAuthTokens(Database::UserId userid) = 0;
};
std::unique_ptr<IAuthTokenService> createAuthTokenService(Database::Db& db, std::size_t maxThrottlerEntryCount);
}
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include "database/Types.hpp"
namespace Database
{
class Db;
class Session;
}
namespace Wt
{
class WEnvironment;
}
namespace Wt::Http
{
class Request;
}
namespace Auth
{
class IEnvService
{
public:
virtual ~IEnvService() = default;
// Auth Token services
struct CheckResult
{
enum class State
{
Granted,
Denied,
Throttled,
};
State state {State::Denied};
std::optional<Database::UserId> userId {};
};
virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0;
virtual CheckResult processRequest(const Wt::Http::Request& request) = 0;
};
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, Database::Db& db);
} // namespace Auth
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#include <Wt/WDateTime.h>
#include <Wt/Dbo/ptr.h>
#include <boost/asio/ip/address.hpp>
#include "auth/Types.hpp"
#include "database/Types.hpp"
namespace Database
{
class Db;
class User;
}
namespace Auth
{
class IAuthTokenService;
class IPasswordService
{
public:
virtual ~IPasswordService() = default;
struct CheckResult
{
enum class State
{
Granted,
Denied,
Throttled,
};
State state {State::Denied};
std::optional<Database::UserId> userId {};
std::optional<Wt::WDateTime> expiry {};
};
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password) = 0;
virtual bool canSetPasswords() const = 0;
enum class PasswordAcceptabilityResult
{
OK,
TooWeak,
MustMatchLoginName,
};
virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0;
virtual void setPassword(Database::UserId userId, std::string_view newPassword) = 0;
};
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, Database::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
}
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include "database/Types.hpp"
#include "utils/Exception.hpp"
namespace Auth
{
class Exception : public ::LmsException
{
using LmsException::LmsException;
};
class NotImplementedException : public Exception
{
public:
NotImplementedException() : Auth::Exception {"Not implemented"} {}
};
class UserNotFoundException : public Exception
{
public:
UserNotFoundException() : Auth::Exception {"User not found"} {}
};
struct PasswordValidationContext
{
std::string loginName;
Database::UserType userType;
};
class PasswordException : public Exception
{
public:
using Exception::Exception;
};
class PasswordTooWeakException : public PasswordException
{
public:
PasswordTooWeakException() : PasswordException {"Password too weak"} {}
};
class PasswordMustMatchLoginNameException : public PasswordException
{
public:
PasswordMustMatchLoginNameException() : PasswordException {"Password must match login name"} {}
};
}