Skip to content

Commit b4ac826

Browse files
authored
Merge pull request #99 from bbockelm/keycache_manip
Provide an API enabling explicit manipulation of the keycache for the end user.
2 parents 4916492 + d9f3c17 commit b4ac826

File tree

6 files changed

+256
-5
lines changed

6 files changed

+256
-5
lines changed

configs/export-symbols

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ global:
33
scitoken*;
44
validator*;
55
enforcer*;
6+
keycache*;
67

78
local:
89
*;

src/scitokens.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,3 +455,65 @@ int enforcer_test(const Enforcer enf, const SciToken scitoken, const Acl *acl, c
455455
}
456456
return 0;
457457
}
458+
459+
460+
int keycache_refresh_jwks(const char *issuer, char **err_msg)
461+
{
462+
if (!issuer) {
463+
if (err_msg) {*err_msg = strdup("Issuer may not be a null pointer");}
464+
return -1;
465+
}
466+
try {
467+
if (!scitokens::Validator::refresh_jwks(issuer)) {
468+
if (err_msg) {*err_msg = strdup("Failed to refresh JWKS cache for issuer.");}
469+
return -1;
470+
}
471+
} catch (std::exception &exc) {
472+
if (err_msg) {*err_msg = strdup(exc.what());}
473+
return -1;
474+
}
475+
return 0;
476+
}
477+
478+
479+
int keycache_get_cached_jwks(const char *issuer, char **jwks, char **err_msg)
480+
{
481+
if (!issuer) {
482+
if (err_msg) {*err_msg = strdup("Issuer may not be a null pointer");}
483+
return -1;
484+
}
485+
if (!jwks) {
486+
if (err_msg) {*err_msg = strdup("JWKS output pointer may not be null.");}
487+
return -1;
488+
}
489+
try {
490+
*jwks = strdup(scitokens::Validator::get_jwks(issuer).c_str());
491+
} catch(std::exception &exc) {
492+
if (err_msg) {*err_msg = strdup(exc.what());}
493+
return -1;
494+
}
495+
return 0;
496+
}
497+
498+
499+
int keycache_set_jwks(const char *issuer, const char *jwks, char **err_msg)
500+
{
501+
if (!issuer) {
502+
if (err_msg) {*err_msg = strdup("Issuer may not be a null pointer");}
503+
return -1;
504+
}
505+
if (!jwks) {
506+
if (err_msg) {*err_msg = strdup("JWKS pointer may not be null.");}
507+
return -1;
508+
}
509+
try {
510+
if (!scitokens::Validator::store_jwks(issuer, jwks)) {
511+
if (err_msg) {*err_msg = strdup("Failed to set the JWKS cache for issuer.");}
512+
return -1;
513+
}
514+
} catch(std::exception &exc) {
515+
if (err_msg) {*err_msg = strdup(exc.what());}
516+
return -1;
517+
}
518+
return 0;
519+
}

src/scitokens.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ void enforcer_acl_free(Acl *acls);
127127

128128
int enforcer_test(const Enforcer enf, const SciToken sci, const Acl *acl, char **err_msg);
129129

130+
131+
/**
132+
* API for explicity managing the key cache.
133+
*
134+
* This manipulates the keycache for the current eUID.
135+
*/
136+
137+
138+
/**
139+
* Refresh the JWKS in the keycache for a given issuer; the refresh will occur
140+
* even if the JWKS is not otherwise due for updates.
141+
* - Returns 0 on success, nonzero on failure.
142+
*/
143+
int keycache_refresh_jwks(const char *issuer, char **err_msg);
144+
145+
/**
146+
* Retrieve the JWKS from the keycache for a given issuer.
147+
* - Returns 0 if successful, nonzero on failure.
148+
* - If the existing JWKS has expired - or does not exist - this does not trigger a new
149+
* download of the JWKS from the issuer. Instead, it will return a JWKS object with
150+
* an empty set of keys.
151+
* - `jwks` is an output variable set to the contents of the JWKS in the key cache.
152+
*/
153+
int keycache_get_cached_jwks(const char *issuer, char **jwks, char **err_msg);
154+
155+
/**
156+
* Replace any existing key cache entry with one provided by the user.
157+
* The expiration and next update time of the user-provided JWKS will utilize
158+
* the same rules as a download from an issuer with no explicit cache lifetime directives.
159+
* - `jwks` is value that will be set in the cache.
160+
*/
161+
int keycache_set_jwks(const char *issuer, const char *jwks, char **err_msg);
162+
130163
#ifdef __cplusplus
131164
}
132165
#endif

src/scitokens_internal.cpp

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,12 @@ normalize_absolute_path(const std::string &path) {
439439
}
440440

441441

442+
void get_default_expiry_time(int &next_update_delta, int &expiry_delta)
443+
{
444+
next_update_delta = 600;
445+
expiry_delta = 4*24*3600;
446+
}
447+
442448
}
443449

444450

