Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jwks parsing (abandoned) #112

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions example/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ target_link_libraries(rsa-create jwt-cpp::jwt-cpp)

add_executable(rsa-verify rsa-verify.cpp)
target_link_libraries(rsa-verify jwt-cpp::jwt-cpp)

add_executable(jwks-verify jwks-verify.cpp)
target_link_libraries(jwks-verify jwt-cpp::jwt-cpp)
52 changes: 52 additions & 0 deletions example/jwks-verify.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include <iostream>
#include <jwt-cpp/jwt.h>

int main()
{
std::string publicKey = R"({"keys": [{
"kid":"internal-gateway-jwt.api.sc.net",
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"x5c": [
"MIIC+DCCAeCgAwIBAgIJBIGjYW6hFpn2MA0GCSqGSIb3DQEBBQUAMCMxITAfBgNVBAMTGGN1c3RvbWVyLWRlbW9zLmF1dGgwLmNvbTAeFw0xNjExMjIyMjIyMDVaFw0zMDA4MDEyMjIyMDVaMCMxITAfBgNVBAMTGGN1c3RvbWVyLWRlbW9zLmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMnjZc5bm/eGIHq09N9HKHahM7Y31P0ul+A2wwP4lSpIwFrWHzxw88/7Dwk9QMc+orGXX95R6av4GF+Es/nG3uK45ooMVMa/hYCh0Mtx3gnSuoTavQEkLzCvSwTqVwzZ+5noukWVqJuMKNwjL77GNcPLY7Xy2/skMCT5bR8UoWaufooQvYq6SyPcRAU4BtdquZRiBT4U5f+4pwNTxSvey7ki50yc1tG49Per/0zA4O6Tlpv8x7Red6m1bCNHt7+Z5nSl3RX/QYyAEUX1a28VcYmR41Osy+o2OUCXYdUAphDaHo4/8rbKTJhlu8jEcc1KoMXAKjgaVZtG/v5ltx6AXY0CAwEAAaMvMC0wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUQxFG602h1cG+pnyvJoy9pGJJoCswDQYJKoZIhvcNAQEFBQADggEBAGvtCbzGNBUJPLICth3mLsX0Z4z8T8iu4tyoiuAshP/Ry/ZBnFnXmhD8vwgMZ2lTgUWwlrvlgN+fAtYKnwFO2G3BOCFw96Nm8So9sjTda9CCZ3dhoH57F/hVMBB0K6xhklAc0b5ZxUpCIN92v/w+xZoz1XQBHe8ZbRHaP1HpRM4M7DJk2G5cgUCyu3UBvYS41sHvzrxQ3z7vIePRA4WF4bEkfX12gvny0RsPkrbVMXX1Rj9t6V7QXrbPYBAO+43JvDGYawxYVvLhz+BJ45x50GFQmHszfY3BR9TPK8xmMmQwtIvLu1PMttNCs7niCYkSiUv2sc2mlq1i3IashGkkgmo="
],
"n": "yeNlzlub94YgerT030codqEztjfU_S6X4DbDA_iVKkjAWtYfPHDzz_sPCT1Axz6isZdf3lHpq_gYX4Sz-cbe4rjmigxUxr-FgKHQy3HeCdK6hNq9ASQvMK9LBOpXDNn7mei6RZWom4wo3CMvvsY1w8tjtfLb-yQwJPltHxShZq5-ihC9irpLI9xEBTgG12q5lGIFPhTl_7inA1PFK97LuSLnTJzW0bj096v_TMDg7pOWm_zHtF53qbVsI0e3v5nmdKXdFf9BjIARRfVrbxVxiZHjU6zL6jY5QJdh1QCmENoejj_ytspMmGW7yMRxzUqgxcAqOBpVm0b-_mW3HoBdjQ",
"e": "AQAB",
"x5t": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg"
}
},
{
"kid":"internal-123456",
"use":"sig",
"x5c":["MIIG1TCCBL2gAwIBAgIIFvMVGp6t\/cMwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCR0IxIDAeBgNVBAoMF1N0YW5kYXJkIENoYXJ0ZXJlZCBCYW5rMTUwMwYDVQQDDCxTdGFuZGFyZCBDaGFydGVyZWQgQmFuayBTaWduaW5nIENBIEcxIC0gU0hBMjAeFw0xODEwMTAxMTI2MzVaFw0yMjEwMTAxMTI2MzVaMIG9MQswCQYDVQQGEwJTRzESMBAGA1UECAwJU2luZ2Fwb3JlMRIwEAYDVQQHDAlTaW5nYXBvcmUxIDAeBgNVBAoMF1N0YW5kYXJkIENoYXJ0ZXJlZCBCYW5rMRwwGgYDVQQLDBNGb3VuZGF0aW9uIFNlcnZpY2VzMSgwJgYDVQQDDB9pbnRlcm5hbC1nYXRld2F5LWp3dC5hcGkuc2MubmV0MRwwGgYJKoZIhvcNAQkBFg1BUElQU1NAc2MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArVWBoIi3IJ4nOWXu7\/SDxczqMou1B+c4c2FdQrOXrK31HxAaz4WEtma9BLXFdFHJ5mCCPIvdUcVxxnCynqhMOkZ\/a7acQbUD9cDzI8isMB9JL7VooDw0CctxHxffjqQQVIEhC2Q7zsM1pQayR7cl+pbBlvHIoRxq2n1B0fFvfoiosjf4kDiCpgHdM+v5Hw9aVYmUbroHxmQWqhB0iRTJQPPLZqqQVC50A1Q\/96gkwoODyotc46Uy9wYEpdGrtDG\/thWay3fmMsjpWR0U25xFIrxTrfCGBblYpD7juukWWml2E9rtE2rHgUxbymxXjEw7xrMwcGrhOGyqwoBqJy1JVwIDAQABo4ICLTCCAikwZAYIKwYBBQUHAQEEWDBWMFQGCCsGAQUFBzABhkhodHRwOi8vY29yZW9jc3AuZ2xvYmFsLnN0YW5kYXJkY2hhcnRlcmVkLmNvbS9lamJjYS9wdWJsaWN3ZWIvc3RhdHVzL29jc3AwHQYDVR0OBBYEFIinW4BNDeVEFcuLf8YjZjtySoW9MAwGA1UdEwEB\/wQCMAAwHwYDVR0jBBgwFoAUfNZMoZi33nKrcmVU3TFVQnuEi\/4wggFCBgNVHR8EggE5MIIBNTCCATGggcKggb+GgbxodHRwOi8vY29yZWNybC5nbG9iYWwuc3RhbmRhcmRjaGFydGVyZWQuY29tL2VqYmNhL3B1YmxpY3dlYi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1jcmwmaXNzdWVyPUNOPVN0YW5kYXJkJTIwQ2hhcnRlcmVkJTIwQmFuayUyMFNpZ25pbmclMjBDQSUyMEcxJTIwLSUyMFNIQTIsTz1TdGFuZGFyZCUyMENoYXJ0ZXJlZCUyMEJhbmssQz1HQqJqpGgwZjE1MDMGA1UEAwwsU3RhbmRhcmQgQ2hhcnRlcmVkIEJhbmsgU2lnbmluZyBDQSBHMSAtIFNIQTIxIDAeBgNVBAoMF1N0YW5kYXJkIENoYXJ0ZXJlZCBCYW5rMQswCQYDVQQGEwJHQjAOBgNVHQ8BAf8EBAMCBsAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUAA4ICAQBtsoRlDHuOTDChcWdfdVUtRgP0U0ijDSeJi8vULN1rgYnqqJc4PdJno50aiu9MGlxY02O7HW7ZVD6QEG\/pqHmZ0sbWpb\/fumMgZSjP65IcGuS53zgcNtLYnyXyEv+v5T\/CK3bk4Li6tUW3ScJPUwVWwP1E0\/u6aBSb5k\/h4lTwS1o88ybS5pJOg6XutXByp991QQrrs7tp7fKNynjNZbFuG3J1e09X+zTfJOpjaDUofQTkt8IyMRI6Cs4wI1eZA+dAIL8B0n8ze1mRl1FOJqgdZrAQjoqZkCTnc0Il5VY\/dUXxGVg6D9e5pfck3FWT107K9\/5EZoxytpqYXFCjMXi5hx4YjK17OUgm82mZhvqkNdzF8Yq2vFuB3LPfyelESq99xFLykvinrVm1NtZKeDTT1Jq\/VvZt6stO\/tovq1RfJJcznpYcwOzxlnhGR6E+hxuBx7aDJzGf0JaoRxQILH1B2XV9WDI3HPYQsP7XtriX+QUJ\/aly28QkV48RmaGYCsly43YZu1MKudSsw+dhnbZzRsg\/aes3dzGW2x137bQPtux7k2LCSpsTXgedhOys28YoGlsoe8kUv0myAU4Stt+I3mrwO3BKUn+tJggvlDiiiyT1tg2HiklyU\/2FxQkZRMeB0eRrXTpg3l9x2mpF+dDFxOMKszxwD2kgoEZgo6o58A=="],
"n":"nr9UsxnPVd21iuiGcIJ_Qli2XVlAZe5VbELA1hO2-L4k5gI4fjHZ3ysUcautLpbOYogOQgsnlpsLrCmvNDvBDVzVp2nMbpguJlt12vHSP1fRJJpipGQ8qU-VaXsC4OjOQf3H9ojAU5Vfnl5gZ7kVCd8g4M29l-IRyNpxE-Ccxc2Y7molsCHT6GHLMMBVsd11JIOXMICJf4hz2YYkQ1t7C8SaB2RFRPuGO5Mn6mfAnwdmRera4TBz6_pIPPCgCbN8KOdJItWkr9F7Tjv_0nhh-ZVlQvbQ9PXHyKTj00g3IYUlbZIWHm0Ley__fzNZk2dyAAVjNA2QSzTZJc33MQx1pQ",
"e":"AQAB",
"x5t":"-qC0akuyiHTV5aFsKVWM9da7lzq6DLrj09I",
"alg":"RS256",
"kty":"RSA"
}
]})";

