cpp-jwt

by arun11299

arun11299 / cpp-jwt

JSON Web Token library for C++

233 Stars 68 Forks Last release: 4 months ago (v1.3) MIT License 180 Commits 7 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

CPP-JWT

A C++14 library for JSON Web Tokens(JWT)



A little library built with lots of ❤︎ for working with JWT easier. By Arun Muralidharan.

Table of Contents

What is it ?

For the uninitiated, JSON Web Token(JWT) is a JSON based standard (RFC-7519) for creating assertions or access tokens that consists of some claims (encoded within the assertion). This assertion can be used in some kind of bearer authentication mechanism that the server will provide to clients, and the clients can make use of the provided assertion for accessing resources.

Few good resources on this material which I found useful are: Anatomy of JWT Learn JWT RFC 7519

Example

Lets dive into see a simple example of encoding and decoding in Python. Taking the example of pyjwt module from its docs.

  >>import jwt
  >>key = 'secret'
  >>
  >>encoded = jwt.encode({'some': 'payload'}, key, algorithm='HS256')
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
  >>
  >>decoded = jwt.decode(encoded, key, algorithms='HS256')
  {'some': 'payload'}

Now, lets look at our C++ code doing the same thing. ```cpp #include #include "jwt/jwt.hpp"

int main() { using namespace jwt::params;

auto key = "secret"; //Secret to use for the algorithm
//Create JWT object
jwt::jwt_object obj{algorithm("HS256"), payload({{"some", "payload"}}), secret(key)};

//Get the encoded string/assertion auto enc_str = obj.signature(); std::cout << enc_str << std::endl;

//Decode auto dec_obj = jwt::decode(enc_str, algorithms({"HS256"}), secret(key)); std::cout << dec_obj.header() << std::endl; std::cout << dec_obj.payload() << std::endl;

return 0;

} ```

It outputs:

  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg
  {"alg":"HS256","typ":"JWT"}
  {"some":"payload"}

Almost the same API, except for some ugliness here and there. But close enough!

Lets take another example in which we will see to add payload claim having type other than string. The payload function used in the above example to create jwt_object object can only take strings. For anything else, it will throw a compilation error.

For adding claims having values other than string, jwtobject class provides addclaim API. We will also see few other APIs in the next example. Make sure to read the comments :).

    #include 
    #include 
    #include 
    #include "jwt/jwt.hpp"

int main() {
  using namespace jwt::params;

  jwt::jwt_object obj{algorithm("HS256"), secret("secret"), payload({{"user", "admin"}})};

  //Use add_claim API to add claim values which are
  // _not_ strings.
  // For eg: `iat` and `exp` claims below.
  // Other claims could have been added in the payload
  // function above as they are just stringy things.
  obj.add_claim("iss", "arun.muralidharan")
     .add_claim("sub", "test")
     .add_claim("id", "a-b-c-d-e-f-1-2-3")
     .add_claim("iat", 1513862371)
     .add_claim("exp", std::chrono::system_clock::now() + std::chrono::seconds{10})
     ;

  //Use `has_claim` to check if the claim exists or not
  assert (obj.has_claim("iss"));
  assert (obj.has_claim("exp"));

  //Use `has_claim_with_value` to check if the claim exists
  //with a specific value or not.
  assert (obj.payload().has_claim_with_value("id", "a-b-c-d-e-f-1-2-3"));
  assert (obj.payload().has_claim_with_value("iat", 1513862371));

  //Remove a claim using `remove_claim` API.
  //Most APIs have an overload which takes enum class type as well
  //It can be used interchangeably with strings.
  obj.remove_claim(jwt::registered_claims::expiration);
  assert (not obj.has_claim("exp"));

  //Using `add_claim` with extra features.
  //Check return status and overwrite
  bool ret = obj.payload().add_claim("sub", "new test", false/*overwrite*/);
  assert (not ret);

  // Overwrite an existing claim
  ret = obj.payload().add_claim("sub", "new test", true/*overwrite*/);
  assert ( ret );

  assert (obj.payload().has_claim_with_value("sub", "new test"));

  return 0;
}

