Add token validation filter based on token signature and expiration date

- changed login request from GET to POST
- implement json body conversion to DTO objects
- set `user_id` and `role` attributes for HttpRequest inside filter
This commit is contained in:
2025-06-14 16:35:18 +04:00
parent 16c7e94345
commit 6df4896628
14 changed files with 285 additions and 64 deletions

View File

@@ -32,6 +32,10 @@ add_executable(${PROJECT_NAME}
src/utils/JsonConversion.h src/utils/JsonConversion.h
src/controllers/AuthController.cpp src/controllers/AuthController.cpp
src/controllers/AuthController.h src/controllers/AuthController.h
src/controllers/filters/TokenValidationFilter.cpp
src/controllers/filters/TokenValidationFilter.h
src/dto/LoginRequestDto.h
src/dto/RegisterRequestDto.h
src/models/Entity.h src/models/Entity.h
src/models/User.h src/models/User.h
src/services/AuthService.cpp src/services/AuthService.cpp

View File

@@ -2,6 +2,8 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include "dependency_injection/DependencyContainer.h" #include "dependency_injection/DependencyContainer.h"
#include "dto/LoginRequestDto.h"
#include "dto/RegisterRequestDto.h"
#include "services/AuthService.h" #include "services/AuthService.h"
#include "utils/JsonConversion.h" #include "utils/JsonConversion.h"
@@ -13,37 +15,57 @@ api::v1::Auth::Auth()
m_logger->info("[api::v1::Auth::Auth] Registered Auth controller"); m_logger->info("[api::v1::Auth::Auth] Registered Auth controller");
} }
void api::v1::Auth::login( void api::v1::Auth::loginUser(
const drogon::HttpRequestPtr& req, const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::function<void(const drogon::HttpResponsePtr&)>&& callback
const std::string& username,
std::string&& password
) )
{ {
m_logger->info("[api::v1::Auth::login] User `{}` tries to login", username); auto body = req->getBody();
auto j = nlohmann::json::parse(body);
auto loginDataDto = j.get<LoginRequestDto>();
std::string token{}; std::string token{};
bool result = m_authService->authenticateUser(username, std::move(password), token); bool result = m_authService->authenticateUser(loginDataDto, token);
if (result) if (result)
{ {
m_logger->info("[api::v1::Auth::login] User `{}` successfully logged in", username); m_logger->info("[api::v1::Auth::loginUser] User `{}` successfully logged in", loginDataDto.username);
nlohmann::json json; auto resp = drogon::HttpResponse::newHttpResponse();
json["result"] = "ok";
auto resp = drogon::HttpResponse::newHttpJsonResponse(nlohmannToJsonValue(json));
resp->addHeader("Authorization", "Bearer " + token); resp->addHeader("Authorization", "Bearer " + token);
callback(resp); callback(resp);
} }
else else
{ {
m_logger->info("[api::v1::Auth::login] User `{}` failed to login", username); m_logger->info("[api::v1::Auth::loginUser] User `{}` failed to login", loginDataDto.username);
nlohmann::json json; auto resp = drogon::HttpResponse::newHttpResponse(drogon::k401Unauthorized, drogon::ContentType::CT_TEXT_PLAIN);
json["result"] = "error"; resp->setBody("Invalid username or password");
json["message"] = "Invalid username or password"; callback(resp);
}
auto resp = drogon::HttpResponse::newHttpJsonResponse(nlohmannToJsonValue(json)); }
resp->setStatusCode(drogon::k401Unauthorized);
void api::v1::Auth::registerUser(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& callback)
{
auto body = req->getBody();
auto j = nlohmann::json::parse(body);
auto registerDataDto = j.get<RegisterRequestDto>();
std::string token{};
bool result = m_authService->registerUser(registerDataDto, token);
if (result)
{
m_logger->info("[api::v1::Auth::registerUser] User `{}` successfully registered", registerDataDto.username);
auto resp = drogon::HttpResponse::newHttpResponse();
resp->addHeader("Authorization", "Bearer " + token);
callback(resp);
}
else
{
nlohmann::json json;
auto resp = drogon::HttpResponse::newHttpResponse(drogon::k406NotAcceptable, drogon::ContentType::CT_TEXT_PLAIN);
resp->setBody("Failed to register new account");
callback(resp); callback(resp);
} }
} }

View File

