#include <chrono>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mw/crypto_mock.hpp>
#include "authentication.h"
#include "startup.h"
namespace
{
class TemporaryDatabase
{
public:
TemporaryDatabase()
: path_(
std::filesystem::path(testing::TempDir()) /
("authentication_service_" + std::to_string(
std::chrono::steady_clock::now()
.time_since_epoch().count()) + ".sqlite3"))
{}
~TemporaryDatabase()
{
std::error_code error;
std::filesystem::remove(path_, error);
std::filesystem::remove(path_.string() + "-shm", error);
std::filesystem::remove(path_.string() + "-wal", error);
}
const std::filesystem::path& path() const
{
return path_;
}
private:
std::filesystem::path path_;
};
class ClockMock final : public ClockInterface
{
public:
std::chrono::system_clock::time_point now() const override
{
return current;
}
std::chrono::system_clock::time_point current =
std::chrono::system_clock::time_point(std::chrono::seconds(100));
};
class EmailSenderMock final : public EmailSenderInterface
{
public:
mw::E<void> send(const AuthenticationEmail& email) override
{
last_email = email;
if(fail)
{
return std::unexpected(mw::runtimeError("Sender failed"));
}
return {};
}
bool usesGlobalQuota() const override
{
return global_quota;
}
std::optional<AuthenticationEmail> last_email;
bool fail = false;
bool global_quota = true;
};
std::string finalPathSegment(const mw::URL& url)
{
const std::string path = url.path();
return path.substr(path.find_last_of('/') + 1);
}
std::vector<std::byte> bytes(unsigned char value)
{
return std::vector<std::byte>(32, static_cast<std::byte>(value));
}
std::string hexToken(char low_nibble)
{
std::string result;
result.reserve(64);
for(int index = 0; index < 32; ++index)
{
result.push_back('0');
result.push_back(low_nibble);
}
return result;
}
} // namespace
TEST(AuthenticationServiceTest, CompletesSingleUseNonSlidingFlow)
{
TemporaryDatabase database;
auto data_source = prepareDataSource(database.path());
ASSERT_TRUE(data_source);
auto base_url = mw::URL::fromStr("http://127.0.0.1/app/");
ASSERT_TRUE(base_url);
ClockMock clock;
EmailSenderMock sender;
mw::CryptoMock crypto;
EXPECT_CALL(crypto, randomBytes(32))
.WillOnce(testing::Return(bytes(1)))
.WillOnce(testing::Return(bytes(2)))
.WillOnce(testing::Return(bytes(3)));
AuthenticationService authentication(
**data_source, sender, clock, crypto, std::move(*base_url), 10);
auto requested = authentication.requestEmail("Player@Example.com");
ASSERT_TRUE(requested) << requested.error().msg();
ASSERT_TRUE(sender.last_email);
EXPECT_EQ(sender.last_email->recipient, "Player@Example.com");
const std::string challenge_token = finalPathSegment(
sender.last_email->confirmation_url);
ASSERT_EQ(challenge_token, hexToken('1'));
auto opened = authentication.validate(challenge_token);
ASSERT_TRUE(opened);
ASSERT_TRUE(*opened);
auto opened_again = authentication.validate(challenge_token);
ASSERT_TRUE(opened_again);
ASSERT_TRUE(*opened_again);
auto established = authentication.confirm(challenge_token);
ASSERT_TRUE(established) << established.error().msg();
EXPECT_EQ(established->user.email, "Player@Example.com");
EXPECT_EQ(established->user.role, UserRole::PLAYER);
EXPECT_FALSE(established->user.username);
EXPECT_EQ(established->token, hexToken('2'));
EXPECT_EQ(
established->csrf_token,
hexToken('3'));
opened = authentication.validate(challenge_token);
ASSERT_TRUE(opened);
EXPECT_FALSE(*opened);
clock.current = std::chrono::system_clock::time_point(
std::chrono::seconds(200));
auto session = authentication.session(established->token);
ASSERT_TRUE(session);
ASSERT_TRUE(*session);
EXPECT_EQ((**session).expires_at, established->expires_at);
ASSERT_TRUE(authentication.logout(established->token));
session = authentication.session(established->token);
ASSERT_TRUE(session);
EXPECT_FALSE(*session);
}
TEST(AuthenticationServiceTest, RateLimitsAndInvalidatesSenderFailure)
{
TemporaryDatabase database;
auto data_source = prepareDataSource(database.path());
ASSERT_TRUE(data_source);
auto base_url = mw::URL::fromStr("http://127.0.0.1/");
ASSERT_TRUE(base_url);
ClockMock clock;
EmailSenderMock sender;
sender.global_quota = false;
mw::CryptoMock crypto;
EXPECT_CALL(crypto, randomBytes(32))
.WillOnce(testing::Return(bytes(4)))
.WillOnce(testing::Return(bytes(5)));
AuthenticationService authentication(
**data_source, sender, clock, crypto, std::move(*base_url), 10);
ASSERT_TRUE(authentication.requestEmail("person@example.com"));
EXPECT_FALSE(authentication.requestEmail("PERSON@example.com"));
clock.current = std::chrono::system_clock::time_point(
std::chrono::seconds(161));
sender.fail = true;
EXPECT_FALSE(authentication.requestEmail("other@example.com"));
ASSERT_TRUE(sender.last_email);
auto challenge = authentication.validate(
finalPathSegment(sender.last_email->confirmation_url));
ASSERT_TRUE(challenge);
EXPECT_FALSE(*challenge);
}