Skip to content

Commit 6d369a9

Browse files
Copilotdjw8605
andcommitted
Add offline support with SCITOKENS_KEYCACHE_FILE and scitokens-keycache tool
Co-authored-by: djw8605 <79268+djw8605@users.noreply.github.com>
1 parent 3191720 commit 6d369a9

File tree

7 files changed

+220
-3
lines changed

7 files changed

+220
-3
lines changed

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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-keycache src/scitokens-keycache.cpp)
79+
target_link_libraries(scitokens-keycache SciTokens)
80+
7881
get_directory_property(TARGETS BUILDSYSTEM_TARGETS)
7982
install(
8083
TARGETS ${TARGETS}

src/scitokens-keycache.cpp

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#include <cstdlib>
2+
#include <cstring>
3+
#include <ctime>
4+
#include <iostream>
5+
#include <fstream>
6+
#include <string>
7+
#include <unistd.h>
8+
9+
#include "scitokens.h"
10+
11+
void print_usage(const char *progname) {
12+
std::cout << "Usage: " << progname << " --cache-file <cache_file> --jwks <jwks_file> --issuer <issuer> --valid-for <seconds>\n";
13+
std::cout << "\n";
14+
std::cout << "Options:\n";
15+
std::cout << " --cache-file <file> Path to the keycache SQLite database file\n";
16+
std::cout << " --jwks <file> Path to the JWKS file to store\n";
17+
std::cout << " --issuer <issuer> Issuer URL for the JWKS\n";
18+
std::cout << " --valid-for <seconds> How long the key should be valid (in seconds)\n";
19+
std::cout << " --help Show this help message\n";
20+
std::cout << "\n";
21+
std::cout << "Example:\n";
22+
std::cout << " " << progname << " --cache-file /tmp/offline.db --jwks keys.json --issuer https://example.com --valid-for 86400\n";
23+
}
24+
25+
std::string read_file(const std::string &filename) {
26+
std::ifstream file(filename);
27+
if (!file.is_open()) {
28+
throw std::runtime_error("Cannot open file: " + filename);
29+
}
30+
31+
std::string content((std::istreambuf_iterator<char>(file)),
32+
std::istreambuf_iterator<char>());
33+
return content;
34+
}
35+
36+
int main(int argc, char *argv[]) {
37+
std::string cache_file;
38+
std::string jwks_file;
39+
std::string issuer;
40+
long valid_for = 0;
41+
42+
// Parse command line arguments
43+
for (int i = 1; i < argc; i++) {
44+
if (strcmp(argv[i], "--help") == 0) {
45+
print_usage(argv[0]);
46+
return 0;
47+
} else if (strcmp(argv[i], "--cache-file") == 0) {
48+
if (i + 1 >= argc) {
49+
std::cerr << "Error: --cache-file requires an argument\n";
50+
return 1;
51+
}
52+
cache_file = argv[++i];
53+
} else if (strcmp(argv[i], "--jwks") == 0) {
54+
if (i + 1 >= argc) {
55+
std::cerr << "Error: --jwks requires an argument\n";
56+
return 1;
57+
}
58+
jwks_file = argv[++i];
59+
} else if (strcmp(argv[i], "--issuer") == 0) {
60+
if (i + 1 >= argc) {
61+
std::cerr << "Error: --issuer requires an argument\n";
62+
return 1;
63+
}
64+
issuer = argv[++i];
65+
} else if (strcmp(argv[i], "--valid-for") == 0) {
66+
if (i + 1 >= argc) {
67+
std::cerr << "Error: --valid-for requires an argument\n";
68+
return 1;
69+
}
70+
char *endptr;
71+
valid_for = strtol(argv[++i], &endptr, 10);
72+
if (*endptr != '\0' || valid_for <= 0) {
73+
std::cerr << "Error: --valid-for must be a positive integer\n";
74+
return 1;
75+
}
76+
} else {
77+
std::cerr << "Error: Unknown option " << argv[i] << "\n";
78+
print_usage(argv[0]);
79+
return 1;
80+
}
81+
}
82+
83+
// Validate required arguments
84+
if (cache_file.empty()) {
85+
std::cerr << "Error: --cache-file is required\n";
86+
print_usage(argv[0]);
87+
return 1;
88+
}
89+
if (jwks_file.empty()) {
90+
std::cerr << "Error: --jwks is required\n";
91+
print_usage(argv[0]);
92+
return 1;
93+
}
94+
if (issuer.empty()) {
95+
std::cerr << "Error: --issuer is required\n";
96+
print_usage(argv[0]);
97+
return 1;
98+
}
99+
if (valid_for == 0) {
100+
std::cerr << "Error: --valid-for is required\n";
101+
print_usage(argv[0]);
102+
return 1;
103+
}
104+
105+
try {
106+
// Set the cache file environment variable
107+
if (setenv("SCITOKENS_KEYCACHE_FILE", cache_file.c_str(), 1) != 0) {
108+
std::cerr << "Error: Failed to set SCITOKENS_KEYCACHE_FILE environment variable\n";
109+
return 1;
110+
}
111+
112+
// Read the JWKS file
113+
std::string jwks_content = read_file(jwks_file);
114+
115+
// Calculate expiration time
116+
time_t now = time(nullptr);
117+
int64_t expires_at = static_cast<int64_t>(now) + valid_for;
118+
119+
// Store the JWKS with expiration
120+
char *err_msg = nullptr;
121+
int result = keycache_set_jwks_with_expiry(issuer.c_str(), jwks_content.c_str(), expires_at, &err_msg);
122+
123+
if (result != 0) {
124+
std::cerr << "Error: Failed to store JWKS: " << (err_msg ? err_msg : "Unknown error") << "\n";
125+
if (err_msg) {
126+
free(err_msg);
127+
}
128+
return 1;
129+
}
130+
131+
std::cout << "Successfully stored JWKS for issuer: " << issuer << "\n";
132+
std::cout << "Cache file: " << cache_file << "\n";
133+
std::cout << "Expires at: " << ctime(&now) << " + " << valid_for << " seconds\n";
134+
135+
if (err_msg) {
136+
free(err_msg);
137+
}
138+
139+
} catch (const std::exception &e) {
140+
std::cerr << "Error: " << e.what() << "\n";
141+
return 1;
142+
}
143+
144+
return 0;
145+
}

src/scitokens.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,36 @@ int keycache_set_jwks(const char *issuer, const char *jwks, char **err_msg) {
989989
return 0;
990990
}
991991

992+
int keycache_set_jwks_with_expiry(const char *issuer, const char *jwks,
993+
int64_t expires_at, char **err_msg) {
994+
if (!issuer) {
995+
if (err_msg) {
996+
*err_msg = strdup("Issuer may not be a null pointer");
997+
}
998+
return -1;
999+
}
1000+
if (!jwks) {
1001+
if (err_msg) {
1002+
*err_msg = strdup("JWKS pointer may not be null.");
1003+
}
1004+
return -1;
1005+
}
1006+
try {
1007+
if (!scitokens::Validator::store_jwks_with_expiry(issuer, jwks, expires_at)) {
1008+
if (err_msg) {
1009+
*err_msg = strdup("Failed to set the JWKS cache for issuer.");
1010+
}
1011+
return -1;
1012+
}
1013+
} catch (std::exception &exc) {
1014+
if (err_msg) {
1015+
*err_msg = strdup(exc.what());
1016+
}
1017+
return -1;
1018+
}
1019+
return 0;
1020+
}
1021+
9921022
int config_set_int(const char *key, int value, char **err_msg) {
9931023
return scitoken_config_set_int(key, value, err_msg);
9941024
}

src/scitokens.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,15 @@ int keycache_get_cached_jwks(const char *issuer, char **jwks, char **err_msg);
290290
*/
291291
int keycache_set_jwks(const char *issuer, const char *jwks, char **err_msg);
292292

293+
/**
294+
* Replace any existing key cache entry with one provided by the user.
295+
* Allows explicit setting of expiration time for offline usage.
296+
* - `jwks` is value that will be set in the cache.
297+
* - `expires_at` is the expiration time as Unix timestamp (seconds since epoch).
298+
*/
299+
int keycache_set_jwks_with_expiry(const char *issuer, const char *jwks,
300+
int64_t expires_at, char **err_msg);
301+
293302
/**
294303
* APIs for managing scitokens configuration parameters.
295304
*/

src/scitokens_cache.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,22 @@ void initialize_cachedb(const std::string &keycache_file) {
4242

4343
/**
4444
* Get the Cache file location
45-
* 1. User-defined through config api
46-
* 2. $XDG_CACHE_HOME
47-
* 3. .cache subdirectory of home directory as returned by the password
45+
* 1. SCITOKENS_KEYCACHE_FILE environment variable (for offline use)
46+
* 2. User-defined through config api
47+
* 3. $XDG_CACHE_HOME
48+
* 4. .cache subdirectory of home directory as returned by the password
4849
* database
4950
*/
5051
std::string get_cache_file() {
5152

53+
// Check for direct cache file location first (offline support)
54+
const char *direct_cache_file = getenv("SCITOKENS_KEYCACHE_FILE");
55+
if (direct_cache_file && strlen(direct_cache_file) > 0) {
56+
std::string keycache_file(direct_cache_file);
57+
initialize_cachedb(keycache_file);
58+
return keycache_file;
59+
}
60+
5261
const char *xdg_cache_home = getenv("XDG_CACHE_HOME");
5362

5463
auto bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);

src/scitokens_internal.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,20 @@ bool Validator::store_jwks(const std::string &issuer,
797797
return store_public_keys(issuer, jwks, next_update, expires);
798798
}
799799

800+
bool Validator::store_jwks_with_expiry(const std::string &issuer,
801+
const std::string &jwks_str,
802+
int64_t expires_at) {
803+
picojson::value jwks;
804+
std::string err = picojson::parse(jwks, jwks_str);
805+
auto now = std::time(NULL);
806+
int next_update_delta = configurer::Configuration::get_next_update_delta();
807+
int64_t next_update = now + next_update_delta;
808+
if (!err.empty()) {
809+
throw JsonException(err);
810+
}
811+
return store_public_keys(issuer, jwks, next_update, expires_at);
812+
}
813+
800814
std::unique_ptr<AsyncStatus>
801815
Validator::get_public_key_pem(const std::string &issuer, const std::string &kid,
802816
std::string &public_pem, std::string &algorithm) {

src/scitokens_internal.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,13 @@ class Validator {
745745
*/
746746
static bool store_jwks(const std::string &issuer, const std::string &jwks);
747747

748+
/**
749+
* Store the contents of a JWKS for a given issuer with explicit expiry time.
750+
*/
751+
static bool store_jwks_with_expiry(const std::string &issuer,
752+
const std::string &jwks_str,
753+
int64_t expires_at);
754+
748755
/**
749756
* Trigger a refresh of the JWKS or a given issuer.
750757
*/

0 commit comments

Comments
 (0)