Skip to content

Commit

Permalink
provide the user access to the credentialSubject via UserInfoClaims s…
Browse files Browse the repository at this point in the history
…tructure (#369)

* first pass at UserInfoClaims

* removed std++20 use

* refactoring per @bifurcation recommendations

* fixed problem found in CI
  • Loading branch information
Greg Hewett authored Sep 10, 2023
1 parent 661758e commit 6a10f78
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 6 deletions.
43 changes: 42 additions & 1 deletion lib/hpke/include/hpke/userinfo_vc.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,51 @@
#include <chrono>
#include <hpke/signature.h>
#include <map>
#include <nlohmann/json.hpp>

using namespace MLS_NAMESPACE::bytes_ns;

namespace MLS_NAMESPACE::hpke {

struct UserInfoClaimsAddress
{
std::optional<std::string> formatted;
std::optional<std::string> street_address;
std::optional<std::string> locality;
std::optional<std::string> region;
std::optional<std::string> postal_code;
std::optional<std::string> country;

static UserInfoClaimsAddress from_json(const nlohmann::json& address_json);
};

struct UserInfoClaims
{

std::optional<std::string> sub;
std::optional<std::string> name;
std::optional<std::string> given_name;
std::optional<std::string> family_name;
std::optional<std::string> middle_name;
std::optional<std::string> nickname;
std::optional<std::string> preferred_username;
std::optional<std::string> profile;
std::optional<std::string> picture;
std::optional<std::string> website;
std::optional<std::string> email;
std::optional<bool> email_verified;
std::optional<std::string> gender;
std::optional<std::string> birthdate;
std::optional<std::string> zoneinfo;
std::optional<std::string> locale;
std::optional<std::string> phone_number;
std::optional<bool> phone_number_verified;
std::optional<UserInfoClaimsAddress> address;
std::optional<uint64_t> updated_at;

static UserInfoClaims from_json(const nlohmann::json& cred_subject_json);
};

struct UserInfoVC
{
private:
Expand All @@ -29,7 +69,8 @@ struct UserInfoVC
std::string key_id() const;
std::chrono::system_clock::time_point not_before() const;
std::chrono::system_clock::time_point not_after() const;
std::map<std::string, std::string> subject() const;
const std::string& raw_credential() const;
const UserInfoClaims& subject() const;
const Signature::PublicJWK& public_key() const;

bool valid_from(const Signature::PublicKey& issuer_key) const;
Expand Down
105 changes: 101 additions & 4 deletions lib/hpke/src/userinfo_vc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ using nlohmann::json;

namespace MLS_NAMESPACE::hpke {

static const std::string name_attr = "name";
static const std::string sub_attr = "sub";
static const std::string given_name_attr = "given_name";
static const std::string family_name_attr = "family_name";
static const std::string middle_name_attr = "middle_name";
static const std::string nickname_attr = "nickname";
static const std::string preferred_username_attr = "preferred_username";
static const std::string profile_attr = "profile";
static const std::string picture_attr = "picture";
static const std::string website_attr = "website";
static const std::string email_attr = "email";
static const std::string email_verified_attr = "email_verified";
static const std::string gender_attr = "gender";
static const std::string birthdate_attr = "birthdate";
static const std::string zoneinfo_attr = "zoneinfo";
static const std::string locale_attr = "locale";
static const std::string phone_number_attr = "phone_number";
static const std::string phone_number_verified_attr = "phone_number_verified";
static const std::string address_attr = "address";
static const std::string address_formatted_attr = "formatted";
static const std::string address_street_address_attr = "street_address";
static const std::string address_locality_attr = "locality";
static const std::string address_region_attr = "region";
static const std::string address_postal_code_attr = "postal_code";
static const std::string address_country_attr = "country";
static const std::string updated_at_attr = "updated_at";

template<typename T>
static std::optional<T>
get_optional(const json& json_object, const std::string& field_name)
{
if (!json_object.contains(field_name)) {
return std::nullopt;
}

return { json_object.at(field_name).get<T>() };
}

///
/// ParsedCredential
///
Expand Down Expand Up @@ -130,7 +168,7 @@ struct UserInfoVC::ParsedCredential
std::chrono::system_clock::time_point not_after; // `exp`

// Credential subject fields
std::map<std::string, std::string> credential_subject;
UserInfoClaims credential_subject;
Signature::PublicJWK public_key;

// Signature verification information
Expand All @@ -142,7 +180,7 @@ struct UserInfoVC::ParsedCredential
std::string issuer_in,
std::chrono::system_clock::time_point not_before_in,
std::chrono::system_clock::time_point not_after_in,
std::map<std::string, std::string> credential_subject_in,
UserInfoClaims credential_subject_in,
Signature::PublicJWK&& public_key_in,
bytes to_be_signed_in,
bytes signature_in)
Expand Down Expand Up @@ -222,7 +260,7 @@ struct UserInfoVC::ParsedCredential
epoch_time(payload.at("nbf").get<int64_t>()),
epoch_time(payload.at("exp").get<int64_t>()),

vc.at("credentialSubject"),
UserInfoClaims::from_json(vc.at("credentialSubject")),
std::move(public_key),

to_be_signed,
Expand All @@ -235,6 +273,59 @@ struct UserInfoVC::ParsedCredential
}
};

///
/// UserInfoClaimsAddress
///
UserInfoClaimsAddress
UserInfoClaimsAddress::from_json(const nlohmann::json& address_json)
{
return {
get_optional<std::string>(address_json, address_formatted_attr),
get_optional<std::string>(address_json, address_street_address_attr),
get_optional<std::string>(address_json, address_locality_attr),
get_optional<std::string>(address_json, address_region_attr),
get_optional<std::string>(address_json, address_postal_code_attr),
get_optional<std::string>(address_json, address_country_attr),
};
}

///
/// UserInfoClaims
///
UserInfoClaims
UserInfoClaims::from_json(const nlohmann::json& cred_subject_json)
{
std::optional<UserInfoClaimsAddress> address_opt = {};

if (cred_subject_json.contains(address_attr)) {
address_opt =
UserInfoClaimsAddress::from_json(cred_subject_json.at(address_attr));
}

return {
get_optional<std::string>(cred_subject_json, sub_attr),
get_optional<std::string>(cred_subject_json, name_attr),
get_optional<std::string>(cred_subject_json, given_name_attr),
get_optional<std::string>(cred_subject_json, family_name_attr),
get_optional<std::string>(cred_subject_json, middle_name_attr),
get_optional<std::string>(cred_subject_json, nickname_attr),
get_optional<std::string>(cred_subject_json, preferred_username_attr),
get_optional<std::string>(cred_subject_json, profile_attr),
get_optional<std::string>(cred_subject_json, picture_attr),
get_optional<std::string>(cred_subject_json, website_attr),
get_optional<std::string>(cred_subject_json, email_attr),
get_optional<bool>(cred_subject_json, email_verified_attr),
get_optional<std::string>(cred_subject_json, gender_attr),
get_optional<std::string>(cred_subject_json, birthdate_attr),
get_optional<std::string>(cred_subject_json, zoneinfo_attr),
get_optional<std::string>(cred_subject_json, locale_attr),
get_optional<std::string>(cred_subject_json, phone_number_attr),
get_optional<bool>(cred_subject_json, phone_number_verified_attr),
address_opt,
get_optional<uint64_t>(cred_subject_json, updated_at_attr),
};
}

///
/// UserInfoVC
///
Expand Down Expand Up @@ -263,7 +354,13 @@ UserInfoVC::valid_from(const Signature::PublicKey& issuer_key) const
return parsed_cred->verify(issuer_key);
}

