diff --git a/.github/actions/install/reflect-cpp/action.yml b/.github/actions/install/reflect-cpp/action.yml new file mode 100644 index 000000000..4a9875b39 --- /dev/null +++ b/.github/actions/install/reflect-cpp/action.yml @@ -0,0 +1,17 @@ +name: Install reflect-cpp +description: Build & install reflect-cpp at a specific commit +inputs: + version: + description: Commit/tag to checkout + required: true +runs: + using: composite + steps: + - shell: bash + run: | + set -eux + git clone https://github.com/getml/reflect-cpp.git _thirdparty/reflect-cpp + cd _thirdparty/reflect-cpp + git checkout "${{ inputs.version }}" + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=20 + sudo cmake --build build --target install -j diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index d73187525..6b8e3fddb 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -273,3 +273,34 @@ jobs: run: | cd build ./tests/jwt-cpp-test + + reflect-cpp-tests: + name: Unit tests (reflect-cpp) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install toolchain & deps + run: | + sudo apt-get update + sudo apt-get install -y g++ libssl-dev + + - name: Install reflect-cpp + uses: ./.github/actions/install/reflect-cpp + with: + version: ca53b225bfc6887dd27e278820c34992cce37c6d + + - name: Configure + run: | + cmake -S . -B build \ + -DJWT_BUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=20 \ + -DJWT_SSL_LIBRARY=OpenSSL + + - name: Build tests + run: cmake --build build --target jwt-cpp-test -j + + - name: Run reflect-cpp tests + working-directory: build + run: ctest -j -R ReflectCppTest --output-on-failure diff --git a/.github/workflows/traits.yml b/.github/workflows/traits.yml index aedcb14af..60f56e767 100644 --- a/.github/workflows/traits.yml +++ b/.github/workflows/traits.yml @@ -18,6 +18,7 @@ jobs: - { name: "nlohmann-json", tag: "3.12.0", version: "v3.12.0" } - { name: "kazuho-picojson", tag: "111c9be5188f7350c2eac9ddaedd8cca3d7bf394", version: "111c9be" } - { name: "open-source-parsers-jsoncpp", tag: "1.9.6", version: "v1.9.6" } + - { name: "reflect-cpp", tag: "ca53b225bfc6887dd27e278820c34992cce37c6d", version: "ca53b22" } steps: - uses: actions/checkout@v4 - uses: lukka/get-cmake@latest @@ -56,6 +57,11 @@ jobs: with: version: ${{matrix.target.tag}} + - if: matrix.target.name == 'reflect-cpp' + uses: ./.github/actions/install/reflect-cpp + with: + version: ${{ matrix.target.tag }} + - name: test working-directory: example/traits run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index c22318810..cdd3533a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ set(JWT_SSL_LIBRARY_OPTIONS OpenSSL LibreSSL wolfSSL) set(JWT_SSL_LIBRARY OpenSSL CACHE STRING "Determines which SSL library to build with") set_property(CACHE JWT_SSL_LIBRARY PROPERTY STRINGS ${JWT_SSL_LIBRARY_OPTIONS}) -set(JWT_JSON_TRAITS_OPTIONS boost-json danielaparker-jsoncons kazuho-picojson nlohmann-json open-source-parsers-jsoncpp) +set(JWT_JSON_TRAITS_OPTIONS boost-json danielaparker-jsoncons kazuho-picojson nlohmann-json open-source-parsers-jsoncpp reflect-cpp) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") diff --git a/docs/traits.md b/docs/traits.md index caa12b8e5..18c6dae8c 100644 --- a/docs/traits.md +++ b/docs/traits.md @@ -11,12 +11,14 @@ For your convenience there are serval traits implementation which provide some p [![jsoncons][jsoncons]](https://github.com/danielaparker/jsoncons) [![boostjson][boostjson]](https://github.com/boostorg/json) [![jsoncpp][jsoncpp]](https://github.com/open-source-parsers/jsoncpp) +[![reflectcpp][reflectcpp]](https://github.com/getml/reflect-cpp) [picojson]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/kazuho-picojson/shields.json [nlohmann]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/nlohmann-json/shields.json [jsoncons]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/danielaparker-jsoncons/shields.json [boostjson]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/boost-json/shields.json [jsoncpp]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/open-source-parsers-jsoncpp/shields.json +[reflectcpp]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/reflect-cpp/shields.json In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. It's possible to directly include the traits defaults for the other JSON libraries. See the [traits examples](https://github.com/Thalhammer/jwt-cpp/tree/master/example/traits) for details. diff --git a/example/traits/CMakeLists.txt b/example/traits/CMakeLists.txt index 4734fabc7..1bfb0f514 100644 --- a/example/traits/CMakeLists.txt +++ b/example/traits/CMakeLists.txt @@ -34,3 +34,18 @@ if(TARGET jsoncpp_static) add_executable(open-source-parsers-jsoncpp open-source-parsers-jsoncpp.cpp) target_link_libraries(open-source-parsers-jsoncpp jsoncpp_static jwt-cpp::jwt-cpp) endif() + +# Build the reflect-cpp trait example only when the dep exists AND we're on C++20 or higher. +find_package(reflectcpp CONFIG) +if(TARGET reflectcpp::reflectcpp AND (NOT DEFINED CMAKE_CXX_STANDARD OR CMAKE_CXX_STANDARD GREATER_EQUAL 20)) + add_executable(reflect-cpp reflect-cpp.cpp) + target_link_libraries(reflect-cpp PRIVATE jwt-cpp::jwt-cpp reflectcpp::reflectcpp) + + # If the project hasn't set a global standard, request C++20 just for this example. + if(NOT DEFINED CMAKE_CXX_STANDARD) + target_compile_features(reflect-cpp PRIVATE cxx_std_20) + endif() +elseif(TARGET reflectcpp::reflectcpp) + # reflect-cpp is present but the user explicitly chose a pre-C++20 standard → skip the example. + message(STATUS "jwt-cpp: skipping reflect-cpp example (CMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} < 20)") +endif() diff --git a/example/traits/reflect-cpp.cpp b/example/traits/reflect-cpp.cpp new file mode 100644 index 000000000..449df0e20 --- /dev/null +++ b/example/traits/reflect-cpp.cpp @@ -0,0 +1,83 @@ +#include "jwt-cpp/jwt.h" +#include "jwt-cpp/traits/reflect-cpp/traits.h" + +#include +#include + +int main() { + using sec = std::chrono::seconds; + using min = std::chrono::minutes; + using traits = jwt::traits::reflect_cpp; + using claim = jwt::basic_claim; + + // Load a raw JSON object into a claim (reflect-cpp: parse -> wrap) + claim from_raw_json; + { + traits::value_type value; + // Mirrors the nlohmann example’s JSON + const auto* const json_text = R"##({"api":{"array":[1,2,3],"null":null}})##"; + if (!traits::parse(value, json_text)) { + std::cerr << "failed to parse raw json\n"; + return 1; + } + from_raw_json = claim{std::move(value)}; + } + + claim::set_t list{"once", "twice"}; + std::vector big_numbers{727663072LL, 770979831LL, 427239169LL, 525936436LL}; + + // Build an array claim from the big_numbers vector + traits::array_type arr; + arr.reserve(big_numbers.size()); + for (auto val : big_numbers) { + arr.emplace_back(val); + } + claim array_claim{traits::value_type{arr}}; + claim strings_claim{list.begin(), list.end()}; + + const auto time = jwt::date::clock::now(); + const auto token = jwt::create() + .set_type("JWT") + .set_issuer("auth.mydomain.io") + .set_audience("mydomain.io") + .set_issued_at(time) + .set_not_before(time) + .set_expires_at(time + min{2} + sec{15}) + .set_payload_claim("boolean", true) + .set_payload_claim("integer", 12345) + .set_payload_claim("precision", 12.3456789) + .set_payload_claim("strings", strings_claim) // <— fixed + .set_payload_claim("array", array_claim) + .set_payload_claim("object", from_raw_json) + .sign(jwt::algorithm::none{}); + + const auto decoded = jwt::decode(token); + + // Access payload /object/api/array using reflect-cpp's Result-returning get() + { + const auto obj_v = decoded.get_payload_claim("object").to_json(); // R::value_type + const auto obj = traits::as_object(obj_v); // rfl::Object + + if (auto api_res = obj.get("api"); api_res) { // rfl::Result + const auto api_obj = traits::as_object(api_res.value()); // nested object + + if (auto arr_res = api_obj.get("array"); arr_res) { + const auto& nested = traits::as_array(arr_res.value()); // vector-like + std::cout << "payload /object/api/array = " << rfl::json::write(nested) << '\n'; + } else { + std::cout << "payload /object/api/array missing\n"; + } + } else { + std::cout << "payload /object/api missing\n"; + } + } + + jwt::verify() + .allow_algorithm(jwt::algorithm::none{}) + .with_issuer("auth.mydomain.io") + .with_audience("mydomain.io") + .with_claim("object", from_raw_json) + .verify(decoded); + + return 0; +} diff --git a/include/jwt-cpp/traits/reflect-cpp/defaults.h b/include/jwt-cpp/traits/reflect-cpp/defaults.h new file mode 100644 index 000000000..044af4a3e --- /dev/null +++ b/include/jwt-cpp/traits/reflect-cpp/defaults.h @@ -0,0 +1,54 @@ +#pragma once + +#ifndef JWT_DISABLE_PICOJSON +#define JWT_DISABLE_PICOJSON +#endif + +#include "traits.h" + +namespace jwt { + + /** + * \brief a class to store a generic reflect-cpp (rfl::Generic) value as claim + * + * This type is the specialization of the \ref basic_claim class which + * uses the reflect-cpp JSON traits. + */ + using claim = basic_claim; + + /** Create a verifier using the default clock */ + inline verifier verify() { + return verify(default_clock{}); + } + + /** Create a builder using the default clock */ + inline builder create() { + return builder(default_clock{}); + } + +#ifndef JWT_DISABLE_BASE64 + /** Decode a token (uses jwt-cpp’s built-in base64 if not disabled) */ + inline decoded_jwt decode(const std::string& token) { + return decoded_jwt(token); + } +#endif + + template + decoded_jwt decode(const std::string& token, Decode decode) { + return decoded_jwt(token, decode); + } + + /** Parse a JWK */ + inline jwk parse_jwk(const traits::reflect_cpp::string_type& token) { + return jwk(token); + } + + /** Parse a JWKS */ + inline jwks parse_jwks(const traits::reflect_cpp::string_type& token) { + return jwks(token); + } + + /** Verify context type alias (for advanced verification ops) */ + using verify_context = verify_ops::verify_context; + +} // namespace jwt diff --git a/include/jwt-cpp/traits/reflect-cpp/traits.h b/include/jwt-cpp/traits/reflect-cpp/traits.h new file mode 100644 index 000000000..3358cc266 --- /dev/null +++ b/include/jwt-cpp/traits/reflect-cpp/traits.h @@ -0,0 +1,93 @@ +#pragma once + +#ifndef JWT_DISABLE_PICOJSON +#define JWT_DISABLE_PICOJSON +#endif + +#include "jwt-cpp/jwt.h" + +#include +#include +#include +#include + +namespace jwt::traits { + + struct reflect_cpp { + + using value_type = rfl::Generic; + using object_type = rfl::Generic::Object; + using array_type = rfl::Generic::Array; + using string_type = std::string; + using number_type = double; + using integer_type = int64_t; + using boolean_type = bool; + + template + struct variant_overloaded : Ts... { + using Ts::operator()...; + }; + + static jwt::json::type get_type(const value_type& val) { + return std::visit( + variant_overloaded{ + [](boolean_type const&) -> jwt::json::type { return jwt::json::type::boolean; }, + [](integer_type const&) -> jwt::json::type { return jwt::json::type::integer; }, + [](number_type const&) -> jwt::json::type { return jwt::json::type::number; }, + [](string_type const&) -> jwt::json::type { return jwt::json::type::string; }, + [](array_type const&) -> jwt::json::type { return jwt::json::type::array; }, + [](object_type const&) -> jwt::json::type { return jwt::json::type::object; }, + [](std::nullopt_t const&) -> jwt::json::type { throw std::logic_error("invalid type"); }, + }, + val.get()); + } + + static object_type as_object(const value_type& val) { + const auto& variant = val.get(); + if (!std::holds_alternative(variant)) throw std::bad_cast(); + return std::get(variant); + } + + static array_type as_array(const value_type& val) { + const auto& variant = val.get(); + if (!std::holds_alternative(variant)) throw std::bad_cast(); + return std::get(variant); + } + + static string_type as_string(const value_type& val) { + const auto& variant = val.get(); + if (!std::holds_alternative(variant)) throw std::bad_cast(); + return std::get(variant); + } + + static integer_type as_integer(const value_type& val) { + const auto& variant = val.get(); + if (!std::holds_alternative(variant)) throw std::bad_cast(); + return std::get(variant); + } + + static boolean_type as_boolean(const value_type& val) { + const auto& variant = val.get(); + if (!std::holds_alternative(variant)) throw std::bad_cast(); + return std::get(variant); + } + + static number_type as_number(const value_type& val) { + const auto& variant = val.get(); + if (!std::holds_alternative(variant)) throw std::bad_cast(); + return std::get(variant); + } + + static bool parse(value_type& out, string_type const& json) { + auto res = rfl::json::read(json); + if (res) { + out = *std::move(res); + return true; + } + return false; + } + + static std::string serialize(const value_type& val) { return rfl::json::write(val); } + }; + +} // namespace jwt::traits diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6c7b75504..b81505478 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,15 @@ if(TARGET jsoncpp_static) list(APPEND TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/traits/OspJsoncppTest.cpp) endif() +find_package(reflectcpp CONFIG QUIET) +if(TARGET reflectcpp::reflectcpp) + if(NOT DEFINED CMAKE_CXX_STANDARD OR CMAKE_CXX_STANDARD GREATER_EQUAL 20) + list(APPEND TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/traits/ReflectCppTest.cpp) + else() + message(STATUS "jwt-cpp: skipping reflect-cpp tests (CMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} < 20)") + endif() +endif() + add_executable(jwt-cpp-test ${TEST_SOURCES}) # NOTE: Don't use space inside a generator expression here, because the function prematurely breaks the expression into @@ -53,6 +62,7 @@ set(JWT_TESTER_CLANG_FLAGS -Weverything -Wno-c++98-compat -Wno-global-constructo target_compile_options( jwt-cpp-test PRIVATE $<$:/W4> $<$:${JWT_TESTER_GCC_FLAGS}> $<$:${JWT_TESTER_CLANG_FLAGS}>) + if(HUNTER_ENABLED) target_link_libraries(jwt-cpp-test PRIVATE GTest::gtest GTest::gtest_main) # Define a compile define to bypass openssl error tests @@ -69,7 +79,17 @@ else() if(TARGET jsoncpp_static) target_link_libraries(jwt-cpp-test PRIVATE jsoncpp_static) endif() + + # reflect-cpp: link only when available and only for C++20 builds + if(TARGET reflectcpp::reflectcpp AND (NOT DEFINED CMAKE_CXX_STANDARD OR CMAKE_CXX_STANDARD GREATER_EQUAL 20)) + # If the user didn't set a project-wide standard, request C++20 for this test target. + if(NOT DEFINED CMAKE_CXX_STANDARD) + target_compile_features(jwt-cpp-test PRIVATE cxx_std_20) + endif() + target_link_libraries(jwt-cpp-test PRIVATE reflectcpp::reflectcpp) + endif() endif() + target_link_libraries(jwt-cpp-test PRIVATE jwt-cpp nlohmann_json::nlohmann_json $<$>:${CMAKE_DL_LIBS}>) diff --git a/tests/traits/ReflectCppTest.cpp b/tests/traits/ReflectCppTest.cpp new file mode 100644 index 000000000..8e8898e53 --- /dev/null +++ b/tests/traits/ReflectCppTest.cpp @@ -0,0 +1,163 @@ +#include "jwt-cpp/traits/reflect-cpp/traits.h" + +#include + +using jwt::algorithm::hs256; + +TEST(ReflectCppTest, BasicClaims) { + const auto string = jwt::basic_claim(jwt::traits::reflect_cpp::string_type("string")); + ASSERT_EQ(string.get_type(), jwt::json::type::string); + + const auto array = jwt::basic_claim(std::set{"string", "string"}); + ASSERT_EQ(array.get_type(), jwt::json::type::array); + + const auto integer = jwt::basic_claim(159816816); + ASSERT_EQ(integer.get_type(), jwt::json::type::integer); +} + +TEST(ReflectCppTest, AudienceAsString) { + jwt::traits::reflect_cpp::string_type token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZXN0In0.WZnM3SIiSRHsbO3O7Z2bmIzTJ4EC32HRBKfLznHhrh4"; + auto decoded = jwt::decode(token); + + ASSERT_TRUE(decoded.has_algorithm()); + ASSERT_TRUE(decoded.has_type()); + ASSERT_FALSE(decoded.has_content_type()); + ASSERT_FALSE(decoded.has_key_id()); + ASSERT_FALSE(decoded.has_issuer()); + ASSERT_FALSE(decoded.has_subject()); + ASSERT_TRUE(decoded.has_audience()); + ASSERT_FALSE(decoded.has_expires_at()); + ASSERT_FALSE(decoded.has_not_before()); + ASSERT_FALSE(decoded.has_issued_at()); + ASSERT_FALSE(decoded.has_id()); + + ASSERT_EQ("HS256", decoded.get_algorithm()); + ASSERT_EQ("JWT", decoded.get_type()); + auto aud = decoded.get_audience(); + ASSERT_EQ(1, aud.size()); + ASSERT_EQ("test", *aud.begin()); +} + +TEST(ReflectCppTest, SetArray) { + // Build the array via the trait first + jwt::traits::reflect_cpp::array_type arr{100, 20, 10}; + + jwt::traits::reflect_cpp::value_type value(arr); + jwt::basic_claim array_claim(value); + + // Use the reflect-cpp trait for create() + auto token = + jwt::create().set_payload_claim("test", array_claim).sign(jwt::algorithm::none{}); + + ASSERT_EQ(token, "eyJhbGciOiJub25lIn0.eyJ0ZXN0IjpbMTAwLDIwLDEwXX0."); +} + +TEST(ReflectCppTest, SetObject) { + // Parse JSON into the trait's value_type + jwt::traits::reflect_cpp::value_type value; + ASSERT_TRUE(jwt::traits::reflect_cpp::parse(value, "{\"api-x\": [1]}")); + + // Wrap into a claim and verify type + jwt::basic_claim object(value); + ASSERT_EQ(object.get_type(), jwt::json::type::object); + + // Build a token using the reflect-cpp trait explicitly + auto token = jwt::create() + .set_payload_claim("namespace", object) + .sign(jwt::algorithm::hs256("test")); + + ASSERT_EQ(token, + "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lc3BhY2UiOnsiYXBpLXgiOlsxXX19.F8I6I2RcSF98bKa0IpIz09fRZtHr1CWnWKx2za-tFQA"); +} + +TEST(ReflectCppTest, VerifyTokenHS256) { + jwt::traits::reflect_cpp::string_type token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; + + const auto decoded = jwt::decode(token); + const auto verify = + jwt::verify().allow_algorithm(jwt::algorithm::hs256{"secret"}).with_issuer("auth0"); + + ASSERT_NO_THROW(verify.verify(decoded)); +} + +TEST(ReflectCppTest, VerifyTokenExpirationValid) { + const auto token = jwt::create() + .set_issuer("auth0") + .set_issued_at(std::chrono::system_clock::now()) + .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{3600}) + .sign(jwt::algorithm::hs256{"secret"}); + + const auto decoded = jwt::decode(token); + const auto verify = + jwt::verify().allow_algorithm(jwt::algorithm::hs256{"secret"}).with_issuer("auth0"); + + ASSERT_NO_THROW(verify.verify(decoded)); +} + +TEST(ReflectCppTest, VerifyTokenExpirationInValid) { + const auto token = jwt::create() + .set_issuer("auth0") + .set_issued_now() + .set_expires_in(std::chrono::seconds{3600}) + .sign(jwt::algorithm::hs256{"secret"}); + + const auto decoded = jwt::decode(token); + const auto verify = + jwt::verify().allow_algorithm(jwt::algorithm::hs256{"secret"}).with_issuer("auth0"); + + ASSERT_NO_THROW(verify.verify(decoded)); +} + +TEST(ReflectCppTest, VerifyTokenExpired) { + const auto token = jwt::create() + .set_issuer("auth0") + .set_issued_at(std::chrono::system_clock::now() - std::chrono::seconds{3601}) + .set_expires_at(std::chrono::system_clock::now() - std::chrono::seconds{1}) + .sign(jwt::algorithm::hs256{"secret"}); + + const auto decoded = jwt::decode(token); + const auto verify = + jwt::verify().allow_algorithm(jwt::algorithm::hs256{"secret"}).with_issuer("auth0"); + + ASSERT_THROW(verify.verify(decoded), jwt::error::token_verification_exception); + + std::error_code errcode; + ASSERT_NO_THROW(verify.verify(decoded, errcode)); + ASSERT_TRUE(errcode); // non-zero + ASSERT_EQ(errcode.category(), jwt::error::token_verification_error_category()); + ASSERT_EQ(errcode.value(), static_cast(jwt::error::token_verification_error::token_expired)); +} + +TEST(ReflectCppTest, VerifyArray) { + jwt::traits::reflect_cpp::string_type token = "eyJhbGciOiJub25lIn0.eyJ0ZXN0IjpbMTAwLDIwLDEwXX0."; + const auto decoded = jwt::decode(token); + + // Build array value_type via the trait + jwt::traits::reflect_cpp::array_type arr{100, 20, 10}; + + jwt::basic_claim array_claim{jwt::traits::reflect_cpp::value_type(arr)}; + + const auto verify = + jwt::verify().allow_algorithm(jwt::algorithm::none{}).with_claim("test", array_claim); + + ASSERT_NO_THROW(verify.verify(decoded)); +} + +TEST(ReflectCppTest, VerifyObject) { + jwt::traits::reflect_cpp::string_type token = + "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lc3BhY2UiOnsiYXBpLXgiOlsxXX19.F8I6I2RcSF98bKa0IpIz09fRZtHr1CWnWKx2za-tFQA"; + const auto decoded_token = jwt::decode(token); + + // Parse into reflect-cpp's JSON value, then wrap as a claim + jwt::traits::reflect_cpp::value_type value; + ASSERT_TRUE(jwt::traits::reflect_cpp::parse(value, "{\"api-x\": [1]}")); + jwt::basic_claim object_claim(value); + + // Use the reflect-cpp trait for verify(), too + const auto verify = + jwt::verify().allow_algorithm(hs256("test")).with_claim("namespace", object_claim); + + ASSERT_NO_THROW(verify.verify(decoded_token)); +}