@@ -12,15 +12,18 @@ class Auth : public drogon::HttpController<Auth>
{ {
public: public:
METHOD_LIST_BEGIN METHOD_LIST_BEGIN
METHOD_ADD(Auth::login, "/login?username={}&password={}", drogon::Get); METHOD_ADD(Auth::loginUser, "/login", drogon::Post);
METHOD_ADD(Auth::registerUser, "/register", drogon::Post);
METHOD_LIST_END METHOD_LIST_END
Auth(); Auth();
void login(const drogon::HttpRequestPtr& req, void loginUser(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::function<void(const drogon::HttpResponsePtr&)>&& callback);
const std::string& username,
std::string&& password); void registerUser(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& callback
);
private: private:
std::shared_ptr<spdlog::logger> m_logger; std::shared_ptr<spdlog::logger> m_logger;

View File

@@ -0,0 +1,47 @@
#include "TokenValidationFilter.h"
#include <nlohmann/json.hpp>
#include "dependency_injection/DependencyContainer.h"
#include "services/UserManager.h"
TokenValidationFilter::TokenValidationFilter()
{
m_userManager = DependencyContainer::Resolve<UserManager>();
}
void TokenValidationFilter::doFilter(
const drogon::HttpRequestPtr& req,
drogon::FilterCallback&& fcb,
drogon::FilterChainCallback&& fccb
)
{
auto authHeader = req->getHeader("Authorization");
if (authHeader.empty() || authHeader.find("Bearer ") != 0)
{
auto res = drogon::HttpResponse::newHttpResponse();
res->setStatusCode(drogon::k401Unauthorized);
res->setBody("Missing or invalid Authorization header");
return fcb(res);
}
std::string token = authHeader.substr(7);
bool validated = m_userManager->validateToken(token);
if (validated)
{
// TODO: set role and user_id attributes here
auto payloadString = m_userManager->getPayloadFromToken(token);
auto payload = nlohmann::json::parse(payloadString);
int user_id = payload["user_id"];
std::string role = payload["role"];
req->attributes()->insert("user_id", user_id);
req->attributes()->insert("role", role);
return fccb();
}
auto res = drogon::HttpResponse::newHttpResponse();
res->setStatusCode(drogon::k401Unauthorized);
res->setBody("Invalid token");
return fcb(res);
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include <drogon/drogon.h>
class UserManager;
class TokenValidationFilter : public drogon::HttpFilter<TokenValidationFilter>
{
public:
TokenValidationFilter();
virtual void doFilter(const drogon::HttpRequestPtr& req,
drogon::FilterCallback&& fcb,
drogon::FilterChainCallback&& fccb) override;
private:
std::shared_ptr<UserManager> m_userManager;
};

13
src/dto/LoginRequestDto.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
#include <nlohmann/json.hpp>
class LoginRequestDto
{
public:
std::string username;
std::string password;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(LoginRequestDto, username, password);
};

View File

@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <nlohmann/json.hpp>
class RegisterRequestDto
{
public:
std::string username;
std::string password;
std::string firstName;
std::string lastName;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(RegisterRequestDto, username, password, firstName, lastName);
};

View File

@@ -12,3 +12,8 @@ std::optional<User> MockUserStorage::getUserByUsername(const std::string& userna
return ret; return ret;
} }
bool MockUserStorage::insertUser(const User& user)
{
return true;
}

View File

@@ -6,4 +6,5 @@ class MockUserStorage : public IUserStorage
{ {
public: public:
virtual std::optional<User> getUserByUsername(const std::string& username) override; virtual std::optional<User> getUserByUsername(const std::string& username) override;
virtual bool insertUser(const User& user) override;
}; };

View File

@@ -3,6 +3,8 @@
#include <optional> #include <optional>
#include "UserManager.h" #include "UserManager.h"
#include "dependency_injection/DependencyContainer.h" #include "dependency_injection/DependencyContainer.h"
#include "dto/LoginRequestDto.h"
#include "dto/RegisterRequestDto.h"
#include "storages/IUserStorage.h" #include "storages/IUserStorage.h"
AuthService::AuthService() AuthService::AuthService()
@@ -12,27 +14,45 @@ AuthService::AuthService()
m_userManager = DependencyContainer::Resolve<UserManager>(); m_userManager = DependencyContainer::Resolve<UserManager>();
} }
bool AuthService::authenticateUser( bool AuthService::authenticateUser(LoginRequestDto& loginData, std::string& outToken)
const std::string& username,
std::string&& password,
std::string& outToken
)
{ {
auto foundUser = m_userStorage->getUserByUsername(username); auto foundUser = m_userStorage->getUserByUsername(loginData.username);
if (!foundUser.has_value()) if (!foundUser.has_value())
{ {
m_logger->info("Failed to find user with username `{}`", username); m_logger->info("Failed to find user with username `{}`", loginData.username);
}
if (m_userManager->validateCredentials(*foundUser, std::move(loginData.password)))
{
outToken = m_userManager->generateToken(*foundUser);
return true;
} }
return m_userManager->validateCredentials(foundUser.value(), std::move(password));
}
bool AuthService::registerUser(
const std::string& username,
const std::string& password,
const std::string& firstName,
const std::string& lastName,
std::string& outToken
)
{
return false; return false;
} }
bool AuthService::registerUser(RegisterRequestDto& registerData, std::string& outToken)
{
if (!m_userManager->validateRegisterData(registerData))
{
return false;
}
auto hashedPassword = m_userManager->hashPassword(std::move(registerData.password));
User user;
user.username = registerData.username;
user.passwordHash = hashedPassword;
user.firstName = registerData.firstName;
user.lastName = registerData.lastName;
user.role = "LIVER";
bool insertResult = m_userStorage->insertUser(user);
if (!insertResult)
{
m_logger->error("Failed to create new user");
return false;
}
outToken = m_userManager->generateToken(user);
return true;
}

