diff --git a/.github/actions/install/stephenberry-glaze/action.yaml b/.github/actions/install/stephenberry-glaze/action.yaml new file mode 100644 index 000000000..70eedd054 --- /dev/null +++ b/.github/actions/install/stephenberry-glaze/action.yaml @@ -0,0 +1,19 @@ +name: Install Glaze +description: Install Glaze header-only library for building test applications +inputs: + version: + description: The desired Glaze version to install + required: false + default: "5.5.4" +runs: + using: composite + steps: + - run: | + cd /tmp + wget https://github.com/stephenberry/glaze/archive/refs/tags/v${{ inputs.version }}.tar.gz + tar -zxf /tmp/v${{ inputs.version }}.tar.gz + cd glaze-${{ inputs.version }} + cmake . -B build + cd build + sudo cmake --install . + shell: bash diff --git a/.github/workflows/jwt.yml b/.github/workflows/jwt.yml index 46eed9718..3177a6cd0 100644 --- a/.github/workflows/jwt.yml +++ b/.github/workflows/jwt.yml @@ -16,6 +16,7 @@ jobs: - uses: ./.github/actions/install/danielaparker-jsoncons - uses: ./.github/actions/install/boost-json - uses: ./.github/actions/install/open-source-parsers-jsoncpp + - uses: ./.github/actions/install/stephenberry-glaze - name: configure run: cmake --preset coverage diff --git a/.github/workflows/traits.yml b/.github/workflows/traits.yml index aedcb14af..7d66321be 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: "stephenberry-glaze", tag: "5.5.5", version: "v5.5.5" } steps: - uses: actions/checkout@v4 - uses: lukka/get-cmake@latest @@ -56,6 +57,11 @@ jobs: with: version: ${{matrix.target.tag}} + - if: matrix.target.name == 'stephenberry-glaze' + uses: ./.github/actions/install/stephenberry-glaze + with: + version: ${{matrix.target.tag}} + - name: test working-directory: example/traits run: | diff --git a/example/traits/CMakeLists.txt b/example/traits/CMakeLists.txt index 4734fabc7..f16bc4947 100644 --- a/example/traits/CMakeLists.txt +++ b/example/traits/CMakeLists.txt @@ -34,3 +34,9 @@ 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() + +find_package(glaze CONFIG) +if(TARGET glaze::glaze) + add_executable(stephenberry-glaze stephenberry-glaze.cpp) + target_link_libraries(stephenberry-glaze glaze::glaze jwt-cpp::jwt-cpp) +endif() diff --git a/example/traits/stephenberry-glaze.cpp b/example/traits/stephenberry-glaze.cpp new file mode 100644 index 000000000..5000b46e3 --- /dev/null +++ b/example/traits/stephenberry-glaze.cpp @@ -0,0 +1,65 @@ +#include "jwt-cpp/traits/stephenberry-glaze/traits.h" +#include +#include +#include +#include + +int main() { + using sec = std::chrono::seconds; + using min = std::chrono::minutes; + + using traits = jwt::traits::stephenberry_glaze; + using claim = jwt::basic_claim; + + // Parse raw JSON into claim + claim from_raw_json; + std::istringstream iss{R"##({"api":{"array":[1,2,3],"null":null}})##"}; + from_raw_json = jwt::basic_claim( + *glz::read_json(iss.str())); + // iss >> from_raw_json; // no >> for glaze + + // Example claim sets + claim::set_t list{"once", "twice"}; + std::vector big_numbers{727663072LL, 770979831LL, 427239169LL, 525936436LL}; + + // JWT creation + 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", claim(list)) + .set_payload_claim("array", claim{big_numbers.begin(), big_numbers.end()}) + .set_payload_claim("object", from_raw_json) + .sign(jwt::algorithm::none{}); + + // Decode + const auto decoded = jwt::decode(token); + + // Access array inside the payload object + const auto array = + traits::as_array(decoded.get_payload_claim("object").to_json().get_object()["api"].get_object()["array"]); + // std::cout << "payload /object/api/array = " << array << '\n'; + std::cout << "payload /object/api/array = [ "; + for (size_t i = 0; i < array.size(); ++i) { + std::cout << array[i].dump().value_or("error"); + if (i + 1 < array.size()) std::cout << ", "; + } + std::cout << " ]\n"; + + // Verification + 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/stephenberry-glaze/defaults.h b/include/jwt-cpp/traits/stephenberry-glaze/defaults.h new file mode 100644 index 000000000..facc15e7e --- /dev/null +++ b/include/jwt-cpp/traits/stephenberry-glaze/defaults.h @@ -0,0 +1,91 @@ +#ifndef JWT_CPP_STEPHENBERRY_GLAZE_DEFAULTS_H +#define JWT_CPP_STEPHENBERRY_GLAZE_DEFAULTS_H + +#ifndef JWT_DISABLE_PICOJSON +#define JWT_DISABLE_PICOJSON +#endif + +#include "traits.h" + +namespace jwt { + /** + * \brief a class to store a generic [glz::json_t](https://github.com/stephenberry/glaze/) value as claim + * + * This type is the specialization of the \ref basic_claim class which + * uses the standard template types. + */ + using claim = basic_claim; + + /** + * Create a verifier using the default clock + * \return verifier instance + */ + inline verifier verify() { + return verify(default_clock{}); + } + + /** + * Create a builder using the default clock + * \return builder instance to create a new token + */ + inline builder create() { + return builder(default_clock{}); + } + +#ifndef JWT_DISABLE_BASE64 + /** + * Decode a token + * \param token Token to decode + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + inline decoded_jwt decode(const std::string& token) { + return decoded_jwt(token); + } +#endif + + /** + * Decode a token + * \tparam Decode is callable, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64url decode and + * return the results. + * \param token Token to decode + * \param decode The token to parse + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const std::string& token, Decode decode) { + return decoded_jwt(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 parse_jwk(const traits::stephenberry_glaze::string_type& token) { + return jwk(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 parse_jwks(const traits::stephenberry_glaze::string_type& token) { + return jwks(token); + } + + /** + * This type is the specialization of the \ref verify_ops::verify_context class which + * uses the standard template types. + */ + using verify_context = verify_ops::verify_context; +} // namespace jwt + +#endif // JWT_CPP_STEPHENBERRY_GLAZE_DEFAULTS_H diff --git a/include/jwt-cpp/traits/stephenberry-glaze/traits.h b/include/jwt-cpp/traits/stephenberry-glaze/traits.h new file mode 100644 index 000000000..0d3bc42b2 --- /dev/null +++ b/include/jwt-cpp/traits/stephenberry-glaze/traits.h @@ -0,0 +1,84 @@ +#ifndef JWT_CPP_STEPHENBERRY_GLAZE_TRAITS_H +#define JWT_CPP_STEPHENBERRY_GLAZE_TRAITS_H + +#include "jwt-cpp/jwt.h" +#include + +namespace jwt { + /** + * \brief Namespace containing all the json_trait implementations for a jwt::basic_claim. + */ + namespace traits { + struct stephenberry_glaze { + using json = glz::json_t; + using value_type = json; // ← ключевое + using object_type = json::object_t; // map + using array_type = json::array_t; // vector + using string_type = std::string; + using number_type = double; + using integer_type = std::int64_t; + using boolean_type = bool; + + static jwt::json::type get_type(const value_type& val) { + using jwt::json::type; + + if (val.is_object()) return type::object; + if (val.is_array()) return type::array; + if (val.is_string()) return type::string; + if (val.is_boolean()) return type::boolean; + + // Если у json_t нет отдельного integer-типа: + if (val.is_number()) return type::number; + + if (val.is_null()) throw std::logic_error("invalid type: null"); + throw std::logic_error("invalid type"); + } + + static object_type as_object(const value_type& val) { + if (!val.is_object()) throw std::bad_cast(); + return std::get(val.data); + } + + static array_type as_array(const value_type& val) { + if (!val.is_array()) throw std::bad_cast(); + return std::get(val.data); + } + + static string_type as_string(const value_type& val) { + if (!val.is_string()) throw std::bad_cast(); + return std::get(val.data); + } + + static number_type as_number(const value_type& val) { + if (!val.is_number()) throw std::bad_cast(); + return std::get(val.data); + } + + static integer_type as_integer(const value_type& val) { + if (!val.is_number()) throw std::bad_cast(); + double d = std::get(val.data); + // optional: ensure it's an exact int64 + auto i = static_cast(d); + if (static_cast(i) != d) throw std::bad_cast(); + return i; + } + + static boolean_type as_boolean(const value_type& val) { + if (!val.is_boolean()) throw std::bad_cast(); + return std::get(val.data); + } + + static bool parse(value_type& val, string_type str) { + if (auto r = glz::read_json(val, str); r) { return false; } + return true; + } + + static string_type serialize(const value_type& val) { + if (auto r = glz::write_json(val); r) return *r; + throw std::runtime_error("serialize failed"); + } + }; + } // namespace traits +} // namespace jwt + +#endif // JWT_CPP_STEPHENBERRY_GLAZE_TRAITS_H \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6c7b75504..cc079b6f0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,11 @@ if(TARGET jsoncpp_static) list(APPEND TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/traits/OspJsoncppTest.cpp) endif() +find_package(glaze CONFIG) +if(TARGET glaze::glaze) + list(APPEND TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/traits/StephenberryGlazeTest.cpp) +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 @@ -69,6 +74,9 @@ else() if(TARGET jsoncpp_static) target_link_libraries(jwt-cpp-test PRIVATE jsoncpp_static) endif() + if(TARGET glaze::glaze) + target_link_libraries(jwt-cpp-test PRIVATE glaze::glaze) + endif() endif() target_link_libraries(jwt-cpp-test PRIVATE jwt-cpp nlohmann_json::nlohmann_json $<$>:${CMAKE_DL_LIBS}>) diff --git a/tests/traits/StephenberryGlazeTest.cpp b/tests/traits/StephenberryGlazeTest.cpp new file mode 100644 index 000000000..9773d12d9 --- /dev/null +++ b/tests/traits/StephenberryGlazeTest.cpp @@ -0,0 +1,113 @@ +#include "jwt-cpp/traits/stephenberry-glaze/traits.h" +#include +#include +#include +#include +#include + +// This is the expanded version of the Mustache template you pasted +TEST(StephenberryGlazeTest, BasicClaims) { + const auto string_claim = + jwt::basic_claim(jwt::traits::stephenberry_glaze::string_type("string")); + ASSERT_EQ(string_claim.get_type(), jwt::json::type::string); + + const auto array_claim = jwt::basic_claim( + std::set{"string", "string"}); + ASSERT_EQ(array_claim.get_type(), jwt::json::type::array); + + const auto integer_claim = jwt::basic_claim(159816816); + ASSERT_EQ(integer_claim.get_type(), jwt::json::type::number); // glaze has no integers in it, only doubles +} + +TEST(StephenberryGlazeTest, AudienceAsString) { + jwt::traits::stephenberry_glaze::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(StephenberryGlazeTest, SetArray) { + std::vector vect = {100, 20, 10}; + auto token = + jwt::create() + .set_payload_claim("test", jwt::basic_claim(vect.begin(), vect.end())) + .sign(jwt::algorithm::none{}); + ASSERT_EQ(token, "eyJhbGciOiJub25lIn0.eyJ0ZXN0IjpbMTAwLDIwLDEwXX0."); +} + +TEST(StephenberryGlazeTest, SetObject) { + std::istringstream iss{"{\"api-x\": [1]}"}; + jwt::basic_claim object; + // iss >> object; // THere is no operator >> for string streams in glz::json_t + object = jwt::basic_claim( + *glz::read_json(iss.str())); + ASSERT_EQ(object.get_type(), jwt::json::type::object); + + auto token = jwt::create() + .set_payload_claim("namespace", object) + .sign(jwt::algorithm::hs256("test")); + ASSERT_EQ(token, + "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lc3BhY2UiOnsiYXBpLXgiOlsxXX19.F8I6I2RcSF98bKa0IpIz09fRZtHr1CWnWKx2za-tFQA"); +} + +TEST(StephenberryGlazeTest, VerifyTokenHS256) { + jwt::traits::stephenberry_glaze::string_type token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; + + const auto decoded_token = jwt::decode(token); + const auto verify = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{"secret"}) + .with_issuer("auth0"); + verify.verify(decoded_token); +} + +TEST(StephenberryGlazeTest, 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_token = jwt::decode(token); + const auto verify = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{"secret"}) + .with_issuer("auth0"); + verify.verify(decoded_token); +} + +TEST(StephenberryGlazeTest, 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_token = jwt::decode(token); + const auto verify = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{"secret"}) + .with_issuer("auth0"); + ASSERT_THROW(verify.verify(decoded_token), jwt::error::token_verification_exception); + + std::error_code ec; + ASSERT_NO_THROW(verify.verify(decoded_token, ec)); + ASSERT_TRUE(!(!ec)); + ASSERT_EQ(ec.category(), jwt::error::token_verification_error_category()); + ASSERT_EQ(ec.value(), static_cast(jwt::error::token_verification_error::token_expired)); +}