Thalhammer / jwt-cpp

A header only library for creating and validating json web tokens in c++
https://thalhammer.github.io/jwt-cpp/
MIT License
865 stars 235 forks source link

static_assert causes Boost.JSON integration to fail #164

Closed secdec24 closed 3 years ago

secdec24 commented 3 years ago

I have written an example on how to integrate Boost.JSON with this library. However, the following static assert:

static_assert(
    details::is_valid_json_types<typename json_traits::value_type,
                                 typename json_traits::string_type,
                                 typename json_traits::object_type,
                                 typename json_traits::array_type>::value,
    "must staisfy json container requirements");

in jwt.h line 1872 causes every set_* function to error when creating a JWT.

The produced error is:

In template: static_assert failed due to requirement 'is_valid_json_object<boost::json::value, std::basic_string<char, std::char_traits<char>, std::allocator<char>>, boost::json::object>::value' "object_type must be a string_type to value_type container"

If I remove that static assert then I am able to successfully create a token. I have also been able to verify the resulting token using a separate library.

Before submitting a pull request to add this example to the code base, I would appreciate some discussion/insight as to why this is occurring.

Here is the complete example of the Boost.JSON integration.

#define JWT_DISABLE_PICOJSON

#include <iostream>
#include <boost/json.hpp>

#include "jwt.h"

namespace json = boost::json;           // from <boost/json/src.hpp>

struct boost_json_traits {
    // Type Specifications
    using value_type = json::value;      // The generic "value type" implementation, most libraries have one
    using object_type = json::object;    // The "map type" string to value
    using array_type = json::array;      // The "list type" array of values
    using string_type = std::string;      // The "list of chars", must be a narrow char
    using number_type = double;       // The "precision type"
    using integer_type = int64_t;       // The "integral type"
    using boolean_type = bool;         // The "boolean type"

    // Translation between the implementation notion of type, to the jwt::json::type equivalent
    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_double())
            return type::number;
        if (val.is_int64() || val.is_uint64())
            return type::integer;
        if (val.is_bool())
            return type::boolean;

        throw std::logic_error("invalid type");
    }

    // Conversion from generic value to specific type
    static object_type as_object(const value_type &val) {
        if (val.is_object()) return val.as_object();
        throw std::bad_cast();
    }
    static array_type as_array(const value_type &val) {
        if (!val.is_array()) return val.as_array();
        throw std::bad_cast();
    }
    static string_type as_string(const value_type &val) {
        if (!val.is_string()) return val.as_string().c_str();
        throw std::bad_cast();
    }
    static number_type as_number(const value_type &val) {
        if (!val.is_double()) return val.as_double();
        throw std::bad_cast();
    }
    static integer_type as_int(const value_type &val) {
        if (val.is_int64()) return val.as_int64();
        if (val.is_uint64()) return val.as_uint64();
        throw std::bad_cast();
    }
    static boolean_type as_bool(const value_type &val) {
        if (!val.is_bool()) return val.as_bool();
        throw std::bad_cast();
    }

    // serialization and parsing
    static bool parse(value_type &val, const string_type& str) {
        val = json::parse(str);
        return true;
    }
    static string_type serialize(const value_type &val) {
        return json::serialize(val);
    }
};

int main() {
    std::string pub_key = "YOUR PUB KEY HERE";
    std::string priv_key = "YOUR PRIV KEY HERE";

    auto token = jwt::create<boost_json_traits>()
            .set_issuer("iss")
            .set_type("JWT")
            .set_issued_at(std::chrono::system_clock::now())
            .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{3600})
            .set_payload_claim("token_use", "access")
            .sign(jwt::algorithm::rs256(pub_key, priv_key, "", ""));

    std::cout << "token:\n" << token << std::endl;
}
prince-chrismc commented 3 years ago

Yep, I had attempted to fix the verification https://github.com/prince-chrismc/jwt-cpp/tree/boost-traits

Thalhammer commented 3 years ago

I have never used boost json (or any boost library except asio a while back), but I think (actually I am pretty sure) the reason it fails is the following (from boost docs, highlight by me):

Instances of the object type are associative containers holding key and value pairs, where the key is a string_view and the mapped type is a value.

jwt-cpp expects a std::string as key which is why it fails to compile. The reason why everything still works fine is because std::string_view is implicitly convertible to std::string and has a very similar interface. So on the first time it hits any other function call or assignment, its converted to std::string and everything just works from there.

Given this, I think it's probably safe to change https://github.com/Thalhammer/jwt-cpp/blob/ac0424b115721e4066d2fb99f72ba0cd58759882/include/jwt-cpp/jwt.h#L1850 to use std::is_convertible instead of std::is_same, which should get rid of the error without being too lax.

prince-chrismc commented 3 years ago

I am not sure it's that easy, because the verification makes a lot of assumptions like adding to shrink strings. Boost.JSON has an internal implementation below C++17... which is not convertible to std::string and does not have the same API. See https://github.com/boostorg/json/blob/5b45854bb0422d9a37d846c018535cc1004e1449/doc/qbk/07_faq.qbk#L61

string also implements an improved interface that replaces extraneous overloads with ones that use __string_view__

As OP mentions, creation is easy but they did not try to verify it with jwt-cpp

If I remove that static assert then I am able to successfully create a token. I have also been able to verify the resulting token using a separate library.

From the tests I have done the verification is a lot harder than it looks. You can check this commit https://github.com/prince-chrismc/jwt-cpp/commit/85d59657ad779ec9a8c6e953adadb4d5ee50597a and this one too https://github.com/prince-chrismc/jwt-cpp/commit/d7585d70a6ce2a2916c6453977a434891dcd497c to see how I tricked it to working along with the short comming I found.

prince-chrismc commented 3 years ago

Is support Boost.JSON standalone with C++17 requirement a good enough solution?


https://github.com/boostorg/json/blob/351603c9b2dee7a80c8433841b50f7294131b25a/doc/qbk/03_06_conversion.qbk#L340

I got it working here

https://github.com/prince-chrismc/jwt-cpp/commit/a236d470fd98f91066fae607f637a31e1b3e7c84#diff-be2369ece29c0945fc04ffe13a39767b7841fc2fabd3df209a26b3bb381a2fb2R1849-R1851