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 all 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)
49 changes: 49 additions & 0 deletions example/jwks-verify.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#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 jwks = jwt::parse_jwks(publicKey);
auto jwk = jwks.get_jwk(decoded_jwt.get_key_id());

auto issuer = decoded_jwt.get_issuer();
auto x5c = jwk.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(60UL); // value in seconds, add some to compensate timeout

verifier.verify(decoded_jwt);
}

}
187 changes: 187 additions & 0 deletions include/jwt-cpp/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
#include <utility>
#include <type_traits>
#include <system_error>
#include <algorithm>
#include <vector>
#include <iterator>

#if __cplusplus >= 201402L
#ifdef __has_include
Expand Down Expand Up @@ -2668,6 +2671,170 @@ namespace jwt {
}
}
};

template<typename json_traits>
class jwk {
using basic_claim_t = basic_claim<json_traits>;

public:
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::string_type& jwk_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;
};

jwk_claims = parse_claims(jwk_token);
}

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

/**
* 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_jwk_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_jwk_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_jwk_claim("x5c").as_array().front()); } //do not use serialize() instead
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the consumer will/may need the array in larger systems as per the rfc 4.7

The PKIX certificate containing the key value MUST be the first
certificate. This MAY be followed by additional certificates, with
each subsequent certificate being the one used to certify the
previous one.

What do you think about having two functions?

typename json_traits::array_type get_x5c() const;
typename json_traits::string_type get_x5c_key_value() const;

Secondly, front might be a little dangerous! Calling front on an empty container is undefined

please throw if empty 💥


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

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

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

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

/**
* Get jwks claim
* \return Requested claim
* \throw std::runtime_error If claim was not present
*/
basic_claim_t get_jwk_claim(const typename json_traits::string_type& name) const {
if (!has_jwk_claim(name)) {
throw std::runtime_error("claim not found");
}
return jwk_claims.at(name);
}

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

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

template<typename json_traits>
class jwks {
using basic_claim_t = basic_claim<json_traits>;
using jwk_t = jwk<json_traits>;

public:

JWT_CLAIM_EXPLICIT jwks(const typename json_traits::string_type& jwks_token) {
auto parse_claims = [](const typename json_traits::string_type& str) {
std::unordered_map<typename json_traits::string_type, jwk_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)) {
size_t id = 0;
for (const typename json_traits::value_type& item : json_traits::as_array(k.second)) {
jwk_t jwk_entry(item);
if (jwk_entry.has_key_id())
res.emplace(jwk_entry.get_key_id(), std::move(jwk_entry));
else
res.emplace(std::to_string(id), std::move(jwk_entry));
++id;
}
}
Comment on lines +2784 to +2794
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep, so I would really like if we could leave the KID outside of the JWKS logic because it's important to support workflows that do not required them.

I will come back with some changes against your branch, I think the biggest limitation is the parsing or "map of claims"

I also see we don't check "keys" explicitly is the JWKS which would open use to a vulnerability


return res;
};

jwks_claims = parse_claims(jwks_token);
}

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

return jwks_claims.at(key_id);
}

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

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

std::vector<jwk_t> to_vector() {
std::vector<jwk_t> v_jwks_claims;
v_jwks_claims.reserve(jwks_claims.size());
for (const auto& element : jwks_claims)
v_jwks_claims.push_back(element.second);
return v_jwks_claims;
}

private:

std::unordered_map<typename json_traits::string_type, jwk_t> jwks_claims;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Only because i've implemented this and used a few different auth servers... I am really unsure about this being a map...

What if there are two keys neither have KIDs? One would be lost, most likely the new key that is starting to issue tokens.

};

/**
* Create a verifier using the given clock
Expand Down Expand Up @@ -2720,6 +2887,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>
jwk<json_traits> parse_jwk(const typename json_traits::string_type& token) {
return jwk<json_traits>(token);
}

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

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

inline
jwk<picojson_traits> parse_jwk(const picojson_traits::string_type& token) {
return jwk<picojson_traits>(token);
}

inline
jwks<picojson_traits> parse_jwks(const picojson_traits::string_type& token) {
return jwks<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