The jwtobject class is basically a composition of the JWT component classes, which are jwtheader & jwtpayload. For convenience jwtobject exposes only few important APIs to the user, the remaining APIs under jwtheader and jwtpayload can be accessed by calling jwtobject::header() and jwtobject::payload() APIs.

API Philosophy

I wanted to make the code easy to read and at the same time make most of the standard library and the modern features. It also uses some metaprogramming tricks to enforce type checks and give better error messages.

The design of

parameters
alleviates the pain of remembering positional arguments. Also makes the APIs more extensible for future enhancements.

The library has 2 sets of APIs for encoding and decoding: - API which takes an instance of std::errorcode These APIs will report the errors by setting the `errorcode`. This does not mean that these API would not throw. Memory allocation errors would still be thrown instead of setting the error_code. - API which throws exceptions All the errors would be thrown as exception.

Support

Algorithms and features supported - [x] HS256 - [x] HS384 - [x] HS512 - [x] RS256 - [x] RS384 - [x] RS512 - [x] ES256 - [x] ES384 - [x] ES512 - [x] Sign - [x] Verify - [x] iss (issuer) check - [x] sub (subject) check - [x] aud (audience) check - [x] exp (expiration time) check - [x] nbf (not before time) check - [x] iat (issued at) check - [x] jti (JWT id) check - [x] JWS header addition support. For eg "kid" support.

External Dependencies

  • OpenSSL (Version >= 1.0.2j) Might work with older version as well, but I did not check that.
  • Google Test Framework For running the tests
  • nlohmann JSON library The awesome JSON library :)

Thanks to...

- ben-collins JWT library
- Howard Hinnant for the stack allocator
- libstd++ code (I took the hashing code for string_view)

Compiler Support

Tested with clang-5.0 and g++-6.4. With issue#12, VS2017 is also supported.

Building the library

using conan

mkdir build
cd build
conan install .. --build missing
cmake ..
cmake --build . -j

using debian

sudo apt install nlohmann-json3-dev 
sudo apt install libgtest-dev
sudo apt install libssl-dev
mkdir build
cd build
cmake ..
cmake --build . -j

Consuming the library

this library is uses cmake as a build system. ```cmake

you can use cmake's
find_package
after installation or
add_subdirectory
when vendoring this repository

find_package(cpp-jwt REQUIRED)

or

addsubdirectory(thirdparty/cpp-jwt)

addexecutable(main main.cpp) targetlink_libraries(main cpp-jwt::cpp-jwt) ```

you can also use this library as a conan package, its available in the conan center: just add

cpp-jwt[>=1.2]
to your conanfile.txt

Parameters

There are two sets of parameters which can be used for creating

jwt_object
and for decoding. All the parameters are basically a function which returns an instance of a type which are modelled after ParameterConcept (see jwt::detail::meta::isparameterconcept).
  • jwt_object creation parameters

    • payload

    Used to populate the claims while creating the

    jwt_object
    instance.

    There are two overloads of this function: - Takes Initializer list of pair

    Easy to pass claims with string values which are all known at the time of object creation. Can be used like:

    cpp
      jwt_object obj {
        payload({
            {"iss", "some-guy"},
            {"sub", "something"},
            {"X-pld", "data1"}
          }),
          ... // Add other parameters
      };
    
    Claim values which are not strings/stringviews cannot be used. - Takes any type which models MappingConcept (see detail::meta::ismapping_concept)

    This overload can accept std::map or std::unordered_map like containers. Can be used like: ```cpp map m; m["iss"] = "some-guy"; m["sub"] = "something"; m["X-pld"] = "data1";

    jwtobject obj{ payload(std::move(m)), ... // Add other parameters }; //OR jwtobject obj{ payload(m), ... // Add other parameters }; ``` - secret

    Used to pass the key which could be some random string or the bytes of the PEM encoded public key file in PEM format (wrapped in -----BEGIN PUBLIC KEY----- block) as string. The passed string type must be convertible to jwt::string_view - algorithm

    Used to pass the type of algorithm to use for encoding. There are two overloads of this function: - Takes jwt::string_view

    Can pass the algorithm value in any case. It is case agnostic. - Takes value of type enum class jwt::algorithm - headers

    Used to populate fields in JWT header. It is very similar to

    payload
    function parameter. There are two overloads for this function which are similar to how payload function is. This parameter can be used to add headers other that alg and typ.

    Same as the case with payload, only string values can be used with this. For adding values of other data types, use addheader API of jwtheader class.

    For example adding

    kid
    header with other additional data fields.
    cpp
    jwt_object obj{
      algorithm("HS256"),
      headers({
        {"kid", "12-34-56"},
        {"xtra", "header"}
      })
      ... // Add other parameters
    };
    
  • Decoding parameters

    • algorithms

    This is a mandatory parameter which takes a sequence of algorithms (as string) which the user would like to permit when validating the JWT. The value in the header for "alg" would be matched against the provided sequence of values. If nothing matches InvalidAlgorithmError exception or InvalidAlgorithm error would be set based upon the API being used.

    There are two overloads for this function: - Takes initializer-list of string values - Takes in any type which satifies the SequenceConcept (see idetail::meta::issequenceconcept)

  jwt::decode(algorithms({"none", "HS256", "RS256"}), ...);

