Addes support for legacy md5 auth token authentication to ease compatibility, ref #865

This commit is contained in:
emeric
2026-07-14 23:18:39 +02:00
parent bb637734bf
commit b767913b64
17 changed files with 446 additions and 96 deletions
+6 -16
View File
@@ -375,18 +375,6 @@ namespace lms::core::stringUtils
return res;
}
std::string bufferToString(std::span<const unsigned char> data)
{
std::ostringstream oss;
for (unsigned char c : data)
{
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
}
return oss.str();
}
bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB)
{
if (strA.size() != strB.size())
@@ -608,16 +596,18 @@ namespace lms::core::stringUtils
return res;
}
std::string toHexString(std::string_view str)
std::string bufferToHexString(std::span<const std::byte> data)
{
constexpr char lut[]{ "0123456789ABCDEF" };
std::string res;
res.reserve(data.size() * 2);
for (char c : str)
for (const std::byte b : data)
{
res.push_back(lut[(c >> 4) & 0xF]);
res.push_back(lut[c & 0xF]);
const unsigned value{ std::to_integer<unsigned>(b) };
res.push_back(lut[(value >> 4) & 0xF]);
res.push_back(lut[value & 0xF]);
}
return res;
+2 -3
View File
@@ -20,6 +20,7 @@
#pragma once
#include <chrono>
#include <cstddef>
#include <optional>
#include <span>
#include <sstream>
@@ -61,8 +62,6 @@ namespace lms::core::stringUtils
void stringToLower(std::string& str);
[[nodiscard]] std::string stringToUpper(const std::string& str);
[[nodiscard]] std::string bufferToString(std::span<const unsigned char> data);
[[nodiscard]] bool stringCaseInsensitiveEqual(std::string_view strA, std::string_view strB);
[[nodiscard]] std::string_view::size_type stringCaseInsensitiveContains(std::string_view str, std::string_view strtoFind);
@@ -122,7 +121,7 @@ namespace lms::core::stringUtils
[[nodiscard]] bool stringEndsWith(std::string_view str, std::string_view ending);
[[nodiscard]] std::optional<std::string> stringFromHex(std::string_view str);
[[nodiscard]] std::string toHexString(std::string_view str);
[[nodiscard]] std::string bufferToHexString(std::span<const std::byte> data);
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
+5 -6
View File
@@ -17,6 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <array>
#include <limits>
#include <gtest/gtest.h>
@@ -424,13 +425,11 @@ namespace lms::core::stringUtils::tests
EXPECT_FALSE(stringCaseInsensitiveContains("", "Foo"));
}
TEST(StringUtils, toHexString)
TEST(StringUtils, bufferToHexString)
{
EXPECT_EQ(toHexString(""), "");
EXPECT_EQ(toHexString("123"), "313233");
EXPECT_EQ(toHexString("1234"), "31323334");
EXPECT_EQ(toHexString("12345"), "3132333435");
EXPECT_EQ(toHexString("Test"), "54657374");
EXPECT_EQ(bufferToHexString({}), "");
EXPECT_EQ(bufferToHexString(std::array{ std::byte{ 0x31 }, std::byte{ 0x32 }, std::byte{ 0x33 } }), "313233");
EXPECT_EQ(bufferToHexString(std::array{ std::byte{ 0x00 }, std::byte{ 0xab }, std::byte{ 0xcd }, std::byte{ 0xff } }), "00ABCDFF");
// test back stringFromHex
EXPECT_EQ(stringFromHex(""), "");
@@ -87,8 +87,19 @@ namespace lms::auth
}
}
bool AuthTokenService::isClientThrottled(const boost::asio::ip::address& clientAddress) const
{
std::shared_lock lock{ _mutex };
return _loginThrottler.isClientThrottled(clientAddress);
}
std::optional<AuthTokenService::AuthTokenInfo> AuthTokenService::processAuthToken(core::LiteralString domain, std::string_view token)
{
// An empty token must never match: some callers use it as a "no candidate" sentinel value,
if (token.empty())
return std::nullopt;
db::Session& session{ getDbSession() };
auto transaction{ session.createWriteTransaction() };
@@ -47,6 +47,7 @@ namespace lms::auth
private:
void registerDomain(core::LiteralString domain, const DomainParameters& params) override;
bool isClientThrottled(const boost::asio::ip::address& clientAddress) const 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;
@@ -55,7 +56,7 @@ namespace lms::auth
std::optional<AuthTokenInfo> processAuthToken(core::LiteralString domain, std::string_view tokenValue);
const DomainParameters& getDomainParameters(core::LiteralString domain) const;
std::shared_mutex _mutex;
mutable std::shared_mutex _mutex;
std::map<core::LiteralString, DomainParameters> _domainParameters;
LoginThrottler _loginThrottler;
};
@@ -72,6 +72,8 @@ namespace lms::auth
virtual void registerDomain(core::LiteralString domain, const DomainParameters& params) = 0;
virtual bool isClientThrottled(const boost::asio::ip::address& clientAddress) const = 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;
+1
View File
@@ -1,5 +1,6 @@
add_library(lmssubsonic STATIC
impl/AuthUtils.cpp
impl/endpoints/transcoding/AudioFileInfo.cpp
impl/endpoints/transcoding/TranscodeDecision.cpp
impl/endpoints/transcoding/TranscodeDecisionTracker.cpp
+184
View File
@@ -0,0 +1,184 @@
/*
* Copyright (C) 2026 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 "AuthUtils.hpp"
#include <string>
#include <boost/asio/ip/address.hpp>
#include "core/Md5.hpp"
#include "core/Service.hpp"
#include "core/String.hpp"
#include "core/Utils.hpp"
#include "database/Session.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicResponse.hpp"
namespace lms::api::subsonic::utils
{
namespace
{
// Finds, among the user's stored "subsonic" API keys, the one that satisfies the legacy "t"+"s" scheme, if any (empty string if none)
std::string findMatchingApiKeyForAuthToken(db::UserId userId, std::string_view salt, std::string_view token)
{
std::string matchedApiKey;
core::Service<auth::IAuthTokenService>::get()->visitAuthTokens("subsonic", userId, [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view apiKey) {
if (matchedApiKey.empty() && checkAuthToken(apiKey, salt, token))
matchedApiKey = apiKey;
});
return matchedApiKey;
}
// Returns null if no such user exists
db::User::pointer getUserFromLoginName(db::Session& session, std::string_view loginName)
{
auto transaction{ session.createReadTransaction() };
return db::User::find(session, loginName);
}
} // namespace
AuthenticationRequest parseAndValidateAuthenticationRequest(const Wt::Http::ParameterMap& parameters, const SubsonicResourceConfig& config)
{
const std::optional<std::string> user{ getParameterAs<std::string>(parameters, "u") };
const std::optional<std::string> password{ getParameterAs<std::string>(parameters, "p") };
const std::optional<std::string> token{ getParameterAs<std::string>(parameters, "t") };
const std::optional<std::string> salt{ getParameterAs<std::string>(parameters, "s") };
const std::optional<std::string> apiKey{ getParameterAs<std::string>(parameters, "apiKey") };
const bool passwordAuthRequested{ password.has_value() };
const bool tokenAuthRequested{ token.has_value() || salt.has_value() };
const bool passwordAuthAttempted{ passwordAuthRequested || (user.has_value() && !tokenAuthRequested) };
if (!config.supportPasswordAuthentication && passwordAuthAttempted)
throw ProvidedAuthenticationMechanismNotSupportedError{};
if (!config.supportTokenAuthentication && tokenAuthRequested)
throw ProvidedAuthenticationMechanismNotSupportedError{};
if (passwordAuthRequested && tokenAuthRequested)
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
if (tokenAuthRequested)
{
if (!user)
throw RequiredParameterMissingError{ "u" };
if (!token)
throw RequiredParameterMissingError{ "t" };
if (!salt)
throw RequiredParameterMissingError{ "s" };
if (apiKey)
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
return TokenAuthentication{ .user = *user, .token = *token, .salt = *salt };
}
if (passwordAuthRequested)
{
if (!user)
throw RequiredParameterMissingError{ "u" };
if (apiKey)
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
return PasswordAuthentication{ .user = *user, .password = *password };
}
if (user)
throw RequiredParameterMissingError{ "p" };
if (!apiKey)
throw RequiredParameterMissingError{ "apiKey" };
return ApiKeyAuthentication{ .apiKey = *apiKey };
}
bool checkAuthToken(std::string_view apiKey, std::string_view salt, std::string_view token)
{
std::string payload;
payload.reserve(apiKey.size() + salt.size());
payload += apiKey;
payload += salt;
return core::stringUtils::stringCaseInsensitiveEqual(core::stringUtils::bufferToHexString(core::md5(payload)), token);
}
db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId)
{
auto transaction{ session.createReadTransaction() };
if (db::User::pointer user{ db::User::find(session, userId) })
return user;
throw UserNotAuthorizedError{};
}
db::UserId authenticateUser(const Wt::Http::Request& request, db::Session& session, const SubsonicResourceConfig& config)
{
const AuthenticationRequest authRequest{ parseAndValidateAuthenticationRequest(request.getParameterMap(), config) };
const auto clientAddress{ boost::asio::ip::make_address(request.clientAddress()) };
auto& authTokenService{ *core::Service<auth::IAuthTokenService>::get() };
const std::string authToken{
std::visit(core::utils::overloads{
[&](const PasswordAuthentication& auth) {
return decodePasswordIfNeeded(auth.password);
},
[&](const ApiKeyAuthentication& auth) {
return auth.apiKey;
},
[&](const TokenAuthentication& auth) {
if (authTokenService.isClientThrottled(clientAddress))
throw LoginThrottledGenericError{};
const db::User::pointer authUser{ getUserFromLoginName(session, auth.user) };
return authUser ? findMatchingApiKeyForAuthToken(authUser->getId(), auth.salt, auth.token) : std::string{};
},
},
authRequest)
};
// It is OK to have an empty authToken string here, as the auth service will reject and counts this as a bad attempt for this client address
const auto authResult{ authTokenService.processAuthToken("subsonic", clientAddress, authToken) };
switch (authResult.state)
{
case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
// Only the password mechanism needs this check as the token mechanism already used the user name
if (const auto* passwordAuth{ std::get_if<PasswordAuthentication>(&authRequest) })
{
const auto authenticatedUser{ getUserFromUserId(session, authResult.authTokenInfo->userId) };
if (!authenticatedUser || authenticatedUser->getLoginName() != passwordAuth->user)
throw WrongUsernameOrPasswordError{};
}
return authResult.authTokenInfo->userId;
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
if (std::holds_alternative<ApiKeyAuthentication>(authRequest))
throw InvalidAPIkeyError{};
else
throw WrongUsernameOrPasswordError{};
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
throw LoginThrottledGenericError{};
}
throw InternalErrorGenericError{ "Cannot authenticate user" };
}
} // namespace lms::api::subsonic::utils
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2026 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 <string_view>
#include <variant>
#include <Wt/Http/Request.h>
#include "database/objects/User.hpp"
#include "database/objects/UserId.hpp"
#include "SubsonicResourceConfig.hpp"
namespace lms::db
{
class Session;
}
namespace lms::api::subsonic::utils
{
struct PasswordAuthentication
{
std::string user;
std::string password;
};
struct TokenAuthentication
{
std::string user;
std::string token;
std::string salt;
};
struct ApiKeyAuthentication
{
std::string apiKey;
};
using AuthenticationRequest = std::variant<PasswordAuthentication, TokenAuthentication, ApiKeyAuthentication>;
// Throws on error
AuthenticationRequest parseAndValidateAuthenticationRequest(const Wt::Http::ParameterMap& parameters, const SubsonicResourceConfig& config);
// Checks token == hex(md5(apiKey + salt)), case insensitive
bool checkAuthToken(std::string_view apiKey, std::string_view salt, std::string_view token);
// Throws UserNotAuthorizedError if no such user exists
db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId);
// Throws on error
db::UserId authenticateUser(const Wt::Http::Request& request, db::Session& session, const SubsonicResourceConfig& config);
} // namespace lms::api::subsonic
+4 -58
View File
@@ -32,10 +32,9 @@
#include "database/IDb.hpp"
#include "database/Session.hpp"
#include "database/objects/User.hpp"
#include "services/auth/IAuthTokenService.hpp"
#include "services/auth/IPasswordService.hpp"
#include "ParameterParsing.hpp"
#include "AuthUtils.hpp"
#include "RequestContext.hpp"
#include "SubsonicResponse.hpp"
#include "endpoints/AlbumSongLists.hpp"
@@ -269,15 +268,6 @@ namespace lms::api::subsonic
TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete;
};
db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId)
{
auto transaction{ session.createReadTransaction() };
if (db::User::pointer user{ db::User::find(session, userId) })
return user;
throw UserNotAuthorizedError{};
}
} // namespace
SubsonicResource::SubsonicResource(db::IDb& db)
@@ -328,7 +318,7 @@ namespace lms::api::subsonic
// Media retrieval endpoints are always authenticated but we don't reauth user for a continuation
db::User::pointer user;
if (!request.continuation())
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
user = utils::getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
requestContext.setUser(user);
@@ -396,7 +386,7 @@ namespace lms::api::subsonic
db::User::pointer user;
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
{
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
user = utils::getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes);
requestContext->setUser(user);
}
@@ -425,50 +415,6 @@ namespace lms::api::subsonic
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
{
const auto& parameters{ request.getParameterMap() };
if (hasParameter(parameters, "t"))
throw ProvidedAuthenticationMechanismNotSupportedError{};
const auto user{ getParameterAs<std::string>(parameters, "u") };
const auto password{ getParameterAs<std::string>(parameters, "p") };
if (!_config.supportUserPasswordAuthentication && (password || user))
throw ProvidedAuthenticationMechanismNotSupportedError{};
const auto apiKey{ getParameterAs<std::string>(parameters, "apiKey") };
if (user && !password)
throw RequiredParameterMissingError{ "p" };
if (!user && password)
throw RequiredParameterMissingError{ "u" };
if (apiKey && password)
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
if (!apiKey && !password)
throw RequiredParameterMissingError{ "apiKey" };
const auto clientAddress{ boost::asio::ip::make_address(request.clientAddress()) };
const std::string authToken{ apiKey ? *apiKey : decodePasswordIfNeeded(*password) };
const auto authResult{ core::Service<auth::IAuthTokenService>::get()->processAuthToken("subsonic", clientAddress, authToken) };
switch (authResult.state)
{
case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
if (user)
{
const auto authenticatedUser{ getUserFromUserId(_db.getTLSSession(), authResult.authTokenInfo->userId) };
if (!authenticatedUser || authenticatedUser->getLoginName() != *user)
throw WrongUsernameOrPasswordError{};
}
return authResult.authTokenInfo->userId;
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
if (apiKey)
throw InvalidAPIkeyError{};
else
throw WrongUsernameOrPasswordError{};
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
throw LoginThrottledGenericError{};
}
throw InternalErrorGenericError{ "Cannot authenticate user" };
return utils::authenticateUser(request, _db.getTLSSession(), _config);
}
} // namespace lms::api::subsonic
@@ -57,7 +57,8 @@ namespace lms::api::subsonic
return SubsonicResourceConfig{
.serverProtocolVersionsByClient = readConfigProtocolVersions(config),
.openSubsonicDisabledClients = readOpenSubsonicDisabledClients(config),
.supportUserPasswordAuthentication = config.getBool("api-subsonic-support-user-password-auth", true)
.supportPasswordAuthentication = config.getBool("api-subsonic-support-password-auth", true),
.supportTokenAuthentication = config.getBool("api-subsonic-support-token-auth", true)
};
}
} // namespace lms::api::subsonic
@@ -36,7 +36,8 @@ namespace lms::api::subsonic
{
std::unordered_map<std::string, ProtocolVersion> serverProtocolVersionsByClient;
std::unordered_set<std::string> openSubsonicDisabledClients;
bool supportUserPasswordAuthentication;
bool supportPasswordAuthentication;
bool supportTokenAuthentication;
};
SubsonicResourceConfig readSubsonicResourceConfig(core::IConfig& _config);
+141
View File
@@ -0,0 +1,141 @@
/*
* Copyright (C) 2026 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 <gtest/gtest.h>
#include "AuthUtils.hpp"
#include "SubsonicResponse.hpp"
namespace lms::api::subsonic::utils::tests
{
namespace
{
const SubsonicResourceConfig bothMechanismsSupported{
.serverProtocolVersionsByClient = {},
.openSubsonicDisabledClients = {},
.supportPasswordAuthentication = true,
.supportTokenAuthentication = true,
};
const SubsonicResourceConfig noneSupported{
.serverProtocolVersionsByClient = {},
.openSubsonicDisabledClients = {},
.supportPasswordAuthentication = false,
.supportTokenAuthentication = false,
};
const SubsonicResourceConfig passwordUnsupported{
.serverProtocolVersionsByClient = {},
.openSubsonicDisabledClients = {},
.supportPasswordAuthentication = false,
.supportTokenAuthentication = true,
};
} // namespace
// Reference values from the Subsonic API documentation (apiKey = "sesame", salt = "c19b2d")
TEST(AuthUtils, checkAuthToken_ValidToken)
{
EXPECT_TRUE(checkAuthToken("sesame", "c19b2d", "26719a1196d2a940705a59634eb18eab"));
}
TEST(AuthUtils, checkAuthToken_ValidToken_UppercaseHex)
{
EXPECT_TRUE(checkAuthToken("sesame", "c19b2d", "26719A1196D2A940705A59634EB18EAB"));
}
TEST(AuthUtils, checkAuthToken_WrongToken)
{
EXPECT_FALSE(checkAuthToken("sesame", "c19b2d", "00000000000000000000000000000000"));
}
TEST(AuthUtils, checkAuthToken_WrongSalt)
{
EXPECT_FALSE(checkAuthToken("sesame", "differentsalt", "26719a1196d2a940705a59634eb18eab"));
}
TEST(AuthUtils, checkAuthToken_WrongApiKey)
{
EXPECT_FALSE(checkAuthToken("wrongkey", "c19b2d", "26719a1196d2a940705a59634eb18eab"));
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_apiKeyOnly)
{
const auto request{ parseAndValidateAuthenticationRequest({ { "apiKey", { "apiKey" } } }, bothMechanismsSupported) };
ASSERT_TRUE(std::holds_alternative<ApiKeyAuthentication>(request));
EXPECT_EQ(std::get<ApiKeyAuthentication>(request).apiKey, "apiKey");
EXPECT_THROW(parseAndValidateAuthenticationRequest({}, bothMechanismsSupported), RequiredParameterMissingError);
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_password)
{
const auto request{ parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } } }, bothMechanismsSupported) };
ASSERT_TRUE(std::holds_alternative<PasswordAuthentication>(request));
EXPECT_EQ(std::get<PasswordAuthentication>(request).user, "user");
EXPECT_EQ(std::get<PasswordAuthentication>(request).password, "password");
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "p", { "password" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing u
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing p
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_token)
{
const auto request{ parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } }, { "s", { "salt" } } }, bothMechanismsSupported) };
ASSERT_TRUE(std::holds_alternative<TokenAuthentication>(request));
EXPECT_EQ(std::get<TokenAuthentication>(request).user, "user");
EXPECT_EQ(std::get<TokenAuthentication>(request).token, "token");
EXPECT_EQ(std::get<TokenAuthentication>(request).salt, "salt");
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "t", { "token" } }, { "s", { "salt" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing u
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "s", { "salt" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing t
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing s
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_conflicts)
{
// password + token
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } }, { "t", { "token" } }, { "s", { "salt" } } }, bothMechanismsSupported), MultipleConflictingAuthenticationMechanismsProvidedError);
// apiKey + password
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } }, { "apiKey", { "apiKey" } } }, bothMechanismsSupported), MultipleConflictingAuthenticationMechanismsProvidedError);
// apiKey + token
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } }, { "s", { "salt" } }, { "apiKey", { "apiKey" } } }, bothMechanismsSupported), MultipleConflictingAuthenticationMechanismsProvidedError);
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_mechanismNotSupported)
{
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "p", { "password" } } }, noneSupported), ProvidedAuthenticationMechanismNotSupportedError);
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } }, { "t", { "token" } }, { "s", { "salt" } } }, noneSupported), ProvidedAuthenticationMechanismNotSupportedError);
EXPECT_NO_THROW(parseAndValidateAuthenticationRequest({ { "apiKey", { "apiKey" } } }, noneSupported)); // apiKey is unaffected by these two flags
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_usernameOnlyTreatedAsPasswordAttempt)
{
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "u", { "user" } } }, passwordUnsupported), ProvidedAuthenticationMechanismNotSupportedError);
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_apiKeyWithPartialPassword)
{
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "p", { "password" } }, { "apiKey", { "apiKey" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing u
}
TEST(AuthUtils, parseAndValidateAuthenticationRequest_apiKeyWithPartialToken)
{
EXPECT_THROW(parseAndValidateAuthenticationRequest({ { "s", { "salt" } }, { "apiKey", { "apiKey" } } }, bothMechanismsSupported), RequiredParameterMissingError); // missing u
}
} // namespace lms::api::subsonic::utils::tests
+2
View File
@@ -1,6 +1,7 @@
include(GoogleTest)
add_executable(test-subsonic
AuthUtils.cpp
ClientInfo.cpp
Subsonic.cpp
SubsonicResponse.cpp
@@ -14,6 +15,7 @@ target_include_directories(test-subsonic PRIVATE
target_link_libraries(test-subsonic PRIVATE
lmscore
lmsaudio
lmsdatabase
lmssubsonic
GTest::GTest
)
+4 -1
View File
@@ -185,7 +185,10 @@ namespace lms::ui
auto* t{ addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Settings.subsonic.template.key")) };
t->addFunction("tr", &Wt::WTemplate::Functions::tr);
t->setCondition("if-has-subsonic-token-usage", core::Service<core::IConfig>::get()->getBool("api-subsonic-support-user-password-auth", true));
{
core::IConfig& config{ *core::Service<core::IConfig>::get() };
t->setCondition("if-has-subsonic-token-usage", config.getBool("api-subsonic-support-password-auth", true) || config.getBool("api-subsonic-support-token-auth", true));
}
std::string currentToken;
{