From 319172018fd4bc14e5a3453dfb2bdbedf20e976b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:36:13 +0000 Subject: [PATCH 1/6] Initial plan From 6d369a9b7ae92d885390344638e192fac06998fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:53:15 +0000 Subject: [PATCH 2/6] Add offline support with SCITOKENS_KEYCACHE_FILE and scitokens-keycache tool Co-authored-by: djw8605 <79268+djw8605@users.noreply.github.com> --- CMakeLists.txt | 3 + src/scitokens-keycache.cpp | 145 +++++++++++++++++++++++++++++++++++++ src/scitokens.cpp | 30 ++++++++ src/scitokens.h | 9 +++ src/scitokens_cache.cpp | 15 +++- src/scitokens_internal.cpp | 14 ++++ src/scitokens_internal.h | 7 ++ 7 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/scitokens-keycache.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 53de423..d97f13c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,9 @@ target_link_libraries(scitokens-list-access SciTokens) add_executable(scitokens-create src/create.cpp) target_link_libraries(scitokens-create SciTokens) +add_executable(scitokens-keycache src/scitokens-keycache.cpp) +target_link_libraries(scitokens-keycache SciTokens) + get_directory_property(TARGETS BUILDSYSTEM_TARGETS) install( TARGETS ${TARGETS} diff --git a/src/scitokens-keycache.cpp b/src/scitokens-keycache.cpp new file mode 100644 index 0000000..c0b07a8 --- /dev/null +++ b/src/scitokens-keycache.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "scitokens.h" + +void print_usage(const char *progname) { + std::cout << "Usage: " << progname << " --cache-file --jwks --issuer --valid-for \n"; + std::cout << "\n"; + std::cout << "Options:\n"; + std::cout << " --cache-file Path to the keycache SQLite database file\n"; + std::cout << " --jwks Path to the JWKS file to store\n"; + std::cout << " --issuer Issuer URL for the JWKS\n"; + std::cout << " --valid-for How long the key should be valid (in seconds)\n"; + std::cout << " --help Show this help message\n"; + std::cout << "\n"; + std::cout << "Example:\n"; + std::cout << " " << progname << " --cache-file /tmp/offline.db --jwks keys.json --issuer https://example.com --valid-for 86400\n"; +} + +std::string read_file(const std::string &filename) { + std::ifstream file(filename); + if (!file.is_open()) { + throw std::runtime_error("Cannot open file: " + filename); + } + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + return content; +} + +int main(int argc, char *argv[]) { + std::string cache_file; + std::string jwks_file; + std::string issuer; + long valid_for = 0; + + // Parse command line arguments + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } else if (strcmp(argv[i], "--cache-file") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --cache-file requires an argument\n"; + return 1; + } + cache_file = argv[++i]; + } else if (strcmp(argv[i], "--jwks") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --jwks requires an argument\n"; + return 1; + } + jwks_file = argv[++i]; + } else if (strcmp(argv[i], "--issuer") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --issuer requires an argument\n"; + return 1; + } + issuer = argv[++i]; + } else if (strcmp(argv[i], "--valid-for") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --valid-for requires an argument\n"; + return 1; + } + char *endptr; + valid_for = strtol(argv[++i], &endptr, 10); + if (*endptr != '\0' || valid_for <= 0) { + std::cerr << "Error: --valid-for must be a positive integer\n"; + return 1; + } + } else { + std::cerr << "Error: Unknown option " << argv[i] << "\n"; + print_usage(argv[0]); + return 1; + } + } + + // Validate required arguments + if (cache_file.empty()) { + std::cerr << "Error: --cache-file is required\n"; + print_usage(argv[0]); + return 1; + } + if (jwks_file.empty()) { + std::cerr << "Error: --jwks is required\n"; + print_usage(argv[0]); + return 1; + } + if (issuer.empty()) { + std::cerr << "Error: --issuer is required\n"; + print_usage(argv[0]); + return 1; + } + if (valid_for == 0) { + std::cerr << "Error: --valid-for is required\n"; + print_usage(argv[0]); + return 1; + } + + try { + // Set the cache file environment variable + if (setenv("SCITOKENS_KEYCACHE_FILE", cache_file.c_str(), 1) != 0) { + std::cerr << "Error: Failed to set SCITOKENS_KEYCACHE_FILE environment variable\n"; + return 1; + } + + // Read the JWKS file + std::string jwks_content = read_file(jwks_file); + + // Calculate expiration time + time_t now = time(nullptr); + int64_t expires_at = static_cast(now) + valid_for; + + // Store the JWKS with expiration + char *err_msg = nullptr; + int result = keycache_set_jwks_with_expiry(issuer.c_str(), jwks_content.c_str(), expires_at, &err_msg); + + if (result != 0) { + std::cerr << "Error: Failed to store JWKS: " << (err_msg ? err_msg : "Unknown error") << "\n"; + if (err_msg) { + free(err_msg); + } + return 1; + } + + std::cout << "Successfully stored JWKS for issuer: " << issuer << "\n"; + std::cout << "Cache file: " << cache_file << "\n"; + std::cout << "Expires at: " << ctime(&now) << " + " << valid_for << " seconds\n"; + + if (err_msg) { + free(err_msg); + } + + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/src/scitokens.cpp b/src/scitokens.cpp index a11c84d..31d6221 100644 --- a/src/scitokens.cpp +++ b/src/scitokens.cpp @@ -989,6 +989,36 @@ int keycache_set_jwks(const char *issuer, const char *jwks, char **err_msg) { return 0; } +int keycache_set_jwks_with_expiry(const char *issuer, const char *jwks, + int64_t expires_at, char **err_msg) { + if (!issuer) { + if (err_msg) { + *err_msg = strdup("Issuer may not be a null pointer"); + } + return -1; + } + if (!jwks) { + if (err_msg) { + *err_msg = strdup("JWKS pointer may not be null."); + } + return -1; + } + try { + if (!scitokens::Validator::store_jwks_with_expiry(issuer, jwks, expires_at)) { + if (err_msg) { + *err_msg = strdup("Failed to set the JWKS cache for issuer."); + } + return -1; + } + } catch (std::exception &exc) { + if (err_msg) { + *err_msg = strdup(exc.what()); + } + return -1; + } + return 0; +} + int config_set_int(const char *key, int value, char **err_msg) { return scitoken_config_set_int(key, value, err_msg); } diff --git a/src/scitokens.h b/src/scitokens.h index b87003a..a8b3e29 100644 --- a/src/scitokens.h +++ b/src/scitokens.h @@ -290,6 +290,15 @@ int keycache_get_cached_jwks(const char *issuer, char **jwks, char **err_msg); */ int keycache_set_jwks(const char *issuer, const char *jwks, char **err_msg); +/** + * Replace any existing key cache entry with one provided by the user. + * Allows explicit setting of expiration time for offline usage. + * - `jwks` is value that will be set in the cache. + * - `expires_at` is the expiration time as Unix timestamp (seconds since epoch). + */ +int keycache_set_jwks_with_expiry(const char *issuer, const char *jwks, + int64_t expires_at, char **err_msg); + /** * APIs for managing scitokens configuration parameters. */ diff --git a/src/scitokens_cache.cpp b/src/scitokens_cache.cpp index 12536c1..d54799f 100644 --- a/src/scitokens_cache.cpp +++ b/src/scitokens_cache.cpp @@ -42,13 +42,22 @@ void initialize_cachedb(const std::string &keycache_file) { /** * Get the Cache file location - * 1. User-defined through config api - * 2. $XDG_CACHE_HOME - * 3. .cache subdirectory of home directory as returned by the password + * 1. SCITOKENS_KEYCACHE_FILE environment variable (for offline use) + * 2. User-defined through config api + * 3. $XDG_CACHE_HOME + * 4. .cache subdirectory of home directory as returned by the password * database */ std::string get_cache_file() { + // Check for direct cache file location first (offline support) + const char *direct_cache_file = getenv("SCITOKENS_KEYCACHE_FILE"); + if (direct_cache_file && strlen(direct_cache_file) > 0) { + std::string keycache_file(direct_cache_file); + initialize_cachedb(keycache_file); + return keycache_file; + } + const char *xdg_cache_home = getenv("XDG_CACHE_HOME"); auto bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); diff --git a/src/scitokens_internal.cpp b/src/scitokens_internal.cpp index 0b37773..d8ca5d9 100644 --- a/src/scitokens_internal.cpp +++ b/src/scitokens_internal.cpp @@ -797,6 +797,20 @@ bool Validator::store_jwks(const std::string &issuer, return store_public_keys(issuer, jwks, next_update, expires); } +bool Validator::store_jwks_with_expiry(const std::string &issuer, + const std::string &jwks_str, + int64_t expires_at) { + picojson::value jwks; + std::string err = picojson::parse(jwks, jwks_str); + auto now = std::time(NULL); + int next_update_delta = configurer::Configuration::get_next_update_delta(); + int64_t next_update = now + next_update_delta; + if (!err.empty()) { + throw JsonException(err); + } + return store_public_keys(issuer, jwks, next_update, expires_at); +} + std::unique_ptr Validator::get_public_key_pem(const std::string &issuer, const std::string &kid, std::string &public_pem, std::string &algorithm) { diff --git a/src/scitokens_internal.h b/src/scitokens_internal.h index c947b46..5ea994f 100644 --- a/src/scitokens_internal.h +++ b/src/scitokens_internal.h @@ -745,6 +745,13 @@ class Validator { */ static bool store_jwks(const std::string &issuer, const std::string &jwks); + /** + * Store the contents of a JWKS for a given issuer with explicit expiry time. + */ + static bool store_jwks_with_expiry(const std::string &issuer, + const std::string &jwks_str, + int64_t expires_at); + /** * Trigger a refresh of the JWKS or a given issuer. */ From c337f91a707b7adce94b1080cc110f7c872105fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:58:16 +0000 Subject: [PATCH 3/6] Add comprehensive tests and documentation for offline cache functionality Co-authored-by: djw8605 <79268+djw8605@users.noreply.github.com> --- OFFLINE_CACHE.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++ src/scitokens.h | 1 + test/main.cpp | 74 ++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 OFFLINE_CACHE.md diff --git a/OFFLINE_CACHE.md b/OFFLINE_CACHE.md new file mode 100644 index 0000000..6183bd9 --- /dev/null +++ b/OFFLINE_CACHE.md @@ -0,0 +1,124 @@ +# SciTokens Offline Cache Support + +This document describes the offline cache functionality added to the scitokens-cpp library. + +## Overview + +The scitokens library now supports offline operation through a direct SQLite cache file that can be pre-populated with JWKS data. This enables environments where external network access to fetch public keys is not available or desired. + +## New Features + +### 1. SCITOKENS_KEYCACHE_FILE Environment Variable + +When set, this environment variable points directly to a SQLite database file that will be used for the key cache, bypassing the normal cache location resolution (XDG_CACHE_HOME, ~/.cache, etc.). + +```bash +export SCITOKENS_KEYCACHE_FILE=/path/to/offline.db +``` + +### 2. scitokens-keycache Command Line Tool + +A new command-line utility for creating and managing offline cache files. + +#### Usage +```bash +scitokens-keycache --cache-file --jwks --issuer --valid-for +``` + +#### Options +- `--cache-file `: Path to the keycache SQLite database file +- `--jwks `: Path to the JWKS file to store +- `--issuer `: Issuer URL for the JWKS +- `--valid-for `: How long the key should be valid (in seconds) +- `--help`: Show help message + +#### Example +```bash +scitokens-keycache --cache-file /opt/scitokens/offline.db \ + --jwks issuer_keys.json \ + --issuer https://tokens.example.com \ + --valid-for 86400 +``` + +### 3. New API Function + +A new C API function allows programmatic storage of JWKS with explicit expiration times: + +```c +int keycache_set_jwks_with_expiry(const char *issuer, const char *jwks, + int64_t expires_at, char **err_msg); +``` + +Where `expires_at` is the expiration time as a Unix timestamp (seconds since epoch). + +## Usage Workflow + +### Setting up an Offline Cache + +1. **Create JWKS file**: Save the issuer's public keys in a JSON Web Key Set format + ```json + { + "keys": [ + { + "kty": "EC", + "kid": "key-1", + "use": "sig", + "alg": "ES256", + "x": "...", + "y": "...", + "crv": "P-256" + } + ] + } + ``` + +2. **Create cache file**: Use the scitokens-keycache tool + ```bash + scitokens-keycache --cache-file /opt/tokens/cache.db \ + --jwks issuer_keys.json \ + --issuer https://tokens.example.com \ + --valid-for 2592000 # 30 days + ``` + +3. **Configure application**: Set the environment variable + ```bash + export SCITOKENS_KEYCACHE_FILE=/opt/tokens/cache.db + ``` + +### Using the Offline Cache + +Once configured, the existing scitokens API functions work normally: + +```c +char *jwks = NULL; +char *err_msg = NULL; +int result = keycache_get_cached_jwks("https://tokens.example.com", &jwks, &err_msg); +if (result == 0 && jwks) { + // Process the JWKS + free(jwks); +} +``` + +## Backward Compatibility + +All existing functionality remains unchanged. The new features are: +- Additive API extensions +- Optional environment variable +- New command-line tool + +Existing code will continue to work without modification. + +## Cache Location Priority + +The cache file location is determined in this order: +1. `SCITOKENS_KEYCACHE_FILE` environment variable (highest priority - for offline use) +2. User-configured cache home via config API +3. `XDG_CACHE_HOME` environment variable +4. `~/.cache` directory (lowest priority) + +## Security Considerations + +- Ensure offline cache files have appropriate file permissions (600 or 640) +- Regularly update offline caches with fresh keys before expiration +- Consider key rotation policies when setting expiration times +- Validate JWKS content before adding to offline caches \ No newline at end of file diff --git a/src/scitokens.h b/src/scitokens.h index a8b3e29..cd35ed1 100644 --- a/src/scitokens.h +++ b/src/scitokens.h @@ -6,6 +6,7 @@ #include #include +#include #ifdef __cplusplus #include diff --git a/test/main.cpp b/test/main.cpp index 6107033..583fc2e 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -841,6 +841,80 @@ TEST_F(KeycacheTest, RefreshExpiredTest) { EXPECT_EQ(jwks_str, "{\"keys\": []}"); } +TEST_F(KeycacheTest, OfflineCacheFileTest) { + char *err_msg = nullptr; + + // Create a temporary file for offline cache + char temp_file[] = "/tmp/test_offline_cache_XXXXXX"; + int fd = mkstemp(temp_file); + ASSERT_TRUE(fd != -1); + close(fd); + unlink(temp_file); // Remove the file so SQLite can create it + + // Set the environment variable + setenv("SCITOKENS_KEYCACHE_FILE", temp_file, 1); + + // Store JWKS with explicit expiry time (1 hour from now) + time_t now = time(nullptr); + int64_t expires_at = static_cast(now) + 3600; + + auto rv = keycache_set_jwks_with_expiry("https://offline.test.com", + demo_scitokens2.c_str(), + expires_at, &err_msg); + ASSERT_TRUE(rv == 0) << err_msg; + + // Retrieve JWKS from offline cache + char *jwks; + rv = keycache_get_cached_jwks("https://offline.test.com", &jwks, &err_msg); + ASSERT_TRUE(rv == 0) << err_msg; + ASSERT_TRUE(jwks != nullptr); + std::string jwks_str(jwks); + free(jwks); + + EXPECT_EQ(demo_scitokens2, jwks_str); + + // Clean up + unlink(temp_file); + unsetenv("SCITOKENS_KEYCACHE_FILE"); +} + +TEST_F(KeycacheTest, OfflineCacheExpiryTest) { + char *err_msg = nullptr; + + // Create a temporary file for offline cache + char temp_file[] = "/tmp/test_offline_expiry_XXXXXX"; + int fd = mkstemp(temp_file); + ASSERT_TRUE(fd != -1); + close(fd); + unlink(temp_file); // Remove the file so SQLite can create it + + // Set the environment variable + setenv("SCITOKENS_KEYCACHE_FILE", temp_file, 1); + + // Store JWKS with expiry time in the past (already expired) + time_t now = time(nullptr); + int64_t expires_at = static_cast(now) - 3600; // 1 hour ago + + auto rv = keycache_set_jwks_with_expiry("https://expired.test.com", + demo_scitokens2.c_str(), + expires_at, &err_msg); + ASSERT_TRUE(rv == 0) << err_msg; + + // Retrieve JWKS from offline cache - should get empty keys due to expiry + char *jwks; + rv = keycache_get_cached_jwks("https://expired.test.com", &jwks, &err_msg); + ASSERT_TRUE(rv == 0) << err_msg; + ASSERT_TRUE(jwks != nullptr); + std::string jwks_str(jwks); + free(jwks); + + EXPECT_EQ(jwks_str, "{\"keys\": []}"); + + // Clean up + unlink(temp_file); + unsetenv("SCITOKENS_KEYCACHE_FILE"); +} + class IssuerSecurityTest : public ::testing::Test { protected: void SetUp() override { From 8bdaa4a48d26b8a8a62a89aeb11f6437c0ba104d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:27:20 +0000 Subject: [PATCH 4/6] Modify scitokens-keycache tool to support subcommands (add, print, location) Co-authored-by: djw8605 <79268+djw8605@users.noreply.github.com> --- src/scitokens-keycache.cpp | 359 +++++++++++++++++++++++++++++++++++-- src/scitokens.cpp | 8 + src/scitokens.h | 5 + src/scitokens_cache.cpp | 113 ++++++------ 4 files changed, 414 insertions(+), 71 deletions(-) diff --git a/src/scitokens-keycache.cpp b/src/scitokens-keycache.cpp index c0b07a8..eaf865e 100644 --- a/src/scitokens-keycache.cpp +++ b/src/scitokens-keycache.cpp @@ -5,11 +5,32 @@ #include #include #include +#include +#include + +#ifndef PICOJSON_USE_INT64 +#define PICOJSON_USE_INT64 +#endif +#include #include "scitokens.h" +// Forward declarations +int test_cache_file_access(const std::string &cache_file); + void print_usage(const char *progname) { - std::cout << "Usage: " << progname << " --cache-file --jwks --issuer --valid-for \n"; + std::cout << "Usage: " << progname << " [options]\n"; + std::cout << "\n"; + std::cout << "Commands:\n"; + std::cout << " add Add JWKS to a keycache file\n"; + std::cout << " print Print table of all public keys stored in cache\n"; + std::cout << " location Print location of scitokens keycache and access status\n"; + std::cout << "\n"; + std::cout << "Run '" << progname << " --help' for command-specific help\n"; +} + +void print_add_usage(const char *progname) { + std::cout << "Usage: " << progname << " add --cache-file --jwks --issuer --valid-for \n"; std::cout << "\n"; std::cout << "Options:\n"; std::cout << " --cache-file Path to the keycache SQLite database file\n"; @@ -19,7 +40,29 @@ void print_usage(const char *progname) { std::cout << " --help Show this help message\n"; std::cout << "\n"; std::cout << "Example:\n"; - std::cout << " " << progname << " --cache-file /tmp/offline.db --jwks keys.json --issuer https://example.com --valid-for 86400\n"; + std::cout << " " << progname << " add --cache-file /tmp/offline.db --jwks keys.json --issuer https://example.com --valid-for 86400\n"; +} + +void print_print_usage(const char *progname) { + std::cout << "Usage: " << progname << " print [--cache-file ]\n"; + std::cout << "\n"; + std::cout << "Options:\n"; + std::cout << " --cache-file Path to the keycache SQLite database file (optional)\n"; + std::cout << " If not specified, uses the default cache location\n"; + std::cout << " --help Show this help message\n"; + std::cout << "\n"; + std::cout << "Example:\n"; + std::cout << " " << progname << " print\n"; + std::cout << " " << progname << " print --cache-file /tmp/offline.db\n"; +} + +void print_location_usage(const char *progname) { + std::cout << "Usage: " << progname << " location\n"; + std::cout << "\n"; + std::cout << "Prints the location of the scitokens keycache file and whether it can be read.\n"; + std::cout << "\n"; + std::cout << "Options:\n"; + std::cout << " --help Show this help message\n"; } std::string read_file(const std::string &filename) { @@ -33,16 +76,23 @@ std::string read_file(const std::string &filename) { return content; } -int main(int argc, char *argv[]) { +std::string truncate_string(const std::string &str, size_t max_length) { + if (str.length() <= max_length) { + return str; + } + return str.substr(0, max_length - 3) + "..."; +} + +int add_command(int argc, char *argv[]) { std::string cache_file; std::string jwks_file; std::string issuer; long valid_for = 0; - // Parse command line arguments - for (int i = 1; i < argc; i++) { + // Parse command line arguments for add command + for (int i = 2; i < argc; i++) { // Start from 2 since argv[1] is "add" if (strcmp(argv[i], "--help") == 0) { - print_usage(argv[0]); + print_add_usage(argv[0]); return 0; } else if (strcmp(argv[i], "--cache-file") == 0) { if (i + 1 >= argc) { @@ -75,7 +125,7 @@ int main(int argc, char *argv[]) { } } else { std::cerr << "Error: Unknown option " << argv[i] << "\n"; - print_usage(argv[0]); + print_add_usage(argv[0]); return 1; } } @@ -83,22 +133,22 @@ int main(int argc, char *argv[]) { // Validate required arguments if (cache_file.empty()) { std::cerr << "Error: --cache-file is required\n"; - print_usage(argv[0]); + print_add_usage(argv[0]); return 1; } if (jwks_file.empty()) { std::cerr << "Error: --jwks is required\n"; - print_usage(argv[0]); + print_add_usage(argv[0]); return 1; } if (issuer.empty()) { std::cerr << "Error: --issuer is required\n"; - print_usage(argv[0]); + print_add_usage(argv[0]); return 1; } if (valid_for == 0) { std::cerr << "Error: --valid-for is required\n"; - print_usage(argv[0]); + print_add_usage(argv[0]); return 1; } @@ -142,4 +192,291 @@ int main(int argc, char *argv[]) { } return 0; +} + +int print_command(int argc, char *argv[]) { + std::string cache_file; + + // Parse command line arguments for print command + for (int i = 2; i < argc; i++) { // Start from 2 since argv[1] is "print" + if (strcmp(argv[i], "--help") == 0) { + print_print_usage(argv[0]); + return 0; + } else if (strcmp(argv[i], "--cache-file") == 0) { + if (i + 1 >= argc) { + std::cerr << "Error: --cache-file requires an argument\n"; + return 1; + } + cache_file = argv[++i]; + } else { + std::cerr << "Error: Unknown option " << argv[i] << "\n"; + print_print_usage(argv[0]); + return 1; + } + } + + // Get cache file location + try { + if (cache_file.empty()) { + const char* cache_location = scitokens_get_cache_file_location(); + if (!cache_location || strlen(cache_location) == 0) { + std::cerr << "Error: Could not determine cache file location\n"; + return 1; + } + cache_file = cache_location; + } else { + // Test if we can access the specified cache file + if (test_cache_file_access(cache_file) != 0) { + std::cerr << "Error: Cannot access cache file: " << cache_file << "\n"; + return 1; + } + } + + sqlite3 *db; + int rc = sqlite3_open(cache_file.c_str(), &db); + if (rc != SQLITE_OK) { + std::cerr << "Error: Cannot open cache database: " << sqlite3_errmsg(db) << "\n"; + sqlite3_close(db); + return 1; + } + + sqlite3_stmt *stmt; + rc = sqlite3_prepare_v2(db, "SELECT issuer, keys FROM keycache ORDER BY issuer", -1, &stmt, NULL); + if (rc != SQLITE_OK) { + std::cerr << "Error: Failed to prepare SQL statement: " << sqlite3_errmsg(db) << "\n"; + sqlite3_close(db); + return 1; + } + + // Print table header + std::cout << std::left; + std::cout << std::setw(40) << "Issuer" + << std::setw(15) << "Key ID" + << std::setw(15) << "Key Type" + << std::setw(20) << "Expires" + << std::setw(20) << "Next Update" + << std::setw(25) << "Public Key (truncated)" << "\n"; + std::cout << std::string(135, '-') << "\n"; + + bool has_entries = false; + while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { + has_entries = true; + const unsigned char *issuer_data = sqlite3_column_text(stmt, 0); + const unsigned char *keys_data = sqlite3_column_text(stmt, 1); + + if (!issuer_data || !keys_data) { + continue; + } + + std::string issuer_str(reinterpret_cast(issuer_data)); + std::string keys_str(reinterpret_cast(keys_data)); + + // Parse the JSON + picojson::value json_obj; + std::string err = picojson::parse(json_obj, keys_str); + if (!err.empty() || !json_obj.is()) { + std::cout << std::setw(40) << truncate_string(issuer_str, 37) + << std::setw(15) << "N/A" + << std::setw(15) << "N/A" + << std::setw(20) << "Invalid JSON" + << std::setw(20) << "N/A" + << std::setw(25) << "N/A" << "\n"; + continue; + } + + auto top_obj = json_obj.get(); + + // Get expiry and next_update + std::string expires_str = "N/A"; + std::string next_update_str = "N/A"; + + auto expires_iter = top_obj.find("expires"); + if (expires_iter != top_obj.end() && expires_iter->second.is()) { + time_t expires_time = static_cast(expires_iter->second.get()); + char time_buf[20]; + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M", gmtime(&expires_time)); + expires_str = time_buf; + } + + auto next_update_iter = top_obj.find("next_update"); + if (next_update_iter != top_obj.end() && next_update_iter->second.is()) { + time_t next_update_time = static_cast(next_update_iter->second.get()); + char time_buf[20]; + strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M", gmtime(&next_update_time)); + next_update_str = time_buf; + } + + // Get JWKS keys + auto jwks_iter = top_obj.find("jwks"); + if (jwks_iter != top_obj.end() && jwks_iter->second.is()) { + auto jwks_obj = jwks_iter->second.get(); + auto keys_array_iter = jwks_obj.find("keys"); + + if (keys_array_iter != jwks_obj.end() && keys_array_iter->second.is()) { + auto keys_array = keys_array_iter->second.get(); + + if (keys_array.empty()) { + std::cout << std::setw(40) << truncate_string(issuer_str, 37) + << std::setw(15) << "N/A" + << std::setw(15) << "No keys" + << std::setw(20) << expires_str + << std::setw(20) << next_update_str + << std::setw(25) << "N/A" << "\n"; + } else { + bool first_key = true; + for (const auto &key_val : keys_array) { + if (!key_val.is()) continue; + + auto key_obj = key_val.get(); + + std::string kid = "N/A"; + std::string kty = "N/A"; + std::string public_key_snippet = "N/A"; + + auto kid_iter = key_obj.find("kid"); + if (kid_iter != key_obj.end() && kid_iter->second.is()) { + kid = kid_iter->second.get(); + } + + auto kty_iter = key_obj.find("kty"); + if (kty_iter != key_obj.end() && kty_iter->second.is()) { + kty = kty_iter->second.get(); + } + + // Try to get some public key material for display + auto n_iter = key_obj.find("n"); + auto x_iter = key_obj.find("x"); + if (n_iter != key_obj.end() && n_iter->second.is()) { + public_key_snippet = n_iter->second.get(); + } else if (x_iter != key_obj.end() && x_iter->second.is()) { + public_key_snippet = x_iter->second.get(); + } + + std::cout << std::setw(40) << (first_key ? truncate_string(issuer_str, 37) : "") + << std::setw(15) << truncate_string(kid, 12) + << std::setw(15) << kty + << std::setw(20) << (first_key ? expires_str : "") + << std::setw(20) << (first_key ? next_update_str : "") + << std::setw(25) << truncate_string(public_key_snippet, 22) << "\n"; + first_key = false; + } + } + } else { + std::cout << std::setw(40) << truncate_string(issuer_str, 37) + << std::setw(15) << "N/A" + << std::setw(15) << "No keys array" + << std::setw(20) << expires_str + << std::setw(20) << next_update_str + << std::setw(25) << "N/A" << "\n"; + } + } else { + std::cout << std::setw(40) << truncate_string(issuer_str, 37) + << std::setw(15) << "N/A" + << std::setw(15) << "No JWKS" + << std::setw(20) << expires_str + << std::setw(20) << next_update_str + << std::setw(25) << "N/A" << "\n"; + } + } + + if (!has_entries) { + std::cout << "No entries found in cache.\n"; + } + + sqlite3_finalize(stmt); + sqlite3_close(db); + + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } + + return 0; +} + +int location_command(int argc, char *argv[]) { + // Parse command line arguments for location command + for (int i = 2; i < argc; i++) { // Start from 2 since argv[1] is "location" + if (strcmp(argv[i], "--help") == 0) { + print_location_usage(argv[0]); + return 0; + } else { + std::cerr << "Error: Unknown option " << argv[i] << "\n"; + print_location_usage(argv[0]); + return 1; + } + } + + try { + const char* cache_location = scitokens_get_cache_file_location(); + + if (!cache_location || strlen(cache_location) == 0) { + std::cout << "Cache file location: Unable to determine\n"; + std::cout << "Access status: Failed - could not determine location\n"; + return 1; + } + + std::string cache_file = cache_location; + std::cout << "Cache file location: " << cache_file << "\n"; + + int access_result = test_cache_file_access(cache_file); + if (access_result == 0) { + std::cout << "Access status: Success - cache file can be read\n"; + } else { + std::cout << "Access status: Failed - cache file cannot be read\n"; + } + + return access_result; + + } catch (const std::exception &e) { + std::cerr << "Error: " << e.what() << "\n"; + return 1; + } +} + +int test_cache_file_access(const std::string &cache_file) { + sqlite3 *db; + int rc = sqlite3_open(cache_file.c_str(), &db); + if (rc != SQLITE_OK) { + sqlite3_close(db); + return 1; + } + + // Try to read from the keycache table + sqlite3_stmt *stmt; + rc = sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM keycache", -1, &stmt, NULL); + if (rc != SQLITE_OK) { + sqlite3_close(db); + return 1; + } + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + return (rc == SQLITE_ROW) ? 0 : 1; +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + print_usage(argv[0]); + return 1; + } + + std::string command = argv[1]; + + if (command == "add") { + return add_command(argc, argv); + } else if (command == "print") { + return print_command(argc, argv); + } else if (command == "location") { + return location_command(argc, argv); + } else if (command == "--help") { + print_usage(argv[0]); + return 0; + } else { + std::cerr << "Error: Unknown command '" << command << "'\n"; + print_usage(argv[0]); + return 1; + } } \ No newline at end of file diff --git a/src/scitokens.cpp b/src/scitokens.cpp index 31d6221..946e8f3 100644 --- a/src/scitokens.cpp +++ b/src/scitokens.cpp @@ -1144,3 +1144,11 @@ int scitoken_config_get_str(const char *key, char **output, char **err_msg) { } return 0; } + +// Forward declaration of get_cache_file from scitokens_cache.cpp +extern std::string get_cache_file(); + +const char* scitokens_get_cache_file_location() { + static std::string cache_location = get_cache_file(); + return cache_location.c_str(); +} diff --git a/src/scitokens.h b/src/scitokens.h index cd35ed1..cee256e 100644 --- a/src/scitokens.h +++ b/src/scitokens.h @@ -339,6 +339,11 @@ int scitoken_config_set_str(const char *key, const char *value, char **err_msg); */ int scitoken_config_get_str(const char *key, char **output, char **err_msg); +/** + * Get the cache file location following scitokens cache rules + */ +const char* scitokens_get_cache_file_location(); + #ifdef __cplusplus } #endif diff --git a/src/scitokens_cache.cpp b/src/scitokens_cache.cpp index d54799f..d8bdf46 100644 --- a/src/scitokens_cache.cpp +++ b/src/scitokens_cache.cpp @@ -40,16 +40,60 @@ void initialize_cachedb(const std::string &keycache_file) { sqlite3_close(db); } -/** - * Get the Cache file location - * 1. SCITOKENS_KEYCACHE_FILE environment variable (for offline use) - * 2. User-defined through config api - * 3. $XDG_CACHE_HOME - * 4. .cache subdirectory of home directory as returned by the password - * database - */ -std::string get_cache_file() { +// Remove issuer_entry function and other namespace functions remain here +// Remove a given issuer from the database. Starts a new transaction +// if `new_transaction` is true. +// If a failure occurs, then this function returns nonzero and closes +// the database handle. +int remove_issuer_entry(sqlite3 *db, const std::string &issuer, + bool new_transaction) { + + int rc; + if (new_transaction) { + if ((rc = sqlite3_exec(db, "BEGIN", 0, 0, 0)) != SQLITE_OK) { + sqlite3_close(db); + return -1; + } + } + + sqlite3_stmt *stmt; + rc = sqlite3_prepare_v2(db, "DELETE FROM keycache WHERE issuer = ?", -1, + &stmt, NULL); + if (rc != SQLITE_OK) { + sqlite3_close(db); + return -1; + } + + if (sqlite3_bind_text(stmt, 1, issuer.c_str(), issuer.size(), + SQLITE_STATIC) != SQLITE_OK) { + sqlite3_finalize(stmt); + sqlite3_close(db); + return -1; + } + + rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + sqlite3_finalize(stmt); + sqlite3_close(db); + return -1; + } + + sqlite3_finalize(stmt); + if (new_transaction) { + if ((rc = sqlite3_exec(db, "COMMIT", 0, 0, 0)) != SQLITE_OK) { + sqlite3_close(db); + return -1; + } + } + + return 0; +} + +} // namespace + +// Expose get_cache_file function for use by scitokens-keycache tool +std::string get_cache_file() { // Check for direct cache file location first (offline support) const char *direct_cache_file = getenv("SCITOKENS_KEYCACHE_FILE"); if (direct_cache_file && strlen(direct_cache_file) > 0) { @@ -104,57 +148,6 @@ std::string get_cache_file() { return keycache_file; } -// Remove a given issuer from the database. Starts a new transaction -// if `new_transaction` is true. -// If a failure occurs, then this function returns nonzero and closes -// the database handle. -int remove_issuer_entry(sqlite3 *db, const std::string &issuer, - bool new_transaction) { - - int rc; - if (new_transaction) { - if ((rc = sqlite3_exec(db, "BEGIN", 0, 0, 0)) != SQLITE_OK) { - sqlite3_close(db); - return -1; - } - } - - sqlite3_stmt *stmt; - rc = sqlite3_prepare_v2(db, "DELETE FROM keycache WHERE issuer = ?", -1, - &stmt, NULL); - if (rc != SQLITE_OK) { - sqlite3_close(db); - return -1; - } - - if (sqlite3_bind_text(stmt, 1, issuer.c_str(), issuer.size(), - SQLITE_STATIC) != SQLITE_OK) { - sqlite3_finalize(stmt); - sqlite3_close(db); - return -1; - } - - rc = sqlite3_step(stmt); - if (rc != SQLITE_DONE) { - sqlite3_finalize(stmt); - sqlite3_close(db); - return -1; - } - - sqlite3_finalize(stmt); - - if (new_transaction) { - if ((rc = sqlite3_exec(db, "COMMIT", 0, 0, 0)) != SQLITE_OK) { - sqlite3_close(db); - return -1; - } - } - - return 0; -} - -} // namespace - bool scitokens::Validator::get_public_keys_from_db(const std::string issuer, int64_t now, picojson::value &keys, From d9f2c49920067970344c2e0d7db5a342b4e3d3d3 Mon Sep 17 00:00:00 2001 From: Derek Weitzel Date: Thu, 14 Aug 2025 17:35:48 +0000 Subject: [PATCH 5/6] Refactor cache file location retrieval to use namespaced function and enhance documentation --- src/scitokens.cpp | 5 +---- src/scitokens_cache.cpp | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/scitokens.cpp b/src/scitokens.cpp index 946e8f3..c9eb636 100644 --- a/src/scitokens.cpp +++ b/src/scitokens.cpp @@ -1145,10 +1145,7 @@ int scitoken_config_get_str(const char *key, char **output, char **err_msg) { return 0; } -// Forward declaration of get_cache_file from scitokens_cache.cpp -extern std::string get_cache_file(); - const char* scitokens_get_cache_file_location() { - static std::string cache_location = get_cache_file(); + static std::string cache_location = scitokens::get_cache_file(); return cache_location.c_str(); } diff --git a/src/scitokens_cache.cpp b/src/scitokens_cache.cpp index d8bdf46..470b5b5 100644 --- a/src/scitokens_cache.cpp +++ b/src/scitokens_cache.cpp @@ -92,8 +92,23 @@ int remove_issuer_entry(sqlite3 *db, const std::string &issuer, } // namespace -// Expose get_cache_file function for use by scitokens-keycache tool -std::string get_cache_file() { +/** + * @brief Determines the location of the SciTokens key cache file. + * + * This function checks environment variables and configuration settings to find + * the appropriate directory for the key cache file. It prioritizes the following: + * 1. SCITOKENS_KEYCACHE_FILE environment variable (direct file path). + * 2. Configured cache directory via Configuration::get_cache_home(). + * 3. XDG_CACHE_HOME environment variable. + * 4. Default to $HOME/.cache if none of the above are set. + * + * The function ensures the cache directory exists, creates it if necessary, + * initializes the SQLite database if needed, and returns the full path to the + * cache file. Returns an empty string on failure. + * + * @return std::string Full path to the key cache file, or empty string on error. + */ +std::string scitokens::get_cache_file() { // Check for direct cache file location first (offline support) const char *direct_cache_file = getenv("SCITOKENS_KEYCACHE_FILE"); if (direct_cache_file && strlen(direct_cache_file) > 0) { From 6639c01e083e7affbfbc4166996264e363bffafd Mon Sep 17 00:00:00 2001 From: Derek Weitzel Date: Thu, 14 Aug 2025 17:36:35 +0000 Subject: [PATCH 6/6] Add detailed documentation for get_cache_file function in scitokens_internal.h --- src/scitokens_internal.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/scitokens_internal.h b/src/scitokens_internal.h index 5ea994f..4b560b9 100644 --- a/src/scitokens_internal.h +++ b/src/scitokens_internal.h @@ -112,6 +112,24 @@ class SimpleCurlGet { } // namespace internal +/** + * @brief Determines the location of the SciTokens key cache file. + * + * This function checks environment variables and configuration settings to find + * the appropriate directory for the key cache file. It prioritizes the following: + * 1. SCITOKENS_KEYCACHE_FILE environment variable (direct file path). + * 2. Configured cache directory via Configuration::get_cache_home(). + * 3. XDG_CACHE_HOME environment variable. + * 4. Default to $HOME/.cache if none of the above are set. + * + * The function ensures the cache directory exists, creates it if necessary, + * initializes the SQLite database if needed, and returns the full path to the + * cache file. Returns an empty string on failure. + * + * @return std::string Full path to the key cache file, or empty string on error. + */ +std::string get_cache_file(); + class UnsupportedKeyException : public std::runtime_error { public: explicit UnsupportedKeyException(const std::string &msg)