From 875c692180ec143fb16fcdedc7b687efb11d42b0 Mon Sep 17 00:00:00 2001 From: ANTON IGNATEV Date: Sat, 18 Apr 2026 22:51:08 +0300 Subject: [PATCH] Add Sesion implementation util/time --- CMakeLists.txt | 5 ++ auth.db | Bin 16384 -> 40960 bytes auth.db-shm | Bin 32768 -> 0 bytes auth.db-wal | Bin 90672 -> 0 bytes src/application/application.cpp | 60 ++++++++++++++++--- src/config/settings/settings.hpp | 8 ++- src/domain/Session.hpp | 13 +++++ src/repo/session_repository.cpp | 77 +++++++++++++++++++++++++ src/repo/session_repository.hpp | 20 +++++++ src/security/session_token_service.cpp | 44 ++++++++++++++ src/security/session_token_service.hpp | 15 +++++ src/util/time_utils.cpp | 31 ++++++++++ src/util/time_utils.hpp | 7 +++ 13 files changed, 268 insertions(+), 12 deletions(-) delete mode 100644 auth.db-shm delete mode 100644 auth.db-wal create mode 100644 src/domain/Session.hpp create mode 100644 src/repo/session_repository.cpp create mode 100644 src/repo/session_repository.hpp create mode 100644 src/security/session_token_service.cpp create mode 100644 src/security/session_token_service.hpp create mode 100644 src/util/time_utils.cpp create mode 100644 src/util/time_utils.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dc41378..86062ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ include(CTest) enable_testing() find_package(SQLite3 REQUIRED) +find_package(OpenSSL REQUIRED) add_subdirectory(submodules/drogon) add_subdirectory(submodules/json) @@ -25,6 +26,9 @@ add_executable(auth_service src/db/transaction.cpp src/db/migrations_runner.cpp src/repo/user_repository.cpp + src/repo/session_repository.cpp + src/util/time_utils.cpp + src/security/session_token_service.cpp ) target_include_directories(auth_service @@ -43,6 +47,7 @@ target_link_libraries(auth_service PRIVATE drogon SQLite::SQLite3 nlohmann_json::nlohmann_json + OpenSSL::Crypto ) add_executable(auth_service_tests diff --git a/auth.db b/auth.db index 6381f6b1921e3c56f66f158980f3d371bfd4f58b..abf1d3a2e1c6720e510197d714783f2255da3bc6 100644 GIT binary patch literal 40960 zcmeI*&2QRf9KdlKN{C^ZD^-;b6OwnsYN=>Cde|;yty7^=CnZK|b{O*FXNuJTDR#0{ zJ51fnVSh`{yY03=WYq~ZKpYW}Ft*1K+Ax+ocubmZ65T(Ma+JV(10?$?nQ|LvW&COrIVzsb{@D zx>hy}L;c}NLY`zL52Xd{BAb&}Aqu@xK871?Pz0vM> zUGr`Vc=3oF9{9QARk8GMmEmQ*B`8}3Idr$wo%FPRxi7Dp<-5YmLowPF;o{5Ug8Qk= z`p6rK+p?x5oBq?KHUB+<;%oVn*Y@uzPBsDvAb zz_y(A*BY%>E1l(^|CfurdHq@1{rvg=d((RYnEzk?6N4TgfB*srAb1AjHYCf}aTf&yNA#@Wp5 z;?mNLjk+a?Nja${74aph#U)UR#W~2;F~n6N#L>yeRRJofpfS0JM^#jzEHS4vRUyPR zB1FN@ABaPJd^9I(@(1}eGBL7?Yilz$T7!+uNKMQs$w rr8%Mg5N2f7W<;})38aOQ{}TiMC!mHG{Cqq>v*eTWb8|CGN>Wn*q`G1# diff --git a/auth.db-shm b/auth.db-shm deleted file mode 100644 index 164ad7cb6bc24ef12195e58621a28a19ef8442d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)yG;W@5CG7zu`%Xn8w|Pvh>+X@wIG@xbU*{t0u?wUBqb!E1w;be;6*}YNEi6H zH_~cWtJU1>yL_tWMP&5WYkFGe-CXB)k5{Yv`RnBJ($vy)SRBt3TeaOMOMw z{rC~(IF8FEzdx*3=IxS4CGV6xE;;+ld;NNC{n`)8S=P!rf&c*m1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZV3P%U_56rojA9&naS+o@ZdGpt8WuQ=S;G~TBhVIsNgTzzEzRnaK%)ZRbGI5T zsu+P*2=wEm6@6-wK=T5F$Z3p%Kon zZ&%gNcXW4TEkoTkG@92f^)j(_O^tG`s*3m4iHBEiQ!2IMS=Kql-k-BD%X6KwZv6fR zv9hk=pc38_jx9eRUQiG~009ILKmY**5I_I{1QsGNIbP?FL?WJj+bsQ-oH1q^qo)1K zyE&<~r8G6wc3p?2+Er>aq>7TwsPRNfYuA!$S2Di6E!n4T)B4o5o>XT%A-3ACB~okc zcN%#;n^RL-Z%R#cibqdJM{&ilZko3Zjb_pVx;Ze@8?t6v@3*q!#?+yl9jIHht!+IW zDRq5uSN)@gZW)=hxB#8uKx4z18P|qlgSGxhQ~D^^Aedfr$owv0{Ic>Hpg zlbh^)4j6jQ8t5M|`Ulfiez;<>#Vj^`OfCCTSi52q1s}0tg_000IagfB*t#w16vKprf(*-Vfebv44(u0e_j8%tKgk zynt_6S6RG(SfC()00IagfB*srAb`M<7r3*@=Z~yj?YS#hOvE-!Gdol;&C;{ql_u@f zo^aN?>|0Nl^^rQ6>&1L^YiQ6Y%$oQ9-`VWO#Bg@h5UKGq=DLp><6_@anefHKZS72I z@%Dtw(2tf*ET$&4ty)q`Y}R&^`u4M#SSUukfE=KJ@dXyu_yY18-|xf=bYFPUi-#`w z@K(kbSn^-RGynkv5I_I{1Q0*~0R#|00D<`kxZ(x$l}A=Ru;#tU9Pt8yvK`AjgayY7 z`0J0l;swMa1px#QKmY**5I_I{1WvBNkl!EKwAwS-IF-@v^wFoMd)1w)z--Cw&Kt_M zkxA}Ob#!`HG2#VeAOrCNV$K8RBe3UzSoHG|$ZPz76EAS=(iM+9_Q|(d#uqrbU&QPU z0R#|0009ILKmY**5I_KdMJnKm7x=jK$z2bg{ptES;st_bZIO8h3yv2E)bFi`7Z4Q` z1Q0*~0R#|0009KfEPzR1Sap54cH=a(|ooj&bkobNU%uM-!W{7e_HBcbn9M_1aJ)dU{+#3D1w<7E0R#|0009IL zKmdW$FE9`+X61X2@5Y}Vuioje56#|_KmBG`Xa00`jCcV#TLJSC=-HeM@i2X}4e?;F zP20Wq^0QBZL_CpM&6P6}kK*XDfITT`DHNHr5 zv!|~(&BjPBYZ+-W~5L%e_xr67O+0tg_000Ib{egWbI zx_dN{fxboS6{+YGqF>ZBWxqc25g6k`gGMH;TXxEQG1Go)XHtu|CuD|xRCdZUA3;bJ zh4BUE7+*kq&)t3czw3(E_lgrQ@c1`}S3ZBm6T26~iE$1HAbe0c!bfGb`g)PCQN>k|)k%Xk4#_%|i|8{-a^S@9A91Q0*~0R#|0 z009ILKmdU%5D0i`BSAT*rYRcZl?`&{DO~f=`#<+R^YBRVzQF5B`1LAiE=@oH0R#|0 z009ILKmY**5I~@s1mYgQ(sFTd<3>5nz_rGNo*&K`>-vZCnX!C+cX~iK2R5{BxT2-? z@|M<(sr4JKX>AohH_TKgSbX9!l}B*sl{KN2uYP!ooMgZgKBkBt3IYfqfB*srAb #include @@ -32,16 +35,55 @@ int Application::run() const { MigrationsRunner runner(db); runner.run_file("migrations/001_init.sql"); - UserRepository user_repository(db); - 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"); + UserRepository user_repository(db); + SessionRepository session_repository(db); - 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'; + SessionTokenService token_service; + + const auto token_pair = token_service.generate(); + + std::cout << "Now UTC: " << now_utc_iso8601() << '\n'; + std::cout << "Expires at: " << expires_at_from_now(settings_.session_ttl) << '\n'; + std::cout << "Generated raw token: " << token_pair.raw_token << '\n'; + std::cout << "Generated token hash: " << token_pair.token_hash << '\n'; + + 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(); diff --git a/src/config/settings/settings.hpp b/src/config/settings/settings.hpp index 527b801..cca761f 100644 --- a/src/config/settings/settings.hpp +++ b/src/config/settings/settings.hpp @@ -4,7 +4,9 @@ #include struct Settings { - std::string host{ "127.0.0.1" }; - std::uint16_t port{ 8080 }; - std::string db_path{ "auth.db" }; + std::string host{ "127.0.0.1" }; + std::uint16_t port{ 8080 }; + std::string db_path{ "auth.db" }; + std::chrono::seconds session_ttl{ 86400 }; + bool secure_cookies{ false }; }; \ No newline at end of file diff --git a/src/domain/Session.hpp b/src/domain/Session.hpp new file mode 100644 index 0000000..2bb14a7 --- /dev/null +++ b/src/domain/Session.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include + +struct Session { + std::int64_t id{}; + std::int64_t user_id{}; + std::string token_hash; + std::string created_at; + std::string expires_at; + std::optional revoked_at; +}; \ No newline at end of file diff --git a/src/repo/session_repository.cpp b/src/repo/session_repository.cpp new file mode 100644 index 0000000..6a67ea9 --- /dev/null +++ b/src/repo/session_repository.cpp @@ -0,0 +1,77 @@ +#include "repo/session_repository.hpp" + +#include "db/statement.hpp" + +#include + +namespace { +Session read_session(Statement &stmt) { + Session session; + session.id = stmt.column_int64(0); + session.user_id = stmt.column_int64(1); + session.token_hash = stmt.column_text(2); + session.created_at = stmt.column_text(3); + session.expires_at = stmt.column_text(4); + + if(!stmt.column_is_null(5)) { + session.revoked_at = stmt.column_text(5); + } + return session; +} +} // namespace + +SessionRepository::SessionRepository(SqlliteDb &db) + : db_(db) {} + +Session SessionRepository::create(std::int64_t user_id, std::string_view token_hash, + std::string_view created_at, std::string_view expires_at) const { + auto stmt = + db_.prepare("INSERT INTO sessions(user_id, token_hash, created_at, expires_at, revoked_at) " + "VALUES(?1, ?2, ?3, ?4, NULL);"); + stmt.bind_int64(1, user_id); + stmt.bind_text(2, token_hash); + stmt.bind_text(3, created_at); + stmt.bind_text(4, expires_at); + + stmt.execute(); + + const auto session_id = db_.last_insert_rowid(); + auto select_stmt = + db_.prepare("SELECT id, user_id, token_hash, created_at, expires_at, revoked_at " + "FROM sessions " + "WHERE id = ?1 " + "LIMIT 1;"); + + select_stmt.bind_int64(1, session_id); + + if(!select_stmt.step()) { + throw std::runtime_error("Failed to load created session"); + } + + return read_session(select_stmt); +} + +std::optional SessionRepository::find_by_token_hash(std::string_view token_hash) const { + auto stmt = db_.prepare("SELECT id, user_id, token_hash, created_at, expires_at, revoked_at " + "FROM sessions " + "WHERE token_hash = ?1 " + "LIMIT 1;"); + stmt.bind_text(1, token_hash); + + if(!stmt.step()) { + return std::nullopt; + } + + return read_session(stmt); +} + +void SessionRepository::revoke_by_token_hash(std::string_view token_hash, + std::string_view revoked_at) const { + auto stmt = db_.prepare("UPDATE sessions " + "SET revoked_at = ?1 " + "WHERE token_hash = ?2 AND revoked_at IS NULL;"); + stmt.bind_text(1, revoked_at); + stmt.bind_text(2, token_hash); + + stmt.execute(); +} \ No newline at end of file diff --git a/src/repo/session_repository.hpp b/src/repo/session_repository.hpp new file mode 100644 index 0000000..1d1ada1 --- /dev/null +++ b/src/repo/session_repository.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "domain/Session.hpp" + +#include +#include +#include +#include + +class SessionRepository { +public: + explicit SessionRepository(SqlliteDb &db); + Session create(std::int64_t user_id, std::string_view token_hash, std::string_view created_at, + std::string_view expires_at) const; + std::optional find_by_token_hash(std::string_view token_hash) const; + void revoke_by_token_hash(std::string_view token_hash, std::string_view revoked_at) const; + +private: + SqlliteDb &db_; +}; \ No newline at end of file diff --git a/src/security/session_token_service.cpp b/src/security/session_token_service.cpp new file mode 100644 index 0000000..a8e899e --- /dev/null +++ b/src/security/session_token_service.cpp @@ -0,0 +1,44 @@ +#include "security/session_token_service.hpp" + +#include +#include +#include +#include +#include + +namespace { +constexpr int kTokenSizeBytes = 32; +std::string bytes_to_hex(const unsigned char *data, std::size_t size) { + std::ostringstream out; + out << std::hex << std::setfill('0'); + for(std::size_t i = 0; i < size; ++i) { + out << std::setw(2) << static_cast(data[i]); + } + + return out.str(); +} +} // namespace + +SessionTokenPair SessionTokenService::generate() const { + const auto raw_token = generate_random_token(); + const auto token_hash = sha256_hex(raw_token); + + return SessionTokenPair{ .raw_token = raw_token, .token_hash = token_hash }; +} + +std::string SessionTokenService::generate_random_token() { + unsigned char buffer[kTokenSizeBytes]; + if(RAND_bytes(buffer, sizeof(buffer)) != 1) { + throw std::runtime_error("Failed to generate secure random session token"); + } + + return bytes_to_hex(buffer, sizeof(buffer)); +} + +std::string SessionTokenService::sha256_hex(const std::string &input) { + unsigned char hash[SHA256_DIGEST_LENGTH]; + + SHA256(reinterpret_cast(input.data()), input.size(), hash); + + return bytes_to_hex(hash, sizeof(hash)); +} \ No newline at end of file diff --git a/src/security/session_token_service.hpp b/src/security/session_token_service.hpp new file mode 100644 index 0000000..84c76d1 --- /dev/null +++ b/src/security/session_token_service.hpp @@ -0,0 +1,15 @@ +#include + +struct SessionTokenPair { + std::string raw_token; + std::string token_hash; +}; + +class SessionTokenService { +public: + SessionTokenPair generate() const; + +private: + static std::string generate_random_token(); + static std::string sha256_hex(const std::string &input); +}; diff --git a/src/util/time_utils.cpp b/src/util/time_utils.cpp new file mode 100644 index 0000000..dc874ee --- /dev/null +++ b/src/util/time_utils.cpp @@ -0,0 +1,31 @@ +#include "util/time_utils.hpp" + +#include +#include +#include +#include + +namespace { +std::string to_iso8601_utc(std::time_t time_value) { + std::tm utc_tm{}; + if(gmtime_r(&time_value, &utc_tm) == nullptr) { + throw std::runtime_error("Failed to convert time to UTC"); + } + + std::ostringstream out; + out << std::put_time(&utc_tm, "%Y-%m-%dT%H:%M:%SZ"); + return out.str(); +} +} // namespace + +std::string now_utc_iso8601() { + const auto now = std::chrono::system_clock::now(); + const auto now_time_t = std::chrono::system_clock::to_time_t(now); + return to_iso8601_utc(now_time_t); +} + +std::string expires_at_from_now(std::chrono::seconds ttl) { + const auto expires_at = std::chrono::system_clock::now() + ttl; + const auto expires_time_t = std::chrono::system_clock::to_time_t(expires_at); + return to_iso8601_utc(expires_time_t); +} diff --git a/src/util/time_utils.hpp b/src/util/time_utils.hpp new file mode 100644 index 0000000..25a54ed --- /dev/null +++ b/src/util/time_utils.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +std::string now_utc_iso8601(); +std::string expires_at_from_now(std::chrono::seconds ttl); \ No newline at end of file