|
3 | 3 | #include <mutex> |
4 | 4 | #include <sstream> |
5 | 5 | #include <unordered_map> |
| 6 | +#include <chrono> |
6 | 7 |
|
7 | 8 | #include <atomic> |
8 | 9 | #include <curl/curl.h> |
@@ -65,6 +66,9 @@ namespace scitokens { |
65 | 66 |
|
66 | 67 | namespace internal { |
67 | 68 |
|
| 69 | +// Forward declaration |
| 70 | +class MonitoringStats; |
| 71 | + |
68 | 72 | class SimpleCurlGet { |
69 | 73 |
|
70 | 74 | int m_maxbytes{1048576}; |
@@ -110,6 +114,54 @@ class SimpleCurlGet { |
110 | 114 | void *userp); |
111 | 115 | }; |
112 | 116 |
|
| 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 | + |
113 | 165 | } // namespace internal |
114 | 166 |
|
115 | 167 | class UnsupportedKeyException : public std::runtime_error { |
@@ -226,6 +278,8 @@ class AsyncStatus { |
226 | 278 | std::string m_jwt_string; |
227 | 279 | std::string m_public_pem; |
228 | 280 | std::string m_algorithm; |
| 281 | + std::chrono::steady_clock::time_point m_start_time; |
| 282 | + bool m_monitoring_started{false}; |
229 | 283 |
|
230 | 284 | struct timeval get_timeout_val(time_t expiry_time) const { |
231 | 285 | auto now = time(NULL); |
@@ -418,17 +472,47 @@ class Validator { |
418 | 472 | } |
419 | 473 |
|
420 | 474 | 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; |
429 | 484 | } |
| 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 | + } |
430 | 495 |
|
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; |
432 | 516 | } |
433 | 517 | } |
434 | 518 |
|
@@ -514,6 +598,9 @@ class Validator { |
514 | 598 | status->m_jwt_string = jwt.get_token(); |
515 | 599 | status->m_public_pem = public_pem; |
516 | 600 | status->m_algorithm = algorithm; |
| 601 | + // Start monitoring timing |
| 602 | + status->m_start_time = std::chrono::steady_clock::now(); |
| 603 | + status->m_monitoring_started = true; |
517 | 604 |
|
518 | 605 | return verify_async_continue(std::move(status)); |
519 | 606 | } |
@@ -677,6 +764,16 @@ class Validator { |
677 | 764 | } |
678 | 765 | } |
679 | 766 | } |
| 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 | + |
680 | 777 | std::unique_ptr<AsyncStatus> result(new AsyncStatus()); |
681 | 778 | result->m_done = true; |
682 | 779 | return result; |
|
0 commit comments