std::string token = "eyJraWQiOiJpbnRlcm5hbC1nYXRld2F5LWp3dC5hcGkuc2MubmV0IiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1Mzk3NjcwMTUsImlhdCI6MTUzOTc2Njk5MiwiaXNzIjoia29uZyIsImh0dHA6XC9cL3dzbzIub3JnXC9nYXRld2F5XC9zdWJzY3JpYmVyIjoidXZ0dXNlcjJAY2FyYm9uLnN1cGVyIiwib3JpZ2luYWxfaXNzIjoiaHR0cDpcL1wvd3NvMi5vcmdcL2dhdGV3YXkiLCJzdWIiOiJ1dnR1c2VyMkBjYXJib24uc3VwZXIiLCJodHRwOlwvXC93c28yLm9yZ1wvZ2F0ZXdheVwvZW5kdXNlciI6InV2dHVzZXIyQGNhcmJvbi5zdXBlciIsImp0aSI6IjI0NmJkZTlhLWQ4OGQtNGRlZC1hODhmLTRhMTNhOWJmODQ4ZiIsImh0dHA6XC9cL3dzbzIub3JnXC9nYXRld2F5XC9hcHBsaWNhdGlvbm5hbWUiOiJ1dnR1c2VyMl9hcHBfMSIsImV4cCI6MTUzOTc2NzkxNX0.foxbo6C30yr_wkF-5EkgtYUMG-4SXNfRsmewdT6MbE-RXVkIPkVk8kDP41yRXmnk4OxburCqawiGlzzEhfHoFf0qv0qZEmwEXSdcyRw-czZTs6ACjWYe8kejOCVmpvUrq01NgOhTwgVg6pv93BlcmNY--zytjx_9hlVm5SS1lZ0I21n45BIWu5JvBD51TZXEURb_XhL7RcF9I8mfzrRpB2fSHW38gj-nogsdOPA_y3S-hJKylmmaqmaQgTF-jP-gYr6eqKyGPVwc6fLZ5zqAup59SefdPEY23-WWmHzj968jlsDSEiCp_YiYTnF3tHVLFWDsrprYKwNb0_p95tBmPA";

