Skip to content

Commit 95d91da

Browse files
Copilotbbockelm
andcommitted
Add monitoring API infrastructure and basic test
Co-authored-by: bbockelm <1093447+bbockelm@users.noreply.github.com>
1 parent 8c4ee54 commit 95d91da

File tree

6 files changed

+372
-10
lines changed

6 files changed

+372
-10
lines changed

CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pkg_check_modules(SQLITE REQUIRED sqlite3)
4444

4545
endif()
4646

47-
add_library(SciTokens SHARED src/scitokens.cpp src/scitokens_internal.cpp src/scitokens_cache.cpp)
47+
add_library(SciTokens SHARED src/scitokens.cpp src/scitokens_internal.cpp src/scitokens_cache.cpp src/scitokens_monitoring.cpp)
4848
target_compile_features(SciTokens PUBLIC cxx_std_11) # Use at least C++11 for building and when linking to scitokens
4949
target_include_directories(SciTokens PUBLIC ${JWT_CPP_INCLUDES} "${PROJECT_SOURCE_DIR}/src" PRIVATE ${CURL_INCLUDE_DIRS} ${OPENSSL_INCLUDE_DIRS} ${LIBCRYPTO_INCLUDE_DIRS} ${SQLITE_INCLUDE_DIRS} ${UUID_INCLUDE_DIRS})
5050

@@ -75,6 +75,9 @@ target_link_libraries(scitokens-list-access SciTokens)
7575
add_executable(scitokens-create src/create.cpp)
7676
target_link_libraries(scitokens-create SciTokens)
7777

78+
add_executable(scitokens-test-monitoring src/test_monitoring.cpp)
79+
target_link_libraries(scitokens-test-monitoring SciTokens)
80+
7881
get_directory_property(TARGETS BUILDSYSTEM_TARGETS)
7982
install(
8083
TARGETS ${TARGETS}

src/scitokens.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,3 +1114,35 @@ int scitoken_config_get_str(const char *key, char **output, char **err_msg) {
11141114
}
11151115
return 0;
11161116
}
1117+
1118+
int scitoken_get_monitoring_json(char **json_out, char **err_msg) {
1119+
if (!json_out) {
1120+
if (err_msg) {
1121+
*err_msg = strdup("JSON output pointer may not be null.");
1122+
}
1123+
return -1;
1124+
}
1125+
try {
1126+
std::string json =
1127+
scitokens::internal::MonitoringStats::instance().get_json();
1128+
*json_out = strdup(json.c_str());
1129+
} catch (std::exception &exc) {
1130+
if (err_msg) {
1131+
*err_msg = strdup(exc.what());
1132+
}
1133+
return -1;
1134+
}
1135+
return 0;
1136+
}
1137+
1138+
int scitoken_reset_monitoring_stats(char **err_msg) {
1139+
try {
1140+
scitokens::internal::MonitoringStats::instance().reset();
1141+
} catch (std::exception &exc) {
1142+
if (err_msg) {
1143+
*err_msg = strdup(exc.what());
1144+
}
1145+
return -1;
1146+
}
1147+
return 0;
1148+
}

src/scitokens.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,26 @@ int scitoken_config_set_str(const char *key, const char *value, char **err_msg);
329329
*/
330330
int scitoken_config_get_str(const char *key, char **output, char **err_msg);
331331

332+
/**
333+
* Get monitoring statistics as a JSON string.
334+
* Returns a JSON object containing per-issuer validation statistics including:
335+
* - successful_validations: count of successful token validations
336+
* - unsuccessful_validations: count of failed token validations
337+
* - expired_tokens: count of expired tokens encountered
338+
* - average_validation_time_ms: average validation time in milliseconds
339+
* - failed_issuer_lookups: count of failed issuer lookups (limited to prevent DDoS)
340+
*
341+
* The returned string must be freed by the caller using free().
342+
* Returns 0 on success, nonzero on failure.
343+
*/
344+
int scitoken_get_monitoring_json(char **json_out, char **err_msg);
345+
346+
/**
347+
* Reset all monitoring statistics.
348+
* Returns 0 on success, nonzero on failure.
349+
*/
350+
int scitoken_reset_monitoring_stats(char **err_msg);
351+
332352
#ifdef __cplusplus
333353
}
334354
#endif

src/scitokens_internal.h

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <mutex>
44
#include <sstream>
55
#include <unordered_map>
6+
#include <chrono>
67

