Add Sesion
implementation util/time
This commit is contained in:
@@ -11,6 +11,7 @@ include(CTest)
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
find_package(SQLite3 REQUIRED)
|
find_package(SQLite3 REQUIRED)
|
||||||
|
find_package(OpenSSL REQUIRED)
|
||||||
|
|
||||||
add_subdirectory(submodules/drogon)
|
add_subdirectory(submodules/drogon)
|
||||||
add_subdirectory(submodules/json)
|
add_subdirectory(submodules/json)
|
||||||
@@ -25,6 +26,9 @@ add_executable(auth_service
|
|||||||
src/db/transaction.cpp
|
src/db/transaction.cpp
|
||||||
src/db/migrations_runner.cpp
|
src/db/migrations_runner.cpp
|
||||||
src/repo/user_repository.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
|
target_include_directories(auth_service
|
||||||
@@ -43,6 +47,7 @@ target_link_libraries(auth_service PRIVATE
|
|||||||
drogon
|
drogon
|
||||||
SQLite::SQLite3
|
SQLite::SQLite3
|
||||||
nlohmann_json::nlohmann_json
|
nlohmann_json::nlohmann_json
|
||||||
|
OpenSSL::Crypto
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(auth_service_tests
|
add_executable(auth_service_tests
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,7 +3,10 @@
|
|||||||
#include "db/migrations_runner.hpp"
|
#include "db/migrations_runner.hpp"
|
||||||
#include "db/sqllite_db.hpp"
|
#include "db/sqllite_db.hpp"
|
||||||
#include "http/controllers/health_controller.hpp"
|
#include "http/controllers/health_controller.hpp"
|
||||||
|
#include "repo/session_repository.hpp"
|
||||||
#include "repo/user_repository.hpp"
|
#include "repo/user_repository.hpp"
|
||||||
|
#include "security/session_token_service.hpp"
|
||||||
|
#include "util/time_utils.hpp"
|
||||||
|
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
#include <format>
|
#include <format>
|
||||||
@@ -32,16 +35,55 @@ int Application::run() const {
|
|||||||
MigrationsRunner runner(db);
|
MigrationsRunner runner(db);
|
||||||
runner.run_file("migrations/001_init.sql");
|
runner.run_file("migrations/001_init.sql");
|
||||||
|
|
||||||
UserRepository user_repository(db);
|
UserRepository user_repository(db);
|
||||||
const auto existing_user = user_repository.find_by_email("test@example.com");
|
SessionRepository session_repository(db);
|
||||||
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';
|
SessionTokenService token_service;
|
||||||
} else {
|
|
||||||
std::cout << "User already exists: id=" << existing_user->id
|
const auto token_pair = token_service.generate();
|
||||||
<< ", email=" << existing_user->email << '\n';
|
|
||||||
|
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();
|
register_routes();
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
struct Settings {
|
struct Settings {
|
||||||
std::string host{ "127.0.0.1" };
|
std::string host{ "127.0.0.1" };
|
||||||
std::uint16_t port{ 8080 };
|
std::uint16_t port{ 8080 };
|
||||||
std::string db_path{ "auth.db" };
|
std::string db_path{ "auth.db" };
|
||||||
|
std::chrono::seconds session_ttl{ 86400 };
|
||||||
|
bool secure_cookies{ false };
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<std::string> revoked_at;
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#include "repo/session_repository.hpp"
|
||||||
|
|
||||||
|
#include "db/statement.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
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<Session> 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "domain/Session.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <db/sqllite_db.hpp>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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<Session> 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_;
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#include "security/session_token_service.hpp"
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
#include <openssl/sha.h>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
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<unsigned int>(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<const unsigned char *>(input.data()), input.size(), hash);
|
||||||
|
|
||||||
|
return bytes_to_hex(hash, sizeof(hash));
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#include <string>
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include "util/time_utils.hpp"
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string now_utc_iso8601();
|
||||||
|
std::string expires_at_from_now(std::chrono::seconds ttl);
|
||||||
Reference in New Issue
Block a user