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..67ef3ed 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); @@ -129,6 +143,126 @@ std::string EncodeBase64Safe(const std::vector& data) { } } +void TokenRequest::Parse(const std::string& request_body) { + client_id_.clear(); + api_key_.clear(); + + auto json_body_opt = SafeLoadJsonBody(request_body); + if (!json_body_opt) { + return; + } + + auto json_body = *json_body_opt; + if (json_body.t() != crow::json::type::Object) { + return; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"client_id"})) { + client_id_ = *parsed_value; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"api_key"})) { + api_key_ = *parsed_value; + } +} + +std::string TokenRequest::ToJson() const { + crow::json::wvalue json; + if (!IsValid()) { + json["error"] = GetValidationError(); + return PrettyPrintJson(json.dump()); + } + + 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_.clear(); + error_status_code_ = 0; + + auto json_body_opt = SafeLoadJsonBody(response_body); + if (!json_body_opt) { + return; + } + + auto json_body = *json_body_opt; + if (json_body.t() != crow::json::type::Object) { + return; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"error"})) { + error_message_ = *parsed_value; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"token"})) { + token_ = *parsed_value; + } + + if (auto parsed_value = SafeGetFromJsonPath(json_body, {"expires_at"})) { + expires_at_ = SafeParseToInt64(*parsed_value); + } +} + +bool TokenResponse::IsValid() const { + return error_message_.empty() && + token_.has_value() && + !token_.value().empty() && + expires_at_.has_value(); +} + +std::string TokenResponse::GetValidationError() const { + if (IsValid()) { + return ""; + } + if (!error_message_.empty()) { + return error_message_; + } + 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; +} + +std::string TokenResponse::ToJson() const { + crow::json::wvalue json; + + if (!IsValid()) { + json["error"] = GetValidationError(); + return json.dump(); + } + + json["token"] = token_.value(); + json["token_type"] = "Bearer"; + 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..23059eb 100644 --- a/src/common/json_request.h +++ b/src/common/json_request.h @@ -327,3 +327,39 @@ class DecryptJsonResponse : public JsonResponse { */ std::string ToJsonString() const override; }; + +/** + * Structure to hold parsed token request data. + */ +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. + */ +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 9e66b6a..84d7f4a 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; @@ -940,3 +959,77 @@ 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_TRUE(req.IsValid()); + ASSERT_EQ(req.client_id_, "client1"); + ASSERT_EQ(req.api_key_, "key1"); + } + { + TokenRequest req; + req.Parse(R"({"api_key":"key1"})"); + ASSERT_FALSE(req.IsValid()); + ASSERT_FALSE(req.GetValidationError().empty()); + } + { + TokenRequest req; + req.Parse("{ invalid json }"); + ASSERT_FALSE(req.IsValid()); + ASSERT_FALSE(req.GetValidationError().empty()); + } +} + +TEST(JsonRequest, TokenRequestToJson) { + TokenRequest req; + req.Parse(R"({"client_id":"clientA","api_key":"keyA"})"); + ASSERT_TRUE(req.IsValid()); + + 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.Parse(R"({"token":"token123","expires_at":1234567890})"); + ASSERT_TRUE(resp.IsValid()); + + 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_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_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); +} diff --git a/src/server/auth_utils.cpp b/src/server/auth_utils.cpp index 6a3bab8..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,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,85 +142,48 @@ 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 TokenWithExpiration{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()) { - 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.value(); + 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 c8334cb..9313ba2 100644 --- a/src/server/auth_utils.h +++ b/src/server/auth_utils.h @@ -27,39 +27,26 @@ #include #include #include +#include +#include "json_request.h" #ifndef DBPS_EXPORT #define DBPS_EXPORT #endif -/** - * Structure to hold parsed token request data. - * - * 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 -}; +// 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. * * - 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. */ class DBPS_EXPORT ClientCredentialStore { public: @@ -101,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; @@ -113,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); @@ -134,9 +126,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 TokenWithExpiration 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..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,28 +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_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_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_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_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 @@ -66,31 +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_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_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_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_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 @@ -101,9 +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_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 @@ -120,32 +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_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_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_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_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 @@ -188,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 7ce3c4d..8be76c4 100644 --- a/src/server/dbps_api_server.cpp +++ b/src/server/dbps_api_server.cpp @@ -103,17 +103,13 @@ 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 - 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