Skip to content

Commit 706dd5b

Browse files
authored
Merge pull request #204 from protegrity/av_auth_031
Adding logic for client-side authentication
2 parents de88d30 + 2e0ee5d commit 706dd5b

25 files changed

Lines changed: 994 additions & 334 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ _deps/
4242

4343
# Docker artifacts
4444
*.tar
45+
46+
# Testing credentials file
47+
testing_credentials.json

CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ target_link_libraries(dbps_server_lib PUBLIC OpenSSL::SSL OpenSSL::Crypto)
160160
# Client components library (depends on httplib, nlohmann/json, and cppcodec)
161161
add_library(dbps_client_lib STATIC
162162
src/client/dbps_api_client.cpp
163+
src/client/http_client_base.cpp
163164
src/client/httplib_client.cpp
164165
src/client/httplib_pool_registry.cpp
165166
src/client/httplib_pooled_client.cpp
@@ -364,6 +365,14 @@ if(BUILD_TESTS)
364365
gtest_main
365366
)
366367

368+
# Http client interface tests
369+
add_executable(http_client_base_test src/client/http_client_base_test.cpp)
370+
target_link_libraries(http_client_base_test
371+
dbps_client_lib
372+
dbps_common_lib
373+
gtest_main
374+
)
375+
367376
# Remote DBPA tests
368377
add_executable(dbpa_remote_test src/common/dbpa_remote_test.cpp)
369378
target_link_libraries(dbpa_remote_test
@@ -393,6 +402,7 @@ if(BUILD_SHARED_LIBS)
393402
src/common/dbps_remote_shared_lib_wrapper.cpp
394403
src/common/dbpa_remote.cpp
395404
src/client/dbps_api_client.cpp
405+
src/client/http_client_base.cpp
396406
src/client/httplib_client.cpp
397407
src/common/json_request.cpp
398408
)
@@ -496,6 +506,7 @@ if(BUILD_TESTS)
496506
dbpa_local_test
497507
httplib_pool_registry_test
498508
httplib_pooled_client_test
509+
http_client_base_test
499510
COMMENT "Building all tests"
500511
)
501512

@@ -517,6 +528,7 @@ if(BUILD_TESTS)
517528
gtest_discover_tests(dbpa_local_test)
518529
gtest_discover_tests(httplib_pool_registry_test)
519530
gtest_discover_tests(httplib_pooled_client_test)
531+
gtest_discover_tests(http_client_base_test)
520532
endif()
521533

522534
# All target (everything)

src/client/dbps_api_client.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ void DecryptApiResponse::SetJsonRequest(const DecryptJsonRequest& request) { jso
159159
bool DecryptApiResponse::HasJsonRequest() const { return json_request_.has_value(); }
160160
const JsonRequest& DecryptApiResponse::GetJsonRequest() const { return json_request_.value(); }
161161

162-
DBPSApiClient::DBPSApiClient(std::shared_ptr<HttpClientInterface> http_client)
162+
DBPSApiClient::DBPSApiClient(std::shared_ptr<HttpClientBase> http_client)
163163
: http_client_(std::move(http_client)) {
164164
}
165165

166166
std::string DBPSApiClient::HealthCheck() {
167-
auto response = http_client_->Get("/healthz");
167+
auto response = http_client_->Get("/healthz", false);
168168

169169
if (!response.error_message.empty()) {
170170
return "Error: " + response.error_message;

src/client/dbps_api_client.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
#include "../common/enum_utils.h"
2727
#include "../common/json_request.h"
2828
#include "tcb/span.hpp"
29-
#include "http_client_interface.h"
29+
#include "http_client_base.h"
3030

3131
using namespace dbps::external;
3232
using namespace dbps::enum_utils;
@@ -143,7 +143,7 @@ class DBPSApiClient {
143143
* The HTTP client is expected to be thread-safe.
144144
* @param http_client Custom HTTP client implementation
145145
*/
146-
explicit DBPSApiClient(std::shared_ptr<HttpClientInterface> http_client);
146+
explicit DBPSApiClient(std::shared_ptr<HttpClientBase> http_client);
147147

148148
/**
149149
* Destructor
@@ -223,5 +223,5 @@ class DBPSApiClient {
223223
);
224224

225225
private:
226-
const std::shared_ptr<HttpClientInterface> http_client_;
226+
const std::shared_ptr<HttpClientBase> http_client_;
227227
};

src/client/dbps_api_client_test.cpp

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
#include <algorithm>
2323
#include "tcb/span.hpp"
2424
#include "dbps_api_client.h"
25-
#include "http_client_interface.h"
25+
#include "http_client_base.h"
2626
#include "../common/enums.h"
2727
#include <nlohmann/json.hpp>
2828
#include <gtest/gtest.h>
@@ -56,8 +56,14 @@ bool CompareJsonStrings(const std::string& json1, const std::string& json2, cons
5656
}
5757

5858
// Mock HTTP client for testing
59-
class MockHttpClient : public HttpClientInterface {
59+
class MockHttpClient : public HttpClientBase {
6060
public:
61+
MockHttpClient()
62+
: HttpClientBase(
63+
"mock://",
64+
ClientCredentials{{"client_id", "test_client_AAAA"}, {"api_key", "test_key_AAAA"}}) {
65+
}
66+
6167
// Mock responses for different endpoints
6268
void SetMockResponse(const std::string& endpoint, const HttpResponse& response) {
6369
mock_responses_[endpoint] = response;
@@ -67,16 +73,21 @@ class MockHttpClient : public HttpClientInterface {
6773
mock_post_responses_[endpoint] = {expected_body, response};
6874
}
6975

70-
// Implement the interface methods
71-
HttpResponse Get(const std::string& endpoint) override {
76+
protected:
77+
HttpResponse DoGet(const std::string& endpoint, const HeaderList& headers) override {
78+
(void)headers;
7279
auto it = mock_responses_.find(endpoint);
7380
if (it != mock_responses_.end()) {
7481
return it->second;
7582
}
7683
return HttpResponse(404, "", "Mock endpoint not found: " + endpoint);
7784
}
7885

79-
HttpResponse Post(const std::string& endpoint, const std::string& json_body) override {
86+
HttpResponse DoPost(const std::string& endpoint, const std::string& json_body, const HeaderList& headers) override {
87+
(void)headers;
88+
if (endpoint == "/token") {
89+
return HttpResponse(200, R"({"token":"mock_jwt","token_type":"Bearer","expires_at":1766138275})");
90+
}
8091
auto it = mock_post_responses_.find(endpoint);
8192
if (it != mock_post_responses_.end()) {
8293
if (CompareJsonStrings(it->second.first, json_body, {"debug"})) {
@@ -299,7 +310,7 @@ TEST(DBPSApiClient, EncryptWithValidData) {
299310
})";
300311

301312
mock_client->SetMockPostResponse("/encrypt", expected_request,
302-
HttpClientInterface::HttpResponse(200, mock_response));
313+
HttpClientBase::HttpResponse(200, mock_response));
303314

304315
// Create DBPSApiClient with mock client
305316
DBPSApiClient client(std::move(mock_client));
@@ -391,7 +402,7 @@ TEST(DBPSApiClient, DecryptWithValidData) {
391402
})";
392403

393404
mock_client->SetMockPostResponse("/decrypt", expected_request,
394-
HttpClientInterface::HttpResponse(200, mock_response));
405+
HttpClientBase::HttpResponse(200, mock_response));
395406

396407
// Create DBPSApiClient with mock client
397408
DBPSApiClient client(std::move(mock_client));
@@ -534,7 +545,7 @@ TEST(DBPSApiClient, EncryptWithInvalidJsonResponse) {
534545
})";
535546

536547
mock_client->SetMockPostResponse("/encrypt", expected_request,
537-
HttpClientInterface::HttpResponse(200, mock_response));
548+
HttpClientBase::HttpResponse(200, mock_response));
538549

539550
// Create DBPSApiClient with mock client
540551
DBPSApiClient client(std::move(mock_client));
@@ -600,7 +611,7 @@ TEST(DBPSApiClient, DecryptWithInvalidJsonResponse) {
600611
})";
601612

602613
mock_client->SetMockPostResponse("/decrypt", expected_request,
603-
HttpClientInterface::HttpResponse(200, mock_response));
614+
HttpClientBase::HttpResponse(200, mock_response));
604615

605616
// Create DBPSApiClient with mock client
606617
DBPSApiClient client(std::move(mock_client));
@@ -677,7 +688,7 @@ TEST(DBPSApiClient, EncryptWithEncodingAttributes) {
677688
})";
678689

679690
mock_client->SetMockPostResponse("/encrypt", expected_request,
680-
HttpClientInterface::HttpResponse(200, mock_response));
691+
HttpClientBase::HttpResponse(200, mock_response));
681692

682693
// Create DBPSApiClient with mock client
683694
DBPSApiClient client(std::move(mock_client));

src/client/http_client_base.cpp

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#include "http_client_base.h"
19+
20+
#include <chrono>
21+
22+
#include "json_request.h"
23+
24+
HttpClientBase::HeaderList HttpClientBase::DefaultJsonGetHeaders() {
25+
HeaderList headers;
26+
headers.insert({"Accept", kJsonContentType});
27+
headers.insert({"User-Agent", kDefaultUserAgent});
28+
return headers;
29+
}
30+
31+
HttpClientBase::HeaderList HttpClientBase::DefaultJsonPostHeaders() {
32+
HeaderList headers;
33+
headers.insert({"Content-Type", kJsonContentType});
34+
headers.insert({"Accept", kJsonContentType});
35+
headers.insert({"User-Agent", kDefaultUserAgent});
36+
return headers;
37+
}
38+
39+
HttpClientBase::HttpResponse HttpClientBase::Get(const std::string& endpoint, bool auth_required) {
40+
// Lambda to build the request and make the actual call.
41+
const auto attempt = [&]() -> HttpResponse {
42+
auto headers = DefaultJsonGetHeaders();
43+
if (auth_required) {
44+
auto auth_error = AddAuthorizationHeader(headers);
45+
if (!auth_error.empty()) {
46+
return HttpResponse(0, "", auth_error);
47+
}
48+
}
49+
return DoGet(endpoint, headers);
50+
};
51+
52+
// First attempt
53+
auto result = attempt();
54+
55+
// If we got 401 Unauthorized and auth was required, invalidate token and retry once
56+
// This handles cases where the cached token expired between validation and use
57+
if (auth_required && result.status_code == 401) {
58+
InvalidateCachedToken();
59+
result = attempt(); // Second (final) attempt with fresh token
60+
}
61+
return result;
62+
}
63+
64+
HttpClientBase::HttpResponse HttpClientBase::Post(const std::string& endpoint,
65+
const std::string& json_body,
66+
bool auth_required) {
67+
// Lambda to build the request and make the actual call.
68+
const auto attempt = [&]() -> HttpResponse {
69+
auto headers = DefaultJsonPostHeaders();
70+
if (auth_required) {
71+
auto auth_error = AddAuthorizationHeader(headers);
72+
if (!auth_error.empty()) {
73+
return HttpResponse(0, "", auth_error);
74+
}
75+
}
76+
return DoPost(endpoint, json_body, headers);
77+
};
78+
79+
// First attempt
80+
auto result = attempt();
81+
82+
// If we got 401 Unauthorized and auth was required, invalidate token and retry once
83+
// This handles cases where the cached token expired between validation and use
84+
if (auth_required && result.status_code == 401) {
85+
InvalidateCachedToken();
86+
result = attempt(); // Second (final) attempt with fresh token
87+
}
88+
return result;
89+
}
90+
91+
std::string HttpClientBase::AddAuthorizationHeader(HeaderList& headers) {
92+
// Get the valid token or an error message.
93+
std::string token_or_error;
94+
auto token_opt = EnsureValidToken(token_or_error);
95+
if (!token_opt.has_value()) {
96+
return token_or_error;
97+
}
98+
99+
// Replace any existing Authorization header with: "<token_type> <token>".
100+
// Return an empty string if successful, otherwise return the error message.
101+
headers.erase(kAuthorizationHeader);
102+
std::string auth_value = token_opt->token_type;
103+
if (auth_value.back() != ' ') {
104+
auth_value.push_back(' ');
105+
}
106+
auth_value += token_opt->token;
107+
headers.insert({kAuthorizationHeader, auth_value});
108+
return "";
109+
}
110+
111+
std::optional<HttpClientBase::TokenWithExpiration> HttpClientBase::EnsureValidToken(std::string& error) {
112+
113+
const auto now_epoch_seconds = []() -> std::int64_t {
114+
const auto now = std::chrono::system_clock::now();
115+
const auto secs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
116+
return static_cast<std::int64_t>(secs);
117+
};
118+
119+
// Adds padding to the expiration time to "expire" the token early and prevent going too close to the expiration time.
120+
const auto is_token_valid_at = [&](std::int64_t now) -> bool {
121+
return cached_token_.has_value() &&
122+
!cached_token_->token.empty() &&
123+
cached_token_->expires_at > (now + kTokenExpirySkewSeconds);
124+
};
125+
126+
error.clear();
127+
const auto now = now_epoch_seconds();
128+
129+
{
130+
std::unique_lock<std::mutex> lock(token_mutex_);
131+
if (is_token_valid_at(now)) {
132+
return cached_token_;
133+
}
134+
while (token_fetch_in_progress_) {
135+
token_cv_.wait(lock);
136+
if (is_token_valid_at(now_epoch_seconds())) {
137+
return cached_token_;
138+
}
139+
}
140+
token_fetch_in_progress_ = true;
141+
} // Release token_mutex_ here
142+
143+
// Fetch token without holding token_mutex_
144+
// - avoids blocking other threads from entering EnsureValidToken() and waiting on token_cv_
145+
// - keeps the critical section small (network I/O can be slow)
146+
// We re-acquire token_mutex_ below to update cached_token_, clear token_fetch_in_progress_, and notify waiters.
147+
148+
std::optional<TokenWithExpiration> fetched = FetchToken(error);
149+
150+
{
151+
std::lock_guard<std::mutex> lock(token_mutex_);
152+
token_fetch_in_progress_ = false;
153+
if (fetched.has_value()) {
154+
cached_token_ = fetched;
155+
}
156+
}
157+
token_cv_.notify_all();
158+
return fetched;
159+
}
160+
161+
std::optional<HttpClientBase::TokenWithExpiration> HttpClientBase::FetchToken(std::string& error) {
162+
error.clear();
163+
TokenRequest token_req;
164+
token_req.credential_values_ = credentials_;
165+
166+
// IMPORTANT: call DoPost directly (authless) to avoid recursion.
167+
auto http_resp = DoPost("/token", token_req.ToJson(), DefaultJsonPostHeaders());
168+
if (!http_resp.error_message.empty() || http_resp.status_code != 200) {
169+
error = http_resp.error_message + " (status code: " + std::to_string(http_resp.status_code) + ")";
170+
return std::nullopt;
171+
}
172+
173+
TokenResponse token_resp;
174+
token_resp.Parse(http_resp.result);
175+
if (!token_resp.IsValid()) {
176+
error = token_resp.GetValidationError();
177+
if (error.empty()) {
178+
error = "While reading token response, found an invalid token response: " + http_resp.result;
179+
}
180+
return std::nullopt;
181+
}
182+
183+
TokenWithExpiration result;
184+
result.token = token_resp.token_.value();
185+
result.token_type = token_resp.token_type_.value();
186+
result.expires_at = token_resp.expires_at_.value();
187+
return result;
188+
}
189+
190+
void HttpClientBase::InvalidateCachedToken() {
191+
std::lock_guard<std::mutex> lock(token_mutex_);
192+
cached_token_ = std::nullopt;
193+
}

0 commit comments

Comments
 (0)