Skip to content
Open
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
17 changes: 17 additions & 0 deletions .github/actions/install/reflect-cpp/action.yml
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions .github/workflows/cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions .github/workflows/traits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
2 changes: 2 additions & 0 deletions docs/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
15 changes: 15 additions & 0 deletions example/traits/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
83 changes: 83 additions & 0 deletions example/traits/reflect-cpp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#include "jwt-cpp/jwt.h"
#include "jwt-cpp/traits/reflect-cpp/traits.h"

#include <chrono>
#include <iostream>

int main() {
using sec = std::chrono::seconds;
using min = std::chrono::minutes;
using traits = jwt::traits::reflect_cpp;
using claim = jwt::basic_claim<traits>;

// 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<int64_t> 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<traits>()
.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<traits>(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<rfl::Generic>

if (auto api_res = obj.get("api"); api_res) { // rfl::Result<rfl::Generic>
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<traits>()
.allow_algorithm(jwt::algorithm::none{})
.with_issuer("auth.mydomain.io")
.with_audience("mydomain.io")
.with_claim("object", from_raw_json)
.verify(decoded);

return 0;
}
54 changes: 54 additions & 0 deletions include/jwt-cpp/traits/reflect-cpp/defaults.h
Original file line number Diff line number Diff line change
@@ -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<traits::reflect_cpp>;

/** Create a verifier using the default clock */
inline verifier<default_clock, traits::reflect_cpp> verify() {
return verify<default_clock, traits::reflect_cpp>(default_clock{});
}

/** Create a builder using the default clock */
inline builder<default_clock, traits::reflect_cpp> create() {
return builder<default_clock, traits::reflect_cpp>(default_clock{});
}

#ifndef JWT_DISABLE_BASE64
/** Decode a token (uses jwt-cpp’s built-in base64 if not disabled) */
inline decoded_jwt<traits::reflect_cpp> decode(const std::string& token) {
return decoded_jwt<traits::reflect_cpp>(token);
}
#endif

template<typename Decode>
decoded_jwt<traits::reflect_cpp> decode(const std::string& token, Decode decode) {
return decoded_jwt<traits::reflect_cpp>(token, decode);
}

/** Parse a JWK */
inline jwk<traits::reflect_cpp> parse_jwk(const traits::reflect_cpp::string_type& token) {
return jwk<traits::reflect_cpp>(token);
}

/** Parse a JWKS */
inline jwks<traits::reflect_cpp> parse_jwks(const traits::reflect_cpp::string_type& token) {
return jwks<traits::reflect_cpp>(token);
}

/** Verify context type alias (for advanced verification ops) */
using verify_context = verify_ops::verify_context<traits::reflect_cpp>;

} // namespace jwt
93 changes: 93 additions & 0 deletions include/jwt-cpp/traits/reflect-cpp/traits.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#pragma once

#ifndef JWT_DISABLE_PICOJSON
#define JWT_DISABLE_PICOJSON
#endif

#include "jwt-cpp/jwt.h"

#include <rfl/Generic.hpp>
#include <rfl/json.hpp>
#include <stdexcept>
#include <variant>

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<class... Ts>
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<object_type>(variant)) throw std::bad_cast();
return std::get<object_type>(variant);
}

static array_type as_array(const value_type& val) {
const auto& variant = val.get();
if (!std::holds_alternative<array_type>(variant)) throw std::bad_cast();
return std::get<array_type>(variant);
}

static string_type as_string(const value_type& val) {
const auto& variant = val.get();
if (!std::holds_alternative<string_type>(variant)) throw std::bad_cast();
return std::get<string_type>(variant);
}

static integer_type as_integer(const value_type& val) {
const auto& variant = val.get();
if (!std::holds_alternative<integer_type>(variant)) throw std::bad_cast();
return std::get<integer_type>(variant);
}

static boolean_type as_boolean(const value_type& val) {
const auto& variant = val.get();
if (!std::holds_alternative<boolean_type>(variant)) throw std::bad_cast();
return std::get<boolean_type>(variant);
}

static number_type as_number(const value_type& val) {
const auto& variant = val.get();
if (!std::holds_alternative<number_type>(variant)) throw std::bad_cast();
return std::get<number_type>(variant);
}

static bool parse(value_type& out, string_type const& json) {
auto res = rfl::json::read<rfl::Generic>(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
20 changes: 20 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +62,7 @@ set(JWT_TESTER_CLANG_FLAGS -Weverything -Wno-c++98-compat -Wno-global-constructo
target_compile_options(
jwt-cpp-test PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/W4> $<$<CXX_COMPILER_ID:GNU>:${JWT_TESTER_GCC_FLAGS}>
$<$<CXX_COMPILER_ID:Clang>:${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
Expand All @@ -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
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:${CMAKE_DL_LIBS}>)

Expand Down
Loading