OpenSubsonic API: added apiKey support, ref #544

This commit is contained in:
emeric
2024-11-24 15:23:14 +01:00
parent 0a320a8b87
commit 360623c569
54 changed files with 870 additions and 623 deletions
@@ -20,7 +20,6 @@
#include "AuthTokenService.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h>
#include "core/Exception.hpp"
@@ -32,72 +31,95 @@
namespace lms::auth
{
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
namespace
{
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries);
AuthTokenService::AuthTokenInfo createAuthTokenInfo(const db::AuthToken::pointer& authToken)
{
return AuthTokenService::AuthTokenInfo{
.userId = authToken->getUser()->getId(),
.expiry = authToken->getExpiry(),
.lastUsed = authToken->getLastUsed(),
.useCount = authToken->getUseCount(),
.maxUseCount = authToken->getMaxUseCount(),
};
}
} // namespace
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount)
{
return std::make_unique<AuthTokenService>(db, maxThrottlerEntryCount);
}
static const Wt::Auth::SHA1HashFunction sha1Function;
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount)
: AuthServiceBase{ db }
, _loginThrottler{ maxThrottlerEntries }
, _loginThrottler{ maxThrottlerEntryCount }
{
}
std::string
AuthTokenService::createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
void AuthTokenService::registerDomain(core::LiteralString domain, const DomainParameters& params)
{
const std::string secret{ Wt::WRandom::generateId(32) };
const std::string secretHash{ sha1Function.compute(secret, {}) };
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
db::AuthToken::pointer authToken{ session.create<db::AuthToken>(secretHash, expiry, user) };
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
if (user->getAuthTokensCount() >= 50)
db::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
return secret;
auto [it, inserted]{ _domainParameters.emplace(domain, params) };
if (!inserted)
throw Exception{ "Auth token domain already registered!" };
}
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
AuthTokenService::processAuthToken(std::string_view secret)
void AuthTokenService::createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token)
{
const std::string secretHash{ sha1Function.compute(std::string{ secret }, {}) };
const DomainParameters& params{ getDomainParameters(domain) };
db::Session& session{ getDbSession() };
const auto now{ Wt::WDateTime::currentDateTime() };
const auto expiry{ params.tokenDuration ? now.addSecs(std::chrono::duration_cast<std::chrono::seconds>(params.tokenDuration.value()).count()) : Wt::WDateTime{} };
{
auto transaction{ session.createWriteTransaction() };
const db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
const db::AuthToken::pointer authToken{ session.create<db::AuthToken>(domain.str(), token, expiry, params.tokenMaxUseCount, user) };
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << authToken->getExpiry().toString() << ", maxUseCount = " << (authToken->getMaxUseCount() ? std::to_string(*authToken->getMaxUseCount()) : "<unset>"));
// TODO per domain
if (user->getAuthTokensCount() >= 50)
db::AuthToken::removeExpiredTokens(session, domain.str(), Wt::WDateTime::currentDateTime());
}
}
std::optional<AuthTokenService::AuthTokenInfo> AuthTokenService::processAuthToken(core::LiteralString domain, std::string_view token)
{
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
db::AuthToken::pointer authToken{ db::AuthToken::find(session, secretHash) };
db::AuthToken::pointer authToken{ db::AuthToken::find(session, domain.str(), token) };
if (!authToken)
return std::nullopt;
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
if (authToken->getExpiry().isValid() && authToken->getExpiry() < Wt::WDateTime::currentDateTime())
{
authToken.remove();
return std::nullopt;
}
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!");
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "' on domain '" << domain.str() << "'");
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res{ authToken->getUser()->getId(), authToken->getExpiry() };
authToken.remove();
AuthTokenInfo res{ createAuthTokenInfo(authToken) };
const std::size_t tokenUseCount{ authToken.modify()->incUseCount() };
authToken.modify()->setLastUsed(Wt::WDateTime::currentDateTime());
if (auto maxUseCount{ authToken->getMaxUseCount() })
{
if (*maxUseCount >= tokenUseCount)
authToken.remove();
}
return res;
}
AuthTokenService::AuthTokenProcessResult
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
AuthTokenService::AuthTokenProcessResult AuthTokenService::processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
{
// Do not waste too much resource on brute force attacks (optim)
{
@@ -107,7 +129,7 @@ namespace lms::auth
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
}
auto res{ processAuthToken(tokenValue) };
auto res{ processAuthToken(domain, tokenValue) };
{
std::unique_lock lock{ _mutex };
@@ -122,22 +144,40 @@ namespace lms::auth
_loginThrottler.onGoodClientAttempt(clientAddress);
onUserAuthenticated(res->userId);
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, std::move(*res) };
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, res };
}
}
void
AuthTokenService::clearAuthTokens(db::UserId userId)
void AuthTokenService::visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor)
{
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
{
auto transaction{ session.createReadTransaction() };
db::User::pointer user{ db::User::find(session, userId) };
if (!user)
throw Exception{ "User deleted" };
user.modify()->clearAuthTokens();
db::AuthToken::find(session, domain.str(), userId, [&](const db::AuthToken::pointer& authToken) {
const AuthTokenInfo info{ createAuthTokenInfo(authToken) };
visitor(info, authToken->getValue());
});
}
}
void AuthTokenService::clearAuthTokens(core::LiteralString domain, db::UserId userId)
{
db::Session& session{ getDbSession() };
{
auto transaction{ session.createWriteTransaction() };
db::AuthToken::clearUserTokens(session, domain.str(), userId);
}
}
const AuthTokenService::DomainParameters& AuthTokenService::getDomainParameters(core::LiteralString domain) const
{
auto it{ _domainParameters.find(domain) };
if (it == std::cend(_domainParameters))
throw Exception{ "Invalid auth token domain" };
return it->second;
}
} // namespace lms::auth
@@ -36,7 +36,7 @@ namespace lms::auth
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
{
public:
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries);
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
AuthTokenService(const AuthTokenService&) = delete;
AuthTokenService& operator=(const AuthTokenService&) = delete;
@@ -44,13 +44,17 @@ namespace lms::auth
AuthTokenService& operator=(AuthTokenService&&) = delete;
private:
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
std::string createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) override;
void clearAuthTokens(db::UserId userId) override;
void registerDomain(core::LiteralString domain, const DomainParameters& params) override;
AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
void visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor) override;
void createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token) override;
void clearAuthTokens(core::LiteralString domain, db::UserId userId) override;
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret);
std::optional<AuthTokenInfo> processAuthToken(core::LiteralString domain, std::string_view tokenValue);
const DomainParameters& getDomainParameters(core::LiteralString domain) const;
std::shared_mutex _mutex;
std::map<core::LiteralString, DomainParameters> _domainParameters;
LoginThrottler _loginThrottler;
};
} // namespace lms::auth
@@ -37,23 +37,20 @@ namespace lms::auth
{
static const Wt::Auth::SHA1HashFunction sha1Function;
std::unique_ptr<IPasswordService>
createPasswordService(std::string_view passwordAuthenticationBackend, db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
std::unique_ptr<IPasswordService> createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount)
{
if (passwordAuthenticationBackend == "internal")
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService);
if (backend == "internal")
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntryCount);
#ifdef LMS_SUPPORT_PAM
else if (passwordAuthenticationBackend == "pam")
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService);
if (backend == "PAM")
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntryCount);
#endif // LMS_SUPPORT_PAM
throw Exception{ "Authentication backend '" + std::string{ passwordAuthenticationBackend } + "' is not supported!" };
throw Exception{ "Authentication backend '" + std::string{ backend } + "' not supported!" };
}
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries)
: AuthServiceBase{ db }
, _loginThrottler{ maxThrottlerEntries }
, _authTokenService{ authTokenService }
{
}
@@ -36,16 +36,13 @@ namespace lms::auth
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
{
public:
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries);
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;
@@ -55,6 +52,5 @@ namespace lms::auth
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
IAuthTokenService& _authTokenService;
};
} // namespace lms::auth
@@ -38,25 +38,25 @@ namespace lms::auth
{
const std::string loginName{ env.headerValue(_fieldName) };
if (loginName.empty())
return { CheckResult::State::Denied };
return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
const db::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
return CheckResult{ .state = CheckResult::State::Granted, .userId = userId };
}
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
{
const std::string loginName{ request.headerValue(_fieldName) };
if (loginName.empty())
return { CheckResult::State::Denied };
return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
const db::UserId userId{ getOrCreateUser(loginName) };
onUserAuthenticated(userId);
return { CheckResult::State::Granted, userId };
return { .state = CheckResult::State::Granted, .userId = userId };
}
} // namespace lms::auth
@@ -25,13 +25,12 @@
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/Types.hpp"
namespace lms::auth
{
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: PasswordServiceBase{ db, maxThrottlerEntries, authTokenService }
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries)
: PasswordServiceBase{ db, maxThrottlerEntries }
{
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
@@ -114,14 +113,13 @@ namespace lms::auth
}
user.modify()->setPasswordHash(passwordHash);
getAuthTokenService().clearAuthTokens(userId);
}
db::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) };
return db::User::PasswordHash{ .salt = salt, .hash = _hashFunc.compute(std::string{ password }, salt) };
}
void
@@ -24,17 +24,15 @@
#include "database/User.hpp"
#include "LoginThrottler.hpp"
#include "PasswordServiceBase.hpp"
#include "services/auth/IPasswordService.hpp"
namespace lms::auth
{
class IAuthTokenService;
class InternalPasswordService : public PasswordServiceBase
{
public:
InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries);
private:
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
@@ -19,18 +19,21 @@
#pragma once
#include <Wt/WDateTime.h>
#include <boost/asio/ip/address.hpp>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <Wt/WDateTime.h>
#include <boost/asio/ip/address.hpp>
#include "core/LiteralString.hpp"
#include "database/UserId.hpp"
namespace lms::db
{
class Db;
class User;
} // namespace lms::db
namespace lms::auth
@@ -40,7 +43,15 @@ namespace lms::auth
public:
virtual ~IAuthTokenService() = default;
// Auth Token services
struct AuthTokenInfo
{
db::UserId userId;
Wt::WDateTime expiry;
Wt::WDateTime lastUsed; // if called by processAuthToken, value is before processing
std::size_t useCount; // if called by processAuthToken, value is before processing
std::optional<std::size_t> maxUseCount;
};
struct AuthTokenProcessResult
{
enum class State
@@ -50,22 +61,25 @@ namespace lms::auth
Denied,
};
struct AuthTokenInfo
{
db::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;
struct DomainParameters
{
std::optional<std::size_t> tokenMaxUseCount;
std::optional<std::chrono::seconds> tokenDuration;
};
// Returns a one time token
virtual std::string createAuthToken(db::UserId userid, const Wt::WDateTime& expiry) = 0;
virtual void clearAuthTokens(db::UserId userid) = 0;
virtual void registerDomain(core::LiteralString domain, const DomainParameters& params) = 0;
// Processing an auth token will make its useCount increase by 1. Token is then automatically deleted if its maxUsecount is reached
virtual AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
virtual void visitAuthTokens(core::LiteralString domain, db::UserId userid, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor) = 0;
virtual void createAuthToken(core::LiteralString domain, db::UserId userid, std::string_view token) = 0;
virtual void clearAuthTokens(core::LiteralString domain, db::UserId userid) = 0;
};
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
@@ -58,12 +58,12 @@ namespace lms::auth
};
State state{ State::Denied };
std::optional<db::UserId> userId{};
db::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, db::Db& db);
std::unique_ptr<IEnvService> createEnvService(std::string_view backend, db::Db& db);
} // namespace lms::auth
@@ -37,8 +37,6 @@ namespace lms::db
namespace lms::auth
{
class IAuthTokenService;
class IPasswordService
{
public:
@@ -53,7 +51,7 @@ namespace lms::auth
Throttled,
};
State state{ State::Denied };
std::optional<db::UserId> userId{};
db::UserId userId{};
std::optional<Wt::WDateTime> expiry{};
};
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
@@ -73,5 +71,5 @@ namespace lms::auth
virtual void setPassword(db::UserId userId, std::string_view newPassword) = 0;
};
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, db::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
std::unique_ptr<IPasswordService> createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount);
} // namespace lms::auth