Add new services

password_hasher

auth_services
This commit is contained in:
2026-05-03 17:00:32 +03:00
parent 875c692180
commit 0502bd62a4
12 changed files with 352 additions and 58 deletions
+5
View File
@@ -0,0 +1,5 @@
submodules/
node_modules/
build/
cmake-build-*/
.cache/
+8
View File
@@ -12,6 +12,9 @@ enable_testing()
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(ARGON2 REQUIRED IMPORTED_TARGET libargon2)
add_subdirectory(submodules/drogon) add_subdirectory(submodules/drogon)
add_subdirectory(submodules/json) add_subdirectory(submodules/json)
@@ -29,6 +32,10 @@ add_executable(auth_service
src/repo/session_repository.cpp src/repo/session_repository.cpp
src/util/time_utils.cpp src/util/time_utils.cpp
src/security/session_token_service.cpp src/security/session_token_service.cpp
src/util/validation.cpp
src/security/password_hasher.cpp
src/service/auth_service.cpp
src/http/controllers/auth_controller.cpp
) )
target_include_directories(auth_service target_include_directories(auth_service
@@ -48,6 +55,7 @@ target_link_libraries(auth_service PRIVATE
SQLite::SQLite3 SQLite::SQLite3
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json
OpenSSL::Crypto OpenSSL::Crypto
PkgConfig::ARGON2
) )
add_executable(auth_service_tests add_executable(auth_service_tests
BIN
View File
Binary file not shown.
+34 -57
View File
@@ -2,27 +2,33 @@
// #include <nlohmann/json.hpp> // #include <nlohmann/json.hpp>
#include "db/migrations_runner.hpp" #include "db/migrations_runner.hpp"
#include "db/sqllite_db.hpp" #include "db/sqllite_db.hpp"
#include "http/controllers/auth_controller.hpp"
#include "http/controllers/health_controller.hpp" #include "http/controllers/health_controller.hpp"
#include "repo/session_repository.hpp" #include "repo/session_repository.hpp"
#include "repo/user_repository.hpp" #include "repo/user_repository.hpp"
#include "security/password_hasher.hpp"
#include "security/session_token_service.hpp" #include "security/session_token_service.hpp"
#include "service/auto_service.hpp"
#include "util/time_utils.hpp" #include "util/time_utils.hpp"
#include "util/validation.hpp"
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <format> #include <format>
#include <iostream> #include <iostream>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#define TEST_FUNCTION 1
namespace { namespace {
void register_routes() { // void register_routes() {
static HealthController health_controller; // static HealthController health_controller;
drogon::app().registerHandler( // drogon::app().registerHandler(
"/health", // "/health",
[](const drogon::HttpRequestPtr &req, // [](const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback) { // std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
health_controller.handle(req, std::move(callback)); // health_controller.handle(req, std::move(callback));
}, // },
{ drogon::Get }); // { drogon::Get });
} // }
} // namespace } // namespace
Application::Application(Settings settings) Application::Application(Settings settings)
@@ -36,57 +42,28 @@ int Application::run() const {
runner.run_file("migrations/001_init.sql"); runner.run_file("migrations/001_init.sql");
UserRepository user_repository(db); UserRepository user_repository(db);
SessionRepository session_repository(db); PasswordHasher password_hasher;
AuthService auth_service(user_repository, password_hasher);
SessionTokenService token_service; HealthController health_controller;
AuthController auth_controller(auth_service);
const auto token_pair = token_service.generate(); drogon::app().registerHandler(
"/register",
[&auth_controller](const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
auth_controller.register_user(req, std::move(callback));
},
{ drogon::Post });
std::cout << "Now UTC: " << now_utc_iso8601() << '\n'; drogon::app().registerHandler(
std::cout << "Expires at: " << expires_at_from_now(settings_.session_ttl) << '\n'; "/health",
std::cout << "Generated raw token: " << token_pair.raw_token << '\n'; [&health_controller](const drogon::HttpRequestPtr &req,
std::cout << "Generated token hash: " << token_pair.token_hash << '\n'; std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
health_controller.handle(req, std::move(callback));
},
{ drogon::Get });
if(false) {
session_repository.revoke_by_token_hash("dummy_token_hash", "2026-04-05T13:00:00Z");
const auto revoked_session = session_repository.find_by_token_hash("dummy_token_hash");
if(revoked_session.has_value() && revoked_session->revoked_at.has_value()) {
std::cout << "Session revoked at: " << *revoked_session->revoked_at << '\n';
}
}
if(false) {
const auto existing_session = session_repository.find_by_token_hash("dummy_token_hash");
if(!existing_session.has_value()) {
const auto created_session = session_repository.create(
1, "dummy_token_hash", "2026-04-05T12:00:00Z", "2026-04-06T12:00:00Z");
std::cout << "Created session: id=" << created_session.id
<< ", user_id=" << created_session.user_id << '\n';
} else {
std::cout << "Session already exists: id=" << existing_session->id
<< ", user_id=" << existing_session->user_id << '\n';
}
}
if(false) {
const auto existing_user = user_repository.find_by_email("test@example.com");
if(!existing_user.has_value()) {
const auto created_user = user_repository.create(
"test@example.com", "dummy_hash", "2026-04-05T12:00:00Z", "2026-04-05T12:00:00Z");
std::cout << "Created user: id=" << created_user.id << ", email=" << created_user.email
<< '\n';
} else {
std::cout << "User already exists: id=" << existing_user->id
<< ", email=" << existing_user->email << '\n';
}
}
register_routes();
drogon::app().addListener(settings_.host, settings_.port); drogon::app().addListener(settings_.host, settings_.port);
printInfo(); printInfo();
drogon::app().run(); drogon::app().run();
+74
View File
@@ -0,0 +1,74 @@
#include "http/controllers/auth_controller.hpp"
#include "service/auto_service.hpp"
#include <drogon/drogon.h>
#include <json/json.h>
AuthController::AuthController(AuthService &auth_service)
: authService_(auth_service) {}
void AuthController::register_user(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback) const {
const auto json = req->getJsonObject();
if(!isValidAuthJson(json)) {
Json::Value error;
error["error"]["code"] = "invalid_request";
error["error"]["message"] = "Expected JSON body with email and password";
auto responce = drogon::HttpResponse::newHttpJsonResponse(error);
responce->setStatusCode(drogon::k400BadRequest);
callback(responce);
return;
}
try {
const auto result = authService_.register_user(RegisterCommand{
.email = (*json)["email"].asString(),
.password = (*json)["password"].asString(),
});
Json::Value body;
body["id"] = static_cast<Json::Int64>(result.id);
body["email"] = result.email;
auto responce = drogon::HttpResponse::newHttpJsonResponse(body);
responce->setStatusCode(drogon::k201Created);
callback(responce);
} catch(const std::exception &ex) {
Json::Value error;
error["error"]["message"] = ex.what();
const std::string message = ex.what();
if(message == "Email already exists") {
error["error"]["code"] = "email_already_exists";
auto response = drogon::HttpResponse::newHttpJsonResponse(error);
response->setStatusCode(drogon::k409Conflict);
callback(response);
return;
}
if(message == "Invalid email" || message == "Invalid password") {
error["error"]["code"] = "validation_error";
auto response = drogon::HttpResponse::newHttpJsonResponse(error);
callback(response);
return;
}
error["error"]["code"] = "internal_error";
auto response = drogon::HttpResponse::newHttpJsonResponse(error);
response->setStatusCode(drogon::k500InternalServerError);
callback(response);
}
}
bool AuthController::isValidAuthJson(const std::shared_ptr<Json::Value> &req) const {
return req != nullptr || req->isMember("email") || req->isMember("password") ||
(*req)["email"].isString() || (*req)["password"].isString();
}
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <drogon/HttpController.h>
#include <functional>
class AuthService;
class AuthController {
public:
explicit AuthController(AuthService &auth_service);
void register_user(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
private:
bool isValidAuthJson(const std::shared_ptr<Json::Value> &req) const;
private:
AuthService &authService_;
};
+55
View File
@@ -0,0 +1,55 @@
#include <argon2.h>
#include <array>
#include <cstdint>
#include <openssl/rand.h>
#include <security/password_hasher.hpp>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
constexpr std::uint32_t timecost = 3;
constexpr std::uint32_t memoryCostKb = 64 * 1024;
constexpr std::uint32_t parallelism = 1;
constexpr std::uint32_t hashLength = 32;
constexpr std::size_t saltLength = 16;
constexpr std::size_t encodingLength = 128;
std::array<std::uint8_t, saltLength> generate_salt() {
std::array<std::uint8_t, saltLength> salt{};
if(RAND_bytes(salt.data(), static_cast<int>(salt.size())) != 1) {
throw std::runtime_error("Failed to generate password salt");
}
return salt;
}
} // namespace
std::string PasswordHasher::hash(std::string_view password) const {
const auto salt = generate_salt();
std::vector<char> encoded(encodingLength);
const int rc =
argon2id_hash_encoded(timecost, memoryCostKb, parallelism, password.data(), password.size(),
salt.data(), salt.size(), hashLength, encoded.data(), encoded.size());
if(rc != ARGON2_OK) {
throw std::runtime_error(std::string{ "Argon2 password hashing failed: " } +
argon2_error_message(rc));
}
return std::string{ encoded.data() };
}
bool PasswordHasher::verify(std::string_view password, std::string_view encoded_hash) const {
const int rc = argon2id_verify(encoded_hash.data(), password.data(), password.size());
if(rc == ARGON2_OK) {
return true;
}
if(rc == ARGON2_VERIFY_MISMATCH) {
return false;
}
throw std::runtime_error(std::string{ "Argon2 password verify failed: " } +
argon2_error_message(rc));
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
#include <string_view>
class PasswordHasher {
public:
std::string hash(std::string_view password) const;
bool verify(std::string_view password, std::string_view encoded_hash) const;
};
+37
View File
@@ -0,0 +1,37 @@
#include "repo/user_repository.hpp"
#include "security/password_hasher.hpp"
#include "util/time_utils.hpp"
#include "util/validation.hpp"
#include <service/auto_service.hpp>
#include <stdexcept>
AuthService::AuthService(UserRepository &users, PasswordHasher &password_hasher)
: users_(users)
, passwordHasher_(password_hasher) {}
RegisterResult AuthService::register_user(const RegisterCommand &command) const {
const auto email = normalize_email(command.email);
if(!is_valid_email(email)) {
throw std::runtime_error("Invalid email");
}
if(!is_valid_password(command.password)) {
throw std::runtime_error("Invalid password");
}
if(users_.find_by_email(email).has_value()) {
throw std::runtime_error("Email already exists");
}
const auto password_hash = passwordHasher_.hash(command.password);
const auto now = now_utc_iso8601();
const auto user = users_.create(email, password_hash, now, now);
return RegisterResult{
.id = user.id,
.email = user.email,
};
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <string>
class UserRepository;
class PasswordHasher;
struct RegisterCommand {
std::string email;
std::string password;
};
struct RegisterResult {
std::int64_t id;
std::string email;
};
class AuthService {
public:
AuthService(UserRepository &users, PasswordHasher &password_hasher);
RegisterResult register_user(const RegisterCommand &command) const;
private:
UserRepository &users_;
PasswordHasher &passwordHasher_;
};
+68
View File
@@ -0,0 +1,68 @@
#include <algorithm>
#include <cctype>
#include <util/validation.hpp>
std::string trim(std::string_view value) {
auto begin = value.begin();
auto end = value.end();
while(begin != end && std::isspace(static_cast<unsigned char>(*begin))) {
++begin;
}
while(begin != end && std::isspace(static_cast<unsigned char>(*(end - 1)))) {
--end;
}
return std::string{ begin, end };
}
std::string normalize_email(std::string_view email) {
auto normalize = trim(email);
std::transform(normalize.begin(), normalize.end(), normalize.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return normalize;
}
bool is_valid_email(std::string_view email) {
if(email.empty() && email.size() > 200) {
return false;
}
const auto at_pos = email.find("@");
if(at_pos == std::string_view::npos) {
return false;
}
if(at_pos == 0 || at_pos == email.size() - 1) {
return false;
}
if(email.find("@", at_pos + 1) != std::string_view::npos) {
return false;
}
const auto domain = email.substr(at_pos + 1);
const auto dot_pos = email.find('.');
if(dot_pos == std::string_view::npos) {
return false;
}
if(dot_pos == 0 || dot_pos == domain.size() - 1) {
return false;
}
if(email.find(' ') != std::string_view::npos) {
return false;
}
return true;
}
bool is_valid_password(std::string_view password) {
constexpr std::size_t minPasswordLen = 8;
constexpr std::size_t maxPasswordLen = 1024;
return password.size() <= maxPasswordLen && password.size() >= minPasswordLen;
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include <string_view>
std::string trim(std::string_view value);
std::string normalize_email(std::string_view email);
bool is_valid_email(std::string_view email);
bool is_valid_password(std::string_view password);