diff --git a/include/ccf/crypto/jwk.h b/include/ccf/crypto/jwk.h index 2a797de631e2..42556572a4d0 100644 --- a/include/ccf/crypto/jwk.h +++ b/include/ccf/crypto/jwk.h @@ -27,12 +27,13 @@ namespace crypto JsonWebKeyType kty; std::optional kid = std::nullopt; std::optional> x5c = std::nullopt; + std::optional issuer = std::nullopt; bool operator==(const JsonWebKey&) const = default; }; DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JsonWebKey); DECLARE_JSON_REQUIRED_FIELDS(JsonWebKey, kty); - DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c); + DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c, issuer); enum class JsonWebKeyECCurve { diff --git a/include/ccf/service/tables/jwt.h b/include/ccf/service/tables/jwt.h index 2457439f003d..6feac3f63a6f 100644 --- a/include/ccf/service/tables/jwt.h +++ b/include/ccf/service/tables/jwt.h @@ -58,18 +58,27 @@ namespace ccf using JwtKeyId = std::string; using Cert = std::vector; + struct JwtIssuerWithConstraint + { + JwtIssuer issuer; + std::optional constraint; + }; + DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtIssuerWithConstraint); + DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerWithConstraint, issuer); + DECLARE_JSON_OPTIONAL_FIELDS(JwtIssuerWithConstraint, constraint); + using JwtIssuers = ServiceMap; using JwtPublicSigningKeys = kv::RawCopySerialisedMap; - using JwtPublicSigningKeyIssuer = - kv::RawCopySerialisedMap; + using JwtPublicSigningKeyIssuers = + ServiceMap>; namespace Tables { static constexpr auto JWT_ISSUERS = "public:ccf.gov.jwt.issuers"; static constexpr auto JWT_PUBLIC_SIGNING_KEYS = "public:ccf.gov.jwt.public_signing_keys"; - static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUER = - "public:ccf.gov.jwt.public_signing_key_issuer"; + static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUERS = + "public:ccf.gov.jwt.public_signing_key_issuers"; } struct JsonWebKeySet diff --git a/src/endpoints/authentication/jwt_auth.cpp b/src/endpoints/authentication/jwt_auth.cpp index ef17ffb9b0bc..fa415ca18f0f 100644 --- a/src/endpoints/authentication/jwt_auth.cpp +++ b/src/endpoints/authentication/jwt_auth.cpp @@ -3,6 +3,7 @@ #include "ccf/endpoints/authentication/jwt_auth.h" +#include "ccf/ds/nonstd.h" #include "ccf/pal/locking.h" #include "ccf/rpc_context.h" #include "ccf/service/tables/jwt.h" @@ -10,6 +11,79 @@ #include "enclave/enclave_time.h" #include "http/http_jwt.h" +namespace +{ + static const std::string multitenancy_indicator{"{tenantid}"}; + static const std::string microsoft_entra_domain{"login.microsoftonline.com"}; + + std::optional first_non_empty_chunk( + const std::vector& chunks) + { + for (auto chunk : chunks) + { + if (!chunk.empty()) + { + return chunk; + } + } + return std::nullopt; + } + + bool validate_issuer( + const http::JwtVerifier::Token& token, std::string issuer) + { + LOG_INFO_FMT( + "Verify token.iss {} and token.tid {} against published key issuer {}", + token.payload_typed.iss, + token.payload_typed.tid, + issuer); + + const bool is_microsoft_entra = + issuer.find(microsoft_entra_domain) != std::string::npos; + if (!is_microsoft_entra) + { + return token.payload_typed.iss == issuer; + } + + // Specify tenant if working with multi-tenant endpoint. + const auto pos = issuer.find(multitenancy_indicator); + if (pos != std::string::npos) + { + issuer.replace( + pos, multitenancy_indicator.size(), token.payload_typed.tid); + } + + // Step 1. Verify the token issuer against the key issuer. + if (token.payload_typed.iss != issuer) + { + return false; + } + + // Step 2. Verify that token.tid is served as a part of token.iss. According + // to the documentation, we only accept this format: + // + // https://domain.com/tenant_id/something_else + // + // Here url.path == "/tenant_id/something_else". + + const auto url = http::parse_url_full(token.payload_typed.iss); + const auto tenant_id = first_non_empty_chunk(nonstd::split(url.path, "/")); + + return (tenant_id && token.payload_typed.tid == tenant_id.value()); + } + + bool validate_issuers( + const http::JwtVerifier::Token& token, + const std::vector& issuers) + { + return std::any_of(issuers.begin(), issuers.end(), [&](const auto& issuer) { + return issuer.constraint.has_value() && + validate_issuer(token, issuer.constraint.value()); + }); + } + +} + namespace ccf { struct VerifiersCache @@ -59,10 +133,11 @@ namespace ccf auto& token = token_opt.value(); auto keys = tx.ro(ccf::Tables::JWT_PUBLIC_SIGNING_KEYS); - auto key_issuers = tx.ro( - ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER); + auto key_issuers = tx.ro( + ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS); const auto key_id = token.header_typed.kid; const auto token_key = keys->get(key_id); + const auto issuers = key_issuers->get(key_id); if (!token_key.has_value()) { @@ -96,10 +171,24 @@ namespace ccf time_now, token.payload_typed.exp); } + else if (issuers->empty()) + { + error_reason = fmt::format( + "Issuer validation failure for kid {} - issuer not found", + key_id); + } + else if (!validate_issuers(token, issuers.value())) + { + error_reason = fmt::format( + "Token issuer for kid {} failed validation agains the key issuer", + key_id); + } else { auto identity = std::make_unique(); - identity->key_issuer = key_issuers->get(key_id).value(); + // TODO(#same-kid-different-issuer) back-propagate the proper issuer + // we checked against in case there are many. + identity->key_issuer = issuers->front().issuer; identity->header = std::move(token.header); identity->payload = std::move(token.payload); return identity; diff --git a/src/http/http_jwt.h b/src/http/http_jwt.h index 64e731d3fa05..e31d490cc065 100644 --- a/src/http/http_jwt.h +++ b/src/http/http_jwt.h @@ -32,9 +32,11 @@ namespace http { size_t nbf; size_t exp; + std::string iss; + std::string tid; }; DECLARE_JSON_TYPE(JwtPayload) - DECLARE_JSON_REQUIRED_FIELDS(JwtPayload, nbf, exp) + DECLARE_JSON_REQUIRED_FIELDS(JwtPayload, nbf, exp, iss, tid) class JwtVerifier { diff --git a/src/node/gov/handlers/service_state.h b/src/node/gov/handlers/service_state.h index 12657e24a0a2..af5d48037eb6 100644 --- a/src/node/gov/handlers/service_state.h +++ b/src/node/gov/handlers/service_state.h @@ -468,8 +468,8 @@ namespace ccf::gov::endpoints ctx.tx.template ro( ccf::Tables::JWT_PUBLIC_SIGNING_KEYS); auto jwt_key_issuers_handle = - ctx.tx.template ro( - ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER); + ctx.tx.template ro( + ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS); jwt_keys_handle->foreach( [&keys, jwt_key_issuers_handle]( @@ -480,10 +480,12 @@ namespace ccf::gov::endpoints const auto cert_pem = crypto::cert_der_to_pem(cert); key_info["certificate"] = cert_pem.str(); - const auto issuer = jwt_key_issuers_handle->get(kid); - if (issuer.has_value()) + // TODO(#same-kid-different-issuer) we must either populate all + // issuers or choose one. To be discussed later on. + const auto issuers = jwt_key_issuers_handle->get(kid); + if (issuers.has_value() && !issuers->empty()) { - key_info["issuer"] = issuer.value(); + key_info["issuer"] = issuers->front().issuer; } else { diff --git a/src/node/jwt_key_auto_refresh.h b/src/node/jwt_key_auto_refresh.h index 9b986d927af8..3b727bc9210f 100644 --- a/src/node/jwt_key_auto_refresh.h +++ b/src/node/jwt_key_auto_refresh.h @@ -137,6 +137,7 @@ namespace ccf void handle_jwt_jwks_response( const std::string& issuer, + const std::optional& issuer_constraint, http_status status, std::vector&& data) { @@ -173,6 +174,20 @@ namespace ccf // call internal endpoint to update keys auto msg = SetJwtPublicSigningKeys{issuer, jwks}; + + // For each key we leave the specified issuer constraint or set a common + // one otherwise (if present). + if (issuer_constraint.has_value()) + { + for (auto& key : jwks.keys) + { + if (!key.issuer.has_value()) + { + key.issuer = issuer_constraint; + } + } + } + send_refresh_jwt_keys(msg); } @@ -201,9 +216,10 @@ namespace ccf issuer); std::string jwks_url_str; + nlohmann::json metadata; try { - auto metadata = nlohmann::json::parse(data); + metadata = nlohmann::json::parse(data); jwks_url_str = metadata.at("jwks_uri").get(); } catch (const std::exception& e) @@ -235,6 +251,13 @@ namespace ccf auto ca_cert = std::make_shared( ca, std::nullopt, std::nullopt, jwks_url.host); + std::optional issuer_constraint{std::nullopt}; + const auto constraint = metadata.find("issuer"); + if (constraint != metadata.end()) + { + issuer_constraint = *constraint; + } + LOG_DEBUG_FMT( "JWT key auto-refresh: Requesting JWKS at https://{}:{}{}", jwks_url.host, @@ -246,9 +269,10 @@ namespace ccf http_client->connect( std::string(jwks_url.host), std::string(jwks_url_port), - [this, issuer]( + [this, issuer, issuer_constraint]( http_status status, http::HeaderMap&&, std::vector&& data) { - handle_jwt_jwks_response(issuer, status, std::move(data)); + handle_jwt_jwks_response( + issuer, issuer_constraint, status, std::move(data)); return true; }); http::Request r(jwks_url.path, HTTP_GET); diff --git a/src/node/rpc/jwt_management.h b/src/node/rpc/jwt_management.h index 012ea43a3f98..12cf225689e7 100644 --- a/src/node/rpc/jwt_management.h +++ b/src/node/rpc/jwt_management.h @@ -24,15 +24,17 @@ namespace ccf static void remove_jwt_public_signing_keys(kv::Tx& tx, std::string issuer) { auto keys = tx.rw(Tables::JWT_PUBLIC_SIGNING_KEYS); - auto key_issuer = - tx.rw(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER); + auto key_issuers = + tx.rw(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS); - key_issuer->foreach( - [&issuer, &keys, &key_issuer](const auto& k, const auto& v) { - if (v == issuer) + // TODO(#same-kid-different-issuer) - we must only delete kids for the + // specific issuer + key_issuers->foreach( + [&issuer, &keys, &key_issuers](const auto& k, const auto& v) { + if (v.front().issuer == issuer) { keys->remove(k); - key_issuer->remove(k); + key_issuers->remove(k); } return true; }); @@ -62,8 +64,8 @@ namespace ccf const JsonWebKeySet& jwks) { auto keys = tx.rw(Tables::JWT_PUBLIC_SIGNING_KEYS); - auto key_issuer = - tx.rw(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER); + auto key_issuers = + tx.rw(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS); auto log_prefix = proposal_id.empty() ? "JWT key auto-refresh" : @@ -76,6 +78,7 @@ namespace ccf return false; } std::map> new_keys; + std::map issuer_constraints; for (auto& jwk : jwks.keys) { if (!jwk.kid.has_value()) @@ -84,14 +87,19 @@ namespace ccf return false; } auto const& kid = jwk.kid.value(); - - if (keys->has(kid) && key_issuer->get(kid).value() != issuer) + if ( + keys->has(kid) && + key_issuers->get(kid).value().front().issuer != issuer) { + // TODO(#same-kid-different-issuer) relax this, because we now support + // same kids from different issuers, but check that public key (x5c) is + // the same. LOG_FAIL_FMT( "{}: key id {} already added for different issuer", log_prefix, kid); return false; } - if (!jwk.x5c.has_value() && jwk.x5c->empty()) + + if (!jwk.x5c.has_value() && jwk.x5c->empty()) // TODO it seems like a bug { LOG_FAIL_FMT("{}: JWKS is invalid (empty x5c)", log_prefix); return false; @@ -192,6 +200,11 @@ namespace ccf } LOG_INFO_FMT("{}: Storing JWT signing key with kid {}", log_prefix, kid); new_keys.emplace(kid, der); + + if (jwk.issuer.has_value()) + { + issuer_constraints.emplace(kid, jwk.issuer.value()); + } } if (new_keys.empty()) { @@ -200,11 +213,14 @@ namespace ccf } std::set existing_kids; - key_issuer->foreach( - [&existing_kids, &issuer](const auto& kid, const auto& issuer_) { - if (issuer_ == issuer) + key_issuers->foreach( + [&existing_kids, &issuer](const auto& kid, const auto& issuers) { + for (const auto& issuer_with_constraint : issuers) { - existing_kids.insert(kid); + if (issuer == issuer_with_constraint.issuer) + { + existing_kids.insert(kid); + } } return true; }); @@ -214,7 +230,32 @@ namespace ccf if (!existing_kids.contains(kid)) { keys->put(kid, der); - key_issuer->put(kid, issuer); + + // Find the constraint + JwtIssuerWithConstraint value{issuer, std::nullopt}; + const auto it = issuer_constraints.find(kid); + if (it != issuer_constraints.end()) + { + value.constraint = it->second; + } + + LOG_INFO_FMT( + "Save JWT issuer for kid {} where issuer: {}, issuer constraint: {}", + kid, + value.issuer, + value.constraint); + + // Update the vector + auto issuers_with_constraints = key_issuers->get(kid); + if (!issuers_with_constraints.has_value()) + { + key_issuers->put(kid, std::vector{value}); + } + else + { + issuers_with_constraints->push_back(std::move(value)); + key_issuers->put(kid, *issuers_with_constraints); + } } } @@ -223,7 +264,8 @@ namespace ccf if (!new_keys.contains(kid)) { keys->remove(kid); - key_issuer->remove(kid); + key_issuers->remove(kid); // TODO(#same-kid-different-issuer) - we must + // only delete kids for the specific issuer } } diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 287c0bb74d6d..d337bc2a594c 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -1082,20 +1082,24 @@ namespace ccf auto get_jwt_keys = [this](auto& ctx, nlohmann::json&& body) { auto keys = ctx.tx.ro(network.jwt_public_signing_keys); - auto keys_to_issuer = ctx.tx.ro(network.jwt_public_signing_key_issuer); + auto keys_to_issuers = + ctx.tx.ro(network.jwt_public_signing_key_issuers); JWTKeyMap kmap; - keys->foreach( - [&kmap, &keys_to_issuer](const auto& kid, const auto& kpem) { - auto issuer = keys_to_issuer->get(kid); - if (!issuer.has_value()) - { - throw std::logic_error(fmt::format("kid {} has no issuer", kid)); - } - kmap.emplace( - kid, KeyIdInfo{issuer.value(), crypto::cert_der_to_pem(kpem)}); - return true; - }); + keys->foreach([&kmap, + &keys_to_issuers](const auto& kid, const auto& kpem) { + auto issuers = keys_to_issuers->get(kid); + if (!issuers.has_value()) + { + throw std::logic_error(fmt::format("kid {} has no issuer", kid)); + } + // TODO(#same-kid-different-issuer) - figure out what to do with + // multiple issuers. + kmap.emplace( + kid, + KeyIdInfo{issuers->front().issuer, crypto::cert_der_to_pem(kpem)}); + return true; + }); return make_success(kmap); }; diff --git a/src/service/network_tables.h b/src/service/network_tables.h index 16ef3548df61..757efcc3b3cf 100644 --- a/src/service/network_tables.h +++ b/src/service/network_tables.h @@ -157,8 +157,8 @@ namespace ccf const JwtIssuers jwt_issuers = {Tables::JWT_ISSUERS}; const JwtPublicSigningKeys jwt_public_signing_keys = { Tables::JWT_PUBLIC_SIGNING_KEYS}; - const JwtPublicSigningKeyIssuer jwt_public_signing_key_issuer = { - Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER}; + const JwtPublicSigningKeyIssuers jwt_public_signing_key_issuers = { + Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS}; inline auto get_all_jwt_tables() const { @@ -166,7 +166,7 @@ namespace ccf ca_cert_bundles, jwt_issuers, jwt_public_signing_keys, - jwt_public_signing_key_issuer); + jwt_public_signing_key_issuers); } // diff --git a/tests/infra/jwt_issuer.py b/tests/infra/jwt_issuer.py index 122f71e061bd..c8273ee85598 100644 --- a/tests/infra/jwt_issuer.py +++ b/tests/infra/jwt_issuer.py @@ -133,6 +133,13 @@ def __init__( self.refresh_keys() else: self.cert_pem = cert + + @property + def issuer_url(self): + name = f"{self.name}" + if self.server: + name += f":{self.server.bind_port}" + return name def refresh_keys(self, kid=None): if not kid: @@ -175,9 +182,8 @@ def register(self, network, kid=None, ca_bundle_name=TEST_CA_BUNDLE_NAME): primary, ca_bundle_name, ca_cert_bundle_fp.name ) - full_name = f"{self.name}:{self.server.bind_port}" if self.server else self.name with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp: - issuer = {"issuer": full_name, "auto_refresh": self.auto_refresh} + issuer = {"issuer": self.issuer_url, "auto_refresh": self.auto_refresh} if self.auto_refresh: issuer.update({"ca_cert_bundle_name": ca_bundle_name}) json.dump(issuer, metadata_fp) @@ -188,7 +194,7 @@ def register(self, network, kid=None, ca_bundle_name=TEST_CA_BUNDLE_NAME): json.dump(self.create_jwks(kid_), jwks_fp) jwks_fp.flush() network.consortium.set_jwt_public_signing_keys( - primary, full_name, jwks_fp.name + primary, self.issuer_url, jwks_fp.name ) def start_openid_server(self, port=0, kid=None): @@ -209,6 +215,10 @@ def issue_jwt(self, kid=None, claims=None): if "exp" not in claims: # Insert default Expiration Time claim, valid for ~1hr claims["exp"] = now + 3600 + if "iss" not in claims: + claims["iss"] = self.issuer_url + if "tid" not in claims: + claims["tid"] = "example.tenant.com" return infra.crypto.create_jwt(claims, self.key_priv_pem, kid_) def wait_for_refresh(self, network, args, kid=None): diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index 56bd7b3fc4a7..d850b8859f9d 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -301,8 +301,7 @@ def create_keypair(local_id, valid_from, validity_days): return network - -@reqs.description("JWT authentication") +@reqs.description("JWT authentication as by OpenID spec") def test_jwt_auth(network, args): primary, _ = network.find_nodes() @@ -316,7 +315,7 @@ def test_jwt_auth(network, args): der_b64 = base64.b64encode(jwt_cert_der).decode("ascii") data = { "issuer": issuer.name, - "jwks": {"keys": [{"kty": "RSA", "kid": jwt_kid, "x5c": [der_b64]}]}, + "jwks": {"keys": [{"kty": "RSA", "kid": jwt_kid, "x5c": [der_b64], "issuer": issuer.name}]}, } json.dump(data, metadata_fp) metadata_fp.flush() @@ -356,9 +355,156 @@ def test_jwt_auth(network, args): ) assert r.status_code == HTTPStatus.UNAUTHORIZED, r.status_code + network.consortium.remove_jwt_issuer(primary, issuer.name) return network +@reqs.description("JWT authentication as by MSFT Entra (single tenant)") +def test_jwt_auth_msft_single_tenant(network, args): + primary, _ = network.find_nodes() + + TENANT_ID = "9188050d-6c67-4c5b-b112-36a304b66da" + ISSUER_TENANT = "https://login.microsoftonline.com/9188050d-6c67-4c5b-b112-36a304b66da/v2.0" + + issuer = infra.jwt_issuer.JwtIssuer(name="https://login.microsoftonline.com") + + jwt_kid = "my_key_id" + with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp: + jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem) + der_b64 = base64.b64encode(jwt_cert_der).decode("ascii") + data = { + "issuer": issuer.name, + "auto_refresh": False, + "jwks": {"keys": [{"kty": "RSA", + "kid": jwt_kid, + "x5c": [der_b64], + "issuer": ISSUER_TENANT}]}, + } + json.dump(data, metadata_fp) + metadata_fp.flush() + network.consortium.set_jwt_issuer(primary, metadata_fp.name) + + with primary.client("user0") as c: + LOG.info("Calling JWT for single-tenancy with garbage tenant") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid, + claims={"iss": ISSUER_TENANT, + "tid": "garbage_tenant"})), + ) + assert r.status_code == HTTPStatus.UNAUTHORIZED, r.status_code + + with primary.client("user0") as c: + LOG.info("Calling JWT for single-tenancy with {tenantid} must fail because we only can use common tenant as pattern in published key, not in the token") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid, + claims={"iss": ISSUER_TENANT, + "tid": "{tenantid}"})), + ) + assert r.status_code == HTTPStatus.UNAUTHORIZED, r.status_code + + with primary.client("user0") as c: + LOG.info("Calling JWT for single-tenancy with microsoft tenant") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid, + claims={"iss": ISSUER_TENANT, + "tid": TENANT_ID})), + ) + assert r.status_code == HTTPStatus.OK, r.status_code + + network.consortium.remove_jwt_issuer(primary, issuer.name) + return network + +@reqs.description("JWT authentication as by MSFT Entra (multiple tenants)") +def test_jwt_auth_msft_multitenancy(network, args): + primary, _ = network.find_nodes() + + COMMNON_ISSUER = "https://login.microsoftonline.com/{tenantid}/v2.0" + TENANT_ID = "9188050d-6c67-4c5b-b112-36a304b66da" + ISSUER_TENANT = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0" + ANOTHER_TENANT_ID = "ANOTHER-6c67-4c5b-b112-36a304b66da" + ISSUER_ANOTHER = f"https://login.microsoftonline.com/{ANOTHER_TENANT_ID}/v2.0" + + issuer = infra.jwt_issuer.JwtIssuer(name="https://login.microsoftonline.com") + + jwt_kid_1 = "my_key_id_1" + jwt_kid_2 = "my_key_id_2" + + with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp: + jwt_cert_der = infra.crypto.cert_pem_to_der(issuer.cert_pem) + der_b64 = base64.b64encode(jwt_cert_der).decode("ascii") + data = { + "issuer": issuer.issuer_url, + "auto_refresh": False, + "jwks": {"keys": [{"kty": "RSA", + "kid": jwt_kid_1, + "x5c": [der_b64], + "issuer": COMMNON_ISSUER}, + {"kty": "RSA", + "kid": jwt_kid_2, + "x5c": [der_b64], + "issuer": ISSUER_TENANT}, + ]}, + } + json.dump(data, metadata_fp) + metadata_fp.flush() + network.consortium.set_jwt_issuer(primary, metadata_fp.name) + + with primary.client("user0") as c: + LOG.info("TODO") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid_1, + claims={"iss": ISSUER_TENANT, + "tid": TENANT_ID})), + ) + assert r.status_code == HTTPStatus.OK, r.status_code + + with primary.client("user0") as c: + LOG.info("TODO") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid_1, + claims={"iss": ISSUER_ANOTHER, + "tid": ANOTHER_TENANT_ID})), + ) + assert r.status_code == HTTPStatus.OK, r.status_code + + with primary.client("user0") as c: + LOG.info("TODO") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid_1, + claims={"iss": ISSUER_TENANT, + "tid": ANOTHER_TENANT_ID})), + ) + assert r.status_code == HTTPStatus.UNAUTHORIZED, r.status_code + + with primary.client("user0") as c: + LOG.info("TODO") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid_2, + claims={"iss": ISSUER_TENANT, + "tid": TENANT_ID})), + ) + assert r.status_code == HTTPStatus.OK, r.status_code + + with primary.client("user0") as c: + LOG.info("TODO") + r = c.get( + "/app/jwt", + headers=infra.jwt_issuer.make_bearer_header(issuer.issue_jwt(jwt_kid_2, + claims={"iss": ISSUER_ANOTHER, + "tid": ANOTHER_TENANT_ID})), + ) + assert r.status_code == HTTPStatus.UNAUTHORIZED, r.status_code + + network.consortium.remove_jwt_issuer(primary, issuer.name) + return network + @reqs.description("Role-based access") def test_role_based_access(network, args): primary, _ = network.find_nodes() @@ -452,6 +598,8 @@ def run_authn(args): network.start_and_open(args) network = test_cert_auth(network, args) network = test_jwt_auth(network, args) + network = test_jwt_auth_msft_single_tenant(network, args) + network = test_jwt_auth_msft_multitenancy(network, args) network = test_role_based_access(network, args)