Skip to content
27 changes: 26 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -229,6 +243,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
Expand Down Expand Up @@ -389,6 +412,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
Expand All @@ -404,6 +428,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)
Expand Down
175 changes: 175 additions & 0 deletions src/server/auth_utils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// 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 <jwt-cpp/jwt.h>
#include <chrono>
#include <iostream>
#include <fstream>
#include <nlohmann/json.hpp>

// Hardcoded JWT secret key for signing tokens
// TODO: Make this configurable (e.g., from environment variable or config file)
Comment thread
argmarco-tkd marked this conversation as resolved.
Outdated
static const std::string JWT_SECRET_KEY = "default-secret-key-change-in-production";

// JWT expiration time: 4 hours in seconds
static const int JWT_EXPIRATION_SECONDS = 4 * 60 * 60; // 14400 seconds

// ClientCredentialStore implementation

// Initialize credential store from a given map.
void ClientCredentialStore::init(const std::map<std::string, std::string>& credentials) {
credentials_.clear();
credentials_ = credentials;
}

// 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();

// 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<std::string>());
} 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;
}
}

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<std::string> 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::cerr << "Error generating JWT: client_id cannot be empty" << std::endl;
return std::nullopt;
}

// Validate credentials before generating JWT
if (!ValidateCredential(client_id, api_key)) {
std::cerr << "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<std::chrono::seconds>(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;
}
}

// ParseAuthRequest implementation
AuthRequest ParseAuthRequest(const std::string& request_body) {
AuthRequest auth_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()) {
auth_req.error_message = "Invalid JSON in request body";
return auth_req;
}

// Extract client_id from request
if (json_body.contains("client_id") && json_body["client_id"].is_string()) {
auth_req.client_id = json_body["client_id"].get<std::string>();
} else {
auth_req.error_message = "Missing required field: client_id";
return auth_req;
}

// Extract api_key from request
if (json_body.contains("api_key") && json_body["api_key"].is_string()) {
auth_req.api_key = json_body["api_key"].get<std::string>();
} else {
auth_req.error_message = "Missing required field: api_key";
return auth_req;
}

return auth_req;
} catch (const nlohmann::json::exception& e) {
auth_req.error_message = "Invalid JSON in request body: " + std::string(e.what());
return auth_req;
} catch (const std::exception& e) {
auth_req.error_message = "Error parsing request: " + std::string(e.what());
return auth_req;
}
}

105 changes: 105 additions & 0 deletions src/server/auth_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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 Identity Provider can be used instead.
//
// TODO: Expand explanation below.
// - Note that no Arrow codebase changes would be needed to replace this with a fully fledged Identity Provider
// as the connection configuration specified on the application level is passed as-is to the DBPS agents for authentication.

#pragma once

#include <map>
#include <string>
#include <optional>

#ifndef DBPS_EXPORT
#define DBPS_EXPORT
#endif

/**
* 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:
ClientCredentialStore() = default;
~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<std::string, std::string>& credentials);

/**
* 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<std::string> GenerateJWT(const std::string& client_id, const std::string& api_key) 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;

// In-memory storage: client_id -> api_key
std::map<std::string, std::string> credentials_;
};

/**
* Structure to hold parsed authentication request data.
*/
struct DBPS_EXPORT AuthRequest {
std::string client_id;
std::string api_key;
std::optional<std::string> error_message; // Error message if parsing failed
};

/**
* Parses an authentication request from JSON body.
*
* @param request_body The raw JSON request body string
* @return AuthRequest struct with parsed client_id and api_key, or error_message if parsing failed
*/
DBPS_EXPORT AuthRequest ParseAuthRequest(const std::string& request_body);
Loading