@@ -22,6 +22,9 @@ namespace {
2222// This handles concurrent access from multiple threads/processes
2323constexpr int SQLITE_BUSY_TIMEOUT_MS = 5000 ;
2424
25+ // Default time before expiry when next_update should occur (4 hours)
26+ constexpr int64_t DEFAULT_NEXT_UPDATE_OFFSET_S = 4 * 3600 ;
27+
2528void initialize_cachedb (const std::string &keycache_file) {
2629
2730 sqlite3 *db;
@@ -257,7 +260,7 @@ bool scitokens::Validator::get_public_keys_from_db(const std::string issuer,
257260 sqlite3_close (db);
258261 iter = top_obj.find (" next_update" );
259262 if (iter == top_obj.end () || !iter->second .is <int64_t >()) {
260- next_update = expiry - 4 * 3600 ;
263+ next_update = expiry - DEFAULT_NEXT_UPDATE_OFFSET_S ;
261264 } else {
262265 next_update = iter->second .get <int64_t >();
263266 }
@@ -406,7 +409,7 @@ scitokens::Validator::get_all_issuers_from_db(int64_t now) {
406409 if (next_update_iter == top_obj.end () ||
407410 !next_update_iter->second .is <int64_t >()) {
408411 // If next_update is not set, default to 4 hours before expiry
409- next_update = expiry - 4 * 3600 ;
412+ next_update = expiry - DEFAULT_NEXT_UPDATE_OFFSET_S ;
410413 } else {
411414 next_update = next_update_iter->second .get <int64_t >();
412415 }
@@ -425,3 +428,138 @@ scitokens::Validator::get_all_issuers_from_db(int64_t now) {
425428 sqlite3_close (db);
426429 return result;
427430}
431+
432+ std::string scitokens::Validator::load_jwks (const std::string &issuer) {
433+ auto now = std::time (NULL );
434+ picojson::value jwks;
435+ int64_t next_update;
436+
437+ try {
438+ // Try to get from cache
439+ if (get_public_keys_from_db (issuer, now, jwks, next_update)) {
440+ // Check if refresh is needed (expired based on next_update)
441+ if (now <= next_update) {
442+ // Still valid, return cached version
443+ return jwks.serialize ();
444+ }
445+ // Past next_update, need to refresh
446+ }
447+ } catch (const NegativeCacheHitException &) {
448+ // Negative cache hit - return empty keys
449+ return std::string (" {\" keys\" : []}" );
450+ }
451+
452+ // Either not in cache or past next_update - refresh
453+ if (!refresh_jwks (issuer)) {
454+ throw CurlException (" Failed to load JWKS for issuer: " + issuer);
455+ }
456+
457+ // Get the newly refreshed JWKS
458+ return get_jwks (issuer);
459+ }
460+
461+ std::string scitokens::Validator::get_jwks_metadata (const std::string &issuer) {
462+ auto now = std::time (NULL );
463+ int64_t next_update = -1 ;
464+ int64_t expires = -1 ;
465+
466+ // Get the metadata from database without expiry check
467+ auto cache_fname = get_cache_file ();
468+ if (cache_fname.size () == 0 ) {
469+ throw std::runtime_error (" Unable to access cache file" );
470+ }
471+
472+ sqlite3 *db;
473+ int rc = sqlite3_open (cache_fname.c_str (), &db);
474+ if (rc) {
475+ sqlite3_close (db);
476+ throw std::runtime_error (" Failed to open cache database" );
477+ }
478+ sqlite3_busy_timeout (db, SQLITE_BUSY_TIMEOUT_MS);
479+
480+ sqlite3_stmt *stmt;
481+ rc = sqlite3_prepare_v2 (db, " SELECT keys from keycache where issuer = ?" ,
482+ -1 , &stmt, NULL );
483+ if (rc != SQLITE_OK) {
484+ sqlite3_close (db);
485+ throw std::runtime_error (" Failed to prepare database query" );
486+ }
487+
488+ if (sqlite3_bind_text (stmt, 1 , issuer.c_str (), issuer.size (),
489+ SQLITE_STATIC) != SQLITE_OK) {
490+ sqlite3_finalize (stmt);
491+ sqlite3_close (db);
492+ throw std::runtime_error (" Failed to bind issuer to query" );
493+ }
494+
495+ rc = sqlite3_step (stmt);
496+ if (rc == SQLITE_ROW) {
497+ const unsigned char *data = sqlite3_column_text (stmt, 0 );
498+ std::string metadata (reinterpret_cast <const char *>(data));
499+ sqlite3_finalize (stmt);
500+ sqlite3_close (db);
501+
502+ picojson::value json_obj;
503+ auto err = picojson::parse (json_obj, metadata);
504+ if (!err.empty () || !json_obj.is <picojson::object>()) {
505+ throw JsonException (" Invalid JSON in cache entry" );
506+ }
507+
508+ auto top_obj = json_obj.get <picojson::object>();
509+
510+ // Extract expires
511+ auto iter = top_obj.find (" expires" );
512+ if (iter != top_obj.end () && iter->second .is <int64_t >()) {
513+ expires = iter->second .get <int64_t >();
514+ }
515+
516+ // Extract next_update
517+ iter = top_obj.find (" next_update" );
518+ if (iter != top_obj.end () && iter->second .is <int64_t >()) {
519+ next_update = iter->second .get <int64_t >();
520+ } else if (expires != -1 ) {
521+ // Default next_update to 4 hours before expiry
522+ next_update = expires - DEFAULT_NEXT_UPDATE_OFFSET_S;
523+ }
524+
525+ // Build metadata JSON (add future keys at top level if needed)
526+ picojson::object metadata_obj;
527+ if (expires != -1 ) {
528+ metadata_obj[" expires" ] = picojson::value (expires);
529+ }
530+ if (next_update != -1 ) {
531+ metadata_obj[" next_update" ] = picojson::value (next_update);
532+ }
533+
534+ return picojson::value (metadata_obj).serialize ();
535+ } else {
536+ sqlite3_finalize (stmt);
537+ sqlite3_close (db);
538+ throw std::runtime_error (" Issuer not found in cache" );
539+ }
540+ }
541+
542+ bool scitokens::Validator::delete_jwks (const std::string &issuer) {
543+ auto cache_fname = get_cache_file ();
544+ if (cache_fname.size () == 0 ) {
545+ return false ;
546+ }
547+
548+ sqlite3 *db;
549+ int rc = sqlite3_open (cache_fname.c_str (), &db);
550+ if (rc) {
551+ sqlite3_close (db);
552+ return false ;
553+ }
554+ sqlite3_busy_timeout (db, SQLITE_BUSY_TIMEOUT_MS);
555+
556+ // Use the existing remove_issuer_entry function
557+ // Note: remove_issuer_entry closes the database on error
558+ if (remove_issuer_entry (db, issuer, true ) != 0 ) {
559+ // Database already closed by remove_issuer_entry
560+ return false ;
561+ }
562+
563+ sqlite3_close (db);
564+ return true ;
565+ }
0 commit comments