auto decoded_jwt = jwt::decode(token);
auto jwkskeys = jwt::parse_jwks_keys(publicKey);
auto jwks = jwkskeys.get_jwks(decoded_jwt.get_key_id());

auto issuer = decoded_jwt.get_issuer();
auto x5c = jwks.get_x5c();

if (!x5c.empty() && !issuer.empty()) {
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::rs256(jwt::helper::convert_base64_der_to_pem(x5c), "", "", ""))
.with_issuer(issuer)
.leeway(10000000000UL); // value in seconds, add some to compensate timeout
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, a simple token with no expiration is possible?


std::error_code err;
verifier.verify(decoded_jwt, err);

jwt::error::throw_if_error(err);
dtiukalov marked this conversation as resolved.
Show resolved Hide resolved
}

}
176 changes: 176 additions & 0 deletions include/jwt-cpp/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -2668,6 +2668,162 @@ namespace jwt {
}
}
};

template<typename json_traits>
class jwks {
dtiukalov marked this conversation as resolved.
Show resolved Hide resolved
using basic_claim_t = basic_claim<json_traits>;
template<typename> friend class jwks_keys;
dtiukalov marked this conversation as resolved.
Show resolved Hide resolved

public:
JWT_CLAIM_EXPLICIT jwks(const typename json_traits::string_type& jwks_token) {
auto parse_claims = [](const typename json_traits::string_type& str) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this block seems to be copy pasted from the original

📓 for myself to look into a small refactor!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, and it costs an extra move, good point to refactor.

using basic_claim_t = basic_claim<json_traits>;
std::unordered_map<typename json_traits::string_type, basic_claim_t> res;
typename json_traits::value_type val;
if (!json_traits::parse(val, str))
throw std::runtime_error("Invalid json");

for (const auto& e : json_traits::as_object(val)) {
res.emplace(e.first, basic_claim_t(e.second));
}

return res;
};

jwks_claims = parse_claims(jwks_token);
}