OR

std::vector<:string> algs{"none", "HS256", "RS256"}; jwt::decode(algorithms(algs), ...); </:string>

  • secret

    Optional parameter. To be supplied only when the algorithm used is not "none". Else would throw/set KeyNotPresentError / KeyNotPresent exception/error.

  • leeway

    Optional parameter. Used with validation of "Expiration" and "Not Before" claims. The value passed should be

    seconds
    to account for clock skew. Default value is
    0
    seconds.
  • verify

    Optional parameter. Suggests if verification of claims should be done or not. Takes a boolean value. By default verification is turned on.

  • issuer

    Optional parameter. Takes a string value. Validates the passed issuer value against the one present in the decoded JWT object. If the values do not match InvalidIssuerError or InvalidIssuer exception or error_code is thrown/set.

  • aud

    Optional parameter. Takes a string value. Validates the passed audience value against the one present in the decoded JWT object. If the values do not match InvalidAudienceError or InvalidAudience exception or error_code is thrown/set.

  • sub

    Optional parameter. Takes a string value. Validates the passed subject value against the one present in the decoded JWT object. If the values do not match InvalidSubjectError or InvalidSubject exception or error_code is thrown/set.

  • validate_iat

    Optional parameter. Takes a boolean value. Validates the IAT claim. Only checks whether the field is present and is of correct type. If not throws/sets InvalidIATError or InvalidIAT.

    Default value is false.

  • validate_jti

    Optional parameter. Takes a boolean value. Validates the JTI claim. Only checks for the presence of the claim. If not throws or sets InvalidJTIError or InvalidJTI.

    Default is false.

Claim Data Types

For the registered claim types the library assumes specific data types for the claim values. Using anything else is not supported and would result in runtime JSON parse error.

Claim                 |  Data Type
-----------------------------------
Expiration(exp)       |  uint64_t (Epoch time in seconds)
-----------------------------------
Not Before(nbf)       |  uint64_t (Epoch time in seconds)
-----------------------------------
Issuer(iss)           |  string
-----------------------------------
Audience(aud)         |  string
-----------------------------------
Issued At(iat)        |  uint64_t (Epoch time in seconds)
-----------------------------------
Subject(sub)          |  string
-----------------------------------
JTI(jti)              | 
-----------------------------------

Advanced Examples

We will see few complete examples which makes use of error code checks and exception handling. The examples are taken from the "tests" section. Users are requested to checkout the tests to find out more ways to use this library.

Expiration verification example (uses error_code): ```cpp

include

include

include "jwt/jwt.hpp"

int main() { using namespace jwt::params;

jwt::jwtobject obj{algorithm("HS256"), secret("secret")}; obj.addclaim("iss", "arun.muralidharan") .addclaim("exp", std::chrono::systemclock::now() - std::chrono::seconds{1}) ;

std::errorcode ec; auto encstr = obj.signature(ec); assert (!ec);

auto decobj = jwt::decode(encstr, algorithms({"HS256"}), ec, secret("secret"), verify(true)); assert (ec); assert (ec.value() == static_cast(jwt::VerificationErrc::TokenExpired));

return 0; } ```

