Skip to content

Commit

Permalink
Add JWKS parsing (#146)
Browse files Browse the repository at this point in the history
* Add JWKS parsing
* Add more unit-tests, modify get_x5c to return an array per review-comments
* Add accessors for remaining jwk fields
* Apply suggestions from code review

Co-authored-by: Chris Mc <[email protected]>
Co-authored-by: Dominik Thalhammer <[email protected]>
  • Loading branch information
3 people authored Jul 1, 2021
1 parent 60122bb commit ae8d1ef
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 1 deletion.
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)
48 changes: 48 additions & 0 deletions example/jwks-verify.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#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_key_value();

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);
}

}
279 changes: 279 additions & 0 deletions include/jwt-cpp/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@

#include <algorithm>
#include <chrono>
#include <iterator>
#include <memory>
#include <set>
#include <system_error>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>

#if __cplusplus >= 201402L
#ifdef __has_include
Expand Down Expand Up @@ -2873,6 +2875,254 @@ namespace jwt {
}
};

/**
* \brief JSON Web Key
*
* https://tools.ietf.org/html/rfc7517
*
* A JSON object that represents a cryptographic key. The members of
* the object represent properties of the key, including its value.
*/
template<typename json_traits>
class jwk {
using basic_claim_t = basic_claim<json_traits>;
const details::map_of_claims<json_traits> jwk_claims;

public:
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::string_type& str)
: jwk_claims(details::map_of_claims<json_traits>::parse_claims(str)) {}

JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json)
: jwk_claims(json_traits::as_object(json)) {}

/**
* Get key type claim
*
* This returns the general type (e.g. RSA or EC), not a specific algorithm value.
* \return key type 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_type() const { return get_jwk_claim("kty").as_string(); }

/**
* Get public key usage claim
* \return usage parameter 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_use() const { return get_jwk_claim("use").as_string(); }

/**
* Get key operation types claim
* \return key operation types as a set of strings
* \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 basic_claim_t::set_t get_key_operations() const { return get_jwk_claim("key_ops").as_set(); }

/**
* 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 an array
* \throw std::runtime_error If claim was not present
* \throw std::bad_cast Claim was present but not a array (Should not happen in a valid token)
*/
typename json_traits::array_type get_x5c() const { return get_jwk_claim("x5c").as_array(); };

/**
* Get X509 URL claim
* \return x5u 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_x5u() const { return get_jwk_claim("x5u").as_string(); };

/**
* Get X509 thumbprint claim
* \return x5t 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_x5t() const { return get_jwk_claim("x5t").as_string(); };

/**
* Get X509 SHA256 thumbprint claim
* \return x5t#S256 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_x5t_sha256() const { return get_jwk_claim("x5t#S256").as_string(); };

/**
* Get x5c claim as a string
* \return x5c as an 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_key_value() const {
auto x5c_array = get_jwk_claim("x5c").as_array();
if (x5c_array.size() == 0) throw error::claim_not_present_exception();

return json_traits::as_string(x5c_array.front());
};

/**
* Check if a key type is present ("kty")
* \return true if present, false otherwise
*/
bool has_key_type() const noexcept { return has_jwk_claim("kty"); }

/**
* Check if a public key usage indication is present ("use")
* \return true if present, false otherwise
*/
bool has_use() const noexcept { return has_jwk_claim("use"); }

/**
* Check if a key operations parameter is present ("key_ops")
* \return true if present, false otherwise
*/
bool has_key_operations() const noexcept { return has_jwk_claim("key_ops"); }

/**
* 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 X509 URL is present ("x5u")
* \return true if present, false otherwise
*/
bool has_x5u() const noexcept { return has_jwk_claim("x5u"); }

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

/**
* Check if a X509 thumbprint is present ("x5t")
* \return true if present, false otherwise
*/
bool has_x5t() const noexcept { return has_jwk_claim("x5t"); }

/**
* Check if a X509 SHA256 thumbprint is present ("x5t#S256")
* \return true if present, false otherwise
*/
bool has_x5t_sha256() const noexcept { return has_jwk_claim("x5t#S256"); }

/**
* 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.has_claim(name);
}

/**
* 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 {
return jwk_claims.get_claim(name);
}

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

/**
* \brief JWK Set
*
* https://tools.ietf.org/html/rfc7517
*
* A JSON object that represents a set of JWKs. The JSON object MUST
* have a "keys" member, which is an array of JWKs.
*
* This container takes a JWKs and simplifies it to a vector of JWKs
*/
template<typename json_traits>
class jwks {
public:
using jwk_t = jwk<json_traits>;
using jwt_vector_t = std::vector<jwk_t>;
using iterator = typename jwt_vector_t::iterator;
using const_iterator = typename jwt_vector_t::const_iterator;

JWT_CLAIM_EXPLICIT jwks(const typename json_traits::string_type& str) {
typename json_traits::value_type val;
if (!json_traits::parse(val, str)) throw error::invalid_json_exception();

const details::map_of_claims<json_traits> jwks_json = json_traits::as_object(val);
if (!jwks_json.has_claim("keys")) throw error::invalid_json_exception();

auto jwk_list = jwks_json.get_claim("keys").as_array();
std::transform(jwk_list.begin(), jwk_list.end(), std::back_inserter(jwk_claims),
[](const typename json_traits::value_type& val) { return jwk_t{val}; });
}

iterator begin() { return jwk_claims.begin(); }
iterator end() { return jwk_claims.end(); }
const_iterator cbegin() const { return jwk_claims.begin(); }
const_iterator cend() const { return jwk_claims.end(); }
const_iterator begin() const { return jwk_claims.begin(); }
const_iterator end() const { return jwk_claims.end(); }

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

/**
* 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 {
const auto maybe = find_by_kid(key_id);
if (maybe == end()) throw error::claim_not_present_exception();
return *maybe;
}

private:
jwt_vector_t jwk_claims;

const_iterator find_by_kid(const typename json_traits::string_type& key_id) const noexcept {
return std::find_if(cbegin(), cend(), [key_id](const jwk_t& jwk) {
if (!jwk.has_key_id()) { return false; }
return jwk.get_key_id() == key_id;
});
}
};

/**
* Create a verifier using the given clock
* \param c Clock instance to use
Expand Down Expand Up @@ -2923,6 +3173,16 @@ namespace jwt {
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 {
using value_type = picojson::value;
Expand Down Expand Up @@ -3024,6 +3284,25 @@ namespace jwt {
decoded_jwt<picojson_traits> decode(const std::string& token, Decode decode) {
return decoded_jwt<picojson_traits>(token, decode);
}
/**
* Parse a jwk
* \param token JWK Token to parse
* \return Parsed JWK
* \throw std::runtime_error Token is not in correct format
*/
inline jwk<picojson_traits> parse_jwk(const picojson_traits::string_type& token) {
return jwk<picojson_traits>(token);
}

/**
* Parse a jwks
* \param token JWKs Token to parse
* \return Parsed JWKs
* \throw std::runtime_error Token is not in correct format
*/
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

0 comments on commit ae8d1ef

Please sign in to comment.