View File

@@ -6,25 +6,16 @@
class IUserStorage; class IUserStorage;
class UserManager; class UserManager;
class LoginRequestDto;
class RegisterRequestDto;
class AuthService class AuthService
{ {
public: public:
AuthService(); AuthService();
bool authenticateUser( bool authenticateUser(LoginRequestDto& loginData, std::string& outToken);
const std::string& username, bool registerUser(RegisterRequestDto& registerData, std::string& outToken);
std::string&& password,
std::string& outToken
);
bool registerUser(
const std::string& username,
const std::string& password,
const std::string& firstName,
const std::string& lastName,
std::string& outToken
);
private: private:
std::shared_ptr<spdlog::logger> m_logger; std::shared_ptr<spdlog::logger> m_logger;

View File

@@ -1,23 +1,99 @@
#include "UserManager.h" #include "UserManager.h"
#include <chrono>
#include <vector>
#include <drogon/utils/Utilities.h> #include <drogon/utils/Utilities.h>
#include <jwt-cpp/jwt.h>
#include <jwt-cpp/traits/nlohmann-json/defaults.h>
#include "dependency_injection/DependencyContainer.h" #include "dependency_injection/DependencyContainer.h"
#include "models/User.h" #include "models/User.h"
#include "services/encryption/PasswordEncryptor.h" #include "services/encryption/PasswordEncryptor.h"
std::string PUBLIC_KEY{
R"(-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGlyZf3ilQWS862UM0wPL9t+B/La
E4QiLOWULVmiHvtg6ly5erX4LeKX9og0LYgdUyialjPRElutRq1dTEJ4x9kgDkT4
o+hwcem+OHKkdbIfGSIdrI1SzWPYq8F9JjL9elPghD8Rul1US+2HWyrJkw3YiAjU
gmZ8mUpu5Hkq+LjzAgMBAAE=
-----END PUBLIC KEY-----)"
};
std::string PRIVATE_KEY{
R"(-----BEGIN RSA PRIVATE KEY-----
MIICWgIBAAKBgGlyZf3ilQWS862UM0wPL9t+B/LaE4QiLOWULVmiHvtg6ly5erX4
LeKX9og0LYgdUyialjPRElutRq1dTEJ4x9kgDkT4o+hwcem+OHKkdbIfGSIdrI1S
zWPYq8F9JjL9elPghD8Rul1US+2HWyrJkw3YiAjUgmZ8mUpu5Hkq+LjzAgMBAAEC
gYATCsimV5OnxQjiUMAOvTNcRN80mPMrSmynLOIfrKpBpRfNlOU3FVFb+StZH2sV
iI4q5/e19cyF5726sv2Bh4Q3Uob6eJZfpcReScUtbhFLhikCSTvWf6g+6ebCBF9m
HoXQBvJhthRo+9BSoweZjxNZirxO1DaqbrxIdufpBJL9aQJBAL/iyzb7mQ/oq8/j
BcVnPlOBK6R3q5ZcynJTgyLMTldAwTB4WZrUPBWt51WUL7LoZhMtQIGNaqApSwyp
ehaRKscCQQCMre5E5QXDvAITYIoGNs1x5qVUzKCYXYIv0hdko21V9GLNaYWLL5ux
BAZsvNLBE2DMbl4mJiL198qJItYk0nR1AkAIXjGSgkJYiUME29eiljAHoDhxAa7/
7af+eFndqJ85+t7x6C2wLNU59M2D0+SIns3kxDJt8+bUeTiGotVqKoZ9AkAELNFK
eCWQpo7FNnNCNfQo8jhr6NrHStcnRivtj7AaAfPAtuYAuHv9Z+os5fm3QzT3PDtN
FIqrFByNr1v9ocVVAkAWvQ4q4Mkw8u/zCAuPiK9NJn377nc4bENQkcn6ZJ4dbMgt
yV8QswCTqtfaXKyOEs46WNYhEtMahiLPeL7V23Mr
-----END RSA PRIVATE KEY-----)"
};
UserManager::UserManager() UserManager::UserManager()
{ {
m_logger = DependencyContainer::Resolve<spdlog::logger>();
m_passwordEncryptor = DependencyContainer::Resolve<PasswordEncryptor>(); m_passwordEncryptor = DependencyContainer::Resolve<PasswordEncryptor>();
} }
bool UserManager::validateCredentials(const User& userData, std::string&& rawPassword) bool UserManager::validateCredentials(const User& userData, std::string&& rawPassword)
{ {
auto hashedPassword = hashPassword(std::move(rawPassword)); auto hashedPassword = hashPassword(std::move(rawPassword));
auto base64Hash = drogon::utils::base64Encode(hashedPassword.data(), hashedPassword.size()); return hashedPassword == userData.passwordHash;
return base64Hash == userData.passwordHash;
} }
std::vector<uint8_t> UserManager::hashPassword(std::string&& rawPassword) bool UserManager::validateRegisterData(const RegisterRequestDto& registerData)
{ {
return m_passwordEncryptor->encryptPassword(std::move(rawPassword)); return true;
}
std::string UserManager::generateToken(const User& userData)
{
return jwt::create()
.set_type("JWT")
.set_payload_claim("user_id", jwt::claim(userData.id))
.set_payload_claim("role", jwt::claim(userData.role))
.set_expires_in(std::chrono::seconds{ 3600 })
.sign(jwt::algorithm::rs256{ "", PRIVATE_KEY });
}
std::string UserManager::hashPassword(std::string&& rawPassword)
{
auto hashData = m_passwordEncryptor->encryptPassword(std::move(rawPassword));
return drogon::utils::base64Encode(hashData.data(), hashData.size());
}
bool UserManager::validateToken(const std::string& token)
{
try
{
auto decoded = jwt::decode(token);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::rs256{ PUBLIC_KEY, "" });
verifier.verify(decoded);
}
catch (const jwt::error::token_verification_exception& ex)
{
m_logger->info("Failed to validate token: {}", ex.what());
return false;
}
return true;
}
std::string UserManager::getPayloadFromToken(const std::string& token)
{
try
{
auto decoded = jwt::decode(token);
return decoded.get_payload();
}
catch (const std::exception& ex)
{ }
} }

View File

@@ -2,11 +2,11 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <spdlog/logger.h>
class User;
class PasswordEncryptor; class PasswordEncryptor;
class RegisterRequestDto;
class User;
class UserManager class UserManager
{ {
@@ -14,10 +14,15 @@ public:
UserManager(); UserManager();
bool validateCredentials(const User& userData, std::string&& rawPassword); bool validateCredentials(const User& userData, std::string&& rawPassword);
bool validateRegisterData(const RegisterRequestDto& registerData);
std::string generateToken(const User& userData);
std::string hashPassword(std::string&& rawPassword);
bool validateToken(const std::string& token);
std::string getPayloadFromToken(const std::string& token);
private: private:
std::vector<uint8_t> hashPassword(std::string&& rawPassword); std::shared_ptr<spdlog::logger> m_logger;
private:
std::shared_ptr<PasswordEncryptor> m_passwordEncryptor; std::shared_ptr<PasswordEncryptor> m_passwordEncryptor;
}; };

View File

@@ -8,4 +8,5 @@ class IUserStorage
{ {
public: public:
virtual std::optional<User> getUserByUsername(const std::string& username) = 0; virtual std::optional<User> getUserByUsername(const std::string& username) = 0;
virtual bool insertUser(const User& user) = 0;
}; };