Expiration verification example (uses exception): ```cpp

include

include

include "jwt/jwt.hpp"

int main() { using namespace jwt::params;

jwt::jwt_object obj{algorithm("HS256"), secret("secret")};

obj.addclaim("iss", "arun.muralidharan") .addclaim("exp", std::chrono::system_clock::now() - std::chrono::seconds{1}) ;

auto enc_str = obj.signature();

try { auto decobj = jwt::decode(encstr, algorithms({"HS256"}), secret("secret"), verify(true)); } catch (const jwt::TokenExpiredError& e) { //Handle Token expired exception here //... } catch (const jwt::SignatureFormatError& e) { //Handle invalid signature format error //... } catch (const jwt::DecodeError& e) { //Handle all kinds of other decode errors //... } catch (const jwt::VerificationError& e) { // Handle the base verification error. //NOTE: There are other derived types of verification errors // which will be discussed in next topic. } catch (...) { std::cerr << "Caught unknown exception\n"; }

return 0; } ```

Invalid issuer test(uses error_code): ```cpp

include

include

include "jwt/jwt.hpp"

int main() { using namespace jwt::params;

jwt::jwt_object obj{algorithm("HS256"), secret("secret"), payload({{"sub", "test"}})};

std::errorcode ec; auto encstr = obj.signature(ec); assert (!ec);

auto decobj = jwt::decode(encstr, algorithms({"HS256"}), ec, secret("secret"), issuer("arun.muralidharan")); assert (ec);

assert (ec.value() == static_cast(jwt::VerificationErrc::InvalidIssuer));

return 0; } ```

Error Codes & Exceptions

The library as we saw earlier supports error reporting via both exceptions and error_code.

Error codes:

The error codes are divided into different categories: - Algorithm Errors

Used for reporting errors at the time of encoding / signature creation.

cpp
  enum class AlgorithmErrc
  {
    SigningErr = 1,
    VerificationErr,
    KeyNotFoundErr,
    NoneAlgorithmUsed, // Not an actual error!
  };

NOTE: NoneAlgorithmUsed will be set in the error_code, but it usually should not be treated as a hard error when NONE algorithm is used intentionally.

  • Decode Errors

Used for reporting errors at the time of decoding. Different categories of decode errors are:

cpp
  enum class DecodeErrc
  {
    // No algorithms provided in decode API
    EmptyAlgoList = 1,
    // The JWT signature has incorrect format
    SignatureFormatError,
    // The JSON library failed to parse
    JsonParseError,
    // Algorithm field in header is missing
    AlgHeaderMiss,
    // Type field in header is missing
    TypHeaderMiss,
    // Unexpected type field value
    TypMismatch,
    // Found duplicate claims
    DuplClaims,
    // Key/Secret not passed as decode argument
    KeyNotPresent,
    // Key/secret passed as argument for NONE algorithm.
    // Not a hard error.
    KeyNotRequiredForNoneAlg,
  };
  • Verification errors

Used for reporting verification errors when the verification falg is set to true in decode API. Different categories of decode errors are:

cpp
  enum class VerificationErrc
  {
    //Algorithms provided does not match with header
    InvalidAlgorithm = 1,
    //Token is expired at the time of decoding
    TokenExpired,
    //The issuer specified does not match with payload
    InvalidIssuer,
    //The subject specified does not match with payload
    InvalidSubject,
    //The field IAT is not present or is of invalid type
    InvalidIAT,
    //Checks for the existence of JTI
    //if validate_jti is passed in decode
    InvalidJTI,
    //The audience specified dowes not match with payload
    InvalidAudience,
    //Decoded before nbf time
    ImmatureSignature,
    //Signature match error
    InvalidSignature,
    // Invalid value type used for known claims
    TypeConversionError,
  };

