From c1bbfdf1671b4d4fbb465126ee8d6275822c6317 Mon Sep 17 00:00:00 2001 From: badaix Date: Mon, 3 Jun 2024 22:21:01 +0200 Subject: [PATCH] JWT improvements --- common/jwt.cpp | 275 ++++++++++++++++++++++++++++++++------------- common/jwt.hpp | 56 ++++++++- test/test_main.cpp | 132 ++++++++++++++++++++-- 3 files changed, 371 insertions(+), 92 deletions(-) diff --git a/common/jwt.cpp b/common/jwt.cpp index aa25f915..3ac3e131 100644 --- a/common/jwt.cpp +++ b/common/jwt.cpp @@ -23,6 +23,11 @@ #include "common/aixlog.hpp" #include "common/base64.h" #include "common/utils/string_utils.hpp" +#include +#include +#include +#include +#include #include // 3rd party headers @@ -45,7 +50,7 @@ static constexpr auto LOG_TAG = "JWT"; namespace { -EVP_PKEY* createPrivate(const std::string& key) +EVP_PKEY* readKey(const std::string& key) { // Reads PEM information and retrieves some details BIO* keybio = BIO_new_mem_buf((void*)key.c_str(), -1); @@ -83,120 +88,230 @@ EVP_PKEY* createPrivate(const std::string& key) } -bool Sign(const std::string& pem_key, const std::string& msg, std::vector& encoded) +EVP_PKEY* readCert(const std::string& key) { - auto* key = createPrivate(pem_key); - EVP_MD_CTX* ctx = EVP_MD_CTX_create(); - if (EVP_DigestSignInit(ctx, nullptr, EVP_sha384(), nullptr, key) <= 0) + // Reads PEM information and retrieves some details + BIO* keybio = BIO_new_mem_buf((void*)key.c_str(), -1); + if (keybio == nullptr) + { + LOG(ERROR, LOG_TAG) << "BIO_new_mem_buf failed\n"; + return nullptr; + } + + char* name = nullptr; + char* header = nullptr; + uint8_t* data = nullptr; + long datalen = 0; + if (PEM_read_bio(keybio, &name, &header, &data, &datalen) == 1) + { + // Copies the data pointer. D2I functions update it + const auto* data_pkey = reinterpret_cast(data); + // Detects type and decodes the private key + X509* x509 = d2i_X509(nullptr, &data_pkey, datalen); + EVP_PKEY* pkey = X509_get_pubkey(x509); + if (pkey == nullptr) + { + LOG(ERROR, LOG_TAG) << "d2i_AutoPrivateKey failed\n"; + } + + // Free is only required after a PEM_bio_read successful return + if (name != nullptr) + OPENSSL_free(name); + if (header != nullptr) + OPENSSL_free(header); + if (data != nullptr) + OPENSSL_free(data); + return pkey; + } + return nullptr; +} + +bool sign(const std::string& pem_key, const std::string& msg, std::vector& encoded) +{ + auto* key = readKey(pem_key); + std::shared_ptr ctx(EVP_MD_CTX_create(), [](auto p) { EVP_MD_CTX_free(p); }); + + if (EVP_DigestSignInit(ctx.get(), nullptr, EVP_sha256(), nullptr, key) <= 0) { LOG(ERROR, LOG_TAG) << "EVP_DigestSignInit failed\n"; return false; } - if (EVP_DigestSignUpdate(ctx, msg.c_str(), msg.size()) <= 0) + if (EVP_DigestSignUpdate(ctx.get(), msg.c_str(), msg.size()) <= 0) { LOG(ERROR, LOG_TAG) << "EVP_DigestSignUpdate failed\n"; return false; } size_t siglen; - if (EVP_DigestSignFinal(ctx, nullptr, &siglen) <= 0) + if (EVP_DigestSignFinal(ctx.get(), nullptr, &siglen) <= 0) { LOG(ERROR, LOG_TAG) << "EVP_DigestSignFinal failed\n"; return false; } encoded.resize(siglen); - if (EVP_DigestSignFinal(ctx, encoded.data(), &siglen) <= 0) + if (EVP_DigestSignFinal(ctx.get(), encoded.data(), &siglen) <= 0) { LOG(ERROR, LOG_TAG) << "EVP_DigestSignFinal failed\n"; return false; } - EVP_MD_CTX_free(ctx); return true; } + +bool verifySignature(const std::string& pem_cert, unsigned char* MsgHash, size_t MsgHashLen, const char* Msg, size_t MsgLen, bool& Authentic) +{ + Authentic = false; + auto* key = readCert(pem_cert); + EVP_MD_CTX* ctx = EVP_MD_CTX_create(); + + if (EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, key) <= 0) + { + LOG(ERROR, LOG_TAG) << "EVP_DigestVerifyInit failed\n"; + return false; + } + if (EVP_DigestVerifyUpdate(ctx, Msg, MsgLen) <= 0) + { + LOG(ERROR, LOG_TAG) << "EVP_DigestVerifyInit failed\n"; + return false; + } + int authStatus = EVP_DigestVerifyFinal(ctx, MsgHash, MsgHashLen); + EVP_MD_CTX_free(ctx); + if (authStatus == 1) + { + Authentic = true; + return true; + } + if (authStatus == 0) + { + Authentic = false; + return true; + } + LOG(ERROR, LOG_TAG) << "EVP_DigestVerifyFinal failed: " << authStatus << "\n"; + return false; +} + } // namespace -Jwt::Jwt() +Jwt::Jwt() : claims({}) { - // { - // "alg": "RS256", - // "typ": "JWT" - // } - json header = {{"alg", "RS384"}, {"typ", "JWT"}}; - - // { - // "sub": "1234567890", - // "name": "Johannes", - // "admin": true, - // "iat": 1516239022 - // } - json payload = {{"sub", "1234567890"}, {"name", "Johannes"}, {"admin", true}, {"iat", 1516239022}}; - - LOG(INFO, LOG_TAG) << "Header: " << header << "\n"; - LOG(INFO, LOG_TAG) << "Payload: " << payload << "\n"; - std::string msg = base64url_encode(header.dump()) + "." + base64url_encode(payload.dump()); - LOG(INFO, LOG_TAG) << "Encoded: " << msg << "\n"; - - auto key = "-----BEGIN PRIVATE KEY-----\n" - "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwFxHHKvV6THj1\n" - "VjvZQJIW+OjIKF9MPfIN8wJTXn+4EkBJCoBy0NfKHG+FCb90YCPzvLrjL8jKcfhT\n" - "/jpaStrYYjABXA1sJS9P+9cwiV9RwTxTrfOiss8F7eZdyZI9UogrPmQa7YbevmwB\n" - "XeXIqKzdWQbtQsOJQgoL3vIR7qjUpVQrXPwAYQ6pc7AxS5RHFy8V2Y3sh/+i0aS9\n" - "bH/276OAKZNwVIfUIcSVVbBAPvrheR2ezUVoKSumUzek/2uq3cLb5YNF4XSe1Ikv\n" - "16lRSGTIQ2KflSYn3mJldFjEKL3sgADTmMhKes+TVveNr7eCvynyDCtHdiLanOKY\n" - "PyyOXFTdAgMBAAECggEAAjunU6UBZsBhrUzKEVZ5XnYKxP+xZlmyat0Iy8QFmcY4\n" - "z076iNo0o6u/efVWb/RIfcQILlmHXJKG7BEWg4Qc/oRPkwjW47xHULvtw0AOt85G\n" - "mfy5UPgfBMvQRs1c/87piqYt0KNFTqhQCCa9GKcTmsf7p5ZtPTLw8Sxt2e6H8LsK\n" - "60Jzc2Yw2t3unEb1NnjsTgshjPwFcdrppyRAa2B0Wk3f4ADA1i4vDmTt2+jTq/Hp\n" - "yFWup58Djs+lyn4RLnp7jFD2KS/q+qVQsTfGcPeXLMIWHHQDwfjfkzdA74zNi+Pn\n" - "C4e/iXIsC7VH4BJ8qrVH20WqTXRyuZ1uEF+32XbcSQKBgQDAtBXbnuGhu2llnfP2\n" - "dxJ5qtRjxTHedZs9UMEuy5pkLMi4JsIdjf072lqizpG7DE9kfiERwXJ/Spe4OMMh\n" - "MvWBpnJieTHnouAMpDVootVbSCpikOSGzClHZwpl6KU7pv9Q+Hv2xkSnTJLsakSt\n" - "qlOsG6cwK56kXiwYG0RsAn6lhQKBgQDp7gNE4J6wf9SZHErpyh+65VqqcF5+chNq\n" - "DTwFlb7U31WcDOA3hUHaQfrlwfblMAgnlVMofopdP4KIIgSWE31cCziBp8ZRy/25\n" - "2/aNkDoPEN59Ibk1RWLYsCzcQIAQrTjvfDMn3An1E9B3qYFzfdLKZItb8p3cxpO/\n" - "sMUwQRqFeQKBgDb7av0lwQUPXwwiXDhnUvsp9b2dxxPNBIUjJGuApkWMzZxVWq9q\n" - "EuXf8FphjA0Nfx2SK0dQpaWSF+X1NB+l1YyvfBWCtO19eGXC+IYpZ6zK02UaKEoZ\n" - "uHFqAfp/vZ1ekZx9uYj4myAM5iLUU1Iltgf2P+arm3EUeYpLRWN39sCtAoGBALZ0\n" - "egBC4gLv8TXqp1Np3w26zdiaBFnDR/kzkVkZztnhx7gLIuaq/Q3q4HJLsvJXYETf\n" - "ZxjyeaD5ZCohvkn/sYsVBWG7JieuX5uTQN5xW5dcpOwcXYR7Nfmkj5jKhhh7wyin\n" - "So8QRIPujG6IuvsFbF+HxFpXBWGpUJv2mBZm8PShAoGBALU/KNl1El0HpxTr3wgj\n" - "YY6eU3snn0Oa5Ci3k/FMWd1QTtVsf4Mym0demxshdC75L+qzCS0m5jAo9tm0keY9\n" - "A4F3VRlsuUyuAja+qDU2xMo3jnFKOIyMfN4mVSiFkqnq3eQ4xHgViyIEyr+8AbA4\n" - "ajjiCZsv+OITxQ+TTHeGDsdD\n" - "-----END PRIVATE KEY-----\n"; - - std::vector encoded; - if (Sign(key, msg, encoded)) - { - std::string signature = base64url_encode(encoded.data(), encoded.size()); - LOG(INFO, LOG_TAG) << "Signature: " << signature << "\n"; - } - else - { - LOG(ERROR, LOG_TAG) << "Failed to sign\n"; - } } +std::optional Jwt::getIat() const +{ + if (!claims.contains("iat")) + return std::nullopt; + return std::chrono::seconds(claims.at("iat").get()); +} -bool Jwt::decode(const std::string& token) +void Jwt::setIat(const std::optional& iat) +{ + if (iat.has_value()) + claims["iat"] = iat->count(); + else if (claims.contains("iat")) + claims.erase("iat"); +} + +std::optional Jwt::getExp() const +{ + if (!claims.contains("exp")) + return std::nullopt; + return std::chrono::seconds(claims.at("exp").get()); +} + +void Jwt::setExp(const std::optional& exp) +{ + if (exp.has_value()) + claims["exp"] = exp->count(); + else if (claims.contains("exp")) + claims.erase("exp"); +} + +std::optional Jwt::getSub() const +{ + if (!claims.contains("sub")) + return std::nullopt; + return claims.at("sub").get(); +} + +void Jwt::setSub(const std::optional& sub) +{ + if (sub.has_value()) + claims["sub"] = sub.value(); + else if (claims.contains("sub")) + claims.erase("sub"); +} + +bool Jwt::parse(const std::string& token, const std::string& pem_cert) { std::vector parts = utils::string::split(token, '.'); if (parts.size() != 3) { + LOG(ERROR, LOG_TAG) << "Token '" << token << "' not in the format
..\n"; return false; } - LOG(INFO, LOG_TAG) << "Header: " << base64url_decode(parts[0]) << "\n"; - LOG(INFO, LOG_TAG) << "Payload: " << base64url_decode(parts[1]) << "\n"; + std::string header = base64url_decode(parts[0]); + std::string payload = base64url_decode(parts[1]); + LOG(DEBUG, LOG_TAG) << "Header: " << header << ", payload: " << payload << "\n"; - header_ = json::parse(base64url_decode(parts[0])); - payload_ = json::parse(base64url_decode(parts[1])); - std::string signature = parts[2]; - - LOG(INFO, LOG_TAG) << "Header: " << header_ << "\n"; - LOG(INFO, LOG_TAG) << "Payload: " << payload_ << "\n"; - LOG(INFO, LOG_TAG) << "Signature: " << signature << "\n"; - - return true; + try + { + json jheader = json::parse(header); + claims = json::parse(payload); + std::string signature = parts[2]; + LOG(INFO, LOG_TAG) << "Header: " << jheader << "\n"; + LOG(INFO, LOG_TAG) << "Payload: " << claims << "\n"; + LOG(INFO, LOG_TAG) << "Signature: " << signature << "\n"; + auto binary = base64url_decode(signature); + std::string msg = parts[0] + "." + parts[1]; + bool auth; + if (!verifySignature(pem_cert, reinterpret_cast(binary.data()), binary.size(), msg.c_str(), msg.size(), auth)) + { + LOG(ERROR, LOG_TAG) << "Failed to verify signature\n"; + return false; + } + if (!auth) + { + LOG(ERROR, LOG_TAG) << "Wrong signature\n"; + return false; + } + return true; + } + catch (std::exception& e) + { + LOG(ERROR, LOG_TAG) << "Error parsing JWT header or payload: " << e.what() << "\n"; + } + return false; +} + + +std::optional Jwt::getToken(const std::string& pem_key) const +{ + json header = {{"typ", "JWT"}}; + if (pem_key.find("-----BEGIN PRIVATE KEY-----") == 0) + header["alg"] = "RS256"; + // if (pem_key.find("-----BEGIN EC PRIVATE KEY-----") == 0) + // header["alg"] = "ES256"; + else + { + LOG(ERROR, LOG_TAG) << "PEM key must be an RSA key\n"; + return std::nullopt; + } + + LOG(DEBUG, LOG_TAG) << "Header: " << header << ", payload: " << claims << "\n"; + std::string msg = base64url_encode(header.dump()) + "." + base64url_encode(claims.dump()); + LOG(DEBUG, LOG_TAG) << "Encoded: " << msg << "\n"; + + std::vector encoded; + if (sign(pem_key, msg, encoded)) + { + std::string signature = base64url_encode(encoded.data(), encoded.size()); + LOG(DEBUG, LOG_TAG) << "Signature: " << signature << "\n"; + auto token = msg + "." + signature; + LOG(DEBUG, LOG_TAG) << "Token: " << token << "\n"; + return token; + } + + LOG(ERROR, LOG_TAG) << "Failed to sign token\n"; + return std::nullopt; } diff --git a/common/jwt.hpp b/common/jwt.hpp index dfb21082..51e1166b 100644 --- a/common/jwt.hpp +++ b/common/jwt.hpp @@ -22,6 +22,8 @@ #include "common/json.hpp" // standard headers +#include +#include #include @@ -53,19 +55,63 @@ https://datatracker.ietf.org/doc/html/rfc7518#section-3 | none | No digital signature or MAC | Optional | | | performed | | +--------------+-------------------------------+--------------------+ -*/ + + +https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims + +Registered claims +The JWT specification defines seven reserved claims that are not required, but are recommended to allow interoperability with third-party applications. These +are: +- iss (issuer): Issuer of the JWT +- sub (subject): Subject of the JWT (the user) +- aud (audience): Recipient for which the JWT is intended +- exp (expiration time): Time after which the JWT expires +- nbf (not before time): Time before which the JWT must not be accepted for processing +- iat (issued at time): Time at which the JWT was issued; can be used to determine age of the JWT +- jti (JWT ID): Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once) + // https://techdocs.akamai.com/iot-token-access-control/docs/generate-jwt-rsa-keys +*/ + using json = nlohmann::json; + +/// Json Web Token in RS256 format class Jwt { public: + /// c'tor Jwt(); - bool decode(const std::string& token); + /// Parse an base64 url encoded token of the form "
.." + /// @param token The token + /// @param pem_cert Certificate in PEM format to verify the signature + /// @return true on success, else false + bool parse(const std::string& token, const std::string& pem_cert); + /// Create an base64 url encoded token of the form "
.." + /// @param pem_key Private key in PEM format to sign the token + /// @return the token or nullopt if failed + std::optional getToken(const std::string& pem_key) const; -private: - json header_; - json payload_; + /// Get the iat "Issued at time" claim + /// @return the claim or nullopt, if not present + std::optional getIat() const; + /// Set the iat "Issued at time" claim, use nullopt to delete the iat + void setIat(const std::optional& iat); + + /// Get the exp "Expiration time" claim + /// @return the claim or nullopt, if not present + std::optional getExp() const; + /// Set the exp "Expiration time" claim, use nullopt to delete the exp + void setExp(const std::optional& exp); + + /// Get the sub "Subject" claim + /// @return the claim or nullopt, if not present + std::optional getSub() const; + /// Set the sub "Subject" claim, use nullopt to delete the sub + void setSub(const std::optional& sub); + + /// The token's raw payload (claims) in json format + json claims; }; diff --git a/test/test_main.cpp b/test/test_main.cpp index bfe699d4..05c056b1 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -47,14 +47,132 @@ TEST_CASE("String utils") TEST_CASE("JWT") { - AixLog::Log::init(AixLog::Severity::debug); - Jwt jwt; +#if 0 + /// ECDSA + { + const auto* key = "-----BEGIN EC PRIVATE KEY-----\n" + "MIHcAgEBBEIAarXYdFyTZIM2NGgbjPj8UBlBRYjDKrEfHIDuP1sYGLCJDqqx/GxH\n" + "DpP8mmpTL2dII8cZSbW7zqRv43ZcwK2dq3OgBwYFK4EEACOhgYkDgYYABADPIxdE\n" + "ubzaxsCtZAhbRnG3SJMZoD/0+sFO+r/5m9o4PPAz7cBnSEG3hBThsa+uBE+rfeah\n" + "RagSvgYzRyn85/N0YQBsE6V3htaOBrmoW4PBo/Lg0GwXe5qz7Fp98DuwpDC3OtXa\n" + "43Mgl0rbcwtQ6e0xcAxkLJ0tDfpomiM9Lj5gM+WT1g==\n" + "-----END EC PRIVATE KEY-----\n"; - jwt.decode( - "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyLCJuYW1lIjoiSm9oYW5uZXMiLCJzdWIiOiIxMjM0NTY3ODkwIn0." - "QooKYcDKXwds9GF6Qgs5qTgXZyjPIf9OD_eBYIako7EoC18dqkvPgpwEar_Npck5wSGDPxcMCm9VxRHwv_LP4yelncf08BMu0JRK7vSqKQ8GGC2YoRILsOXD4nIf2mDUJ4CVo5fCbKuhwxE_" - "lBdiQXFGF6NaQ-02LYTnoRub2x1wtHrQam5eYTaNPjaY2ANvSpRK8CCA6jWd6P5qqgedcPtE4J2HLDFR2GefFjhOYYaZP-LMhdbHDEoThk05-bQVyXXXg-7RCqCfzE_" - "H5fx4w8t6JYudM8PsRoK3kE8vGsR7tHRGi0CBJqTMTxIYnjaQPQUd8OYFbux3TxEV0Kr9kQ"); + const auto* cert = "-----BEGIN CERTIFICATE-----\n" + "MIICVjCCAbigAwIBAgIUBrGbikLGcul0Cia5tBCsPDm7/P4wCgYIKoZIzj0EAwIw\n" + "PTELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEPMA0GA1UEBwwGQWFjaGVuMQ8w\n" + "DQYDVQQKDAZCYWRBaXgwHhcNMjQwNjAzMTk0NzEzWhcNMjUwNjAzMTk0NzEzWjA9\n" + "MQswCQYDVQQGEwJERTEMMAoGA1UECAwDTlJXMQ8wDQYDVQQHDAZBYWNoZW4xDzAN\n" + "BgNVBAoMBkJhZEFpeDCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAM8jF0S5vNrG\n" + "wK1kCFtGcbdIkxmgP/T6wU76v/mb2jg88DPtwGdIQbeEFOGxr64ET6t95qFFqBK+\n" + "BjNHKfzn83RhAGwTpXeG1o4Guahbg8Gj8uDQbBd7mrPsWn3wO7CkMLc61drjcyCX\n" + "SttzC1Dp7TFwDGQsnS0N+miaIz0uPmAz5ZPWo1MwUTAdBgNVHQ4EFgQUy+1q8Nh5\n" + "qbrckn9hdMT7WXhha2gwHwYDVR0jBBgwFoAUy+1q8Nh5qbrckn9hdMT7WXhha2gw\n" + "DwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgOBiwAwgYcCQgCYarPzdeupTvEs\n" + "CC5OlWj9pzUJDY4R0kaEe94g+KwTsgyaY1OzhHKwmu4E2ggrU0BN/HTpRYrYhtFz\n" + "HnDpYIN5XAJBK9fgry78uAiN4RZHPfWBKJ/NMcCZO/dXLbxoFlh2+6FXD/TmABHW\n" + "XsA/SmQzK2kJOKkBu9jBwNPiFZUQrJJ5msE=\n" + "-----END CERTIFICATE-----\n"; + + Jwt jwt; + jwt.setIat(std::chrono::seconds(1516239022)); + jwt.setSub("Badaix"); + std::optional token = jwt.getToken(key); + REQUIRE(token.has_value()); + LOG(INFO, "TEST") << "Token: " << *token << "\n"; + REQUIRE(token.value() == "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6IkJhZGFpeCJ9.MIGHAkEDxp53c--" + "MwMQAiTR5pnXlCunmqruZ8U6owKIfm06zgcAVswmTAhXFMIF28X9Yu2_F0o2LjgSWfBP9NEKg6BXy-gJCAevKxaKwxv1bbPOf7kGOeBzESuq8h_" + "pCJwGEA1WW9tflQgpsU52oBmIA-6fAHvl_EVPh4KIXoTQ71BRfacDMDzG2AA"); + + REQUIRE(jwt.parse(token.value(), cert)); + REQUIRE(jwt.getSub().has_value()); + REQUIRE(jwt.getSub().value() == "Badaix"); + REQUIRE(jwt.getIat().has_value()); + REQUIRE(jwt.getIat().value() == std::chrono::seconds(1516239022)); + REQUIRE(!jwt.getExp().has_value()); + } +#endif + + /// RSA keys + { + AixLog::Log::init(AixLog::Severity::debug); + const auto* key = "-----BEGIN PRIVATE KEY-----\n" + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwFxHHKvV6THj1\n" + "VjvZQJIW+OjIKF9MPfIN8wJTXn+4EkBJCoBy0NfKHG+FCb90YCPzvLrjL8jKcfhT\n" + "/jpaStrYYjABXA1sJS9P+9cwiV9RwTxTrfOiss8F7eZdyZI9UogrPmQa7YbevmwB\n" + "XeXIqKzdWQbtQsOJQgoL3vIR7qjUpVQrXPwAYQ6pc7AxS5RHFy8V2Y3sh/+i0aS9\n" + "bH/276OAKZNwVIfUIcSVVbBAPvrheR2ezUVoKSumUzek/2uq3cLb5YNF4XSe1Ikv\n" + "16lRSGTIQ2KflSYn3mJldFjEKL3sgADTmMhKes+TVveNr7eCvynyDCtHdiLanOKY\n" + "PyyOXFTdAgMBAAECggEAAjunU6UBZsBhrUzKEVZ5XnYKxP+xZlmyat0Iy8QFmcY4\n" + "z076iNo0o6u/efVWb/RIfcQILlmHXJKG7BEWg4Qc/oRPkwjW47xHULvtw0AOt85G\n" + "mfy5UPgfBMvQRs1c/87piqYt0KNFTqhQCCa9GKcTmsf7p5ZtPTLw8Sxt2e6H8LsK\n" + "60Jzc2Yw2t3unEb1NnjsTgshjPwFcdrppyRAa2B0Wk3f4ADA1i4vDmTt2+jTq/Hp\n" + "yFWup58Djs+lyn4RLnp7jFD2KS/q+qVQsTfGcPeXLMIWHHQDwfjfkzdA74zNi+Pn\n" + "C4e/iXIsC7VH4BJ8qrVH20WqTXRyuZ1uEF+32XbcSQKBgQDAtBXbnuGhu2llnfP2\n" + "dxJ5qtRjxTHedZs9UMEuy5pkLMi4JsIdjf072lqizpG7DE9kfiERwXJ/Spe4OMMh\n" + "MvWBpnJieTHnouAMpDVootVbSCpikOSGzClHZwpl6KU7pv9Q+Hv2xkSnTJLsakSt\n" + "qlOsG6cwK56kXiwYG0RsAn6lhQKBgQDp7gNE4J6wf9SZHErpyh+65VqqcF5+chNq\n" + "DTwFlb7U31WcDOA3hUHaQfrlwfblMAgnlVMofopdP4KIIgSWE31cCziBp8ZRy/25\n" + "2/aNkDoPEN59Ibk1RWLYsCzcQIAQrTjvfDMn3An1E9B3qYFzfdLKZItb8p3cxpO/\n" + "sMUwQRqFeQKBgDb7av0lwQUPXwwiXDhnUvsp9b2dxxPNBIUjJGuApkWMzZxVWq9q\n" + "EuXf8FphjA0Nfx2SK0dQpaWSF+X1NB+l1YyvfBWCtO19eGXC+IYpZ6zK02UaKEoZ\n" + "uHFqAfp/vZ1ekZx9uYj4myAM5iLUU1Iltgf2P+arm3EUeYpLRWN39sCtAoGBALZ0\n" + "egBC4gLv8TXqp1Np3w26zdiaBFnDR/kzkVkZztnhx7gLIuaq/Q3q4HJLsvJXYETf\n" + "ZxjyeaD5ZCohvkn/sYsVBWG7JieuX5uTQN5xW5dcpOwcXYR7Nfmkj5jKhhh7wyin\n" + "So8QRIPujG6IuvsFbF+HxFpXBWGpUJv2mBZm8PShAoGBALU/KNl1El0HpxTr3wgj\n" + "YY6eU3snn0Oa5Ci3k/FMWd1QTtVsf4Mym0demxshdC75L+qzCS0m5jAo9tm0keY9\n" + "A4F3VRlsuUyuAja+qDU2xMo3jnFKOIyMfN4mVSiFkqnq3eQ4xHgViyIEyr+8AbA4\n" + "ajjiCZsv+OITxQ+TTHeGDsdD\n" + "-----END PRIVATE KEY-----\n"; + + const auto* cert = "-----BEGIN CERTIFICATE-----\n" + "MIIE3TCCAsWgAwIBAgIUW+CDwjHiUAMf5yeFct8FatvUWhYwDQYJKoZIhvcNAQEL\n" + "BQAwVDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEPMA0GA1UEBwwGQWFjaGVu\n" + "MQ8wDQYDVQQKDAZCYWRhaXgxFTATBgNVBAMMDGxhcHRvcC5sb2NhbDAeFw0yNDA1\n" + "MTAxNTA2NDdaFw0yNTA5MjIxNTA2NDdaMFQxCzAJBgNVBAYTAkRFMQwwCgYDVQQI\n" + "DANOUlcxDzANBgNVBAcMBkFhY2hlbjEPMA0GA1UECgwGQmFkYWl4MRUwEwYDVQQD\n" + "DAxsYXB0b3AubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw\n" + "FxHHKvV6THj1VjvZQJIW+OjIKF9MPfIN8wJTXn+4EkBJCoBy0NfKHG+FCb90YCPz\n" + "vLrjL8jKcfhT/jpaStrYYjABXA1sJS9P+9cwiV9RwTxTrfOiss8F7eZdyZI9Uogr\n" + "PmQa7YbevmwBXeXIqKzdWQbtQsOJQgoL3vIR7qjUpVQrXPwAYQ6pc7AxS5RHFy8V\n" + "2Y3sh/+i0aS9bH/276OAKZNwVIfUIcSVVbBAPvrheR2ezUVoKSumUzek/2uq3cLb\n" + "5YNF4XSe1Ikv16lRSGTIQ2KflSYn3mJldFjEKL3sgADTmMhKes+TVveNr7eCvyny\n" + "DCtHdiLanOKYPyyOXFTdAgMBAAGjgaYwgaMwHwYDVR0jBBgwFoAUG790F3RT+ckl\n" + "UB16DIH7Hs5D0fYwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwSQYDVR0RBEIwQIIM\n" + "bGFwdG9wLmxvY2FsggwxOTIuMTY4LjAuMzmCCTEyNy4wLjAuMYIKcnBpNS5sb2Nh\n" + "bIILMTkyLjE2OC4wLjMwHQYDVR0OBBYEFNxTG9vR5KC+XgGVsRb76DA7EKbiMA0G\n" + "CSqGSIb3DQEBCwUAA4ICAQA9cTmiDYiQKz/0BUMYA3xfy48ajS5F3PVOGRQsoOyO\n" + "gRcut5mxH9xzhJc2rbwRKyuZBZV8EuEDImxNf41L72nUUyrfOfC2AV92IeXMGVzg\n" + "Qvw8ypXTI3wRFFeWmjyuFK42wcVXlZd8Rx6hWz7BVDR9fBVSHV3I2Iyv98RVukwv\n" + "X2hnZu+6Bn8fJ890YMmpX1sAfgcSFY863zNyj3n3/tjoMYHSYwWMC5cEa6BotSjH\n" + "dAkjoIQwWwUAJWxzwOJwSolj81oM/7BcZ8UmHhGWuVYmxr/NfSfe7pFb0H6Fkktb\n" + "S+8qUjhIm8YsrC2qO6DCCiOWqSwxoyjmX1tap0IyciSfRyyFlgSBs4v8/Y33wIlW\n" + "ktH0A4i9CZPlOGTy8hDGhwAxGY8unfuDSoAUr4RQpoJDGSIEeyUpWbfUVxdXhSI9\n" + "FgkhWQq28cauJPRcbNILCmz+iUh2DlehlBW4lc6PwcBF5PRknAT+81lRRWPnw1nV\n" + "Qmg7wFnm+G1K6Ip2yz69hvywQddkZ3nTFcIxDfXWpD+8w+RdITI0Q9jNeKAuHRIq\n" + "eHg9/cG/dph/CE/W/mN1sjO5E+oj8wbguBmBe+r6Ga/nQUwSYAGsvr5JlmDFDwqR\n" + "Q3Q4ZajYb45p1McbRnN1ImovW9o6GOS3JUZcMBBLyZH0fmbikUb80bj++artyfAD\n" + "Iw==\n" + "-----END CERTIFICATE-----\n"; + + Jwt jwt; + jwt.setIat(std::chrono::seconds(1516239022)); + jwt.setSub("Badaix"); + std::optional token = jwt.getToken(key); + REQUIRE(token.has_value()); + REQUIRE(token.value() == + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MTYyMzkwMjIsInN1YiI6IkJhZGFpeCJ9.LtKDGnT2OSgvWLECReajyMmUv7ApJeRu83MZhDM7d_" + "1t1zy2Z08BQZEEB58WzR1vZAtRGHDVrYytJaVCPzibQN4eZ1F4m0gDk83hxQPTAKsbwjtzi7pUJzvaBa1ni4ysc9POtoi_M1OtNk5xxziyk5VP1Ph-" + "TQbm9BCPfpA8bSUCx0LFrm5gyCD3irkww_W3RwDc2ghrjDCRNCyu4R9__lrCRGdx3Z8i0YMB_obuShcYzJXFSxG8adTSs3PQ_R4NXR94-vydVrvBxqe79apocFVrs_" + "c9Ub8TIFynzqp9L_s206nb2N3C1WfUkKeQ1E7gAgVq8b4SM0OZsmkERQ0P0w"); + + REQUIRE(jwt.parse(token.value(), cert)); + REQUIRE(jwt.getSub().has_value()); + REQUIRE(jwt.getSub().value() == "Badaix"); + REQUIRE(jwt.getIat().has_value()); + REQUIRE(jwt.getIat().value() == std::chrono::seconds(1516239022)); + REQUIRE(!jwt.getExp().has_value()); + } }