78
#include <atomic>
89
#include <curl/curl.h>
@@ -65,6 +66,9 @@ namespace scitokens {
6566

6667
namespace internal {
6768

69+
// Forward declaration
70+
class MonitoringStats;
71+
6872
class SimpleCurlGet {
6973

7074
int m_maxbytes{1048576};
@@ -110,6 +114,54 @@ class SimpleCurlGet {
110114
void *userp);
111115
};
112116

117+
/**
118+
* Statistics for monitoring token validation per issuer.
119+
*/
120+
struct IssuerStats {
121+
std::atomic<uint64_t> successful_validations{0};
122+
std::atomic<uint64_t> unsuccessful_validations{0};
123+
std::atomic<uint64_t> expired_tokens{0};
124+
std::atomic<uint64_t> total_time_ns{0}; // Total time in nanoseconds
125+
std::atomic<uint64_t> validation_count{0}; // For computing average
126+
};
127+
128+
/**
129+
* Monitoring statistics singleton.
130+
* Tracks per-issuer validation statistics and protects against
131+
* resource exhaustion from invalid issuers.
132+
*/
133+
class MonitoringStats {
134+
public:
135+
static MonitoringStats &instance();
136+
137+
void record_validation_start(const std::string &issuer);
138+
void record_validation_success(const std::string &issuer,
139+
uint64_t duration_ns);
140+
void record_validation_failure(const std::string &issuer,
141+
uint64_t duration_ns);
142+
void record_expired_token(const std::string &issuer);
143+
void record_failed_issuer_lookup(const std::string &issuer);
144+
145+
std::string get_json() const;
146+
void reset();
147+
148+
private:
149+
MonitoringStats() = default;
150+
~MonitoringStats() = default;
151+
MonitoringStats(const MonitoringStats &) = delete;
152+
MonitoringStats &operator=(const MonitoringStats &) = delete;
153+
154+
// Limit the number of failed issuer entries to prevent DDoS
155+
static constexpr size_t MAX_FAILED_ISSUERS = 100;
156+
157+
mutable std::mutex m_mutex;
158+
std::unordered_map<std::string, IssuerStats> m_issuer_stats;
159+
std::unordered_map<std::string, std::atomic<uint64_t>> m_failed_issuer_lookups;
160+
161+
std::string sanitize_issuer_for_json(const std::string &issuer) const;
162+
void prune_failed_issuers();
163+
};
164+
113165
} // namespace internal
114166

115167
class UnsupportedKeyException : public std::runtime_error {
@@ -226,6 +278,8 @@ class AsyncStatus {
226278
std::string m_jwt_string;
227279
std::string m_public_pem;
228280
std::string m_algorithm;
281+
std::chrono::steady_clock::time_point m_start_time;
282+
bool m_monitoring_started{false};
229283

230284
struct timeval get_timeout_val(time_t expiry_time) const {
231285
auto now = time(NULL);
@@ -418,17 +472,47 @@ class Validator {
418472
}
419473

420474
void verify(const SciToken &scitoken, time_t expiry_time) {
421-
auto result = verify_async(scitoken);
422-
while (!result->m_done) {
423-
auto timeout_val = result->get_timeout_val(expiry_time);
424-
select(result->get_max_fd() + 1, result->get_read_fd_set(),
425-
result->get_write_fd_set(), result->get_exc_fd_set(),
426-
&timeout_val);
427-
if (time(NULL) >= expiry_time) {
428-
throw CurlException("Timeout when loading the OIDC metadata.");
475+
std::string issuer;
476+
auto start_time = std::chrono::steady_clock::now();
477+
bool has_issuer = false;
478+
479+
try {
480+
// Try to extract issuer for monitoring
481+
if (scitoken.m_decoded && scitoken.m_decoded->has_payload_claim("iss")) {
482+
issuer = scitoken.m_decoded->get_issuer();
483+
has_issuer = true;
429484
}
485+
486+
auto result = verify_async(scitoken);
487+
while (!result->m_done) {
488+
auto timeout_val = result->get_timeout_val(expiry_time);
489+
select(result->get_max_fd() + 1, result->get_read_fd_set(),
490+
result->get_write_fd_set(), result->get_exc_fd_set(),
491+
&timeout_val);
492+
if (time(NULL) >= expiry_time) {
493+
throw CurlException("Timeout when loading the OIDC metadata.");
494+
}
430495

431-
result = verify_async_continue(std::move(result));
496+
result = verify_async_continue(std::move(result));
497+
}
498+
} catch (const std::exception &e) {
499+
// Record failure if we have an issuer
500+
if (has_issuer) {
501+
auto end_time = std::chrono::steady_clock::now();
502+
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
503+
end_time - start_time);
504+
505+
// Check if this is an expiration error
506+
std::string error_msg = e.what();
507+
if (error_msg.find("exp") != std::string::npos ||
508+
error_msg.find("expir") != std::string::npos) {
509+
internal::MonitoringStats::instance().record_expired_token(issuer);
510+
}
511+
512+
internal::MonitoringStats::instance().record_validation_failure(
513+
issuer, duration.count());
514+
}
515+
throw;
432516
}
433517
}
434518

@@ -514,6 +598,9 @@ class Validator {
514598
status->m_jwt_string = jwt.get_token();
515599
status->m_public_pem = public_pem;
516600
status->m_algorithm = algorithm;
601+
// Start monitoring timing
602+
status->m_start_time = std::chrono::steady_clock::now();
603+
status->m_monitoring_started = true;
517604

518605
return verify_async_continue(std::move(status));
519606
}
@@ -677,6 +764,16 @@ class Validator {
677764
}
678765
}
679766
}
767+
768+
// Record successful validation
769+
if (status->m_monitoring_started) {
770+
auto end_time = std::chrono::steady_clock::now();
771+
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(
772+
end_time - status->m_start_time);
773+
internal::MonitoringStats::instance().record_validation_success(
774+
status->m_issuer, duration.count());
775+
}
776+
680777
std::unique_ptr<AsyncStatus> result(new AsyncStatus());
681778
result->m_done = true;
682779
return result;

0 commit comments

Comments
 (0)