diff --git a/CMakeLists.txt b/CMakeLists.txt index ded474d..6b50f72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,13 @@ FetchContent_Declare( GIT_TAG v1.14.0 ) +# jwt-cpp is a header-only JWT library +FetchContent_Declare( + jwt-cpp + GIT_REPOSITORY https://github.com/Thalhammer/jwt-cpp.git + GIT_TAG master +) + # Fetch dependencies # Disable dependency tests for cppcodec @@ -85,7 +92,7 @@ set(BUILD_TESTING OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(cppcodec) set(BUILD_TESTING ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(crow httplib nlohmann_json cxxopts googletest) +FetchContent_MakeAvailable(crow httplib nlohmann_json cxxopts googletest jwt-cpp) # Make GoogleTest helper macros available (gtest_discover_tests) include(GoogleTest) @@ -109,14 +116,21 @@ target_link_libraries(dbps_common_lib PUBLIC tcb_span) # Server components library add_library(dbps_server_lib STATIC src/server/encryption_sequencer.cpp + src/server/auth_utils.cpp src/server/decoding_utils.cpp ) target_link_libraries(dbps_server_lib PUBLIC dbps_common_lib) target_include_directories(dbps_server_lib PUBLIC src/server ${CMAKE_BINARY_DIR}/_deps/cppcodec-src + ${CMAKE_BINARY_DIR}/_deps/jwt-cpp-src/include + ${CMAKE_BINARY_DIR}/_deps/nlohmann_json-src/include ) +# Find and link OpenSSL (required by jwt-cpp) +find_package(OpenSSL REQUIRED) +target_link_libraries(dbps_server_lib PUBLIC OpenSSL::SSL OpenSSL::Crypto) + # Client components library (depends on httplib, nlohmann/json, and cppcodec) add_library(dbps_client_lib STATIC src/client/dbps_api_client.cpp @@ -177,6 +191,7 @@ target_link_libraries(dbps_api_server ) target_include_directories(dbps_api_server PRIVATE ${CMAKE_BINARY_DIR}/_deps/crow-src/include + ${CMAKE_BINARY_DIR}/_deps/cxxopts-src/include ) # DBPA Remote Test Script executable @@ -229,6 +244,15 @@ if(BUILD_TESTS) ) target_include_directories(decoding_utils_test PRIVATE src/server) + # Auth utils tests + add_executable(auth_utils_test src/server/auth_utils_test.cpp) + target_link_libraries(auth_utils_test + dbps_server_lib + dbps_common_lib + gtest_main + ) + target_include_directories(auth_utils_test PRIVATE src/server) + # DBPA interface tests add_executable(dbpa_interface_test src/common/dbpa_interface_test.cpp) target_link_libraries(dbpa_interface_test @@ -389,6 +413,7 @@ if(BUILD_TESTS) enum_utils_test encryption_sequencer_test decoding_utils_test + auth_utils_test dbpa_interface_test dbpa_utils_test dbps_api_client_test @@ -404,6 +429,7 @@ if(BUILD_TESTS) gtest_discover_tests(enum_utils_test) gtest_discover_tests(encryption_sequencer_test) gtest_discover_tests(decoding_utils_test) + gtest_discover_tests(auth_utils_test) gtest_discover_tests(dbpa_interface_test) gtest_discover_tests(dbpa_utils_test) gtest_discover_tests(dbps_api_client_test) diff --git a/src/server/auth_utils.cpp b/src/server/auth_utils.cpp new file mode 100644 index 0000000..6a3bab8 --- /dev/null +++ b/src/server/auth_utils.cpp @@ -0,0 +1,286 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "auth_utils.h" +#include +#include +#include +#include +#include + +// JWT expiration time: 4 hours in seconds +static const int JWT_EXPIRATION_SECONDS = 4 * 60 * 60; // 14400 seconds + +// ClientCredentialStore implementation + +// Constructor +ClientCredentialStore::ClientCredentialStore(const std::string& jwt_secret_key) + : jwt_secret_key_(jwt_secret_key) { +} + +// Initialize credential store from a given map. +void ClientCredentialStore::init(const std::map& credentials) { + credentials_.clear(); + credentials_ = credentials; + enable_credential_check_ = true; // Default: enable credential checking +} + +// Set the enable_credential_check flag. +void ClientCredentialStore::init(bool enable_credential_check) { + enable_credential_check_ = enable_credential_check; +} + +// Initialize credential store from a JSON file +bool ClientCredentialStore::init(const std::string& file_path) { + try { + // Open and read the JSON file + std::ifstream file(file_path); + if (!file.is_open()) { + std::cerr << "Error: Cannot open credentials file: " << file_path << std::endl; + return false; + } + + // Parse JSON + nlohmann::json json_data; + file >> json_data; + file.close(); + + // Validate that it's an object + if (!json_data.is_object()) { + std::cerr << "Error: Credentials file must contain a JSON object" << std::endl; + return false; + } + + // Clear existing credentials + credentials_.clear(); + enable_credential_check_ = true; // Default: enable credential checking + + // Load each client_id:api_key pair + for (auto& [client_id, api_key_value] : json_data.items()) { + if (api_key_value.is_string()) { + AddCredential(client_id, api_key_value.get()); + } else { + std::cerr << "Warning: Skipping invalid api_key for client_id: " << client_id << std::endl; + } + } + + return true; + } catch (const nlohmann::json::exception& e) { + std::cerr << "Error parsing JSON credentials file: " << e.what() << std::endl; + return false; + } catch (const std::exception& e) { + std::cerr << "Error loading credentials file: " << e.what() << std::endl; + return false; + } +} + +// Get the enable_credential_check flag. +bool ClientCredentialStore::GetEnableCredentialCheck() const { + return enable_credential_check_; +} + +void ClientCredentialStore::AddCredential(const std::string& client_id, const std::string& api_key) { + credentials_[client_id] = api_key; +} + +bool ClientCredentialStore::ValidateCredential(const std::string& client_id, const std::string& api_key) const { + auto it = credentials_.find(client_id); + if (it == credentials_.end()) { + return false; + } + return it->second == api_key; +} + +bool ClientCredentialStore::HasClientId(const std::string& client_id) const { + return credentials_.find(client_id) != credentials_.end(); +} + +// GenerateJWT implementation +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; + return std::nullopt; + } + + // Skip credential validation if flag is not set + if (!enable_credential_check_) { + std::cout << "Warning: Credential checking is skipped. Generating JWT without validation for client_id: " << client_id << std::endl; + } + // Validate credentials before generating JWT + else if (!ValidateCredential(client_id, api_key)) { + std::cout << "Error generating JWT: Invalid credentials for client_id: " << client_id << std::endl; + return std::nullopt; + } + + try { + // Get current time + auto now = std::chrono::system_clock::now(); + auto now_seconds = std::chrono::duration_cast(now.time_since_epoch()).count(); + + // Calculate expiration time (4 hours from now) + auto exp_seconds = now_seconds + JWT_EXPIRATION_SECONDS; + + // Create JWT token with client_id, iat, and exp claims + auto token = jwt::create() + .set_type("JWT") + .set_payload_claim("client_id", jwt::claim(client_id)) + .set_issued_at(std::chrono::system_clock::from_time_t(now_seconds)) + .set_expires_at(std::chrono::system_clock::from_time_t(exp_seconds)) + .sign(jwt::algorithm::hs256{jwt_secret_key_}); + + return token; + } 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); + + // 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; + return response; + } + + // Log the request for debugging + std::cout << "=== ProcessTokenRequest ===" << 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); + + if (!token.has_value()) { + response.error_message = "Invalid credentials"; + response.error_status_code = 401; + return response; + } + + response.token = token.value(); + + std::cout << "=== ProcessTokenRequest (Success) ===" << std::endl; + std::cout << "Token generated for client_id: " << token_req.client_id << std::endl; + std::cout << "=================================" << std::endl; + + return response; +} + +// VerifyJWT implementation +std::optional VerifyJWT(const std::string& token, const std::string& jwt_secret_key) { + try { + // Decode and verify the JWT token + auto decoded = jwt::decode(token); + + // Verify the signature using the same secret key + auto verifier = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{jwt_secret_key}); + + verifier.verify(decoded); + + // Extract client_id from the token payload + if (decoded.has_payload_claim("client_id")) { + auto client_id_claim = decoded.get_payload_claim("client_id"); + return client_id_claim.as_string(); + } else { + std::cerr << "Error verifying JWT: Missing client_id claim in token" << std::endl; + return std::nullopt; + } + } catch (const jwt::error::token_verification_exception& e) { + std::cerr << "Error verifying JWT: Token verification failed - " << e.what() << std::endl; + return std::nullopt; + } catch (const std::exception& e) { + std::cerr << "Error verifying JWT: " << e.what() << std::endl; + return std::nullopt; + } +} + +// VerifyTokenForEndpoint implementation +std::optional ClientCredentialStore::VerifyTokenForEndpoint(const std::string& authorization_header) const { + // Skip verification if credential checking is disabled + if (!enable_credential_check_) { + return std::nullopt; + } + + // Extract Bearer token from Authorization header + std::optional token = std::nullopt; + const std::string bearer_prefix = "Bearer "; + if (authorization_header.length() > bearer_prefix.length() && + authorization_header.substr(0, bearer_prefix.length()) == bearer_prefix) { + token = authorization_header.substr(bearer_prefix.length()); + } + + // Verify JWT token + if (!token.has_value()) { + return "Unauthorized: JWT token is missing"; + } + + auto client_id = VerifyJWT(token.value(), jwt_secret_key_); + if (!client_id.has_value()) { + return "Unauthorized: Invalid JWT token"; + } + + std::cout << "JWT verified for client_id: " << client_id.value() << std::endl; + return std::nullopt; +} diff --git a/src/server/auth_utils.h b/src/server/auth_utils.h new file mode 100644 index 0000000..c8334cb --- /dev/null +++ b/src/server/auth_utils.h @@ -0,0 +1,149 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Note for Protegrity integration: +// - This is a simplified Authentication module used to complete the client integration. +// - It is fully functional, but for production deployment a fully fledged Certificate Authority or Identity Provider can be used instead. +// +// - Note that no Arrow codebase changes would be needed to replace this with a fully fledged Certificate Authority (CA) or Identity Provider +// as the connection configuration specified on the application level is passed as-is to the DBPS agents for authentication. + +#pragma once + +#include +#include +#include + +#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 +}; + +/** + * 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: + /** + * Constructor for ClientCredentialStore. + * @param jwt_secret_key The secret key used for signing and verifying JWT tokens + */ + explicit ClientCredentialStore(const std::string& jwt_secret_key); + ~ClientCredentialStore() = default; + + /** + * Initializes the credential store by loading credentials from a JSON file. + * The file should contain a JSON object with client_id as keys and api_key as values. + * Example format: {"client1": "api_key_1", "client2": "api_key_2"} + */ + bool init(const std::string& file_path); + + /** + * Initializes the credential store with a pre-built map of client_id to api_key. + * @param credentials Map of client_id to api_key pairs + */ + void init(const std::map& credentials); + + /** + * Sets the enable_credential_check flag. + * @param enable_credential_check If true, credential validation will be enabled during GenerateJWT + */ + void init(bool enable_credential_check); + + /** + * Gets the enable_credential_check flag. + * @return true if credential validation is enabled, false otherwise + */ + bool GetEnableCredentialCheck() const; + + /** + * Processes a token request from JSON body and generates a JWT token. + * This method encapsulates all token request processing logic, hiding the details + * 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 + */ + TokenResponse ProcessTokenRequest(const std::string& request_body) const; + + /** + * Verifies JWT token from Authorization header for protected endpoints. + * @param authorization_header The Authorization header value (e.g., "Bearer ") + * @return Error message if verification fails, or std::nullopt if verification succeeds + */ + std::optional VerifyTokenForEndpoint(const std::string& authorization_header) const; + +private: + // Adds a client credential to the in-memory storage. + void AddCredential(const std::string& client_id, const std::string& api_key); + + // Check if a client credential is valid before generating a JWT token. + bool ValidateCredential(const std::string& client_id, const std::string& api_key) const; + + // Check if a a credential for a client_id exists + bool HasClientId(const std::string& client_id) const; + + /** + * Generates a JWT token for the given client_id if the credentials are valid. + * + * The JWT includes: + * - client_id: The client identifier + * - iat: Issued at timestamp + * - exp: Expiration timestamp (4 hours from issue time) + * + * Uses HS256 algorithm with a hardcoded secret key. + * + * @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 + */ + std::optional GenerateJWT(const std::string& client_id, const std::string& api_key) const; + + // In-memory storage: client_id -> api_key + std::map credentials_; + + // Flag to indicate if credential checking is enabled during GenerateJWT + bool enable_credential_check_ = true; + + // JWT secret key for signing and verifying tokens + std::string jwt_secret_key_; +}; diff --git a/src/server/auth_utils_test.cpp b/src/server/auth_utils_test.cpp new file mode 100644 index 0000000..f8f44d4 --- /dev/null +++ b/src/server/auth_utils_test.cpp @@ -0,0 +1,202 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "auth_utils.h" +#include +#include +#include + +// Test ClientCredentialStore initialization with map +TEST(AuthUtilsTest, InitWithMap) { + ClientCredentialStore store("test-secret-key"); + std::map credentials = { + {"client1", "key1"}, + {"client2", "key2"} + }; + + store.init(credentials); + + // 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()); + + 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()); + + // 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); + + 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); +} + +// Test ProcessTokenRequest parsing and validation +TEST(AuthUtilsTest, ProcessTokenRequestParsing) { + ClientCredentialStore store("test-secret-key"); + std::map credentials = {{"test_client", "test_key"}}; + store.init(credentials); + + // 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()); + + // 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); + + // 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); + + // 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); +} + +// Test ProcessTokenRequest with empty client_id +TEST(AuthUtilsTest, ProcessTokenRequestEmptyClientId) { + ClientCredentialStore store("test-secret-key"); + std::map credentials = {{"client1", "key1"}}; + store.init(credentials); + + 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); +} + +// Test init with enable_credential_check flag +TEST(AuthUtilsTest, InitWithEnableCredentialCheck) { + ClientCredentialStore store("test-secret-key"); + std::map credentials = {{"client1", "key1"}}; + + // Initialize with credentials first + store.init(credentials); + + // Test with enable_credential_check = false (skip checking) + store.init(false); + + // 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()); + + // 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()); + + // 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); + + // 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()); +} + +// Test VerifyTokenForEndpoint with enable_credential_check = false +TEST(AuthUtilsTest, VerifyTokenForEndpointSkipCheck) { + ClientCredentialStore store("test-secret-key"); + store.init(false); // Disable credential checking + + // Should succeed (return nullopt) regardless of header when skipping check + auto result1 = store.VerifyTokenForEndpoint(""); + EXPECT_FALSE(result1.has_value()); + + auto result2 = store.VerifyTokenForEndpoint("Invalid header"); + EXPECT_FALSE(result2.has_value()); + + auto result3 = store.VerifyTokenForEndpoint("Bearer invalid_token"); + EXPECT_FALSE(result3.has_value()); +} + +// Test VerifyTokenForEndpoint with enable_credential_check = true +TEST(AuthUtilsTest, VerifyTokenForEndpointWithCheck) { + ClientCredentialStore store("test-secret-key"); + std::map credentials = {{"clientAAAA", "keyAAAA"}}; + store.init(credentials); // This sets enable_credential_check_ to true + + // Test with missing/empty Authorization header + auto result1 = store.VerifyTokenForEndpoint(""); + EXPECT_TRUE(result1.has_value()); + EXPECT_TRUE(result1.value().find("Unauthorized") != std::string::npos); + + // Test with invalid Bearer format (no "Bearer " prefix) + auto result2 = store.VerifyTokenForEndpoint("invalid_token"); + EXPECT_TRUE(result2.has_value()); + EXPECT_TRUE(result2.value().find("Unauthorized") != std::string::npos); + + // Test with invalid JWT token + auto result3 = store.VerifyTokenForEndpoint("Bearer invalid.jwt.token"); + EXPECT_TRUE(result3.has_value()); + EXPECT_TRUE(result3.value().find("Unauthorized") != std::string::npos); + + // 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()); + + 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(); + 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 c3664fd..7ce3c4d 100644 --- a/src/server/dbps_api_server.cpp +++ b/src/server/dbps_api_server.cpp @@ -17,8 +17,11 @@ #include #include +#include +#include #include "json_request.h" #include "encryption_sequencer.h" +#include "auth_utils.h" // Helper function to create error response crow::response CreateErrorResponse(const std::string& error_msg, int status_code = 400) { @@ -28,22 +31,99 @@ crow::response CreateErrorResponse(const std::string& error_msg, int status_code return crow::response(status_code, error_response); } -int main() { +// Helper function to verify JWT token from request +// Returns error message if verification fails, or nullopt if verification succeeds +std::optional VerifyJWTFromRequest(const crow::request& req, const ClientCredentialStore& credential_store) { + std::string auth_header = ""; + auto auth_header_it = req.headers.find("Authorization"); + if (auth_header_it != req.headers.end()) { + auth_header = auth_header_it->second; + } + return credential_store.VerifyTokenForEndpoint(auth_header); +} + +int main(int argc, char* argv[]) { + // Initialize credentials file path and JWT secret key with parsed command line options + std::optional credentials_file_path = std::nullopt; + std::string jwt_secret_key = "default-secret-key-overwritten-by-command-line"; + try { + cxxopts::Options options("dbps_api_server", "Data Batch Protection Service API Server"); + options.add_options() + ("c,credentials", "Path to credentials JSON file", cxxopts::value()) + ("j,jwt-secret", "JWT secret key for signing and verifying tokens", cxxopts::value()); + auto result = options.parse(argc, argv); + if (result.count("credentials")) { + credentials_file_path = result["credentials"].as(); + } + if (result.count("jwt-secret")) { + jwt_secret_key = result["jwt-secret"].as(); + } + } catch (const std::exception& e) { + std::cerr << "Error parsing command line options: " << e.what() << std::endl; + return 1; + } + + // Initialize credential store with JWT secret key + ClientCredentialStore credential_store(jwt_secret_key); + if (credentials_file_path.has_value()) { + // Load credentials from file + if (!credential_store.init(credentials_file_path.value())) { + std::cerr << "Error: Failed to load credentials file: " << credentials_file_path.value() << std::endl; + return 1; + } + std::cout << "Credentials loaded successfully from: " << credentials_file_path.value() << std::endl; + } else { + // No credentials file provided, disable credential checking + credential_store.init(false); + std::cout << "No credentials file provided. Credential checking will be skipped." << std::endl; + } + + // Initialize API server crow::SimpleApp app; CROW_ROUTE(app, "/healthz")([] { - return "OK"; + return crow::response(200, "OK"); + }); + + CROW_ROUTE(app, "/statusz")([&credential_store](const crow::request& req){ + // Verify JWT token + auto auth_error = VerifyJWTFromRequest(req, credential_store); + if (auth_error.has_value()) { + return CreateErrorResponse(auth_error.value(), 401); + } + + crow::json::wvalue response; + response["enable_credential_check"] = credential_store.GetEnableCredentialCheck(); + return crow::response(200, response); }); - CROW_ROUTE(app, "/statusz")([]{ + // Token authentication endpoint - POST /token + CROW_ROUTE(app, "/token").methods("POST"_method)([&credential_store](const crow::request& req) { + // Process token request + 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); + } + + // Create success response crow::json::wvalue response; - response["status"] = "chill"; - response["system_settings"] = "set to thrill!"; + response["token"] = token_response.token.value(); + response["token_type"] = "Bearer"; + response["expires_in"] = 14400; // 4 hours in seconds + return crow::response(200, response); }); // Encryption endpoint - POST /encrypt - CROW_ROUTE(app, "/encrypt").methods("POST"_method)([](const crow::request& req) { + CROW_ROUTE(app, "/encrypt").methods("POST"_method)([&credential_store](const crow::request& req) { + // Verify JWT token + auto auth_error = VerifyJWTFromRequest(req, credential_store); + if (auth_error.has_value()) { + return CreateErrorResponse(auth_error.value(), 401); + } + // Parse and validate request using our new class EncryptJsonRequest request; request.Parse(req.body); @@ -107,7 +187,13 @@ int main() { }); // Decryption endpoint - POST /decrypt - CROW_ROUTE(app, "/decrypt").methods("POST"_method)([](const crow::request& req) { + CROW_ROUTE(app, "/decrypt").methods("POST"_method)([&credential_store](const crow::request& req) { + // Verify JWT token + auto auth_error = VerifyJWTFromRequest(req, credential_store); + if (auth_error.has_value()) { + return CreateErrorResponse(auth_error.value(), 401); + } + // Parse and validate request using our new class DecryptJsonRequest request; request.Parse(req.body);