std::map<std::string, std::string>
const std::string&
UserInfoVC::raw_credential() const
{
return raw;
}

const UserInfoClaims&
UserInfoVC::subject() const
{
return parsed_cred->credential_subject;
Expand Down
97 changes: 96 additions & 1 deletion lib/hpke/test/userinfo_vc.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <doctest/doctest.h>
#include <hpke/userinfo_vc.h>
#include <nlohmann/json.hpp>

#include <tls/compat.h>
namespace opt = MLS_NAMESPACE::tls::opt;
Expand Down Expand Up @@ -127,6 +128,100 @@ TEST_CASE("UserInfoVC Parsing and Validation")
CHECK(vc.key_id() == opt::get(issuer_jwk.key_id));
CHECK(vc.not_before().time_since_epoch() == known_not_before);
CHECK(vc.not_after().time_since_epoch() == known_not_after);
CHECK(vc.subject() == known_subject);
CHECK(vc.public_key() == known_subject_jwk);

const auto& subject = vc.subject();
CHECK(subject.sub.value_or("") == known_subject.at("sub"));

CHECK(vc.subject().name.value_or("") == known_subject.at("name"));
CHECK(vc.subject().given_name.value_or("") == known_subject.at("given_name"));
CHECK(vc.subject().family_name.value_or("") ==
known_subject.at("family_name"));
CHECK(vc.subject().preferred_username.value_or("") ==
known_subject.at("preferred_username"));
CHECK(vc.subject().email.value_or("") == known_subject.at("email"));
CHECK(vc.subject().picture.value_or("") == known_subject.at("picture"));
}

