Thalhammer / jwt-cpp

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

Partial claim verification #212

Closed sirzooro closed 2 years ago

sirzooro commented 2 years ago

Describe the impediment RedHat SSO allows configuration of roles for given service. When service gets its JWT token from SSO, it contains following claim with these roles:

"resource_access": {
   "my-service": {
     "roles": [
       "foo",
       "bar",
       "baz"
     ]
   }
 },

Trying to obtain I need to verify that service called "my-service" has role "bar", and ignore any other roles present in this claim. I found example where custom claim value is checked against JSON object, but this is not exactly my case - I need to check for this JSON structure ("resource_access", "my-service", "roles" elements) and verify that roles array contains proper element. Is there a way to do this using .with_claim("resource_access", ...)? Or do I have to use verifier.verify() first, and then manually extract and verify this claim piece by piece?

Desktop:

prince-chrismc commented 2 years ago

Hmm, the docs are lacking in this area, thanks for bringing this up!

When I built on top of jwt-cpp (v0.5.0), I had two application requirements

The first was easy, I was already using RapidJSON to do that. The second, before the verifier API, I just accessed the nested private claim (which I knew was there from the JSON schema) and looked up my two required roles.

We now have https://github.com/Thalhammer/jwt-cpp/blob/9d1a010fea3ed3293112edec9745dc435a85a849/include/jwt-cpp/jwt.h#L3012 so that might help

However only the "top level" claims are passed to the verifier, https://github.com/Thalhammer/jwt-cpp/blob/master/include/jwt-cpp/jwt.h#L3299

That's likely the best way to

verify this claim piece by piece

Hopefully that helps answer your questions 🤞

sirzooro commented 2 years ago

Thanks! With these hints I was able to successfully create my custom validator.

During my work I found few issues. They do not block me, but it would be nice if you could address them in the future:

sirzooro commented 2 years ago

I have noticed one more issue: error message for exception (returned by what()) thrown after jwt::error::token_verification_error::claim_value_missmatch error is reported does not include claim name which did not pass validation. This name would be useful during debugging of connection issues caused by incorrect token claims. This issue probably also applies to other generic claim verification errors.

Thalhammer commented 2 years ago

error is reported does not include claim name which did not pass validation

I agree that this would be helpfull, but thats hard to do. The way it works is that jwt-cpp builds a std::error_code and throws a std::system_error containing the error code. While this is usually a good design because it provides a very consistent and stl friendly interface, it makes adding runtime information to the message/exception near impossible. std::error_code only stores a int error code and a pointer to an error category. However it does not own the category (it needs to be valid for the lifetime of the error_code, that usually means a static singleton). If the message is requested it calls a method on the category passing the error code, which than returns a (also static) c style string. Including runtime info usually means some kind of hack or a memory leak.

sirzooro commented 2 years ago

error is reported does not include claim name which did not pass validation

I agree that this would be helpfull, but thats hard to do. The way it works is that jwt-cpp builds a std::error_code and throws a std::system_error containing the error code. While this is usually a good design because it provides a very consistent and stl friendly interface, it makes adding runtime information to the message/exception near impossible. std::error_code only stores a int error code and a pointer to an error category. However it does not own the category (it needs to be valid for the lifetime of the error_code, that usually means a static singleton). If the message is requested it calls a method on the category passing the error code, which than returns a (also static) c style string. Including runtime info usually means some kind of hack or a memory leak.

Custom exception class derived from std::system_error could have extra field to store claim name, and some method like claim_name() to get it. This would be enough for me to build meaningful log message, so you could leave default implementation of what().

prince-chrismc commented 2 years ago
  • code from rsa-create.cpp example does not compile - compiler complained about unknown jwt::claim type. It compiled after I removed this explicit type cast;

Hmm 🤔 that's very odd.... https://github.com/Thalhammer/jwt-cpp/runs/5156063246?check_suite_focus=true#step:7:10 it's Passing in CI.

  • I had to explicitly specify jwt::traits::nlohmann_json as a template parameter in few places. Please provide some #define to specify default traits type, it would simplify code a bit;

That explains above, it made for the default choice of picojson!

Did you try including https://github.com/Thalhammer/jwt-cpp/blob/master/include/jwt-cpp/traits/nlohmann-json/defaults.h? There's no docs so sorry for the confusion. Let me know if that helps

all jwt-cpp exception types are derived directly from std::system_error. It would be handy if you could use some library-specific base class instead, e.g. jwt::validation_error. This would allow to write bigger block of code, and at the end