/**
* Get algorithm claim
* \return algorithm as string
* \throw std::runtime_error If claim was not present
* \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token)
*/
typename json_traits::string_type get_algorithm() const { return get_jwks_claim("alg").as_string(); }

/**
* Get key id claim
* \return key id as string
* \throw std::runtime_error If claim was not present
* \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token)
*/
typename json_traits::string_type get_key_id() const { return get_jwks_claim("kid").as_string(); }

/**
* Get x5c claim
* \return x5c as string
* \throw std::runtime_error If claim was not present
* \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token)
*/
typename json_traits::string_type get_x5c() const { return json_traits::as_string(get_jwks_claim("x5c").as_array().front()); } //do not use serialize() instead

/**
* Check if algortihm is present ("alg")
* \return true if present, false otherwise
*/
bool has_algorithm() const noexcept { return has_jwks_claim("alg"); }

/**
* Check if key id is present ("kid")
* \return true if present, false otherwise
*/
bool has_key_id() const noexcept { return has_jwks_claim("kid"); }

/**
* Check if x5c is present ("x5c")
* \return true if present, false otherwise
*/
bool has_x5c_id() const noexcept { return has_jwks_claim("x5c"); }

/**
* Check if a jwks claim is present
* \return true if claim was present, false otherwise
*/
bool has_jwks_claim(const typename json_traits::string_type& name) const noexcept { return jwks_claims.count(name) != 0; }

/**
* Get jwks claim
* \return Requested claim
* \throw std::runtime_error If claim was not present
*/
basic_claim_t get_jwks_claim(const typename json_traits::string_type& name) const {
if (!has_jwks_claim(name)) {
typename json_traits::string_type error_msg = "claim \"";
error_msg += name;
error_msg += "\" not found";
throw std::runtime_error(error_msg.c_str());
dtiukalov marked this conversation as resolved.
Show resolved Hide resolved
}
return jwks_claims.at(name);
}

