Skip to content
Merged
28 changes: 27 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 @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
286 changes: 286 additions & 0 deletions src/server/auth_utils.cpp
Original file line number Diff line number Diff line change
@@ -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 <jwt-cpp/jwt.h>
#include <chrono>
#include <iostream>
#include <fstream>
#include <nlohmann/json.hpp>

// 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<std::string, std::string>& 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<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;
}
}

// 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<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::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<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;
}
}

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