TEST_CASE("UserInfoClaims Field Parsing")
{
nlohmann::json credentialSubject = {
{ "test", "test" },
{ "sub", "sub" },
{ "name", "name" },
{ "given_name", "given_name" },
{ "family_name", "family_name" },
{ "middle_name", "middle_name" },
{ "nickname", "nickname" },
{ "preferred_username", "preferred_username" },
{ "profile", "profile" },
{ "picture", "picture" },
{ "website", "website" },
{ "email", "email" },
{ "email_verified", true },
{ "gender", "gender" },
{ "birthdate", "birthdate" },
{ "zoneinfo", "zoneinfo" },
{ "locale", "locale" },
{ "phone_number", "phone_number" },
{ "phone_number_verified", true },
{ "address",
{ { "formatted", "formatted" },
{ "street_address", "street_address" },
{ "locality", "locality" },
{ "region", "region" },
{ "postal_code", "postal_code" },
{ "country", "country" } } },
{ "updated_at", 42 }
};

const auto userinfo_claims = UserInfoClaims::from_json(credentialSubject);

CHECK(userinfo_claims.sub == credentialSubject.at("sub"));
CHECK(userinfo_claims.name == credentialSubject.at("name"));
CHECK(userinfo_claims.given_name == credentialSubject.at("given_name"));
CHECK(userinfo_claims.family_name == credentialSubject.at("family_name"));
CHECK(userinfo_claims.middle_name == credentialSubject.at("middle_name"));
CHECK(userinfo_claims.nickname == credentialSubject.at("nickname"));
CHECK(userinfo_claims.preferred_username ==
credentialSubject.at("preferred_username"));
CHECK(userinfo_claims.profile == credentialSubject.at("profile"));
CHECK(userinfo_claims.picture == credentialSubject.at("picture"));
CHECK(userinfo_claims.website == credentialSubject.at("website"));
CHECK(userinfo_claims.email == credentialSubject.at("email"));
CHECK(userinfo_claims.email_verified ==
credentialSubject.at("email_verified"));
CHECK(userinfo_claims.gender == credentialSubject.at("gender"));
CHECK(userinfo_claims.birthdate == credentialSubject.at("birthdate"));
CHECK(userinfo_claims.zoneinfo == credentialSubject.at("zoneinfo"));
CHECK(userinfo_claims.locale == credentialSubject.at("locale"));
CHECK(userinfo_claims.phone_number == credentialSubject.at("phone_number"));
CHECK(userinfo_claims.phone_number_verified ==
credentialSubject.at("phone_number_verified"));
CHECK(userinfo_claims.updated_at == credentialSubject.at("updated_at"));

auto address = userinfo_claims.address.value_or(UserInfoClaimsAddress());
CHECK(address.formatted == credentialSubject.at("address").at("formatted"));
CHECK(address.street_address ==
credentialSubject.at("address").at("street_address"));
CHECK(address.locality == credentialSubject.at("address").at("locality"));
CHECK(address.region == credentialSubject.at("address").at("region"));
CHECK(address.postal_code ==
credentialSubject.at("address").at("postal_code"));
CHECK(address.country == credentialSubject.at("address").at("country"));
}

TEST_CASE("UserInfoClaims Edge Cases")
{
CHECK_THROWS_WITH(
UserInfoClaims::from_json({ { "updated_at", "42" } }),
"[json.exception.type_error.302] type must be number, but is string");

CHECK_THROWS_WITH(
UserInfoClaims::from_json({ { "name", true } }),
"[json.exception.type_error.302] type must be string, but is boolean");

CHECK_THROWS_WITH(
UserInfoClaims::from_json({ { "email_verified", "true" } }),
"[json.exception.type_error.302] type must be boolean, but is string");
}

0 comments on commit 6a10f78

Please sign in to comment.