This is a really interesting point! Feel free to open a new issue so we can track that idea 🙏

and of course provide some example how to create and use custom validator

If you can share you code, I would love to turn it into an example!

prince-chrismc commented 2 years ago

Thank you so much for the feedback, I am glad there was just enough flexibility to keep you moving forward 🚀

sirzooro commented 2 years ago
  • I had to explicitly specify jwt::traits::nlohmann_json as a template parameter in few places. Please provide some #define to specify default traits type, it would simplify code a bit;

That explains above, it made for the default choice of picojson!

Did you try including https://github.com/Thalhammer/jwt-cpp/blob/master/include/jwt-cpp/traits/nlohmann-json/defaults.h? There's no docs so sorry for the confusion. Let me know if that helps

Thanks, this helped and provided new default traits in most places. I still had to specify it for jwt::verify_ops::verify_context<>. I copied #include from jwt-cpp/example/traits/nlohmann-json.cpp, please update that example too.

all jwt-cpp exception types are derived directly from std::system_error. It would be handy if you could use some library-specific base class instead, e.g. jwt::validation_error. This would allow to write bigger block of code, and at the end

This is a really interesting point! Feel free to open a new issue so we can track that idea 🙏

Will do.

and of course provide some example how to create and use custom validator

If you can share you code, I would love to turn it into an example!

Here you are. It is mostly copy/paste of your examples. My code uses nlohmann::json directly to handle JSON stuff, so you would have to update it for PicoJson:

#include <jwt-cpp/traits/nlohmann-json/defaults.h>
#include <iostream>

int main()
{
    std::string rsa_priv_key = R"(-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4ZtdaIrd1BPIJ
tfnF0TjIK5inQAXZ3XlCrUlJdP+XHwIRxdv1FsN12XyMYO/6ymLmo9ryoQeIrsXB
XYqlET3zfAY+diwCb0HEsVvhisthwMU4gZQu6TYW2s9LnXZB5rVtcBK69hcSlA2k
ZudMZWxZcj0L7KMfO2rIvaHw/qaVOE9j0T257Z8Kp2CLF9MUgX0ObhIsdumFRLaL
DvDUmBPr2zuh/34j2XmWwn1yjN/WvGtdfhXW79Ki1S40HcWnygHgLV8sESFKUxxQ
mKvPUTwDOIwLFL5WtE8Mz7N++kgmDcmWMCHc8kcOIu73Ta/3D4imW7VbKgHZo9+K
3ESFE3RjAgMBAAECggEBAJTEIyjMqUT24G2FKiS1TiHvShBkTlQdoR5xvpZMlYbN
tVWxUmrAGqCQ/TIjYnfpnzCDMLhdwT48Ab6mQJw69MfiXwc1PvwX1e9hRscGul36
ryGPKIVQEBsQG/zc4/L2tZe8ut+qeaK7XuYrPp8bk/X1e9qK5m7j+JpKosNSLgJj
NIbYsBkG2Mlq671irKYj2hVZeaBQmWmZxK4fw0Istz2WfN5nUKUeJhTwpR+JLUg4
ELYYoB7EO0Cej9UBG30hbgu4RyXA+VbptJ+H042K5QJROUbtnLWuuWosZ5ATldwO
u03dIXL0SH0ao5NcWBzxU4F2sBXZRGP2x/jiSLHcqoECgYEA4qD7mXQpu1b8XO8U
6abpKloJCatSAHzjgdR2eRDRx5PMvloipfwqA77pnbjTUFajqWQgOXsDTCjcdQui
wf5XAaWu+TeAVTytLQbSiTsBhrnoqVrr3RoyDQmdnwHT8aCMouOgcC5thP9vQ8Us
rVdjvRRbnJpg3BeSNimH+u9AHgsCgYEA0EzcbOltCWPHRAY7B3Ge/AKBjBQr86Kv
TdpTlxePBDVIlH+BM6oct2gaSZZoHbqPjbq5v7yf0fKVcXE4bSVgqfDJ/sZQu9Lp
PTeV7wkk0OsAMKk7QukEpPno5q6tOTNnFecpUhVLLlqbfqkB2baYYwLJR3IRzboJ
FQbLY93E8gkCgYB+zlC5VlQbbNqcLXJoImqItgQkkuW5PCgYdwcrSov2ve5r/Acz
FNt1aRdSlx4176R3nXyibQA1Vw+ztiUFowiP9WLoM3PtPZwwe4bGHmwGNHPIfwVG
m+exf9XgKKespYbLhc45tuC08DATnXoYK7O1EnUINSFJRS8cezSI5eHcbQKBgQDC
PgqHXZ2aVftqCc1eAaxaIRQhRmY+CgUjumaczRFGwVFveP9I6Gdi+Kca3DE3F9Pq
PKgejo0SwP5vDT+rOGHN14bmGJUMsX9i4MTmZUZ5s8s3lXh3ysfT+GAhTd6nKrIE
kM3Nh6HWFhROptfc6BNusRh1kX/cspDplK5x8EpJ0QKBgQDWFg6S2je0KtbV5PYe
RultUEe2C0jYMDQx+JYxbPmtcopvZQrFEur3WKVuLy5UAy7EBvwMnZwIG7OOohJb
vkSpADK6VPn9lbqq7O8cTedEHttm6otmLt8ZyEl3hZMaL3hbuRj6ysjmoFKx6CrX
rK0/Ikt5ybqUzKCMJZg2VKGTxg==
-----END PRIVATE KEY-----)";

    auto testRoleClaim = nlohmann::json{
        {"my-service", {
            {"roles",  { "foo", "bar", "baz" }}
        }}
    };

    auto token = jwt::create()
                     .set_issuer("auth0")
                     .set_type("JWT")
                     .set_id("rsa-create-example")
                     .set_issued_at(std::chrono::system_clock::now())
                     .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{36000})
                     .set_payload_claim("resource_access", testRoleClaim)
                     .sign(jwt::algorithm::rs256("", rsa_priv_key, "", ""));

    std::cout << "token:\n" << token << std::endl;

    std::string rsa_pub_key = R"(-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4
yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9
83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs
WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT
69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8
AziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0
YwIDAQAB
-----END PUBLIC KEY-----)";

    auto decoded = jwt::decode(token);

    for (const auto& e : decoded.get_payload_claims())
        std::cout << e.first << " = " << e.second << std::endl;

    std::cout << std::endl;

    auto roleVerifier = [](const jwt::verify_ops::verify_context<jwt::traits::nlohmann_json>& ctx, std::error_code& ec)
    {
        auto c = ctx.get_claim(false, ec);
        if (ec)
            return;
        if (c.get_type() == jwt::json::type::object)
        {
            auto obj = c.to_json();
            try
            {
                auto roles = obj["my-service"]["roles"].get<nlohmann::json::array_t>();
                if (roles.end() == std::find(roles.begin(), roles.end(), "foo"))
                    ec = jwt::error::token_verification_error::claim_value_missmatch;
            }
            catch (const std::exception& ex)
            {
                ec = jwt::error::token_verification_error::claim_value_missmatch;
            }
        }
        else
            ec = jwt::error::token_verification_error::claim_type_missmatch;
    };

    auto verifier = jwt::verify()
        .allow_algorithm(jwt::algorithm::rs256(rsa_pub_key, "", "", ""))
        .with_issuer("auth0")
        .with_claim("resource_access", roleVerifier);

    try
    {
        verifier.verify(decoded);
        std::cout << "Success!" << std::endl;
    }
    catch (const std::exception& ex)
    {
        std::cout << "Error: " << ex.what() << std::endl;
    }

    return 0;
}
prince-chrismc commented 2 years ago

Thank you so much! Hopefully in the coming weeks we can fold in your suggestions 🤗

JSoet commented 1 month ago

@prince-chrismc

Sorry to comment on this old issue, I'm just wondering about one thing that you mentioned here:

The first was easy, I was already using RapidJSON to do that.

I was just wondering if this library supports using rapidjson as the json parser using the json traits, I was trying to find some info and found this other issue which seems to suggest it isn't supported: https://github.com/Thalhammer/jwt-cpp/issues/96#issuecomment-673352191

But then also found your comment above which seems to suggest that you were using rapidjson? Were you using rapidjson as the parser for jwt-cpp using the traits, or do you just mean you were using it separately just taking the json and parsing it again using rapidjson or something like that?

prince-chrismc commented 1 month ago

Ughs 4 jobs ago... 🤔 The later. We used picojson with jwt-cpp and Marshalled the data back and forth to use it with rapidjson for schema validation.

Rapidjson could be used with jwt-cpp, the effort required probably isn't worth it. Its likely just as much work to switch for another json library.

JSoet commented 3 weeks ago

Thanks for the reply, just wanted to check, yeah, we'll just use another library for use with jwt-cpp then.