Skip to content

Commit 21c8d85

Browse files
committed
Add support for multiple token profiles.
Allows one to parse and validate the WLCG and SciTokens 2.0 profile.
1 parent 5f56481 commit 21c8d85

File tree

4 files changed

+213
-13
lines changed

4 files changed

+213
-13
lines changed

src/scitokens.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ int scitoken_set_claim_string(SciToken token, const char *key, const char *value
6767
}
6868

6969

70+
void scitoken_set_serialize_mode(SciToken token, SciTokenProfile profile) {
71+
scitokens::SciToken *real_token = reinterpret_cast<scitokens::SciToken*>(token);
72+
if (real_token == nullptr) {return;}
73+
74+
real_token->set_serialize_mode(static_cast<scitokens::SciToken::Profile>(profile));
75+
}
76+
77+
7078
int scitoken_get_claim_string(const SciToken token, const char *key, char **value, char **err_msg) {
7179
scitokens::SciToken *real_token = reinterpret_cast<scitokens::SciToken*>(token);
7280
std::string claim_str;
@@ -165,6 +173,13 @@ Validator validator_create() {
165173
return new Validator();
166174
}
167175

176+
177+
void validator_set_token_profile(Validator validator, SciTokenProfile profile) {
178+
if (validator == nullptr) {return;}
179+
auto real_validator = reinterpret_cast<scitokens::Validator*>(validator);
180+
real_validator->set_validate_profile(static_cast<scitokens::SciToken::Profile>(profile));
181+
}
182+
168183
int validator_add(Validator validator, const char *claim, StringValidatorFunction validator_func, char **err_msg) {
169184
if (validator == nullptr) {
170185
if (err_msg) {*err_msg = strdup("Validator may not be a null pointer");}
@@ -257,6 +272,14 @@ void enforcer_acl_free(Acl *acls) {
257272
}
258273

259274

275+
void enforcer_set_validate_profile(Enforcer enf, SciTokenProfile profile) {
276+
if (enf == nullptr) {return;}
277+
278+
auto real_enf = reinterpret_cast<scitokens::Enforcer*>(enf);
279+
real_enf->set_validate_profile(static_cast<scitokens::SciToken::Profile>(profile));
280+
}
281+
282+
260283
int enforcer_generate_acls(const Enforcer enf, const SciToken scitoken, Acl **acls, char **err_msg) {
261284
if (enf == nullptr) {
262285
if (err_msg) {*err_msg = strdup("Enforcer may not be a null pointer");}

src/scitokens.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ typedef struct Acl_s {
2020
}
2121
Acl;
2222

23+
/**
24+
* Determine the mode we will use to validate tokens.
25+
* - COMPAT mode (default) indicates any supported token format
26+
* is acceptable. Where possible, the scope names are translated into
27+
* equivalent SciTokens 1.0 claim names (i.e., storage.read -> read; storage.write -> write).
28+
* - SCITOKENS_1_0, SCITOKENS_2_0, WLCG_1_0: only accept these specific profiles.
29+
* No automatic translation is performed.
30+
*/
31+
typedef enum _profile {
32+
COMPAT = 0,
33+
SCITOKENS_1_0,
34+
SCITOKENS_2_0,
35+
WLCG_1_0
36+
} SciTokenProfile;
37+
2338
SciTokenKey scitoken_key_create(const char *key_id, const char *algorithm, const char *public_contents, const char *private_contents, char **err_msg);
2439

2540
void scitoken_key_destroy(SciTokenKey private_key);
@@ -38,10 +53,22 @@ void scitoken_set_lifetime(SciToken token, int lifetime);
3853

3954
int scitoken_serialize(const SciToken token, char **value, char **err_msg);
4055

56+
/**
57+
* Set the profile used for serialization; if COMPAT mode is used, then
58+
* the library default is utilized (currently, scitokens 1.0).
59+
*/
60+
void scitoken_set_serialize_mode(SciToken token, SciTokenProfile profile);
61+
4162
int scitoken_deserialize(const char *value, SciToken *token, char const* const* allowed_issuers, char **err_msg);
4263

4364
Validator validator_create();
4465

66+
/**
67+
* Set the profile used for validating the tokens; COMPAT (default) will accept any known token
68+
* type while others will only support that specific profile.
69+
*/
70+
void validator_set_token_profile(Validator, SciTokenProfile profile);
71+
4572
int validator_add(Validator validator, const char *claim, StringValidatorFunction validator_func, char **err_msg);
4673

4774
int validator_add_critical_claims(Validator validator, const char **claims, char **err_msg);
@@ -52,6 +79,12 @@ Enforcer enforcer_create(const char *issuer, const char **audience, char **err_m
5279

5380
void enforcer_destroy(Enforcer);
5481

82+
/**
83+
* Set the profile used for enforcing ACLs; when set to COMPAT (default), then the authorizations
84+
* will be converted to SciTokens 1.0-style authorizations (so, WLCG's storage.read becomes read).
85+
*/
86+
void enforcer_set_validate_profile(Enforcer, SciTokenProfile profile);
87+
5588
int enforcer_generate_acls(const Enforcer enf, const SciToken scitokens, Acl **acls, char **err_msg);
5689

5790
void enforcer_acl_free(Acl *acls);

src/scitokens_internal.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,9 @@ SciToken::deserialize(const std::string &data, const std::vector<std::string> al
336336

337337
// Set all the claims
338338
m_claims = m_decoded->get_payload_claims();
339+
340+
// Copy over the profile
341+
m_profile = val.get_profile();
339342
}
340343

341344

@@ -491,6 +494,7 @@ scitokens::Enforcer::scope_validator(const jwt::claim &claim, void *myself) {
491494
std::string requested_path = normalize_absolute_path(me->m_test_path);
492495
auto scope_iter = scope.begin();
493496
//std::cout << "Comparing scope " << scope << " against test accesses " << me->m_test_authz << ":" << requested_path << std::endl;
497+
bool compat_modify = false, compat_create = false, compat_cancel = false;
494498
while (scope_iter != scope.end()) {
495499
while (*scope_iter == ' ') {scope_iter++;}
496500
auto next_scope_iter = std::find(scope_iter, scope.end(), ' ');
@@ -507,6 +511,25 @@ scitokens::Enforcer::scope_validator(const jwt::claim &claim, void *myself) {
507511
}
508512
path = normalize_absolute_path(path);
509513

514+
// If we are in compatibility mode and this is a WLCG token, then translate the authorization
515+
// names to utilize the SciToken-style names.
516+
if (me->m_validate_profile == SciToken::Profile::COMPAT &&
517+
me->m_validator.get_profile() == SciToken::Profile::WLCG_1_0) {
518+
if (authz == "storage.read") {
519+
authz = "read";
520+
} else if (authz == "storage.write") {
521+
authz = "write";
522+
} else if (authz == "compute.read") {
523+
authz = "condor:/READ";
524+
} else if (authz == "compute.modify") {
525+
compat_modify = true;
526+
} else if (authz == "compute.create") {
527+
compat_create = true;
528+
} else if (authz == "compute.cancel") {
529+
compat_cancel = true;
530+
}
531+
}
532+
510533
if (me->m_test_authz.empty()) {
511534
me->m_gen_acls.emplace_back(authz, path);
512535
} else if ((me->m_test_authz == authz) &&
@@ -516,5 +539,17 @@ scitokens::Enforcer::scope_validator(const jwt::claim &claim, void *myself) {
516539

517540
scope_iter = next_scope_iter;
518541
}
542+
543+
// Compatibility mode: the combination on compute modify, create, and cancel mode are equivalent
544+
// to the condor:/WRITE authorization.
545+
if (compat_modify && compat_create && compat_cancel) {
546+
if (me->m_test_authz.empty()) {
547+
me->m_gen_acls.emplace_back("condor", "/WRITE");
548+
} else if ((me->m_test_authz == "condor") &&
549+
(requested_path.substr(0, 6) == "/WRITE")) {
550+
return true;
551+
}
552+
}
553+
519554
return me->m_test_authz.empty();
520555
}

src/scitokens_internal.h

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ class SciToken {
111111
friend class scitokens::Validator;
112112

113113
public:
114+
115+
enum class Profile {
116+
COMPAT = 0,
117+
SCITOKENS_1_0,
118+
SCITOKENS_2_0,
119+
WLCG_1_0
120+
};
121+
114122
SciToken(SciTokenKey &signing_algorithm)
115123
: m_key(signing_algorithm)
116124
{}
@@ -121,6 +129,11 @@ friend class scitokens::Validator;
121129
if (key == "iss") {m_issuer_set = true;}
122130
}
123131

132+
void
133+
set_serialize_mode(Profile profile) {
134+
m_serialize_profile = profile;
135+
}
136+
124137
const jwt::claim
125138
get_claim(const std::string &key) {
126139
return m_claims[key];
@@ -162,6 +175,20 @@ friend class scitokens::Validator;
162175
uuid_unparse_lower(uuid, uuid_str);
163176
m_claims["jti"] = std::string(uuid_str);
164177

178+
if (m_serialize_profile == Profile::SCITOKENS_2_0) {
179+
m_claims["ver"] = std::string("scitokens:2.0");
180+
auto iter = m_claims.find("aud");
181+
if (iter == m_claims.end()) {
182+
m_claims["aud"] = std::string("ANY");
183+
}
184+
} else if (m_serialize_profile == Profile::WLCG_1_0) {
185+
m_claims["wlcg_ver"] = std::string("1.0");
186+
auto iter = m_claims.find("aud");
187+
if (iter == m_claims.end()) {
188+
m_claims["aud"] = std::string("https://wlcg.cern.ch/jwt/v1/any");
189+
}
190+
}
191+
165192
// Set all the payload claims
166193
for (auto it : m_claims) {
167194
builder.set_payload_claim(it.first, it.second);
@@ -176,6 +203,8 @@ friend class scitokens::Validator;
176203
private:
177204
bool m_issuer_set{false};
178205
int m_lifetime{600};
206+
Profile m_profile{Profile::SCITOKENS_1_0};
207+
Profile m_serialize_profile{Profile::COMPAT};
179208
std::unordered_map<std::string, jwt::claim> m_claims;
180209
std::unique_ptr<jwt::decoded_jwt> m_decoded;
181210
SciTokenKey &m_key;
@@ -252,19 +281,62 @@ class Validator {
252281
throw jwt::token_verification_exception("'ver' claim value must be a string (if present)");
253282
}
254283
std::string ver_string = claim.as_string();
255-
if (ver_string == "scitoken:2.0") {
284+
if (ver_string == "scitokens:2.0") {
256285
must_verify_everything = false;
286+
if ((m_validate_profile != SciToken::Profile::COMPAT) &&
287+
(m_validate_profile != SciToken::Profile::SCITOKENS_2_0))
288+
{
289+
throw jwt::token_verification_exception("Invalidate token type; not expecting a SciToken 2.0.");
290+
}
291+
m_profile = SciToken::Profile::SCITOKENS_2_0;
257292
if (!jwt.has_payload_claim("aud")) {
258293
throw jwt::token_verification_exception("'aud' claim required for SciTokens 2.0 profile");
259294
}
260295
}
261-
else if (ver_string == "scitokens:1.0") must_verify_everything = m_validate_all_claims;
262-
else {
296+
else if (ver_string == "scitokens:1.0") {
297+
must_verify_everything = m_validate_all_claims;
298+
if ((m_validate_profile != SciToken::Profile::COMPAT) &&
299+
(m_validate_profile != SciToken::Profile::SCITOKENS_1_0))
300+
{
301+
throw jwt::token_verification_exception("Invalidate token type; not expecting a SciToken 1.0.");
302+
}
303+
m_profile = SciToken::Profile::SCITOKENS_1_0;
304+
} else {
263305
std::stringstream ss;
264306
ss << "Unknown profile version in token: " << ver_string;
265307
throw jwt::token_verification_exception(ss.str());
266308
}
309+
// Handle WLCG common JWT profile.
310+
} else if (jwt.has_payload_claim("wlcg.ver")) {
311+
if ((m_validate_profile != SciToken::Profile::COMPAT) &&
312+
(m_validate_profile != SciToken::Profile::WLCG_1_0))
313+
{
314+
throw jwt::token_verification_exception("Invalidate token type; not expecting a WLCG 1.0.");
315+
}
316+
317+
m_profile = SciToken::Profile::WLCG_1_0;
318+
must_verify_everything = false;
319+
const jwt::claim &claim = jwt.get_payload_claim("wlcg.ver");
320+
if (claim.get_type() != jwt::claim::type::string) {
321+
throw jwt::token_verification_exception("'ver' claim value must be a string (if present)");
322+
}
323+
std::string ver_string = claim.as_string();
324+
if (ver_string != "1.0") {
325+
std::stringstream ss;
326+
ss << "Unknown WLCG profile version in token: " << ver_string;
327+
throw jwt::token_verification_exception(ss.str());
328+
}
329+
if (!jwt.has_payload_claim("aud")) {
330+
throw jwt::token_verification_exception("Malformed token: 'aud' claim required for WLCG profile");
331+
}
267332
} else {
333+
if ((m_validate_profile != SciToken::Profile::COMPAT) &&
334+
(m_validate_profile != SciToken::Profile::SCITOKENS_1_0))
335+
{
336+
throw jwt::token_verification_exception("Invalidate token type; not expecting a SciToken 1.0.");
337+
}
338+
339+
m_profile = SciToken::Profile::SCITOKENS_1_0;
268340
must_verify_everything = m_validate_all_claims;
269341
}
270342

@@ -339,13 +411,38 @@ class Validator {
339411
m_validate_all_claims = new_val;
340412
}
341413

414+
/**
415+
* Get the profile of the last validated token.
416+
*
417+
* If there has been no validation - or the validation failed,
418+
* then the return value is unspecified.
419+
*
420+
* Will not return Profile::COMPAT.
421+
*/
422+
SciToken::Profile get_profile() const {
423+
if (m_profile == SciToken::Profile::COMPAT) {
424+
throw jwt::token_verification_exception("Token profile has not been set.");
425+
}
426+
return m_profile;
427+
}
428+
429+
/**
430+
* Set the profile that will be used for validation; COMPAT indicates any supported profile
431+
* is allowable.
432+
*/
433+
void set_validate_profile(SciToken::Profile profile) {
434+
m_validate_profile = profile;
435+
}
436+
342437
private:
343438
void get_public_key_pem(const std::string &issuer, const std::string &kid, std::string &public_pem, std::string &algorithm);
344439
void get_public_keys_from_web(const std::string &issuer, picojson::value &keys, int64_t &next_update, int64_t &expires);
345440
bool get_public_keys_from_db(const std::string issuer, int64_t now, picojson::value &keys, int64_t &next_update);
346441
bool store_public_keys(const std::string &issuer, const picojson::value &keys, int64_t next_update, int64_t expires);
347442

348443
bool m_validate_all_claims{true};
444+
SciToken::Profile m_profile{SciToken::Profile::COMPAT};
445+
SciToken::Profile m_validate_profile{SciToken::Profile::COMPAT};
349446
ClaimStringValidatorMap m_validators;
350447
ClaimValidatorMap m_claim_validators;
351448

@@ -377,6 +474,10 @@ class Enforcer {
377474
m_validator.add_critical_claims(critical_claims);
378475
}
379476

477+
void set_validate_profile(SciToken::Profile profile) {
478+
m_validate_profile = profile;
479+
}
480+
380481
bool test(const SciToken &scitoken, const std::string &authz, const std::string &path) {
381482
reset_state();
382483
m_test_path = path;
@@ -407,21 +508,26 @@ class Enforcer {
407508

408509
static bool aud_validator(const jwt::claim &claim, void *myself) {
409510
auto me = reinterpret_cast<scitokens::Enforcer*>(myself);
511+
std::vector<std::string> jwt_audiences;
410512
if (claim.get_type() == jwt::claim::type::string) {
411513
const std::string &audience = claim.as_string();
412-
if ((audience == "ANY") && !me->m_audiences.empty()) {return true;}
413-
for (const auto &aud : me->m_audiences) {
414-
if (aud == audience) {return true;}
415-
}
514+
jwt_audiences.push_back(audience);
416515
} else if (claim.get_type() == jwt::claim::type::array) {
417516
const picojson::array &audiences = claim.as_array();
418517
for (const auto &aud_value : audiences) {
419-
if (!aud_value.is<std::string>()) {continue;}
420-
std::string audience = aud_value.get<std::string>();
421-
if ((audience == "ANY") && !me->m_audiences.empty()) {return true;}
422-
for (const auto &aud : me->m_audiences) {
423-
if (aud == audience) {return true;}
424-
}
518+
const std::string &audience = aud_value.get<std::string>();
519+
jwt_audiences.push_back(audience);
520+
}
521+
}
522+
for (const auto &aud_value : jwt_audiences) {
523+
if (((me->m_validator.get_profile() == SciToken::Profile::SCITOKENS_2_0) && (aud_value == "ANY")) ||
524+
((me->m_validator.get_profile() == SciToken::Profile::WLCG_1_0) && (aud_value == "https://wlcg.cern.ch/jwt/v1/any"))
525+
)
526+
{
527+
return true;
528+
}
529+
for (const auto &aud : me->m_audiences) {
530+
if (aud == aud_value) {return true;}
425531
}
426532
}
427533
return false;
@@ -431,8 +537,11 @@ class Enforcer {
431537
m_test_path = "";
432538
m_test_authz = "";
433539
m_gen_acls.clear();
540+
m_validator.set_validate_profile(m_validate_profile);
434541
}
435542

543+
SciToken::Profile m_validate_profile{SciToken::Profile::COMPAT};
544+
436545
std::string m_test_path;
437546
std::string m_test_authz;
438547
AclsList m_gen_acls;

0 commit comments

Comments
 (0)