From 8daff84310c4a9b296ccb98d0b4bb84a52131a1d Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Thu, 18 Dec 2025 17:12:07 -0600 Subject: [PATCH 1/6] - Cleanups and small refactors for /token endpoint and client http requests. --- src/client/http_client_interface.h | 21 ++++++ src/client/httplib_client.cpp | 13 +--- src/client/httplib_pooled_client.cpp | 17 +---- src/common/json_request.cpp | 104 +++++++++++++++++++++++++++ src/common/json_request.h | 26 +++++++ src/common/json_request_test.cpp | 61 ++++++++++++++++ src/server/auth_utils.cpp | 52 +++----------- src/server/auth_utils.h | 35 +++------ src/server/auth_utils_test.cpp | 13 ++++ src/server/dbps_api_server.cpp | 7 +- 10 files changed, 251 insertions(+), 98 deletions(-) diff --git a/src/client/http_client_interface.h b/src/client/http_client_interface.h index eef9c27..48b8a3e 100644 --- a/src/client/http_client_interface.h +++ b/src/client/http_client_interface.h @@ -18,6 +18,7 @@ #pragma once #include +#include /** * Interface for HTTP client implementations. @@ -28,6 +29,11 @@ class HttpClientInterface { public: virtual ~HttpClientInterface() = default; + + using HeaderList = httplib::Headers; + + static constexpr const char* kJsonContentType = "application/json"; + static constexpr const char* kDefaultUserAgent = "DBPSApiClient/1.0"; struct HttpResponse { int status_code; @@ -43,6 +49,21 @@ class HttpClientInterface { : status_code(code), result(std::move(response_result)), error_message(std::move(error)) {} }; + static HeaderList DefaultJsonGetHeaders() { + HeaderList headers; + headers.insert({"Accept", kJsonContentType}); + headers.insert({"User-Agent", kDefaultUserAgent}); + return headers; + } + + static HeaderList DefaultJsonPostHeaders() { + HeaderList headers; + headers.insert({"Content-Type", kJsonContentType}); + headers.insert({"Accept", kJsonContentType}); + headers.insert({"User-Agent", kDefaultUserAgent}); + return headers; + } + virtual HttpResponse Get(const std::string& endpoint) = 0; virtual HttpResponse Post(const std::string& endpoint, const std::string& json_body) = 0; }; diff --git a/src/client/httplib_client.cpp b/src/client/httplib_client.cpp index 5c55603..a6676ec 100644 --- a/src/client/httplib_client.cpp +++ b/src/client/httplib_client.cpp @@ -29,10 +29,7 @@ HttpClientInterface::HttpResponse HttplibClient::Get(const std::string& endpoint client.set_read_timeout(30); // Set headers to indicate JSON responses - httplib::Headers headers = { - {"Accept", "application/json"}, - {"User-Agent", "DBPSApiClient/1.0"} - }; + auto headers = HttpClientInterface::DefaultJsonGetHeaders(); // Make the GET request auto result = client.Get(endpoint, headers); @@ -56,14 +53,10 @@ HttpClientInterface::HttpResponse HttplibClient::Post(const std::string& endpoin client.set_read_timeout(30); // Set headers for JSON content - httplib::Headers headers = { - {"Content-Type", "application/json"}, - {"Accept", "application/json"}, - {"User-Agent", "DBPSApiClient/1.0"} - }; + auto headers = HttpClientInterface::DefaultJsonPostHeaders(); // Make the POST request - auto result = client.Post(endpoint, headers, json_body, "application/json"); + auto result = client.Post(endpoint, headers, json_body, HttpClientInterface::kJsonContentType); if (!result) { return HttpResponse(0, "", "HTTP POST request failed: no response received"); diff --git a/src/client/httplib_pooled_client.cpp b/src/client/httplib_pooled_client.cpp index c6e375b..f048a7b 100644 --- a/src/client/httplib_pooled_client.cpp +++ b/src/client/httplib_pooled_client.cpp @@ -170,24 +170,13 @@ void HttplibPooledClient::WorkerLoop() { auto perform_once = [&](RequestTask& t) -> std::pair { try { if (t.kind == RequestTask::Kind::Get) { - //TODO: these are hardcoded and copied from httplib_client.cpp - // we should move them to a common place (header file). - httplib::Headers headers = { - {"Accept", "application/json"}, - {"User-Agent", "DBPSApiClient/1.0"} - }; + auto headers = HttpClientInterface::DefaultJsonGetHeaders(); auto res = client->Get(t.endpoint, headers); if (!res) return {false, HttpResponse(0, "", "HTTP GET failed")}; return {true, HttpResponse(res->status, res->body)}; } else { - //TODO: these are hardcoded and copied from httplib_client.cpp - // we should move them to a common place (header file). - httplib::Headers headers = { - {"Content-Type", "application/json"}, - {"Accept", "application/json"}, - {"User-Agent", "DBPSApiClient/1.0"} - }; - auto res = client->Post(t.endpoint, headers, t.json_body, "application/json"); + auto headers = HttpClientInterface::DefaultJsonPostHeaders(); + auto res = client->Post(t.endpoint, headers, t.json_body, HttpClientInterface::kJsonContentType); if (!res) return {false, HttpResponse(0, "", "HTTP POST failed")}; return {true, HttpResponse(res->status, res->body)}; } diff --git a/src/common/json_request.cpp b/src/common/json_request.cpp index 3a9b974..7a50c3d 100644 --- a/src/common/json_request.cpp +++ b/src/common/json_request.cpp @@ -129,6 +129,110 @@ std::string EncodeBase64Safe(const std::vector& data) { } } +void TokenRequest::Parse(const std::string& request_body) { + client_id.clear(); + api_key.clear(); + error_message = std::nullopt; + + auto json_body_opt = SafeLoadJsonBody(request_body); + if (!json_body_opt) { + error_message = "Invalid JSON in request body. Invalid format."; + return; + } + + auto json_body = *json_body_opt; + if (json_body.t() != crow::json::type::Object) { + error_message = "Invalid JSON in request body. Invalid format."; + return; + } + + if (json_body.has("client_id") && json_body["client_id"].t() == crow::json::type::String) { + client_id = std::string(json_body["client_id"]); + } else { + error_message = "Missing required field: client_id"; + return; + } + + if (json_body.has("api_key") && json_body["api_key"].t() == crow::json::type::String) { + api_key = std::string(json_body["api_key"]); + } else { + error_message = "Missing required field: api_key"; + return; + } +} + +std::string TokenRequest::ToJson() const { + crow::json::wvalue json; + if (error_message.has_value()) { + json["error"] = error_message.value(); + return PrettyPrintJson(json.dump()); + } + + json["client_id"] = client_id; + json["api_key"] = api_key; + return PrettyPrintJson(json.dump()); +} + +void TokenResponse::Parse(const std::string& response_body) { + token = std::nullopt; + expires_at = std::nullopt; + error_message = std::nullopt; + error_status_code = 400; + + auto json_body_opt = SafeLoadJsonBody(response_body); + if (!json_body_opt) { + error_message = "Invalid JSON in response body. Invalid format."; + return; + } + + auto json_body = *json_body_opt; + if (json_body.t() != crow::json::type::Object) { + error_message = "Invalid JSON in response body. Invalid format."; + return; + } + + if (json_body.has("error")) { + error_message = std::string(json_body["error"]); + return; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"token"})) { + token = *parsed_value; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"expires_at"})) { + try { + expires_at = std::stoll(*parsed_value); + } catch (const std::exception&) { + error_message = "Invalid expires_at field"; + return; + } + } +} + +std::string TokenResponse::ToJson() const { + crow::json::wvalue json; + + if (error_message.has_value()) { + json["error"] = error_message.value(); + return json.dump(); + } + + if (!token.has_value()) { + json["error"] = "Invalid token response"; + return json.dump(); + } + + json["token"] = token.value(); + json["token_type"] = "Bearer"; + + if (expires_at.has_value()) { + json["expires_at"] = expires_at.value(); + } + + return PrettyPrintJson(json.dump()); +} + // JsonRequest implementation void JsonRequest::ParseCommon(const std::string& request_body) { // Load and validate JSON first diff --git a/src/common/json_request.h b/src/common/json_request.h index a84dd6e..d5bdb88 100644 --- a/src/common/json_request.h +++ b/src/common/json_request.h @@ -22,6 +22,7 @@ #include #include #include +#include #include "enums.h" #include "enum_utils.h" @@ -327,3 +328,28 @@ class DecryptJsonResponse : public JsonResponse { */ std::string ToJsonString() const override; }; + +/** + * Structure to hold parsed token request data. + */ +struct TokenRequest { + std::string client_id; + std::string api_key; + std::optional error_message; + + void Parse(const std::string& request_body); + std::string ToJson() const; +}; + +/** + * Structure to hold token generation result. + */ +struct TokenResponse { + std::optional token; + std::optional expires_at; + std::optional error_message; + int error_status_code = 400; + + void Parse(const std::string& response_body); + std::string ToJson() const; +}; diff --git a/src/common/json_request_test.cpp b/src/common/json_request_test.cpp index 9e66b6a..dd8c07c 100644 --- a/src/common/json_request_test.cpp +++ b/src/common/json_request_test.cpp @@ -940,3 +940,64 @@ TEST(JsonRequest, DecryptJsonResponseInvalidDatatypeLength) { std::string error = response.GetValidationError(); ASSERT_TRUE(error.find("data_batch.datatype_info.length (invalid integer value)") != std::string::npos); } + +// Token request and response tests (simplified) +TEST(JsonRequest, TokenRequestParse) { + { + TokenRequest req; + req.Parse(R"({"client_id":"client1","api_key":"key1"})"); + ASSERT_FALSE(req.error_message.has_value()); + ASSERT_EQ(req.client_id, "client1"); + ASSERT_EQ(req.api_key, "key1"); + } + { + TokenRequest req; + req.Parse(R"({"api_key":"key1"})"); + ASSERT_TRUE(req.error_message.has_value()); + } + { + TokenRequest req; + req.Parse("{ invalid json }"); + ASSERT_TRUE(req.error_message.has_value()); + } +} + +TEST(JsonRequest, TokenRequestToJson) { + TokenRequest req; + req.client_id = "clientA"; + req.api_key = "keyA"; + + auto json = crow::json::load(req.ToJson()); + ASSERT_TRUE(json); + ASSERT_EQ(std::string(json["client_id"]), "clientA"); + ASSERT_EQ(std::string(json["api_key"]), "keyA"); +} + +TEST(JsonRequest, TokenResponseRoundTrip) { + TokenResponse resp; + resp.token = "token123"; + resp.expires_at = 1234567890; + + auto json = crow::json::load(resp.ToJson()); + ASSERT_TRUE(json); + ASSERT_EQ(std::string(json["token"]), "token123"); + ASSERT_EQ(std::string(json["token_type"]), "Bearer"); + + auto expires_at_str = SafeGetFromJsonPath(json, {"expires_at"}); + ASSERT_TRUE(expires_at_str.has_value()); + ASSERT_EQ(std::stoll(*expires_at_str), 1234567890); + + TokenResponse parsed; + parsed.Parse(resp.ToJson()); + ASSERT_FALSE(parsed.error_message.has_value()); + ASSERT_TRUE(parsed.token.has_value()); + ASSERT_TRUE(parsed.expires_at.has_value()); + ASSERT_EQ(parsed.token.value(), "token123"); + ASSERT_EQ(parsed.expires_at.value(), 1234567890); +} + +TEST(JsonRequest, TokenResponseParseError) { + TokenResponse resp; + resp.Parse(R"({"error":"unauthorized"})"); + ASSERT_TRUE(resp.error_message.has_value()); +} diff --git a/src/server/auth_utils.cpp b/src/server/auth_utils.cpp index 6a3bab8..95739da 100644 --- a/src/server/auth_utils.cpp +++ b/src/server/auth_utils.cpp @@ -110,7 +110,9 @@ bool ClientCredentialStore::HasClientId(const std::string& client_id) const { } // GenerateJWT implementation -std::optional ClientCredentialStore::GenerateJWT(const std::string& client_id, const std::string& api_key) const { +std::optional> ClientCredentialStore::GenerateJWT( + const std::string& client_id, + const std::string& api_key) const { // Validate that client_id is not empty if (client_id.empty()) { std::cout << "Error generating JWT: client_id cannot be empty" << std::endl; @@ -143,59 +145,20 @@ std::optional ClientCredentialStore::GenerateJWT(const std::string& .set_expires_at(std::chrono::system_clock::from_time_t(exp_seconds)) .sign(jwt::algorithm::hs256{jwt_secret_key_}); - return token; + return std::make_pair(token, static_cast(exp_seconds)); } catch (const std::exception& e) { std::cerr << "Error generating JWT: " << e.what() << std::endl; return std::nullopt; } } -// ParseTokenRequest implementation -TokenRequest ParseTokenRequest(const std::string& request_body) { - TokenRequest token_req; - - try { - // Parse JSON request body using nlohmann::json - nlohmann::json json_body = nlohmann::json::parse(request_body); - - // Validate that it's an object - if (!json_body.is_object()) { - token_req.error_message = "Invalid JSON in request body. Invalid format."; - return token_req; - } - - // Extract client_id from request - if (json_body.contains("client_id") && json_body["client_id"].is_string()) { - token_req.client_id = json_body["client_id"].get(); - } else { - token_req.error_message = "Missing required field: client_id"; - return token_req; - } - - // Extract api_key from request - if (json_body.contains("api_key") && json_body["api_key"].is_string()) { - token_req.api_key = json_body["api_key"].get(); - } else { - token_req.error_message = "Missing required field: api_key"; - return token_req; - } - - return token_req; - } catch (const nlohmann::json::exception& e) { - token_req.error_message = "Invalid JSON in request body: " + std::string(e.what()); - return token_req; - } catch (const std::exception& e) { - token_req.error_message = "Error parsing request: " + std::string(e.what()); - return token_req; - } -} - // ProcessTokenRequest implementation TokenResponse ClientCredentialStore::ProcessTokenRequest(const std::string& request_body) const { TokenResponse response; // Parse token request - TokenRequest token_req = ParseTokenRequest(request_body); + TokenRequest token_req; + token_req.Parse(request_body); // Check if parsing resulted in an error if (token_req.error_message.has_value()) { @@ -218,7 +181,8 @@ TokenResponse ClientCredentialStore::ProcessTokenRequest(const std::string& requ return response; } - response.token = token.value(); + response.token = token->first; + response.expires_at = token->second; std::cout << "=== ProcessTokenRequest (Success) ===" << std::endl; std::cout << "Token generated for client_id: " << token_req.client_id << std::endl; diff --git a/src/server/auth_utils.h b/src/server/auth_utils.h index c8334cb..11ddcd3 100644 --- a/src/server/auth_utils.h +++ b/src/server/auth_utils.h @@ -27,40 +27,25 @@ #include #include #include +#include +#include +#include "json_request.h" #ifndef DBPS_EXPORT #define DBPS_EXPORT #endif /** - * Structure to hold parsed token request data. + * ClientCredentialStore manages client_id to api_key mappings for authentication. + * + * - Loads client credentials from a Json file and stores them in-memory. + * - Generates a JWT token for a given client_id. * * Integration point for Protegrity: * - This request can be updated with a production configuration for authentication or credentials checking. * - The specific fields are transparent to the library users. The API call payload of the token request is passed as-is to the module, * so library users don't parse the request payload. */ - struct DBPS_EXPORT TokenRequest { - std::string client_id; - std::string api_key; - std::optional error_message; // Error message if parsing failed -}; - -/** - * Structure to hold token generation result. - */ -struct DBPS_EXPORT TokenResponse { - std::optional token; - std::optional error_message; - int error_status_code = 400; // HTTP status code for error response -}; - -/** - * ClientCredentialStore manages client_id to api_key mappings for authentication. - * - * - Loads client credentials from a Json file and stores them in-memory. - * - Generates a JWT token for a given client_id. - */ class DBPS_EXPORT ClientCredentialStore { public: /** @@ -134,9 +119,11 @@ class DBPS_EXPORT ClientCredentialStore { * * @param client_id The client identifier to validate and include in the JWT * @param api_key The API key to validate against the stored credential - * @return The JWT token as a string if credentials are valid, or std::nullopt on error or invalid credentials + * @return Pair of (token, expires_at epoch seconds) if credentials are valid, or std::nullopt on error or invalid credentials */ - std::optional GenerateJWT(const std::string& client_id, const std::string& api_key) const; + std::optional> GenerateJWT( + const std::string& client_id, + const std::string& api_key) const; // In-memory storage: client_id -> api_key std::map credentials_; diff --git a/src/server/auth_utils_test.cpp b/src/server/auth_utils_test.cpp index f8f44d4..2e1054c 100644 --- a/src/server/auth_utils_test.cpp +++ b/src/server/auth_utils_test.cpp @@ -35,24 +35,28 @@ TEST(AuthUtilsTest, InitWithMap) { auto response1 = store.ProcessTokenRequest(valid_json1); EXPECT_TRUE(response1.token.has_value()); EXPECT_FALSE(response1.token.value().empty()); + EXPECT_TRUE(response1.expires_at.has_value()); EXPECT_FALSE(response1.error_message.has_value()); std::string valid_json2 = R"({"client_id": "client2", "api_key": "key2"})"; auto response2 = store.ProcessTokenRequest(valid_json2); EXPECT_TRUE(response2.token.has_value()); EXPECT_FALSE(response2.token.value().empty()); + EXPECT_TRUE(response2.expires_at.has_value()); EXPECT_FALSE(response2.error_message.has_value()); // Test ProcessTokenRequest with invalid credentials std::string invalid_json1 = R"({"client_id": "client1", "api_key": "wrong_key"})"; auto response3 = store.ProcessTokenRequest(invalid_json1); EXPECT_FALSE(response3.token.has_value()); + EXPECT_FALSE(response3.expires_at.has_value()); EXPECT_TRUE(response3.error_message.has_value()); EXPECT_EQ(response3.error_status_code, 401); std::string invalid_json2 = R"({"client_id": "nonexistent", "api_key": "key1"})"; auto response4 = store.ProcessTokenRequest(invalid_json2); EXPECT_FALSE(response4.token.has_value()); + EXPECT_FALSE(response4.expires_at.has_value()); EXPECT_TRUE(response4.error_message.has_value()); EXPECT_EQ(response4.error_status_code, 401); } @@ -67,12 +71,14 @@ TEST(AuthUtilsTest, ProcessTokenRequestParsing) { std::string valid_json = R"({"client_id": "test_client", "api_key": "test_key"})"; auto response = store.ProcessTokenRequest(valid_json); EXPECT_TRUE(response.token.has_value()); + EXPECT_TRUE(response.expires_at.has_value()); EXPECT_FALSE(response.error_message.has_value()); // Missing client_id std::string missing_client_id = R"({"api_key": "test_key"})"; response = store.ProcessTokenRequest(missing_client_id); EXPECT_FALSE(response.token.has_value()); + EXPECT_FALSE(response.expires_at.has_value()); EXPECT_TRUE(response.error_message.has_value()); EXPECT_TRUE(response.error_message.value().find("client_id") != std::string::npos); EXPECT_EQ(response.error_status_code, 400); @@ -81,6 +87,7 @@ TEST(AuthUtilsTest, ProcessTokenRequestParsing) { std::string missing_api_key = R"({"client_id": "test_client"})"; response = store.ProcessTokenRequest(missing_api_key); EXPECT_FALSE(response.token.has_value()); + EXPECT_FALSE(response.expires_at.has_value()); EXPECT_TRUE(response.error_message.has_value()); EXPECT_TRUE(response.error_message.value().find("api_key") != std::string::npos); EXPECT_EQ(response.error_status_code, 400); @@ -89,6 +96,7 @@ TEST(AuthUtilsTest, ProcessTokenRequestParsing) { std::string invalid_json = "{invalid json}"; response = store.ProcessTokenRequest(invalid_json); EXPECT_FALSE(response.token.has_value()); + EXPECT_FALSE(response.expires_at.has_value()); EXPECT_TRUE(response.error_message.has_value()); EXPECT_EQ(response.error_status_code, 400); } @@ -102,6 +110,7 @@ TEST(AuthUtilsTest, ProcessTokenRequestEmptyClientId) { std::string empty_client_id_json = R"({"client_id": "", "api_key": "key1"})"; auto response = store.ProcessTokenRequest(empty_client_id_json); EXPECT_FALSE(response.token.has_value()); + EXPECT_FALSE(response.expires_at.has_value()); EXPECT_TRUE(response.error_message.has_value()); EXPECT_EQ(response.error_status_code, 401); } @@ -122,6 +131,7 @@ TEST(AuthUtilsTest, InitWithEnableCredentialCheck) { auto response1 = store.ProcessTokenRequest(wrong_key_json); EXPECT_TRUE(response1.token.has_value()); EXPECT_FALSE(response1.token.value().empty()); + EXPECT_TRUE(response1.expires_at.has_value()); EXPECT_FALSE(response1.error_message.has_value()); // Should succeed even with nonexistent client_id when skipping credential check @@ -129,6 +139,7 @@ TEST(AuthUtilsTest, InitWithEnableCredentialCheck) { auto response2 = store.ProcessTokenRequest(nonexistent_json); EXPECT_TRUE(response2.token.has_value()); EXPECT_FALSE(response2.token.value().empty()); + EXPECT_TRUE(response2.expires_at.has_value()); EXPECT_FALSE(response2.error_message.has_value()); // Test with enable_credential_check = true (enable checking) @@ -137,6 +148,7 @@ TEST(AuthUtilsTest, InitWithEnableCredentialCheck) { // Should fail with wrong api_key when checking credentials auto response3 = store.ProcessTokenRequest(wrong_key_json); EXPECT_FALSE(response3.token.has_value()); + EXPECT_FALSE(response3.expires_at.has_value()); EXPECT_TRUE(response3.error_message.has_value()); EXPECT_EQ(response3.error_status_code, 401); @@ -145,6 +157,7 @@ TEST(AuthUtilsTest, InitWithEnableCredentialCheck) { auto response4 = store.ProcessTokenRequest(correct_json); EXPECT_TRUE(response4.token.has_value()); EXPECT_FALSE(response4.token.value().empty()); + EXPECT_TRUE(response4.expires_at.has_value()); EXPECT_FALSE(response4.error_message.has_value()); } diff --git a/src/server/dbps_api_server.cpp b/src/server/dbps_api_server.cpp index 7ce3c4d..b5189de 100644 --- a/src/server/dbps_api_server.cpp +++ b/src/server/dbps_api_server.cpp @@ -108,12 +108,7 @@ int main(int argc, char* argv[]) { } // Create success response - crow::json::wvalue response; - response["token"] = token_response.token.value(); - response["token_type"] = "Bearer"; - response["expires_in"] = 14400; // 4 hours in seconds - - return crow::response(200, response); + return crow::response(200, token_response.ToJson()); }); // Encryption endpoint - POST /encrypt From cf927fd8865739a2f6f160e16b6b2e04aba58d54 Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Thu, 18 Dec 2025 23:27:01 -0600 Subject: [PATCH 2/6] - Moving error messages out of TokenRequest. - Updated logic to populate TokenResponse with error message and status code. - Added TokenWithExpiration struct to auth_utils.h and auth_utils.cpp. --- src/common/json_request.cpp | 98 ++++++++++++++--------- src/common/json_request.h | 30 +++++--- src/common/json_request_test.cpp | 44 +++++++---- src/server/auth_utils.cpp | 28 ++++--- src/server/auth_utils.h | 15 +++- src/server/auth_utils_test.cpp | 128 +++++++++++++++++-------------- src/server/dbps_api_server.cpp | 11 ++- 7 files changed, 216 insertions(+), 138 deletions(-) diff --git a/src/common/json_request.cpp b/src/common/json_request.cpp index 7a50c3d..3e2230e 100644 --- a/src/common/json_request.cpp +++ b/src/common/json_request.cpp @@ -130,105 +130,133 @@ std::string EncodeBase64Safe(const std::vector& data) { } void TokenRequest::Parse(const std::string& request_body) { - client_id.clear(); - api_key.clear(); - error_message = std::nullopt; + client_id_.clear(); + api_key_.clear(); auto json_body_opt = SafeLoadJsonBody(request_body); if (!json_body_opt) { - error_message = "Invalid JSON in request body. Invalid format."; return; } auto json_body = *json_body_opt; if (json_body.t() != crow::json::type::Object) { - error_message = "Invalid JSON in request body. Invalid format."; return; } if (json_body.has("client_id") && json_body["client_id"].t() == crow::json::type::String) { - client_id = std::string(json_body["client_id"]); + client_id_ = std::string(json_body["client_id"]); } else { - error_message = "Missing required field: client_id"; return; } if (json_body.has("api_key") && json_body["api_key"].t() == crow::json::type::String) { - api_key = std::string(json_body["api_key"]); + api_key_ = std::string(json_body["api_key"]); } else { - error_message = "Missing required field: api_key"; return; } } std::string TokenRequest::ToJson() const { crow::json::wvalue json; - if (error_message.has_value()) { - json["error"] = error_message.value(); + if (!IsValid()) { + json["error"] = GetValidationError(); return PrettyPrintJson(json.dump()); } - json["client_id"] = client_id; - json["api_key"] = api_key; + json["client_id"] = client_id_; + json["api_key"] = api_key_; return PrettyPrintJson(json.dump()); } +bool TokenRequest::IsValid() const { + return !client_id_.empty() && !api_key_.empty(); +} + +std::string TokenRequest::GetValidationError() const { + if (IsValid()) { + return ""; + } + std::vector missing_fields; + if (client_id_.empty()) missing_fields.push_back("client_id"); + if (api_key_.empty()) missing_fields.push_back("api_key"); + return BuildValidationError(missing_fields); +} + void TokenResponse::Parse(const std::string& response_body) { - token = std::nullopt; - expires_at = std::nullopt; - error_message = std::nullopt; - error_status_code = 400; + token_ = std::nullopt; + expires_at_ = std::nullopt; + error_message_.clear(); + error_status_code_ = 0; auto json_body_opt = SafeLoadJsonBody(response_body); if (!json_body_opt) { - error_message = "Invalid JSON in response body. Invalid format."; return; } auto json_body = *json_body_opt; if (json_body.t() != crow::json::type::Object) { - error_message = "Invalid JSON in response body. Invalid format."; return; } if (json_body.has("error")) { - error_message = std::string(json_body["error"]); + error_message_ = std::string(json_body["error"]); return; } if (auto parsed_value = SafeGetFromJsonPath(json_body, {"token"})) { - token = *parsed_value; + token_ = *parsed_value; } if (auto parsed_value = SafeGetFromJsonPath(json_body, {"expires_at"})) { try { - expires_at = std::stoll(*parsed_value); + expires_at_ = std::stoll(*parsed_value); } catch (const std::exception&) { - error_message = "Invalid expires_at field"; return; } } } -std::string TokenResponse::ToJson() const { - crow::json::wvalue json; +bool TokenResponse::IsValid() const { + return error_message_.empty() && + token_.has_value() && + !token_.value().empty() && + expires_at_.has_value(); +} - if (error_message.has_value()) { - json["error"] = error_message.value(); - return json.dump(); +std::string TokenResponse::GetValidationError() const { + if (error_status_code_ == 401) { + return "Invalid credentials"; + } + if (!error_message_.empty()) { + return error_message_; } + if (IsValid()) { + return ""; + } + std::vector missing_fields; + if (!token_.has_value() || token_.value().empty()) missing_fields.push_back("token"); + if (!expires_at_.has_value()) missing_fields.push_back("expires_at"); + return BuildValidationError(missing_fields); +} + +void TokenResponse::SetErrorStatusCodeAndClearToken(int status_code) { + token_ = std::nullopt; + expires_at_ = std::nullopt; + error_message_.clear(); + error_status_code_ = status_code; +} - if (!token.has_value()) { - json["error"] = "Invalid token response"; +std::string TokenResponse::ToJson() const { + crow::json::wvalue json; + + if (!IsValid()) { + json["error"] = GetValidationError(); return json.dump(); } - json["token"] = token.value(); + json["token"] = token_.value(); json["token_type"] = "Bearer"; - - if (expires_at.has_value()) { - json["expires_at"] = expires_at.value(); - } + json["expires_at"] = expires_at_.value(); return PrettyPrintJson(json.dump()); } diff --git a/src/common/json_request.h b/src/common/json_request.h index d5bdb88..23059eb 100644 --- a/src/common/json_request.h +++ b/src/common/json_request.h @@ -22,7 +22,6 @@ #include #include #include -#include #include "enums.h" #include "enum_utils.h" @@ -332,24 +331,35 @@ class DecryptJsonResponse : public JsonResponse { /** * Structure to hold parsed token request data. */ -struct TokenRequest { - std::string client_id; - std::string api_key; - std::optional error_message; +class TokenRequest { +public: + TokenRequest() = default; + + std::string client_id_; + std::string api_key_; void Parse(const std::string& request_body); + bool IsValid() const; + std::string GetValidationError() const; std::string ToJson() const; }; /** * Structure to hold token generation result. */ -struct TokenResponse { - std::optional token; - std::optional expires_at; - std::optional error_message; - int error_status_code = 400; +class TokenResponse { +public: + TokenResponse() = default; + + std::optional token_; + std::optional expires_at_; + std::string error_message_; + int error_status_code_ = 0; void Parse(const std::string& response_body); + bool IsValid() const; + std::string GetValidationError() const; std::string ToJson() const; + + void SetErrorStatusCodeAndClearToken(int status_code); }; diff --git a/src/common/json_request_test.cpp b/src/common/json_request_test.cpp index dd8c07c..9fa5dbc 100644 --- a/src/common/json_request_test.cpp +++ b/src/common/json_request_test.cpp @@ -946,26 +946,28 @@ TEST(JsonRequest, TokenRequestParse) { { TokenRequest req; req.Parse(R"({"client_id":"client1","api_key":"key1"})"); - ASSERT_FALSE(req.error_message.has_value()); - ASSERT_EQ(req.client_id, "client1"); - ASSERT_EQ(req.api_key, "key1"); + ASSERT_TRUE(req.IsValid()); + ASSERT_EQ(req.client_id_, "client1"); + ASSERT_EQ(req.api_key_, "key1"); } { TokenRequest req; req.Parse(R"({"api_key":"key1"})"); - ASSERT_TRUE(req.error_message.has_value()); + ASSERT_FALSE(req.IsValid()); + ASSERT_FALSE(req.GetValidationError().empty()); } { TokenRequest req; req.Parse("{ invalid json }"); - ASSERT_TRUE(req.error_message.has_value()); + ASSERT_FALSE(req.IsValid()); + ASSERT_FALSE(req.GetValidationError().empty()); } } TEST(JsonRequest, TokenRequestToJson) { TokenRequest req; - req.client_id = "clientA"; - req.api_key = "keyA"; + req.Parse(R"({"client_id":"clientA","api_key":"keyA"})"); + ASSERT_TRUE(req.IsValid()); auto json = crow::json::load(req.ToJson()); ASSERT_TRUE(json); @@ -975,8 +977,8 @@ TEST(JsonRequest, TokenRequestToJson) { TEST(JsonRequest, TokenResponseRoundTrip) { TokenResponse resp; - resp.token = "token123"; - resp.expires_at = 1234567890; + resp.Parse(R"({"token":"token123","expires_at":1234567890})"); + ASSERT_TRUE(resp.IsValid()); auto json = crow::json::load(resp.ToJson()); ASSERT_TRUE(json); @@ -989,15 +991,27 @@ TEST(JsonRequest, TokenResponseRoundTrip) { TokenResponse parsed; parsed.Parse(resp.ToJson()); - ASSERT_FALSE(parsed.error_message.has_value()); - ASSERT_TRUE(parsed.token.has_value()); - ASSERT_TRUE(parsed.expires_at.has_value()); - ASSERT_EQ(parsed.token.value(), "token123"); - ASSERT_EQ(parsed.expires_at.value(), 1234567890); + ASSERT_TRUE(parsed.IsValid()); + ASSERT_EQ(parsed.token_.value(), "token123"); + ASSERT_EQ(parsed.expires_at_.value(), 1234567890); } TEST(JsonRequest, TokenResponseParseError) { TokenResponse resp; resp.Parse(R"({"error":"unauthorized"})"); - ASSERT_TRUE(resp.error_message.has_value()); + ASSERT_FALSE(resp.IsValid()); + ASSERT_FALSE(resp.GetValidationError().empty()); +} + +TEST(JsonRequest, TokenResponseSetErrorStatusCodeAndClearToken) { + TokenResponse resp; + resp.Parse(R"({"token":"token123","expires_at":1234567890})"); + ASSERT_TRUE(resp.IsValid()); + + resp.SetErrorStatusCodeAndClearToken(401); + ASSERT_FALSE(resp.IsValid()); + ASSERT_FALSE(resp.token_.has_value()); + ASSERT_FALSE(resp.expires_at_.has_value()); + ASSERT_EQ(resp.error_status_code_, 401); + ASSERT_TRUE(resp.GetValidationError().find("Invalid credentials") != std::string::npos); } diff --git a/src/server/auth_utils.cpp b/src/server/auth_utils.cpp index 95739da..5d39e43 100644 --- a/src/server/auth_utils.cpp +++ b/src/server/auth_utils.cpp @@ -22,9 +22,6 @@ #include #include -// JWT expiration time: 4 hours in seconds -static const int JWT_EXPIRATION_SECONDS = 4 * 60 * 60; // 14400 seconds - // ClientCredentialStore implementation // Constructor @@ -110,7 +107,7 @@ bool ClientCredentialStore::HasClientId(const std::string& client_id) const { } // GenerateJWT implementation -std::optional> ClientCredentialStore::GenerateJWT( +std::optional ClientCredentialStore::GenerateJWT( const std::string& client_id, const std::string& api_key) const { // Validate that client_id is not empty @@ -145,7 +142,7 @@ std::optional> ClientCredentialStore::Gener .set_expires_at(std::chrono::system_clock::from_time_t(exp_seconds)) .sign(jwt::algorithm::hs256{jwt_secret_key_}); - return std::make_pair(token, static_cast(exp_seconds)); + return TokenWithExpiration{token, static_cast(exp_seconds)}; } catch (const std::exception& e) { std::cerr << "Error generating JWT: " << e.what() << std::endl; return std::nullopt; @@ -161,31 +158,32 @@ TokenResponse ClientCredentialStore::ProcessTokenRequest(const std::string& requ token_req.Parse(request_body); // Check if parsing resulted in an error - if (token_req.error_message.has_value()) { - response.error_message = token_req.error_message.value(); - response.error_status_code = 400; + if (!token_req.IsValid()) { + response.SetErrorStatusCodeAndClearToken(400); + response.error_message_ = token_req.GetValidationError(); return response; } // Log the request for debugging std::cout << "=== ProcessTokenRequest ===" << std::endl; - std::cout << "client_id: " << token_req.client_id << std::endl; + std::cout << "client_id: " << token_req.client_id_ << std::endl; std::cout << "====================" << std::endl; // Generate JWT token (validates credentials internally) - auto token = GenerateJWT(token_req.client_id, token_req.api_key); + auto token = GenerateJWT(token_req.client_id_, token_req.api_key_); if (!token.has_value()) { - response.error_message = "Invalid credentials"; - response.error_status_code = 401; + response.SetErrorStatusCodeAndClearToken(401); + response.error_message_ = "Invalid credentials"; return response; } - response.token = token->first; - response.expires_at = token->second; + response.token_ = token->token; + response.expires_at_ = token->expires_at; + response.error_status_code_ = 200; std::cout << "=== ProcessTokenRequest (Success) ===" << std::endl; - std::cout << "Token generated for client_id: " << token_req.client_id << std::endl; + std::cout << "Token generated for client_id: " << token_req.client_id_ << std::endl; std::cout << "=================================" << std::endl; return response; diff --git a/src/server/auth_utils.h b/src/server/auth_utils.h index 11ddcd3..9313ba2 100644 --- a/src/server/auth_utils.h +++ b/src/server/auth_utils.h @@ -27,7 +27,6 @@ #include #include #include -#include #include #include "json_request.h" @@ -35,6 +34,9 @@ #define DBPS_EXPORT #endif +// JWT expiration time: 4 hours in seconds +inline constexpr int JWT_EXPIRATION_SECONDS = 4 * 60 * 60; // 14400 seconds + /** * ClientCredentialStore manages client_id to api_key mappings for authentication. * @@ -86,7 +88,7 @@ class DBPS_EXPORT ClientCredentialStore { * of TokenRequest structure from the caller. * * @param request_body The raw JSON request body string - * @return TokenResponse with token if successful, or error_message and error_status_code if failed + * @return TokenResponse with token if successful, or GetValidationError() and error_status_code if failed */ TokenResponse ProcessTokenRequest(const std::string& request_body) const; @@ -98,6 +100,11 @@ class DBPS_EXPORT ClientCredentialStore { std::optional VerifyTokenForEndpoint(const std::string& authorization_header) const; private: + struct TokenWithExpiration { + std::string token; + std::int64_t expires_at; + }; + // Adds a client credential to the in-memory storage. void AddCredential(const std::string& client_id, const std::string& api_key); @@ -119,9 +126,9 @@ class DBPS_EXPORT ClientCredentialStore { * * @param client_id The client identifier to validate and include in the JWT * @param api_key The API key to validate against the stored credential - * @return Pair of (token, expires_at epoch seconds) if credentials are valid, or std::nullopt on error or invalid credentials + * @return TokenWithExpiration if credentials are valid, or std::nullopt on error or invalid credentials */ - std::optional> GenerateJWT( + std::optional GenerateJWT( const std::string& client_id, const std::string& api_key) const; diff --git a/src/server/auth_utils_test.cpp b/src/server/auth_utils_test.cpp index 2e1054c..121837a 100644 --- a/src/server/auth_utils_test.cpp +++ b/src/server/auth_utils_test.cpp @@ -17,9 +17,17 @@ #include "auth_utils.h" #include +#include #include #include +static void ExpectExpiresAtInFuture(const TokenResponse& response) { + ASSERT_TRUE(response.expires_at_.has_value()); + const auto now_seconds = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + EXPECT_GT(response.expires_at_.value(), now_seconds); +} + // Test ClientCredentialStore initialization with map TEST(AuthUtilsTest, InitWithMap) { ClientCredentialStore store("test-secret-key"); @@ -33,32 +41,36 @@ TEST(AuthUtilsTest, InitWithMap) { // Test ProcessTokenRequest with valid credentials std::string valid_json1 = R"({"client_id": "client1", "api_key": "key1"})"; auto response1 = store.ProcessTokenRequest(valid_json1); - EXPECT_TRUE(response1.token.has_value()); - EXPECT_FALSE(response1.token.value().empty()); - EXPECT_TRUE(response1.expires_at.has_value()); - EXPECT_FALSE(response1.error_message.has_value()); + EXPECT_TRUE(response1.token_.has_value()); + EXPECT_FALSE(response1.token_.value().empty()); + EXPECT_TRUE(response1.expires_at_.has_value()); + ExpectExpiresAtInFuture(response1); + EXPECT_TRUE(response1.IsValid()); std::string valid_json2 = R"({"client_id": "client2", "api_key": "key2"})"; auto response2 = store.ProcessTokenRequest(valid_json2); - EXPECT_TRUE(response2.token.has_value()); - EXPECT_FALSE(response2.token.value().empty()); - EXPECT_TRUE(response2.expires_at.has_value()); - EXPECT_FALSE(response2.error_message.has_value()); + EXPECT_TRUE(response2.token_.has_value()); + EXPECT_FALSE(response2.token_.value().empty()); + EXPECT_TRUE(response2.expires_at_.has_value()); + ExpectExpiresAtInFuture(response2); + EXPECT_TRUE(response2.IsValid()); // Test ProcessTokenRequest with invalid credentials std::string invalid_json1 = R"({"client_id": "client1", "api_key": "wrong_key"})"; auto response3 = store.ProcessTokenRequest(invalid_json1); - EXPECT_FALSE(response3.token.has_value()); - EXPECT_FALSE(response3.expires_at.has_value()); - EXPECT_TRUE(response3.error_message.has_value()); - EXPECT_EQ(response3.error_status_code, 401); + EXPECT_FALSE(response3.token_.has_value()); + EXPECT_FALSE(response3.expires_at_.has_value()); + EXPECT_EQ(response3.error_status_code_, 401); + EXPECT_FALSE(response3.IsValid()); + EXPECT_TRUE(response3.GetValidationError().find("Invalid credentials") != std::string::npos); std::string invalid_json2 = R"({"client_id": "nonexistent", "api_key": "key1"})"; auto response4 = store.ProcessTokenRequest(invalid_json2); - EXPECT_FALSE(response4.token.has_value()); - EXPECT_FALSE(response4.expires_at.has_value()); - EXPECT_TRUE(response4.error_message.has_value()); - EXPECT_EQ(response4.error_status_code, 401); + EXPECT_FALSE(response4.token_.has_value()); + EXPECT_FALSE(response4.expires_at_.has_value()); + EXPECT_EQ(response4.error_status_code_, 401); + EXPECT_FALSE(response4.IsValid()); + EXPECT_TRUE(response4.GetValidationError().find("Invalid credentials") != std::string::npos); } // Test ProcessTokenRequest parsing and validation @@ -70,35 +82,36 @@ TEST(AuthUtilsTest, ProcessTokenRequestParsing) { // Valid request std::string valid_json = R"({"client_id": "test_client", "api_key": "test_key"})"; auto response = store.ProcessTokenRequest(valid_json); - EXPECT_TRUE(response.token.has_value()); - EXPECT_TRUE(response.expires_at.has_value()); - EXPECT_FALSE(response.error_message.has_value()); + EXPECT_TRUE(response.token_.has_value()); + EXPECT_TRUE(response.expires_at_.has_value()); + ExpectExpiresAtInFuture(response); + EXPECT_TRUE(response.IsValid()); // Missing client_id std::string missing_client_id = R"({"api_key": "test_key"})"; response = store.ProcessTokenRequest(missing_client_id); - EXPECT_FALSE(response.token.has_value()); - EXPECT_FALSE(response.expires_at.has_value()); - EXPECT_TRUE(response.error_message.has_value()); - EXPECT_TRUE(response.error_message.value().find("client_id") != std::string::npos); - EXPECT_EQ(response.error_status_code, 400); + EXPECT_FALSE(response.token_.has_value()); + EXPECT_FALSE(response.expires_at_.has_value()); + EXPECT_EQ(response.error_status_code_, 400); + EXPECT_FALSE(response.IsValid()); + EXPECT_FALSE(response.GetValidationError().empty()); // Missing api_key std::string missing_api_key = R"({"client_id": "test_client"})"; response = store.ProcessTokenRequest(missing_api_key); - EXPECT_FALSE(response.token.has_value()); - EXPECT_FALSE(response.expires_at.has_value()); - EXPECT_TRUE(response.error_message.has_value()); - EXPECT_TRUE(response.error_message.value().find("api_key") != std::string::npos); - EXPECT_EQ(response.error_status_code, 400); + EXPECT_FALSE(response.token_.has_value()); + EXPECT_FALSE(response.expires_at_.has_value()); + EXPECT_EQ(response.error_status_code_, 400); + EXPECT_FALSE(response.IsValid()); + EXPECT_FALSE(response.GetValidationError().empty()); // Invalid JSON std::string invalid_json = "{invalid json}"; response = store.ProcessTokenRequest(invalid_json); - EXPECT_FALSE(response.token.has_value()); - EXPECT_FALSE(response.expires_at.has_value()); - EXPECT_TRUE(response.error_message.has_value()); - EXPECT_EQ(response.error_status_code, 400); + EXPECT_FALSE(response.token_.has_value()); + EXPECT_FALSE(response.expires_at_.has_value()); + EXPECT_EQ(response.error_status_code_, 400); + EXPECT_FALSE(response.IsValid()); } // Test ProcessTokenRequest with empty client_id @@ -109,10 +122,10 @@ TEST(AuthUtilsTest, ProcessTokenRequestEmptyClientId) { std::string empty_client_id_json = R"({"client_id": "", "api_key": "key1"})"; auto response = store.ProcessTokenRequest(empty_client_id_json); - EXPECT_FALSE(response.token.has_value()); - EXPECT_FALSE(response.expires_at.has_value()); - EXPECT_TRUE(response.error_message.has_value()); - EXPECT_EQ(response.error_status_code, 401); + EXPECT_FALSE(response.token_.has_value()); + EXPECT_FALSE(response.expires_at_.has_value()); + EXPECT_EQ(response.error_status_code_, 400); + EXPECT_FALSE(response.IsValid()); } // Test init with enable_credential_check flag @@ -129,36 +142,39 @@ TEST(AuthUtilsTest, InitWithEnableCredentialCheck) { // Should succeed even with wrong api_key when skipping credential check std::string wrong_key_json = R"({"client_id": "client1", "api_key": "wrong_key"})"; auto response1 = store.ProcessTokenRequest(wrong_key_json); - EXPECT_TRUE(response1.token.has_value()); - EXPECT_FALSE(response1.token.value().empty()); - EXPECT_TRUE(response1.expires_at.has_value()); - EXPECT_FALSE(response1.error_message.has_value()); + EXPECT_TRUE(response1.token_.has_value()); + EXPECT_FALSE(response1.token_.value().empty()); + EXPECT_TRUE(response1.expires_at_.has_value()); + ExpectExpiresAtInFuture(response1); + EXPECT_TRUE(response1.IsValid()); // Should succeed even with nonexistent client_id when skipping credential check std::string nonexistent_json = R"({"client_id": "nonexistent", "api_key": "any_key"})"; auto response2 = store.ProcessTokenRequest(nonexistent_json); - EXPECT_TRUE(response2.token.has_value()); - EXPECT_FALSE(response2.token.value().empty()); - EXPECT_TRUE(response2.expires_at.has_value()); - EXPECT_FALSE(response2.error_message.has_value()); + EXPECT_TRUE(response2.token_.has_value()); + EXPECT_FALSE(response2.token_.value().empty()); + EXPECT_TRUE(response2.expires_at_.has_value()); + ExpectExpiresAtInFuture(response2); + EXPECT_TRUE(response2.IsValid()); // Test with enable_credential_check = true (enable checking) store.init(true); // Should fail with wrong api_key when checking credentials auto response3 = store.ProcessTokenRequest(wrong_key_json); - EXPECT_FALSE(response3.token.has_value()); - EXPECT_FALSE(response3.expires_at.has_value()); - EXPECT_TRUE(response3.error_message.has_value()); - EXPECT_EQ(response3.error_status_code, 401); + EXPECT_FALSE(response3.token_.has_value()); + EXPECT_FALSE(response3.expires_at_.has_value()); + EXPECT_EQ(response3.error_status_code_, 401); + EXPECT_FALSE(response3.IsValid()); // Should succeed with correct credentials std::string correct_json = R"({"client_id": "client1", "api_key": "key1"})"; auto response4 = store.ProcessTokenRequest(correct_json); - EXPECT_TRUE(response4.token.has_value()); - EXPECT_FALSE(response4.token.value().empty()); - EXPECT_TRUE(response4.expires_at.has_value()); - EXPECT_FALSE(response4.error_message.has_value()); + EXPECT_TRUE(response4.token_.has_value()); + EXPECT_FALSE(response4.token_.value().empty()); + EXPECT_TRUE(response4.expires_at_.has_value()); + ExpectExpiresAtInFuture(response4); + EXPECT_TRUE(response4.IsValid()); } // Test VerifyTokenForEndpoint with enable_credential_check = false @@ -201,14 +217,14 @@ TEST(AuthUtilsTest, VerifyTokenForEndpointWithCheck) { // Test with valid JWT token std::string valid_token_json = R"({"client_id": "clientAAAA", "api_key": "keyAAAA"})"; auto token_response = store.ProcessTokenRequest(valid_token_json); - ASSERT_TRUE(token_response.token.has_value()); + ASSERT_TRUE(token_response.token_.has_value()); - std::string bearer_token = "Bearer " + token_response.token.value(); + std::string bearer_token = "Bearer " + token_response.token_.value(); auto result4 = store.VerifyTokenForEndpoint(bearer_token); EXPECT_FALSE(result4.has_value()); // Should succeed (return nullopt) // Test with valid JWT token but wrong format (missing space after Bearer) - std::string invalid_bearer = "Bearer" + token_response.token.value(); + std::string invalid_bearer = "Bearer" + token_response.token_.value(); auto result5 = store.VerifyTokenForEndpoint(invalid_bearer); EXPECT_TRUE(result5.has_value()); EXPECT_TRUE(result5.value().find("Unauthorized") != std::string::npos); diff --git a/src/server/dbps_api_server.cpp b/src/server/dbps_api_server.cpp index b5189de..f3832f8 100644 --- a/src/server/dbps_api_server.cpp +++ b/src/server/dbps_api_server.cpp @@ -74,7 +74,11 @@ int main(int argc, char* argv[]) { std::cout << "Credentials loaded successfully from: " << credentials_file_path.value() << std::endl; } else { // No credentials file provided, disable credential checking - credential_store.init(false); + + // +++++ test credentials -- remove before merging +++++ + // credential_store.init(false); + credential_store.init({{"test_client_AAAA", "test_key_AAAA"}}); + std::cout << "No credentials file provided. Credential checking will be skipped." << std::endl; } @@ -103,8 +107,9 @@ int main(int argc, char* argv[]) { TokenResponse token_response = credential_store.ProcessTokenRequest(req.body); // Check if processing resulted in an error - if (token_response.error_message.has_value()) { - return CreateErrorResponse(token_response.error_message.value(), token_response.error_status_code); + auto validation_error = token_response.GetValidationError(); + if (!validation_error.empty()) { + return CreateErrorResponse(validation_error, token_response.error_status_code_); } // Create success response From 947132583f518116f3c88b2d8b63af17ddc5d0d3 Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Fri, 19 Dec 2025 00:09:02 -0600 Subject: [PATCH 3/6] - Cleanup validations for Json parsing --- src/common/json_request.cpp | 36 +++++++++++++++++--------------- src/common/json_request_test.cpp | 19 +++++++++++++++++ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/src/common/json_request.cpp b/src/common/json_request.cpp index 3e2230e..b9aff67 100644 --- a/src/common/json_request.cpp +++ b/src/common/json_request.cpp @@ -65,6 +65,20 @@ std::optional SafeParseToInt(const std::string& str) { } } +// Helper function to safely parse a string to 64-bit integer +std::optional SafeParseToInt64(const std::string& str) { + try { + std::size_t idx = 0; + long long value = std::stoll(str, &idx); + if (idx != str.size()) { + return std::nullopt; + } + return static_cast(value); + } catch (const std::exception&) { + return std::nullopt; + } +} + // Helper function to safely load JSON body and return nullopt if invalid or null static std::optional SafeLoadJsonBody(const std::string& json_string) { auto json_body = crow::json::load(json_string); @@ -145,14 +159,10 @@ void TokenRequest::Parse(const std::string& request_body) { if (json_body.has("client_id") && json_body["client_id"].t() == crow::json::type::String) { client_id_ = std::string(json_body["client_id"]); - } else { - return; } if (json_body.has("api_key") && json_body["api_key"].t() == crow::json::type::String) { api_key_ = std::string(json_body["api_key"]); - } else { - return; } } @@ -198,9 +208,8 @@ void TokenResponse::Parse(const std::string& response_body) { return; } - if (json_body.has("error")) { - error_message_ = std::string(json_body["error"]); - return; + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"error"})) { + error_message_ = *parsed_value; } if (auto parsed_value = SafeGetFromJsonPath(json_body, {"token"})) { @@ -208,11 +217,7 @@ void TokenResponse::Parse(const std::string& response_body) { } if (auto parsed_value = SafeGetFromJsonPath(json_body, {"expires_at"})) { - try { - expires_at_ = std::stoll(*parsed_value); - } catch (const std::exception&) { - return; - } + expires_at_ = SafeParseToInt64(*parsed_value); } } @@ -224,15 +229,12 @@ bool TokenResponse::IsValid() const { } std::string TokenResponse::GetValidationError() const { - if (error_status_code_ == 401) { - return "Invalid credentials"; + if (IsValid()) { + return ""; } if (!error_message_.empty()) { return error_message_; } - if (IsValid()) { - return ""; - } std::vector missing_fields; if (!token_.has_value() || token_.value().empty()) missing_fields.push_back("token"); if (!expires_at_.has_value()) missing_fields.push_back("expires_at"); diff --git a/src/common/json_request_test.cpp b/src/common/json_request_test.cpp index 9fa5dbc..bef90cc 100644 --- a/src/common/json_request_test.cpp +++ b/src/common/json_request_test.cpp @@ -53,6 +53,7 @@ std::string BinaryToString(const std::vector& binary_data) { // Forward declarations for internal functions from json_request.cpp std::optional SafeGetFromJsonPath(const crow::json::rvalue& json_body, const std::vector& path); +std::optional SafeParseToInt64(const std::string& str); std::optional SafeParseToInt(const std::string& str); // Test-specific derived class to access protected methods @@ -442,6 +443,24 @@ TEST(JsonRequest, SafeGetFromJsonPathInvalidPath) { ASSERT_FALSE(result.has_value()); } +TEST(JsonRequest, SafeParseToInt64StrictParsing) { + // Valid numeric string + auto v1 = SafeParseToInt64("123"); + ASSERT_TRUE(v1.has_value()); + ASSERT_EQ(v1.value(), 123); + + // Typical epoch-seconds value + auto epoch = SafeParseToInt64("1766138275"); + ASSERT_TRUE(epoch.has_value()); + ASSERT_EQ(epoch.value(), 1766138275); + + // Invalid: numeric prefix + junk + ASSERT_FALSE(SafeParseToInt64("123abc").has_value()); + + // Invalid: trailing whitespace + ASSERT_FALSE(SafeParseToInt64("123 ").has_value()); +} + // Test JSON generation functionality TEST(JsonRequest, EncryptJsonRequestToJson) { EncryptJsonRequest request; From a398897d2d811ab63ae94f4f2f4c9f4be9b186f5 Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Fri, 19 Dec 2025 00:21:24 -0600 Subject: [PATCH 4/6] - Fixing the issue with parsing the integer value from the JSON string. --- src/common/json_request.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/json_request.cpp b/src/common/json_request.cpp index b9aff67..67ef3ed 100644 --- a/src/common/json_request.cpp +++ b/src/common/json_request.cpp @@ -157,12 +157,12 @@ void TokenRequest::Parse(const std::string& request_body) { return; } - if (json_body.has("client_id") && json_body["client_id"].t() == crow::json::type::String) { - client_id_ = std::string(json_body["client_id"]); + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"client_id"})) { + client_id_ = *parsed_value; } - if (json_body.has("api_key") && json_body["api_key"].t() == crow::json::type::String) { - api_key_ = std::string(json_body["api_key"]); + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"api_key"})) { + api_key_ = *parsed_value; } } From 65b9d4e32a584dded0644e902ee3698daa0108bf Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Fri, 19 Dec 2025 00:26:57 -0600 Subject: [PATCH 5/6] - Update unit tests for TokenResponse. --- src/common/json_request_test.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/common/json_request_test.cpp b/src/common/json_request_test.cpp index bef90cc..84d7f4a 100644 --- a/src/common/json_request_test.cpp +++ b/src/common/json_request_test.cpp @@ -1032,5 +1032,4 @@ TEST(JsonRequest, TokenResponseSetErrorStatusCodeAndClearToken) { ASSERT_FALSE(resp.token_.has_value()); ASSERT_FALSE(resp.expires_at_.has_value()); ASSERT_EQ(resp.error_status_code_, 401); - ASSERT_TRUE(resp.GetValidationError().find("Invalid credentials") != std::string::npos); } From 5574e26131f1458588c3a96006fcaa814b52ba73 Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Fri, 19 Dec 2025 00:30:31 -0600 Subject: [PATCH 6/6] - Remove test credentials from the API server. --- src/server/dbps_api_server.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/server/dbps_api_server.cpp b/src/server/dbps_api_server.cpp index f3832f8..8be76c4 100644 --- a/src/server/dbps_api_server.cpp +++ b/src/server/dbps_api_server.cpp @@ -74,11 +74,7 @@ int main(int argc, char* argv[]) { std::cout << "Credentials loaded successfully from: " << credentials_file_path.value() << std::endl; } else { // No credentials file provided, disable credential checking - - // +++++ test credentials -- remove before merging +++++ - // credential_store.init(false); - credential_store.init({{"test_client_AAAA", "test_key_AAAA"}}); - + credential_store.init(false); std::cout << "No credentials file provided. Credential checking will be skipped." << std::endl; }