@@ -511,8 +517,49 @@ Validator::get_public_keys_from_web(const std::string &issuer, picojson::value &
511517

512518
keys = json_obj;
513519

514-
next_update = now + 600;
515-
expires = now + 4*24*3600;
520+
int next_update_delta, expiry_delta;
521+
get_default_expiry_time(next_update_delta, expiry_delta);
522+
next_update = now + next_update_delta;
523+
expires = now + expiry_delta;
524+
}
525+
526+
527+
std::string
528+
Validator::get_jwks(const std::string &issuer)
529+
{
530+
auto now = std::time(NULL);
531+
picojson::value jwks;
532+
int64_t next_update;
533+
if (get_public_keys_from_db(issuer, now, jwks, next_update)) {
534+
return jwks.serialize();
535+
}
536+
return std::string("{\"keys\": []}");
537+
}
538+
539+
540+
bool
541+
Validator::refresh_jwks(const std::string &issuer)
542+
{
543+
int64_t next_update, expires;
544+
picojson::value keys;
545+
get_public_keys_from_web(issuer, keys, next_update, expires);
546+
return store_public_keys(issuer, keys, next_update, expires);
547+
}
548+
549+
550+
bool
551+
Validator::store_jwks(const std::string &issuer, const std::string &jwks_str)
552+
{
553+
picojson::value jwks;
554+
std::string err = picojson::parse(jwks, jwks_str);
555+
auto now = std::time(NULL);
556+
int next_update_delta, expiry_delta;
557+
get_default_expiry_time(next_update_delta, expiry_delta);
558+
int64_t next_update = now + next_update_delta, expires = now + expiry_delta;
559+
if (!err.empty()) {
560+
throw JsonException(err);
561+
}
562+
return store_public_keys(issuer, jwks, next_update, expires);
516563
}
517564

518565
void
@@ -691,7 +738,9 @@ scitokens::Validator::store_public_ec_key(const std::string &issuer, const std::
691738
picojson::value top_value(top_obj);
692739

693740
auto now = std::time(NULL);
694-
return store_public_keys(issuer, top_value, now + 600, now + 4*3600);
741+
int next_update_delta, expiry_delta;
742+
get_default_expiry_time(next_update_delta, expiry_delta);
743+
return store_public_keys(issuer, top_value, now + next_update_delta, now + expiry_delta);
695744
}
696745

697746