bool empty() const noexcept { return jwks_claims.empty(); }

protected:
JWT_CLAIM_EXPLICIT jwks(const typename json_traits::value_type& claim) {
for (const auto& e : json_traits::as_object(claim)) {
jwks_claims.emplace(e.first, basic_claim_t(e.second));
}
}

private:
std::unordered_map<typename json_traits::string_type, basic_claim_t> jwks_claims;
};

template<typename json_traits>
class jwks_keys {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this "parser" could be a static method that returns a vector, this was the implementation is not limited on KIDs being used in all the keys

Thoughts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_vector() is added

using basic_claim_t = basic_claim<json_traits>;
using jwks_t = jwks<json_traits>;

public:

JWT_CLAIM_EXPLICIT jwks_keys(const typename json_traits::string_type& jwks_keys_token) {
auto parse_claims = [](const typename json_traits::string_type& str) {
std::unordered_map<typename json_traits::string_type, jwks_t> res;
typename json_traits::value_type val;
if (!json_traits::parse(val, str))
throw std::runtime_error("Invalid json");

for (const auto& k : json_traits::as_object(val)) {
for (const typename json_traits::value_type& item : json_traits::as_array(k.second)) {
jwks_t jwks_entry(item);
res.emplace(jwks_entry.get_key_id(), std::move(jwks_entry));
dtiukalov marked this conversation as resolved.
Show resolved Hide resolved
}
}

return res;
};

jwks_keys_claims = parse_claims(jwks_keys_token);
}

/**
* Get jwks
* \return Requested jwks by key_id
* \throw std::runtime_error If jwks was not present
*/
jwks_t get_jwks(const typename json_traits::string_type& key_id) const {
if (!has_jwks(key_id)) {
typename json_traits::string_type error_msg = "jwks with key id \"";
error_msg += key_id;
error_msg += "\" not found";
throw std::runtime_error(error_msg.c_str());
}

return jwks_keys_claims.at(key_id);
}

/**
* Check if a jwks with the kid is present
* \return true if jwks was present, false otherwise
*/
bool has_jwks(const typename json_traits::string_type& key_id) const noexcept { return jwks_keys_claims.count(key_id) != 0; }

bool empty() const noexcept { return jwks_keys_claims.empty(); }

private:

std::unordered_map<typename json_traits::string_type, jwks_t> jwks_keys_claims;
};

/**
* Create a verifier using the given clock
Expand Down Expand Up @@ -2720,6 +2876,16 @@ namespace jwt {
decoded_jwt<json_traits> decode(const typename json_traits::string_type& token) {
return decoded_jwt<json_traits>(token);
}

template<typename json_traits>
jwks<json_traits> parse_jwks(const typename json_traits::string_type& token) {
return jwks<json_traits>(token);
}

template<typename json_traits>
jwks_keys<json_traits> parse_jwks_keys(const typename json_traits::string_type& token) {
return jwks_keys<json_traits>(token);
}

#ifndef JWT_DISABLE_PICOJSON
struct picojson_traits {
Expand Down Expand Up @@ -2839,6 +3005,16 @@ namespace jwt {
decoded_jwt<picojson_traits> decode(const std::string& token, Decode decode) {
return decoded_jwt<picojson_traits>(token, decode);
}

inline
jwks<picojson_traits> parse_jwks(const picojson_traits::string_type& token) {
return jwks<picojson_traits>(token);
}

inline
jwks_keys<picojson_traits> parse_jwks_keys(const picojson_traits::string_type& token) {
return jwks_keys<picojson_traits>(token);
}
#endif
} // namespace jwt

Expand Down
2 changes: 1 addition & 1 deletion tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ set(TEST_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/Keys.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HelperTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TestMain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TokenFormatTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TokenTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/NlohmannTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/OpenSSLErrorTest.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/OpenSSLErrorTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/JwksTest.cpp)

add_executable(jwt-cpp-test ${TEST_SOURCES})

Expand Down
Loading