Exceptions: There are exception types created for almost all the error codes above.

  • MemoryAllocationException

Derived from std::bad_alloc. Thrown for memory allocation errors in OpenSSL C API.

  • SigningError

Derived from std::runtime_error. Thrown for failures in OpenSSL APIs while signing.

  • DecodeError

Derived from std::runtime_error. Base class for all decoding related exceptions.

  • SignatureFormatError

    Thrown if the format of the signature is not as expected.

  • KeyNotPresentError

    Thrown if key/secret is not passed in with the decode API if the algorithm used is something other than "none".

    • VerificationError

Derived from std::runtime_error. Base class exception for all kinds of verification errors. Verification errors are thrown only when the verify decode parameter is set to true.

  • InvalidAlgorithmError
  • TokenExpiredError
  • InvalidIssuerError
  • InvalidAudienceError
  • InvalidSubjectError
  • InvalidIATError
  • InvalidJTIError
  • ImmatureSignatureError
  • InvalidSignatureError
  • TypeConversionError

NOTE: See the error code section for explanation on above verification errors or checkout exceptions.hpp header for more details.

Additional Header Data

Generally the header consists only of

type
and
algorithm
fields. But there could be a need to add additional header fields. For example, to provide some kind of hint about what algorithm was used to sign the JWT. Checkout JOSE header section in RFC-7515.

The library provides APIs to do that as well.

#include 
#include 
#include "jwt/jwt.hpp"

int main() { using namespace jwt::params;

jwt::jwt_object obj{ headers({ {"alg", "none"}, {"typ", "jwt"}, }), payload({ {"iss", "arun.muralidharan"}, {"sub", "nsfw"}, {"x-pld", "not my ex"} }) };

bool ret = obj.header().add_header("kid", 1234567); assert (ret);

ret = obj.header().add_header("crit", std::array<:string>{"exp"}); assert (ret);

std::error_code ec; auto enc_str = obj.signature();

auto dec_obj = jwt::decode(enc_str, algorithms({"none"}), ec, verify(false));

// Should not be a hard error in general assert (ec.value() == static_cast(jwt::AlgorithmErrc::NoneAlgorithmUsed)); } </:string>

Things for improvement

Many things! Encoding and decoding JWT is fairly a simple task and could be done in a single source file. I have tried my best to get the APIs and design correct in how much ever time I could give for this project. Still, there are quite a few places (or all the places :( ? ) where things are not correct or may not be the best approach.

With C++, it is pretty easy to go overboard and create something very difficult or something very straightforward (not worth to be a library). My intention was to make a sane library easier for end users to use while also making the life of someone reading the source have fairly good time debugging some issue.

Things one may have questions about - There is a stringview implementation. Why not use boost::stringref ?

Sorry, I love boost! But, do not want it to be part of dependency. If you use C++17 or greater

std::string_view
gets used instead and
jwt::string_view
implementation does not get included.
  • You are not using the stack allocator or the shart string anywhere. Why to include it then ?

I will be using it in few places where I am sure I need not use

std::string
especially in the signing code.
  • Why the complete
    nlohmann JSON
    is part of your library ?

Honestly did not know any better way. I know there are ways to use third party github repositories, but I do not know how to do that. Once I figure that out, I may move it out.

  • Am I bound to use
    nlohmann JSON
    ? Can I use some other JSON library ?

As of now, ys. You cannot use any other JSON library unless you change the code. I would have liked to provide some adaptors for JSON interface. Perhaps in future, if required.

  • Error codes and exceptions....heh?

Yeah, I often wonder if that was the right approach. I could have just stuck with error codes and be happy. But that was a good learning time for me.

  • Where to find more about the usage ?

Checkout the tests. It has examples for all the algorithms which are supported.

  • Support for C++11 seems trivial based on the changes required. Why not support C+11 then ?

Its 2018 now! If I ever start getting requests to have support for C++11, then I will surely consider it.

  • The Metaprogramming concept checks for Sequence and Mapping looks sad.

Yeah I know. Just hacked something very basic.

License

MIT License

Copyright (c) 2017 Arun Muralidharan

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.