Skip to content

Commit

Permalink
JWT WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed May 21, 2024
1 parent b9c2499 commit b656996
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 55 deletions.
3 changes: 2 additions & 1 deletion include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ namespace crypto
JsonWebKeyType kty;
std::optional<std::string> kid = std::nullopt;
std::optional<std::vector<std::string>> x5c = std::nullopt;
std::optional<std::string> 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
{
Expand Down
17 changes: 13 additions & 4 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,27 @@ namespace ccf
using JwtKeyId = std::string;
using Cert = std::vector<uint8_t>;

struct JwtIssuerWithConstraint
{
JwtIssuer issuer;
std::optional<JwtIssuer> constraint;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtIssuerWithConstraint);
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerWithConstraint, issuer);
DECLARE_JSON_OPTIONAL_FIELDS(JwtIssuerWithConstraint, constraint);

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
using JwtPublicSigningKeys = kv::RawCopySerialisedMap<JwtKeyId, Cert>;
using JwtPublicSigningKeyIssuer =
kv::RawCopySerialisedMap<JwtKeyId, JwtIssuer>;
using JwtPublicSigningKeyIssuers =
ServiceMap<JwtKeyId, std::vector<JwtIssuerWithConstraint>>;

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
Expand Down
95 changes: 92 additions & 3 deletions src/endpoints/authentication/jwt_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,87 @@

#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"
#include "ds/lru.h"
#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<std::string_view> first_non_empty_chunk(
const std::vector<std::string_view>& 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<ccf::JwtIssuerWithConstraint>& 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
Expand Down Expand Up @@ -59,10 +133,11 @@ namespace ccf
auto& token = token_opt.value();
auto keys =
tx.ro<JwtPublicSigningKeys>(ccf::Tables::JWT_PUBLIC_SIGNING_KEYS);
auto key_issuers = tx.ro<JwtPublicSigningKeyIssuer>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER);
auto key_issuers = tx.ro<JwtPublicSigningKeyIssuers>(
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())
{
Expand Down Expand Up @@ -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<JwtAuthnIdentity>();
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;
Expand Down
4 changes: 3 additions & 1 deletion src/http/http_jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
12 changes: 7 additions & 5 deletions src/node/gov/handlers/service_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,8 @@ namespace ccf::gov::endpoints
ctx.tx.template ro<ccf::JwtPublicSigningKeys>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEYS);
auto jwt_key_issuers_handle =
ctx.tx.template ro<ccf::JwtPublicSigningKeyIssuer>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER);
ctx.tx.template ro<ccf::JwtPublicSigningKeyIssuers>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS);

jwt_keys_handle->foreach(
[&keys, jwt_key_issuers_handle](
Expand All @@ -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
{
Expand Down
30 changes: 27 additions & 3 deletions src/node/jwt_key_auto_refresh.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ namespace ccf

void handle_jwt_jwks_response(
const std::string& issuer,
const std::optional<std::string>& issuer_constraint,
http_status status,
std::vector<uint8_t>&& data)
{
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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<std::string>();
}
catch (const std::exception& e)
Expand Down Expand Up @@ -235,6 +251,13 @@ namespace ccf
auto ca_cert = std::make_shared<tls::Cert>(
ca, std::nullopt, std::nullopt, jwks_url.host);

std::optional<std::string> 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,
Expand All @@ -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<uint8_t>&& 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);
Expand Down
Loading

0 comments on commit b656996

Please sign in to comment.