src/scitokens_internal.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,26 @@ class Validator {
516516
*/
517517
static bool store_public_ec_key(const std::string &issuer, const std::string &kid, const std::string &key);
518518

519+
/**
520+
* Store the contents of a JWKS for a given issuer.
521+
*/
522+
static bool store_jwks(const std::string &issuer, const std::string &jwks);
523+
524+
/**
525+
* Trigger a refresh of the JWKS or a given issuer.
526+
*/
527+
static bool refresh_jwks(const std::string &issuer);
528+
529+
/**
530+
* Fetch the contents of fa JWKS for a given issuer (do not trigger a refresh).
531+
* Will return an empty JWKS if no valid JWKS is available.
532+
*/
533+
static std::string get_jwks(const std::string &issuer);
534+
519535
private:
520536
void get_public_key_pem(const std::string &issuer, const std::string &kid, std::string &public_pem, std::string &algorithm);
521-
void get_public_keys_from_web(const std::string &issuer, picojson::value &keys, int64_t &next_update, int64_t &expires);
522-
bool get_public_keys_from_db(const std::string issuer, int64_t now, picojson::value &keys, int64_t &next_update);
537+
static void get_public_keys_from_web(const std::string &issuer, picojson::value &keys, int64_t &next_update, int64_t &expires);
538+
static bool get_public_keys_from_db(const std::string issuer, int64_t now, picojson::value &keys, int64_t &next_update);
523539
static bool store_public_keys(const std::string &issuer, const picojson::value &keys, int64_t next_update, int64_t expires);
524540

525541
bool m_validate_all_claims{true};

test/main.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "../src/scitokens.h"
22

3+
#include <memory>
34
#include <gtest/gtest.h>
45

56
namespace {
@@ -58,6 +59,95 @@ TEST(SciTokenTest, SignToken) {
5859
}
5960

6061

62+
class KeycacheTest : public ::testing::Test
63+
{
64+
protected:
65+
std::string demo_scitokens_url = "https://demo.scitokens.org";
66+
67+
void SetUp() override {
68+
char *err_msg;
69+
auto rv = keycache_set_jwks(demo_scitokens_url.c_str(), demo_scitokens.c_str(), &err_msg);
70+
ASSERT_TRUE(rv == 0);
71+
}
72+
73+
// Reference copy of the keys at https://demo.scitokens.org/oauth2/certs; may need
74+
// to be updated periodically.
75+
std::string demo_scitokens = "{\"keys\":[{\"alg\":\"RS256\",\"e\":\"AQAB\",\"kid\":\"key-rs256\",\"kty\":\"RSA\",\"n\":\"uGDGTLXnqh3mfopjys6sFUBvFl3F4Qt6NEYphq_u_aBhtN1X9NEyb78uB_I1KjciJNGLIQU0ECsJiFx6qV1hR9xE1dPyrS3bU92AVtnBrvzUtTU-aUZAmZQiuAC_rC0-z_TOQr6qJkkUgZtxR9n9op55ZBpRfZD5dzhkW4Dm146vfTKt0D4cIMoMNJS5xQx9nibeB4E8hryZDW_fPeD0XZDcpByNyP0jFDYkxdUtQFvyRpz4WMZ4ejUfvW3gf4LRAfGZJtMnsZ7ZW4RfoQbhiXKMfWeBEjQDiXh0r-KuZLykxhYJtpf7fTnPna753IzMgRMmW3F69iQn2LQN3LoSMw==\",\"use\":\"sig\"},{\"alg\":\"ES256\",\"kid\":\"key-es256\",\"kty\":\"EC\",\"use\":\"sig\",\"x\":\"ncSCrGTBTXXOhNiAOTwNdPjwRz1hVY4saDNiHQK9Bh4=\",\"y\":\"sCsFXvx7FAAklwq3CzRCBcghqZOFPB2dKUayS6LY_Lo=\"}]}";
76+
std::string demo_scitokens2 = "{\"keys\":[{\"alg\":\"ES256\",\"kid\":\"key-es256\",\"kty\":\"EC\",\"use\":\"sig\",\"x\":\"ncSCrGTBTXXOhNiAOTwNdPjwRz1hVY4saDNiHQK9Bh4=\",\"y\":\"sCsFXvx7FAAklwq3CzRCBcghqZOFPB2dKUayS6LY_Lo=\"}]}";
77+
};
78+
79+
80+
TEST_F(KeycacheTest, RefreshTest) {
81+
char *err_msg;
82+
auto rv = keycache_refresh_jwks(demo_scitokens_url.c_str(), &err_msg);
83+
ASSERT_TRUE(rv == 0);
84+
85+
char *output_jwks;
86+
rv = keycache_get_cached_jwks(demo_scitokens_url.c_str(), &output_jwks, &err_msg);
87+
ASSERT_TRUE(rv == 0);
88+
ASSERT_TRUE(output_jwks != nullptr);
89+
std::string output_jwks_str(output_jwks);
90+
free(output_jwks);
91+
92+
EXPECT_EQ(demo_scitokens, output_jwks_str);
93+
}
94+
95+
96+
TEST_F(KeycacheTest, RefreshInvalid)
97+
{
98+
char *err_msg, *jwks;
99+
auto rv = keycache_refresh_jwks("https://demo.scitokens.org/invalid", &err_msg);
100+
ASSERT_FALSE(rv == 0);
101+
102+
rv = keycache_get_cached_jwks("https://demo.scitokens.org/invalid", &jwks, &err_msg);
103+
ASSERT_TRUE(rv == 0);
104+
ASSERT_TRUE(jwks != nullptr);
105+
std::string jwks_str(jwks);
106+
free(jwks);
107+
108+
EXPECT_EQ(jwks_str, "{\"keys\": []}");
109+
}
110+
111+
112+
TEST_F(KeycacheTest, GetInvalid)
113+
{
114+
char *err_msg, *jwks;
115+
auto rv = keycache_get_cached_jwks("https://demo.scitokens.org/unknown", &jwks, &err_msg);
116+
ASSERT_TRUE(rv == 0);
117+
ASSERT_TRUE(jwks != nullptr);
118+
std::string jwks_str(jwks);
119+
free(jwks);
120+
}
121+
122+
123+
TEST_F(KeycacheTest, GetTest) {
124+
char *err_msg, *jwks;
125+
auto rv = keycache_get_cached_jwks(demo_scitokens_url.c_str(), &jwks, &err_msg);
126+
ASSERT_TRUE(rv == 0);
127+
ASSERT_TRUE(jwks != nullptr);
128+
std::string jwks_str(jwks);
129+
free(jwks);
130+
131+
EXPECT_EQ(demo_scitokens, jwks_str);
132+
}
133+
134+
135+
TEST_F(KeycacheTest, SetGetTest) {
136+
char *err_msg;
137+
auto rv = keycache_set_jwks(demo_scitokens_url.c_str(), demo_scitokens2.c_str(), &err_msg);
138+
ASSERT_TRUE(rv == 0);
139+
140+
char *jwks;
141+
rv = keycache_get_cached_jwks(demo_scitokens_url.c_str(), &jwks, &err_msg);
142+
ASSERT_TRUE(rv == 0);
143+
ASSERT_TRUE(jwks != nullptr);
144+
std::string jwks_str(jwks);
145+
free(jwks);
146+
147+
EXPECT_EQ(demo_scitokens2, jwks_str);
148+
}
149+
150+
61151
class SerializeTest : public ::testing::Test {
62152
protected:
63153
void SetUp() override {

0 commit comments

Comments
 (0)