Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial claim verification #212

Closed
sirzooro opened this issue Feb 24, 2022 · 12 comments · Fixed by #229
Closed

Partial claim verification #212

sirzooro opened this issue Feb 24, 2022 · 12 comments · Fixed by #229
Assignees
Milestone

Comments

@sirzooro
Copy link
Contributor

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:

  • OS: RedHat 8
  • Compiler g++ 10.3.1
  • Version jwt-cpp v0.6.0
@prince-chrismc
Copy link
Collaborator

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

  • valid JSON schema
  • granted correct roles

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

struct is_subset_claim {
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
Copy link
Contributor Author

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:

  • 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;
  • 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;
  • jwt::basic_claim has no as_object() method. Fortunately to_json() could be used instead, at least for nlohmann::json;
  • I wonder if json traits class should also have extra methods to check if object field exists, and to get it. The same to check/get array element. They may be necessary for integration with some JSON libraries;
  • 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 add few catch sections for different kinds of errors (e.g. network error, jwt validation error, etc.);
  • library does not provide a way to report user-defined validation errors from custom validators;
  • and of course provide some example how to create and use custom validator :)

@sirzooro
Copy link
Contributor Author

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
Copy link
Owner

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
Copy link
Contributor Author

sirzooro commented Feb 28, 2022

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
Copy link
Collaborator

prince-chrismc commented Feb 28, 2022

  • 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
Copy link
Collaborator

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

@sirzooro
Copy link
Contributor Author

  • 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
Copy link
Collaborator

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

@prince-chrismc prince-chrismc changed the title [question] Partial claim verification Partial claim verification Mar 14, 2022
@prince-chrismc prince-chrismc self-assigned this Mar 14, 2022
@prince-chrismc prince-chrismc added this to the 0.6.1 milestone Mar 14, 2022
@prince-chrismc prince-chrismc modified the milestones: 0.6.1, 0.7.0 Sep 16, 2022
@JSoet
Copy link

JSoet commented Sep 26, 2024

@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:
#96 (comment)

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
Copy link
Collaborator

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
Copy link

JSoet commented Oct 3, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants