arun11299 / cpp-jwt

JSON Web Token library for C++
MIT License
387 stars 112 forks source link
cpp11 cpp14 cpp17 jwt jwt-header security

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.

  #include <iostream>
  #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, jwt_object class provides add_claim API. We will also see few other APIs in the next example. Make sure to read the comments :).

    #include <chrono>
    #include <cassert>
    #include <iostream>
    #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 (!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 (!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 jwt_object class is basically a composition of the JWT component classes, which are jwt_header & jwt_payload. For convenience jwt_object exposes only few important APIs to the user, the remaining APIs under jwt_header and jwt_payload can be accessed by calling jwt_object::header() and jwt_object::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:

Support

Algorithms and features supported

External Dependencies

Thanks to...

- <a href="https://github.com/benmcollins/libjwt">ben-collins JWT library</a>
- 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.

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

find_package(cpp-jwt REQUIRED)
# or
add_subdirectory(third_party/cpp-jwt)

add_executable(main main.cpp)
target_link_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.

It can also be installed using vcpkg by adding "cpp-jwt" to the dependencies in your vcpkg.json file.

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::is_parameter_concept).

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)              | <Value type not checked by library. Upto application.>
-----------------------------------

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):

#include <cassert>
#include <iostream>
#include "jwt/jwt.hpp"

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

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

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

  auto dec_obj = jwt::decode(enc_str, algorithms({"HS256"}), ec, secret("secret"), verify(true));
  assert (ec);
  assert (ec.value() == static_cast<int>(jwt::VerificationErrc::TokenExpired));

  return 0;
}

Expiration verification example (uses exception):

#include <cassert>
#include <iostream>
#include "jwt/jwt.hpp"

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

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

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

  auto enc_str = obj.signature();

  try {
    auto dec_obj = jwt::decode(enc_str, 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):

#include <cassert>
#include <iostream>
#include "jwt/jwt.hpp"

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

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

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

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

  assert (ec.value() == static_cast<int>(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:

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

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 <cassert>
#include <iostream>
#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<std::string, 1>{"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<int>(jwt::AlgorithmErrc::NoneAlgorithmUsed));
}

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

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.