feat(CIE): Implement OIDC RP Service with login, callback, and session management
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
#include "common/Crypto.h"
|
||||
|
||||
#include <drogon/utils/Utilities.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -10,6 +16,12 @@ namespace visione::crypto
|
||||
{
|
||||
namespace
|
||||
{
|
||||
using BnPtr = std::unique_ptr<BIGNUM, decltype(&BN_free)>;
|
||||
using RsaPtr = std::unique_ptr<RSA, decltype(&RSA_free)>;
|
||||
using EvpKeyPtr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
|
||||
using BioPtr = std::unique_ptr<BIO, decltype(&BIO_free)>;
|
||||
using BnCtxPtr = std::unique_ptr<BN_CTX, decltype(&BN_CTX_free)>;
|
||||
|
||||
std::string toHex(const unsigned char *data, size_t size)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
@@ -20,6 +32,39 @@ std::string toHex(const unsigned char *data, size_t size)
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string base64UrlDecode(std::string value)
|
||||
{
|
||||
while (value.size() % 4 != 0)
|
||||
{
|
||||
value.push_back('=');
|
||||
}
|
||||
for (char &ch : value)
|
||||
{
|
||||
if (ch == '-')
|
||||
{
|
||||
ch = '+';
|
||||
}
|
||||
else if (ch == '_')
|
||||
{
|
||||
ch = '/';
|
||||
}
|
||||
}
|
||||
return drogon::utils::base64Decode(value);
|
||||
}
|
||||
|
||||
BnPtr bnFromBase64Url(const std::string &value)
|
||||
{
|
||||
const auto decoded = base64UrlDecode(value);
|
||||
BIGNUM *raw = BN_bin2bn(reinterpret_cast<const unsigned char *>(decoded.data()),
|
||||
static_cast<int>(decoded.size()),
|
||||
nullptr);
|
||||
if (!raw)
|
||||
{
|
||||
throw std::runtime_error("invalid_jwk_bn");
|
||||
}
|
||||
return {raw, &BN_free};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string sha256(const std::string &value)
|
||||
@@ -47,8 +92,151 @@ std::string randomToken()
|
||||
return drogon::utils::getUuid() + drogon::utils::getUuid();
|
||||
}
|
||||
|
||||
std::string randomBytes(size_t size)
|
||||
{
|
||||
return drogon::utils::genRandomString(size);
|
||||
}
|
||||
|
||||
std::string hashSecret(const std::string &secret)
|
||||
{
|
||||
return sha256("visione:" + secret);
|
||||
}
|
||||
|
||||
std::string pemFromRsaJwk(const std::string &n,
|
||||
const std::string &e,
|
||||
const std::string &d,
|
||||
const std::string &p,
|
||||
const std::string &q)
|
||||
{
|
||||
RsaPtr rsa(RSA_new(), &RSA_free);
|
||||
if (!rsa)
|
||||
{
|
||||
throw std::runtime_error("rsa_new_failed");
|
||||
}
|
||||
|
||||
auto nBn = bnFromBase64Url(n);
|
||||
auto eBn = bnFromBase64Url(e);
|
||||
BIGNUM *nRaw = nBn.release();
|
||||
BIGNUM *eRaw = eBn.release();
|
||||
if (RSA_set0_key(rsa.get(), nRaw, eRaw, nullptr) != 1)
|
||||
{
|
||||
BN_free(nRaw);
|
||||
BN_free(eRaw);
|
||||
throw std::runtime_error("rsa_set_public_failed");
|
||||
}
|
||||
|
||||
const bool includePrivate = !d.empty();
|
||||
if (includePrivate)
|
||||
{
|
||||
auto dBn = bnFromBase64Url(d);
|
||||
BIGNUM *dRaw = dBn.release();
|
||||
if (RSA_set0_key(rsa.get(), nullptr, nullptr, dRaw) != 1)
|
||||
{
|
||||
BN_free(dRaw);
|
||||
throw std::runtime_error("rsa_set_private_failed");
|
||||
}
|
||||
|
||||
if (p.empty() || q.empty())
|
||||
{
|
||||
throw std::runtime_error("missing_private_jwk_fields");
|
||||
}
|
||||
|
||||
auto pBn = bnFromBase64Url(p);
|
||||
auto qBn = bnFromBase64Url(q);
|
||||
BIGNUM *pRaw = pBn.release();
|
||||
BIGNUM *qRaw = qBn.release();
|
||||
if (RSA_set0_factors(rsa.get(), pRaw, qRaw) != 1)
|
||||
{
|
||||
BN_free(pRaw);
|
||||
BN_free(qRaw);
|
||||
throw std::runtime_error("rsa_set_factors_failed");
|
||||
}
|
||||
|
||||
const BIGNUM *rsaD = nullptr;
|
||||
const BIGNUM *rsaP = nullptr;
|
||||
const BIGNUM *rsaQ = nullptr;
|
||||
const BIGNUM *rsaE = nullptr;
|
||||
RSA_get0_key(rsa.get(), nullptr, &rsaE, &rsaD);
|
||||
RSA_get0_factors(rsa.get(), &rsaP, &rsaQ);
|
||||
|
||||
BnCtxPtr bnCtx(BN_CTX_new(), &BN_CTX_free);
|
||||
if (!bnCtx)
|
||||
{
|
||||
throw std::runtime_error("bn_ctx_new_failed");
|
||||
}
|
||||
|
||||
BIGNUM *dp = BN_new();
|
||||
BIGNUM *dq = BN_new();
|
||||
BIGNUM *qi = BN_new();
|
||||
BIGNUM *pMinus1 = BN_dup(rsaP);
|
||||
BIGNUM *qMinus1 = BN_dup(rsaQ);
|
||||
if (!dp || !dq || !qi || !pMinus1 || !qMinus1)
|
||||
{
|
||||
BN_free(dp);
|
||||
BN_free(dq);
|
||||
BN_free(qi);
|
||||
BN_free(pMinus1);
|
||||
BN_free(qMinus1);
|
||||
throw std::runtime_error("bn_alloc_failed");
|
||||
}
|
||||
|
||||
BN_sub_word(pMinus1, 1);
|
||||
BN_sub_word(qMinus1, 1);
|
||||
if (BN_mod(dp, rsaD, pMinus1, bnCtx.get()) != 1 ||
|
||||
BN_mod(dq, rsaD, qMinus1, bnCtx.get()) != 1 ||
|
||||
BN_mod_inverse(qi, rsaQ, rsaP, bnCtx.get()) == nullptr)
|
||||
{
|
||||
BN_free(dp);
|
||||
BN_free(dq);
|
||||
BN_free(qi);
|
||||
BN_free(pMinus1);
|
||||
BN_free(qMinus1);
|
||||
throw std::runtime_error("rsa_crt_params_failed");
|
||||
}
|
||||
BN_free(pMinus1);
|
||||
BN_free(qMinus1);
|
||||
|
||||
if (RSA_set0_crt_params(rsa.get(), dp, dq, qi) != 1)
|
||||
{
|
||||
BN_free(dp);
|
||||
BN_free(dq);
|
||||
BN_free(qi);
|
||||
throw std::runtime_error("rsa_set_crt_failed");
|
||||
}
|
||||
}
|
||||
|
||||
EVP_PKEY *rawPkey = EVP_PKEY_new();
|
||||
if (!rawPkey)
|
||||
{
|
||||
throw std::runtime_error("evp_pkey_new_failed");
|
||||
}
|
||||
EvpKeyPtr pkey(rawPkey, &EVP_PKEY_free);
|
||||
if (EVP_PKEY_assign_RSA(pkey.get(), rsa.release()) != 1)
|
||||
{
|
||||
throw std::runtime_error("evp_assign_rsa_failed");
|
||||
}
|
||||
|
||||
BioPtr bio(BIO_new(BIO_s_mem()), &BIO_free);
|
||||
if (!bio)
|
||||
{
|
||||
throw std::runtime_error("bio_new_failed");
|
||||
}
|
||||
|
||||
const int ok = includePrivate
|
||||
? PEM_write_bio_PrivateKey(
|
||||
bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr)
|
||||
: PEM_write_bio_PUBKEY(bio.get(), pkey.get());
|
||||
if (ok != 1)
|
||||
{
|
||||
throw std::runtime_error("pem_write_failed");
|
||||
}
|
||||
|
||||
BUF_MEM *buffer = nullptr;
|
||||
BIO_get_mem_ptr(bio.get(), &buffer);
|
||||
if (!buffer || !buffer->data || buffer->length == 0)
|
||||
{
|
||||
throw std::runtime_error("pem_buffer_failed");
|
||||
}
|
||||
return {buffer->data, buffer->length};
|
||||
}
|
||||
} // namespace visione::crypto
|
||||
|
||||
@@ -7,5 +7,11 @@ namespace visione::crypto
|
||||
std::string sha256(const std::string &value);
|
||||
std::string randomNumericCode(int digits);
|
||||
std::string randomToken();
|
||||
std::string randomBytes(size_t size);
|
||||
std::string hashSecret(const std::string &secret);
|
||||
std::string pemFromRsaJwk(const std::string &n,
|
||||
const std::string &e,
|
||||
const std::string &d = {},
|
||||
const std::string &p = {},
|
||||
const std::string &q = {});
|
||||
} // namespace visione::crypto
|
||||
|
||||
+8
-1
@@ -22,6 +22,14 @@ drogon::HttpResponsePtr error(const std::string &code,
|
||||
return json(std::move(body), status);
|
||||
}
|
||||
|
||||
drogon::HttpResponsePtr redirect(const std::string &url,
|
||||
drogon::HttpStatusCode status)
|
||||
{
|
||||
auto response = drogon::HttpResponse::newRedirectionResponse(url);
|
||||
response->setStatusCode(status);
|
||||
return response;
|
||||
}
|
||||
|
||||
Json::Value requireJson(const drogon::HttpRequestPtr &request)
|
||||
{
|
||||
const auto &body = request->getJsonObject();
|
||||
@@ -32,4 +40,3 @@ Json::Value requireJson(const drogon::HttpRequestPtr &request)
|
||||
return *body;
|
||||
}
|
||||
} // namespace visione::http
|
||||
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ drogon::HttpResponsePtr json(Json::Value body,
|
||||
drogon::HttpResponsePtr error(const std::string &code,
|
||||
drogon::HttpStatusCode status,
|
||||
const std::string &message = {});
|
||||
drogon::HttpResponsePtr redirect(const std::string &url,
|
||||
drogon::HttpStatusCode status = drogon::k302Found);
|
||||
Json::Value requireJson(const drogon::HttpRequestPtr &request);
|
||||
} // namespace visione::http
|
||||
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
#include "common/OidcCrypto.h"
|
||||
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Json::Value parseJsonText(const std::string &text)
|
||||
{
|
||||
Json::CharReaderBuilder builder;
|
||||
builder["collectComments"] = false;
|
||||
std::string errors;
|
||||
Json::Value value;
|
||||
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
||||
if (!reader->parse(text.data(), text.data() + text.size(), &value, &errors))
|
||||
{
|
||||
throw std::runtime_error("invalid_json");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
using EvpKeyPtr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
|
||||
using BnPtr = std::unique_ptr<BIGNUM, decltype(&BN_free)>;
|
||||
using RsaPtr = std::unique_ptr<RSA, decltype(&RSA_free)>;
|
||||
using PkeyCtxPtr = std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)>;
|
||||
using CipherCtxPtr =
|
||||
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)>;
|
||||
|
||||
BnPtr bnFromBase64Url(const std::string &value)
|
||||
{
|
||||
const auto decoded = base64UrlDecode(value);
|
||||
BIGNUM *raw = BN_bin2bn(reinterpret_cast<const unsigned char *>(decoded.data()),
|
||||
static_cast<int>(decoded.size()),
|
||||
nullptr);
|
||||
if (!raw)
|
||||
{
|
||||
throw std::runtime_error("invalid_jwk_bn");
|
||||
}
|
||||
return {raw, &BN_free};
|
||||
}
|
||||
|
||||
EvpKeyPtr keyFromJwk(const Jwk &jwk, bool includePrivate)
|
||||
{
|
||||
if (jwk.kty != "RSA")
|
||||
{
|
||||
throw std::runtime_error("unsupported_jwk_type");
|
||||
}
|
||||
|
||||
RsaPtr rsa(RSA_new(), &RSA_free);
|
||||
if (!rsa)
|
||||
{
|
||||
throw std::runtime_error("rsa_new_failed");
|
||||
}
|
||||
|
||||
auto n = bnFromBase64Url(jwk.n);
|
||||
auto e = bnFromBase64Url(jwk.e);
|
||||
BIGNUM *nRaw = n.release();
|
||||
BIGNUM *eRaw = e.release();
|
||||
if (RSA_set0_key(rsa.get(), nRaw, eRaw, nullptr) != 1)
|
||||
{
|
||||
BN_free(nRaw);
|
||||
BN_free(eRaw);
|
||||
throw std::runtime_error("rsa_set_public_failed");
|
||||
}
|
||||
|
||||
if (includePrivate)
|
||||
{
|
||||
if (jwk.d.empty() || jwk.p.empty() || jwk.q.empty())
|
||||
{
|
||||
throw std::runtime_error("missing_private_jwk_fields");
|
||||
}
|
||||
|
||||
auto d = bnFromBase64Url(jwk.d);
|
||||
auto p = bnFromBase64Url(jwk.p);
|
||||
auto q = bnFromBase64Url(jwk.q);
|
||||
BIGNUM *dRaw = d.release();
|
||||
if (RSA_set0_key(rsa.get(), nullptr, nullptr, dRaw) != 1)
|
||||
{
|
||||
BN_free(dRaw);
|
||||
throw std::runtime_error("rsa_set_private_failed");
|
||||
}
|
||||
|
||||
BIGNUM *pRaw = p.release();
|
||||
BIGNUM *qRaw = q.release();
|
||||
if (RSA_set0_factors(rsa.get(), pRaw, qRaw) != 1)
|
||||
{
|
||||
BN_free(pRaw);
|
||||
BN_free(qRaw);
|
||||
throw std::runtime_error("rsa_set_factors_failed");
|
||||
}
|
||||
}
|
||||
|
||||
EVP_PKEY *rawPkey = EVP_PKEY_new();
|
||||
if (!rawPkey)
|
||||
{
|
||||
throw std::runtime_error("evp_pkey_new_failed");
|
||||
}
|
||||
EvpKeyPtr pkey(rawPkey, &EVP_PKEY_free);
|
||||
if (EVP_PKEY_assign_RSA(pkey.get(), rsa.release()) != 1)
|
||||
{
|
||||
throw std::runtime_error("evp_assign_rsa_failed");
|
||||
}
|
||||
return pkey;
|
||||
}
|
||||
|
||||
std::vector<std::string> split(const std::string &text, char separator)
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
std::string current;
|
||||
for (char ch : text)
|
||||
{
|
||||
if (ch == separator)
|
||||
{
|
||||
parts.push_back(current);
|
||||
current.clear();
|
||||
continue;
|
||||
}
|
||||
current.push_back(ch);
|
||||
}
|
||||
parts.push_back(current);
|
||||
return parts;
|
||||
}
|
||||
|
||||
std::string rsaOaepDecrypt(const std::string &ciphertext,
|
||||
const Jwk &jwk,
|
||||
const std::string &algorithm)
|
||||
{
|
||||
auto pkey = keyFromJwk(jwk, true);
|
||||
PkeyCtxPtr ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr), &EVP_PKEY_CTX_free);
|
||||
if (!ctx)
|
||||
{
|
||||
throw std::runtime_error("jwe_pkey_ctx_new_failed");
|
||||
}
|
||||
if (EVP_PKEY_decrypt_init(ctx.get()) != 1)
|
||||
{
|
||||
throw std::runtime_error("jwe_pkey_decrypt_init_failed");
|
||||
}
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) != 1)
|
||||
{
|
||||
throw std::runtime_error("jwe_rsa_padding_failed");
|
||||
}
|
||||
|
||||
const EVP_MD *md = EVP_sha1();
|
||||
if (algorithm == "RSA-OAEP-256")
|
||||
{
|
||||
md = EVP_sha256();
|
||||
}
|
||||
else if (algorithm != "RSA-OAEP")
|
||||
{
|
||||
throw std::runtime_error("unsupported_jwe_alg");
|
||||
}
|
||||
if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), md) != 1 ||
|
||||
EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), md) != 1)
|
||||
{
|
||||
throw std::runtime_error("jwe_rsa_oaep_md_failed");
|
||||
}
|
||||
|
||||
size_t outLen = 0;
|
||||
if (EVP_PKEY_decrypt(ctx.get(),
|
||||
nullptr,
|
||||
&outLen,
|
||||
reinterpret_cast<const unsigned char *>(ciphertext.data()),
|
||||
ciphertext.size()) != 1)
|
||||
{
|
||||
throw std::runtime_error("jwe_cek_size_failed");
|
||||
}
|
||||
|
||||
std::string plaintext(outLen, '\0');
|
||||
if (EVP_PKEY_decrypt(
|
||||
ctx.get(),
|
||||
reinterpret_cast<unsigned char *>(plaintext.data()),
|
||||
&outLen,
|
||||
reinterpret_cast<const unsigned char *>(ciphertext.data()),
|
||||
ciphertext.size()) != 1)
|
||||
{
|
||||
throw std::runtime_error("jwe_cek_decrypt_failed");
|
||||
}
|
||||
plaintext.resize(outLen);
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
std::array<unsigned char, 8> bigEndianBitLength(uint64_t lenBytes)
|
||||
{
|
||||
const auto lenBits = lenBytes * 8;
|
||||
return {
|
||||
static_cast<unsigned char>((lenBits >> 56) & 0xff),
|
||||
static_cast<unsigned char>((lenBits >> 48) & 0xff),
|
||||
static_cast<unsigned char>((lenBits >> 40) & 0xff),
|
||||
static_cast<unsigned char>((lenBits >> 32) & 0xff),
|
||||
static_cast<unsigned char>((lenBits >> 24) & 0xff),
|
||||
static_cast<unsigned char>((lenBits >> 16) & 0xff),
|
||||
static_cast<unsigned char>((lenBits >> 8) & 0xff),
|
||||
static_cast<unsigned char>(lenBits & 0xff),
|
||||
};
|
||||
}
|
||||
|
||||
void requireExact(bool ok, const char *error)
|
||||
{
|
||||
if (!ok)
|
||||
{
|
||||
throw std::runtime_error(error);
|
||||
}
|
||||
}
|
||||
|
||||
std::string decryptCbcHmac(const JweParts &parts,
|
||||
const std::string &cek,
|
||||
size_t expectedCekSize,
|
||||
size_t ivSize,
|
||||
size_t tagSize,
|
||||
const EVP_CIPHER *cipher,
|
||||
const EVP_MD *md)
|
||||
{
|
||||
if (cek.size() != expectedCekSize)
|
||||
{
|
||||
throw std::runtime_error("invalid_jwe_cek_size");
|
||||
}
|
||||
|
||||
const auto iv = base64UrlDecode(parts.encodedIv);
|
||||
const auto ciphertext = base64UrlDecode(parts.encodedCiphertext);
|
||||
const auto tag = base64UrlDecode(parts.encodedTag);
|
||||
if (iv.size() != ivSize || tag.size() != tagSize)
|
||||
{
|
||||
throw std::runtime_error("invalid_jwe_parts");
|
||||
}
|
||||
|
||||
const auto macKeySize = expectedCekSize / 2;
|
||||
const auto encKeySize = expectedCekSize / 2;
|
||||
const std::string macKey = cek.substr(0, macKeySize);
|
||||
const std::string encKey = cek.substr(macKeySize, encKeySize);
|
||||
const auto al = bigEndianBitLength(parts.encodedHeader.size());
|
||||
|
||||
std::string macInput;
|
||||
macInput.reserve(parts.encodedHeader.size() + iv.size() + ciphertext.size() +
|
||||
al.size());
|
||||
macInput.append(parts.encodedHeader);
|
||||
macInput.append(iv);
|
||||
macInput.append(ciphertext);
|
||||
macInput.append(reinterpret_cast<const char *>(al.data()), al.size());
|
||||
|
||||
unsigned char hmac[EVP_MAX_MD_SIZE];
|
||||
unsigned int hmacLen = 0;
|
||||
if (!HMAC(md,
|
||||
macKey.data(),
|
||||
static_cast<int>(macKey.size()),
|
||||
reinterpret_cast<const unsigned char *>(macInput.data()),
|
||||
macInput.size(),
|
||||
hmac,
|
||||
&hmacLen))
|
||||
{
|
||||
throw std::runtime_error("jwe_hmac_failed");
|
||||
}
|
||||
if (hmacLen < tag.size() ||
|
||||
CRYPTO_memcmp(hmac, tag.data(), tag.size()) != 0)
|
||||
{
|
||||
throw std::runtime_error("jwe_auth_tag_mismatch");
|
||||
}
|
||||
|
||||
CipherCtxPtr ctx(EVP_CIPHER_CTX_new(), &EVP_CIPHER_CTX_free);
|
||||
if (!ctx)
|
||||
{
|
||||
throw std::runtime_error("jwe_cipher_ctx_new_failed");
|
||||
}
|
||||
requireExact(EVP_DecryptInit_ex(
|
||||
ctx.get(),
|
||||
cipher,
|
||||
nullptr,
|
||||
reinterpret_cast<const unsigned char *>(encKey.data()),
|
||||
reinterpret_cast<const unsigned char *>(iv.data())) == 1,
|
||||
"jwe_decrypt_init_failed");
|
||||
|
||||
std::string plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH, '\0');
|
||||
int outLen1 = 0;
|
||||
requireExact(EVP_DecryptUpdate(
|
||||
ctx.get(),
|
||||
reinterpret_cast<unsigned char *>(plaintext.data()),
|
||||
&outLen1,
|
||||
reinterpret_cast<const unsigned char *>(ciphertext.data()),
|
||||
ciphertext.size()) == 1,
|
||||
"jwe_decrypt_update_failed");
|
||||
|
||||
int outLen2 = 0;
|
||||
requireExact(EVP_DecryptFinal_ex(
|
||||
ctx.get(),
|
||||
reinterpret_cast<unsigned char *>(plaintext.data()) + outLen1,
|
||||
&outLen2) == 1,
|
||||
"jwe_decrypt_final_failed");
|
||||
plaintext.resize(outLen1 + outLen2);
|
||||
return plaintext;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
JweParts parseJwe(const std::string &jwe)
|
||||
{
|
||||
const auto parts = split(jwe, '.');
|
||||
if (parts.size() != 5)
|
||||
{
|
||||
throw std::runtime_error("invalid_jwe");
|
||||
}
|
||||
|
||||
JweParts parsed;
|
||||
parsed.encodedHeader = parts[0];
|
||||
parsed.encodedEncryptedKey = parts[1];
|
||||
parsed.encodedIv = parts[2];
|
||||
parsed.encodedCiphertext = parts[3];
|
||||
parsed.encodedTag = parts[4];
|
||||
parsed.header = parseJsonText(base64UrlDecode(parts[0]));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
std::string decryptJwe(const std::string &jwe, const Jwk &jwk)
|
||||
{
|
||||
const auto parsed = parseJwe(jwe);
|
||||
const auto alg = parsed.header.get("alg", "").asString();
|
||||
const auto enc = parsed.header.get("enc", "").asString();
|
||||
|
||||
const auto cek =
|
||||
rsaOaepDecrypt(base64UrlDecode(parsed.encodedEncryptedKey), jwk, alg);
|
||||
if (enc == "A256CBC-HS512")
|
||||
{
|
||||
return decryptCbcHmac(parsed,
|
||||
cek,
|
||||
64,
|
||||
16,
|
||||
32,
|
||||
EVP_aes_256_cbc(),
|
||||
EVP_sha512());
|
||||
}
|
||||
if (enc == "A128CBC-HS256")
|
||||
{
|
||||
return decryptCbcHmac(parsed,
|
||||
cek,
|
||||
32,
|
||||
16,
|
||||
16,
|
||||
EVP_aes_128_cbc(),
|
||||
EVP_sha256());
|
||||
}
|
||||
throw std::runtime_error("unsupported_jwe_enc");
|
||||
}
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/OidcJwt.h"
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
struct JweParts
|
||||
{
|
||||
Json::Value header;
|
||||
std::string encodedHeader;
|
||||
std::string encodedEncryptedKey;
|
||||
std::string encodedIv;
|
||||
std::string encodedCiphertext;
|
||||
std::string encodedTag;
|
||||
};
|
||||
|
||||
JweParts parseJwe(const std::string &jwe);
|
||||
std::string decryptJwe(const std::string &jwe, const Jwk &jwk);
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "common/OidcHttp.h"
|
||||
|
||||
#include <drogon/HttpClient.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
namespace
|
||||
{
|
||||
HttpResponseData sendGet(const std::string &url,
|
||||
const std::map<std::string, std::string> &headers,
|
||||
double timeoutSeconds)
|
||||
{
|
||||
auto client = drogon::HttpClient::newHttpClient(url);
|
||||
auto request = drogon::HttpRequest::newHttpRequest();
|
||||
request->setMethod(drogon::Get);
|
||||
for (const auto &[key, value] : headers)
|
||||
{
|
||||
request->addHeader(key, value);
|
||||
}
|
||||
|
||||
const auto [result, response] = client->sendRequest(request, timeoutSeconds);
|
||||
if (result != drogon::ReqResult::Ok || !response)
|
||||
{
|
||||
throw std::runtime_error("http_get_failed");
|
||||
}
|
||||
|
||||
HttpResponseData out;
|
||||
out.statusCode = static_cast<int>(response->statusCode());
|
||||
out.body = response->body();
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
HttpResponseData httpGet(const std::string &url, double timeoutSeconds)
|
||||
{
|
||||
return sendGet(url, {}, timeoutSeconds);
|
||||
}
|
||||
|
||||
HttpResponseData httpGet(const std::string &url,
|
||||
const std::map<std::string, std::string> &headers,
|
||||
double timeoutSeconds)
|
||||
{
|
||||
return sendGet(url, headers, timeoutSeconds);
|
||||
}
|
||||
|
||||
HttpResponseData httpPostForm(const std::string &url,
|
||||
const std::map<std::string, std::string> ¶ms,
|
||||
double timeoutSeconds)
|
||||
{
|
||||
auto client = drogon::HttpClient::newHttpClient(url);
|
||||
auto request = drogon::HttpRequest::newHttpFormPostRequest();
|
||||
request->setMethod(drogon::Post);
|
||||
for (const auto &[key, value] : params)
|
||||
{
|
||||
request->setBodyParameter(key, value);
|
||||
}
|
||||
|
||||
const auto [result, response] = client->sendRequest(request, timeoutSeconds);
|
||||
if (result != drogon::ReqResult::Ok || !response)
|
||||
{
|
||||
throw std::runtime_error("http_post_failed");
|
||||
}
|
||||
|
||||
HttpResponseData out;
|
||||
out.statusCode = static_cast<int>(response->statusCode());
|
||||
out.body = response->body();
|
||||
return out;
|
||||
}
|
||||
|
||||
Json::Value parseJsonResponse(const HttpResponseData &response)
|
||||
{
|
||||
Json::CharReaderBuilder builder;
|
||||
builder["collectComments"] = false;
|
||||
std::string errors;
|
||||
Json::Value value;
|
||||
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
||||
if (!reader->parse(response.body.data(),
|
||||
response.body.data() + response.body.size(),
|
||||
&value,
|
||||
&errors))
|
||||
{
|
||||
throw std::runtime_error("invalid_http_json");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
struct HttpResponseData
|
||||
{
|
||||
int statusCode{0};
|
||||
std::string body;
|
||||
std::map<std::string, std::string> headers;
|
||||
};
|
||||
|
||||
HttpResponseData httpGet(const std::string &url, double timeoutSeconds = 5.0);
|
||||
HttpResponseData httpGet(const std::string &url,
|
||||
const std::map<std::string, std::string> &headers,
|
||||
double timeoutSeconds = 5.0);
|
||||
HttpResponseData httpPostForm(const std::string &url,
|
||||
const std::map<std::string, std::string> ¶ms,
|
||||
double timeoutSeconds = 5.0);
|
||||
Json::Value parseJsonResponse(const HttpResponseData &response);
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "common/OidcJwt.h"
|
||||
|
||||
#include <drogon/utils/Utilities.h>
|
||||
|
||||
#include <jwt-cpp/traits/open-source-parsers-jsoncpp/defaults.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
namespace
|
||||
{
|
||||
using BnPtr = std::unique_ptr<BIGNUM, decltype(&BN_free)>;
|
||||
using RsaPtr = std::unique_ptr<RSA, decltype(&RSA_free)>;
|
||||
using EvpKeyPtr = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
|
||||
using BioPtr = std::unique_ptr<BIO, decltype(&BIO_free)>;
|
||||
using BnCtxPtr = std::unique_ptr<BN_CTX, decltype(&BN_CTX_free)>;
|
||||
|
||||
BnPtr bnFromBase64Url(const std::string &value)
|
||||
{
|
||||
const auto decoded = base64UrlDecode(value);
|
||||
BIGNUM *raw = BN_bin2bn(reinterpret_cast<const unsigned char *>(decoded.data()),
|
||||
static_cast<int>(decoded.size()),
|
||||
nullptr);
|
||||
if (!raw)
|
||||
{
|
||||
throw std::runtime_error("invalid_jwk_bn");
|
||||
}
|
||||
return {raw, &BN_free};
|
||||
}
|
||||
|
||||
EvpKeyPtr keyFromJwk(const Jwk &jwk, bool includePrivate)
|
||||
{
|
||||
if (jwk.kty != "RSA")
|
||||
{
|
||||
throw std::runtime_error("unsupported_jwk_type");
|
||||
}
|
||||
|
||||
RsaPtr rsa(RSA_new(), &RSA_free);
|
||||
if (!rsa)
|
||||
{
|
||||
throw std::runtime_error("rsa_new_failed");
|
||||
}
|
||||
|
||||
auto n = bnFromBase64Url(jwk.n);
|
||||
auto e = bnFromBase64Url(jwk.e);
|
||||
BIGNUM *nRaw = n.release();
|
||||
BIGNUM *eRaw = e.release();
|
||||
if (RSA_set0_key(rsa.get(), nRaw, eRaw, nullptr) != 1)
|
||||
{
|
||||
BN_free(nRaw);
|
||||
BN_free(eRaw);
|
||||
throw std::runtime_error("rsa_set_public_failed");
|
||||
}
|
||||
|
||||
if (includePrivate)
|
||||
{
|
||||
if (jwk.d.empty() || jwk.p.empty() || jwk.q.empty())
|
||||
{
|
||||
throw std::runtime_error("missing_private_jwk_fields");
|
||||
}
|
||||
|
||||
auto d = bnFromBase64Url(jwk.d);
|
||||
auto p = bnFromBase64Url(jwk.p);
|
||||
auto q = bnFromBase64Url(jwk.q);
|
||||
BIGNUM *dRaw = d.release();
|
||||
if (RSA_set0_key(rsa.get(), nullptr, nullptr, dRaw) != 1)
|
||||
{
|
||||
BN_free(dRaw);
|
||||
throw std::runtime_error("rsa_set_private_failed");
|
||||
}
|
||||
|
||||
BIGNUM *pRaw = p.release();
|
||||
BIGNUM *qRaw = q.release();
|
||||
if (RSA_set0_factors(rsa.get(), pRaw, qRaw) != 1)
|
||||
{
|
||||
BN_free(pRaw);
|
||||
BN_free(qRaw);
|
||||
throw std::runtime_error("rsa_set_factors_failed");
|
||||
}
|
||||
|
||||
const BIGNUM *rsaD = nullptr;
|
||||
const BIGNUM *rsaP = nullptr;
|
||||
const BIGNUM *rsaQ = nullptr;
|
||||
const BIGNUM *rsaE = nullptr;
|
||||
RSA_get0_key(rsa.get(), nullptr, &rsaE, &rsaD);
|
||||
RSA_get0_factors(rsa.get(), &rsaP, &rsaQ);
|
||||
|
||||
BnCtxPtr bnCtx(BN_CTX_new(), &BN_CTX_free);
|
||||
if (!bnCtx)
|
||||
{
|
||||
throw std::runtime_error("bn_ctx_new_failed");
|
||||
}
|
||||
|
||||
BIGNUM *dp = BN_new();
|
||||
BIGNUM *dq = BN_new();
|
||||
BIGNUM *qi = BN_new();
|
||||
BIGNUM *pMinus1 = BN_dup(rsaP);
|
||||
BIGNUM *qMinus1 = BN_dup(rsaQ);
|
||||
if (!dp || !dq || !qi || !pMinus1 || !qMinus1)
|
||||
{
|
||||
BN_free(dp);
|
||||
BN_free(dq);
|
||||
BN_free(qi);
|
||||
BN_free(pMinus1);
|
||||
BN_free(qMinus1);
|
||||
throw std::runtime_error("bn_alloc_failed");
|
||||
}
|
||||
|
||||
BN_sub_word(pMinus1, 1);
|
||||
BN_sub_word(qMinus1, 1);
|
||||
if (BN_mod(dp, rsaD, pMinus1, bnCtx.get()) != 1 ||
|
||||
BN_mod(dq, rsaD, qMinus1, bnCtx.get()) != 1 ||
|
||||
BN_mod_inverse(qi, rsaQ, rsaP, bnCtx.get()) == nullptr)
|
||||
{
|
||||
BN_free(dp);
|
||||
BN_free(dq);
|
||||
BN_free(qi);
|
||||
BN_free(pMinus1);
|
||||
BN_free(qMinus1);
|
||||
throw std::runtime_error("rsa_crt_params_failed");
|
||||
}
|
||||
BN_free(pMinus1);
|
||||
BN_free(qMinus1);
|
||||
|
||||
if (RSA_set0_crt_params(rsa.get(), dp, dq, qi) != 1)
|
||||
{
|
||||
BN_free(dp);
|
||||
BN_free(dq);
|
||||
BN_free(qi);
|
||||
throw std::runtime_error("rsa_set_crt_failed");
|
||||
}
|
||||
}
|
||||
|
||||
EVP_PKEY *rawPkey = EVP_PKEY_new();
|
||||
if (!rawPkey)
|
||||
{
|
||||
throw std::runtime_error("evp_pkey_new_failed");
|
||||
}
|
||||
EvpKeyPtr pkey(rawPkey, &EVP_PKEY_free);
|
||||
if (EVP_PKEY_assign_RSA(pkey.get(), rsa.release()) != 1)
|
||||
{
|
||||
throw std::runtime_error("evp_assign_rsa_failed");
|
||||
}
|
||||
return pkey;
|
||||
}
|
||||
|
||||
std::string pemFromJwk(const Jwk &jwk, bool includePrivate)
|
||||
{
|
||||
auto pkey = keyFromJwk(jwk, includePrivate);
|
||||
BioPtr bio(BIO_new(BIO_s_mem()), &BIO_free);
|
||||
if (!bio)
|
||||
{
|
||||
throw std::runtime_error("bio_new_failed");
|
||||
}
|
||||
|
||||
const int ok = includePrivate
|
||||
? PEM_write_bio_PrivateKey(
|
||||
bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr)
|
||||
: PEM_write_bio_PUBKEY(bio.get(), pkey.get());
|
||||
if (ok != 1)
|
||||
{
|
||||
throw std::runtime_error("pem_write_failed");
|
||||
}
|
||||
|
||||
BUF_MEM *buffer = nullptr;
|
||||
BIO_get_mem_ptr(bio.get(), &buffer);
|
||||
if (!buffer || !buffer->data || buffer->length == 0)
|
||||
{
|
||||
throw std::runtime_error("pem_buffer_failed");
|
||||
}
|
||||
return {buffer->data, buffer->length};
|
||||
}
|
||||
|
||||
Json::Value parseJsonText(const std::string &text)
|
||||
{
|
||||
Json::CharReaderBuilder builder;
|
||||
builder["collectComments"] = false;
|
||||
std::string errors;
|
||||
Json::Value value;
|
||||
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
||||
if (!reader->parse(text.data(), text.data() + text.size(), &value, &errors))
|
||||
{
|
||||
throw std::runtime_error("invalid_json");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
template <typename Builder>
|
||||
void setClaims(Builder &builder,
|
||||
const Json::Value &value,
|
||||
bool header)
|
||||
{
|
||||
if (!value.isObject())
|
||||
{
|
||||
throw std::runtime_error("invalid_jwt_claims_json");
|
||||
}
|
||||
|
||||
for (const auto &name : value.getMemberNames())
|
||||
{
|
||||
if (header)
|
||||
{
|
||||
builder.set_header_claim(name, value[name]);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.set_payload_claim(name, value[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Jwk parseJwk(const Json::Value &value)
|
||||
{
|
||||
Jwk jwk;
|
||||
jwk.kty = value.get("kty", "").asString();
|
||||
jwk.use = value.get("use", "").asString();
|
||||
jwk.alg = value.get("alg", "").asString();
|
||||
jwk.kid = value.get("kid", "").asString();
|
||||
jwk.n = value.get("n", "").asString();
|
||||
jwk.e = value.get("e", "").asString();
|
||||
jwk.d = value.get("d", "").asString();
|
||||
jwk.p = value.get("p", "").asString();
|
||||
jwk.q = value.get("q", "").asString();
|
||||
return jwk;
|
||||
}
|
||||
|
||||
Json::Value jwkToPublicJson(const Jwk &jwk)
|
||||
{
|
||||
Json::Value value;
|
||||
value["kty"] = jwk.kty;
|
||||
value["use"] = jwk.use;
|
||||
if (!jwk.alg.empty())
|
||||
{
|
||||
value["alg"] = jwk.alg;
|
||||
}
|
||||
value["kid"] = jwk.kid;
|
||||
value["n"] = jwk.n;
|
||||
value["e"] = jwk.e;
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string base64UrlEncode(const std::string &value)
|
||||
{
|
||||
return drogon::utils::base64EncodeUnpadded(value, true);
|
||||
}
|
||||
|
||||
std::string base64UrlDecode(const std::string &value)
|
||||
{
|
||||
std::string padded = value;
|
||||
while (padded.size() % 4 != 0)
|
||||
{
|
||||
padded.push_back('=');
|
||||
}
|
||||
for (char &ch : padded)
|
||||
{
|
||||
if (ch == '-')
|
||||
{
|
||||
ch = '+';
|
||||
}
|
||||
else if (ch == '_')
|
||||
{
|
||||
ch = '/';
|
||||
}
|
||||
}
|
||||
return drogon::utils::base64Decode(padded);
|
||||
}
|
||||
|
||||
std::string randomUrlSafeString(size_t sizeBytes)
|
||||
{
|
||||
return base64UrlEncode(drogon::utils::genRandomString(sizeBytes));
|
||||
}
|
||||
|
||||
std::string signJwt(const Json::Value &header,
|
||||
const Json::Value &payload,
|
||||
const Jwk &jwk)
|
||||
{
|
||||
auto builder = jwt::create();
|
||||
setClaims(builder, header, true);
|
||||
setClaims(builder, payload, false);
|
||||
return builder.sign(
|
||||
jwt::algorithm::rs256(pemFromJwk(jwk, false), pemFromJwk(jwk, true)));
|
||||
}
|
||||
|
||||
bool verifyJwtSignature(const std::string &jwtText, const Jwk &jwk)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto decoded = jwt::decode(jwtText);
|
||||
auto verifier = jwt::verify().allow_algorithm(
|
||||
jwt::algorithm::rs256(pemFromJwk(jwk, false)));
|
||||
verifier.verify(decoded);
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
JwtParts parseJwt(const std::string &jwtText)
|
||||
{
|
||||
const auto decoded = jwt::decode(jwtText);
|
||||
|
||||
JwtParts parsed;
|
||||
parsed.encodedHeader = decoded.get_header_base64();
|
||||
parsed.encodedPayload = decoded.get_payload_base64();
|
||||
parsed.encodedSignature = decoded.get_signature_base64();
|
||||
parsed.header = decoded.get_header_json();
|
||||
parsed.payload = decoded.get_payload_json();
|
||||
return parsed;
|
||||
}
|
||||
|
||||
Json::Value parseJsonObjectJwtPayload(const std::string &jwtText)
|
||||
{
|
||||
const auto parsed = parseJwt(jwtText);
|
||||
if (!parsed.payload.isObject())
|
||||
{
|
||||
throw std::runtime_error("invalid_jwt_payload");
|
||||
}
|
||||
return parsed.payload;
|
||||
}
|
||||
|
||||
std::optional<Jwk> findJwkByKid(const std::vector<Jwk> &jwks,
|
||||
const std::string &kid)
|
||||
{
|
||||
for (const auto &jwk : jwks)
|
||||
{
|
||||
if (jwk.kid == kid)
|
||||
{
|
||||
return jwk;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string sha256Base64Url(const std::string &value)
|
||||
{
|
||||
unsigned char digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256(reinterpret_cast<const unsigned char *>(value.data()),
|
||||
value.size(),
|
||||
digest);
|
||||
return drogon::utils::base64EncodeUnpadded(digest,
|
||||
SHA256_DIGEST_LENGTH,
|
||||
true);
|
||||
}
|
||||
|
||||
std::string leftHashBase64Url(const std::string &value)
|
||||
{
|
||||
const auto digest = base64UrlDecode(sha256Base64Url(value));
|
||||
return base64UrlEncode(digest.substr(0, digest.size() / 2));
|
||||
}
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
struct Jwk
|
||||
{
|
||||
std::string kty;
|
||||
std::string use;
|
||||
std::string alg;
|
||||
std::string kid;
|
||||
std::string n;
|
||||
std::string e;
|
||||
std::string d;
|
||||
std::string p;
|
||||
std::string q;
|
||||
};
|
||||
|
||||
struct JwtParts
|
||||
{
|
||||
Json::Value header;
|
||||
Json::Value payload;
|
||||
std::string encodedHeader;
|
||||
std::string encodedPayload;
|
||||
std::string encodedSignature;
|
||||
};
|
||||
|
||||
Jwk parseJwk(const Json::Value &value);
|
||||
Json::Value jwkToPublicJson(const Jwk &jwk);
|
||||
|
||||
std::string base64UrlEncode(const std::string &value);
|
||||
std::string base64UrlDecode(const std::string &value);
|
||||
std::string randomUrlSafeString(size_t sizeBytes);
|
||||
|
||||
std::string signJwt(const Json::Value &header,
|
||||
const Json::Value &payload,
|
||||
const Jwk &jwk);
|
||||
bool verifyJwtSignature(const std::string &jwt, const Jwk &jwk);
|
||||
JwtParts parseJwt(const std::string &jwt);
|
||||
Json::Value parseJsonObjectJwtPayload(const std::string &jwt);
|
||||
|
||||
std::optional<Jwk> findJwkByKid(const std::vector<Jwk> &jwks,
|
||||
const std::string &kid);
|
||||
|
||||
std::string sha256Base64Url(const std::string &value);
|
||||
std::string leftHashBase64Url(const std::string &value);
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "common/OidcRpConfig.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::vector<std::string> readStringArray(const Json::Value &value)
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
if (!value.isArray())
|
||||
{
|
||||
return out;
|
||||
}
|
||||
|
||||
out.reserve(value.size());
|
||||
for (const auto &item : value)
|
||||
{
|
||||
if (item.isString())
|
||||
{
|
||||
out.push_back(item.asString());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Json::Value readJsonFile(const std::string &path)
|
||||
{
|
||||
std::ifstream input(path);
|
||||
if (!input.is_open())
|
||||
{
|
||||
throw std::runtime_error("oidc_jwks_file_not_found");
|
||||
}
|
||||
Json::Value value;
|
||||
input >> value;
|
||||
return value;
|
||||
}
|
||||
|
||||
bool fileExists(const std::string &path)
|
||||
{
|
||||
return !path.empty() && std::filesystem::exists(path);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
RpConfig loadRpConfig()
|
||||
{
|
||||
const auto &root = drogon::app().getCustomConfig();
|
||||
const auto &oidcRp = root["oidc_rp"];
|
||||
const auto &federation = root["oidc_federation"];
|
||||
|
||||
RpConfig config;
|
||||
config.enabled = oidcRp.get("enabled", false).asBool();
|
||||
config.baseUrl = oidcRp.get("base_url", "").asString();
|
||||
config.clientId = oidcRp.get("client_id", "").asString();
|
||||
config.redirectUri = oidcRp.get("redirect_uri", "").asString();
|
||||
config.postLoginRedirectUri =
|
||||
oidcRp.get("post_login_redirect_uri", "").asString();
|
||||
config.organizationName =
|
||||
oidcRp.get("organization_name", "").asString();
|
||||
config.clientName = oidcRp.get("client_name", "").asString();
|
||||
config.contacts = readStringArray(oidcRp["contacts"]);
|
||||
config.jwksPath = oidcRp.get("jwks_path", "").asString();
|
||||
config.federationJwksPath =
|
||||
oidcRp.get("federation_jwks_path", "").asString();
|
||||
config.trustAnchors = readStringArray(federation["trust_anchors"]);
|
||||
|
||||
const auto &providers = federation["identity_providers"];
|
||||
if (providers.isObject())
|
||||
{
|
||||
for (const auto &name : providers.getMemberNames())
|
||||
{
|
||||
const auto &item = providers[name];
|
||||
RpProviderConfig provider;
|
||||
provider.profile = item.get("profile", name).asString();
|
||||
provider.issuer = item.get("issuer", "").asString();
|
||||
provider.trustAnchor = item.get("trust_anchor", "").asString();
|
||||
config.providers.push_back(std::move(provider));
|
||||
}
|
||||
}
|
||||
|
||||
const auto jwksRoot = readJsonFile(config.jwksPath);
|
||||
Json::Value federationRoot;
|
||||
if (fileExists(config.federationJwksPath))
|
||||
{
|
||||
federationRoot = readJsonFile(config.federationJwksPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
federationRoot["keys"] = jwksRoot["federation"];
|
||||
}
|
||||
|
||||
const auto &federationKeys = federationRoot.isObject()
|
||||
? federationRoot["keys"]
|
||||
: federationRoot;
|
||||
if (federationKeys.isArray())
|
||||
{
|
||||
for (const auto &item : federationKeys)
|
||||
{
|
||||
config.federationJwks.push_back(parseJwk(item));
|
||||
}
|
||||
}
|
||||
if (jwksRoot["core"].isArray())
|
||||
{
|
||||
for (const auto &item : jwksRoot["core"])
|
||||
{
|
||||
config.coreJwks.push_back(parseJwk(item));
|
||||
}
|
||||
}
|
||||
config.trustMark = jwksRoot["trust_mark"];
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Json::Value rpMetadataDocument(const RpConfig &config)
|
||||
{
|
||||
Json::Value metadata;
|
||||
metadata["openid_relying_party"]["organization_name"] =
|
||||
config.organizationName;
|
||||
metadata["openid_relying_party"]["application_type"] = "web";
|
||||
metadata["openid_relying_party"]["client_id"] = config.clientId;
|
||||
metadata["openid_relying_party"]["client_registration_types"].append(
|
||||
"automatic");
|
||||
metadata["openid_relying_party"]["client_name"] = config.clientName;
|
||||
metadata["openid_relying_party"]["subject_type"] = "pairwise";
|
||||
metadata["openid_relying_party"]["redirect_uris"].append(config.redirectUri);
|
||||
metadata["openid_relying_party"]["response_types"].append("code");
|
||||
metadata["openid_relying_party"]["grant_types"].append("refresh_token");
|
||||
metadata["openid_relying_party"]["grant_types"].append("authorization_code");
|
||||
metadata["openid_relying_party"]["token_endpoint_auth_method"] =
|
||||
"private_key_jwt";
|
||||
metadata["openid_relying_party"]["id_token_signed_response_alg"] = "RS256";
|
||||
metadata["openid_relying_party"]["userinfo_signed_response_alg"] = "RS256";
|
||||
metadata["openid_relying_party"]["userinfo_encrypted_response_alg"] =
|
||||
"RSA-OAEP";
|
||||
metadata["openid_relying_party"]["userinfo_encrypted_response_enc"] =
|
||||
"A256CBC-HS512";
|
||||
|
||||
Json::Value publicJwks;
|
||||
publicJwks["keys"] = Json::arrayValue;
|
||||
for (const auto &jwk : config.coreJwks)
|
||||
{
|
||||
publicJwks["keys"].append(jwkToPublicJson(jwk));
|
||||
}
|
||||
metadata["openid_relying_party"]["jwks"] = publicJwks;
|
||||
metadata["openid_relying_party"]["jwks_uri"] =
|
||||
config.clientId + "/openid_relying_party/jwks.json";
|
||||
metadata["openid_relying_party"]["signed_jwks_uri"] =
|
||||
config.clientId + "/openid_relying_party/jwks.jose";
|
||||
|
||||
for (const auto &contact : config.contacts)
|
||||
{
|
||||
metadata["openid_relying_party"]["contacts"].append(contact);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
Json::Value rpPublicJwksDocument(const RpConfig &config)
|
||||
{
|
||||
Json::Value body;
|
||||
body["keys"] = Json::arrayValue;
|
||||
for (const auto &jwk : config.coreJwks)
|
||||
{
|
||||
body["keys"].append(jwkToPublicJson(jwk));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
Json::Value entityConfigurationDocument(const RpConfig &config)
|
||||
{
|
||||
Json::Value body;
|
||||
body["exp"] = static_cast<Json::Int64>(std::time(nullptr) + 2880 * 60);
|
||||
body["iat"] = static_cast<Json::Int64>(std::time(nullptr));
|
||||
body["iss"] = config.clientId;
|
||||
body["sub"] = config.clientId;
|
||||
body["jwks"]["keys"] = Json::arrayValue;
|
||||
for (const auto &jwk : config.federationJwks)
|
||||
{
|
||||
body["jwks"]["keys"].append(jwkToPublicJson(jwk));
|
||||
}
|
||||
body["metadata"] = rpMetadataDocument(config);
|
||||
body["metadata"]["federation_entity"]["federation_resolve_endpoint"] =
|
||||
config.clientId + "/resolve";
|
||||
body["metadata"]["federation_entity"]["organization_name"] =
|
||||
config.organizationName;
|
||||
body["metadata"]["federation_entity"]["homepage_uri"] = config.baseUrl;
|
||||
body["metadata"]["federation_entity"]["policy_uri"] = config.baseUrl;
|
||||
body["metadata"]["federation_entity"]["logo_uri"] = config.baseUrl;
|
||||
for (const auto &contact : config.contacts)
|
||||
{
|
||||
body["metadata"]["federation_entity"]["contacts"].append(contact);
|
||||
}
|
||||
for (const auto &anchor : config.trustAnchors)
|
||||
{
|
||||
body["authority_hints"].append(anchor);
|
||||
}
|
||||
if (!config.trustMark.isNull())
|
||||
{
|
||||
body["trust_marks"].append(config.trustMark);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
std::optional<RpProviderConfig> findProviderConfig(const RpConfig &config,
|
||||
const std::string &provider)
|
||||
{
|
||||
for (const auto &candidate : config.providers)
|
||||
{
|
||||
if (candidate.profile == provider || candidate.issuer == provider)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include "common/OidcJwt.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
struct RpProviderConfig
|
||||
{
|
||||
std::string issuer;
|
||||
std::string trustAnchor;
|
||||
std::string profile;
|
||||
};
|
||||
|
||||
struct RpConfig
|
||||
{
|
||||
bool enabled{false};
|
||||
std::string baseUrl;
|
||||
std::string clientId;
|
||||
std::string redirectUri;
|
||||
std::string postLoginRedirectUri;
|
||||
std::string organizationName;
|
||||
std::string clientName;
|
||||
std::vector<std::string> contacts;
|
||||
std::string jwksPath;
|
||||
std::string federationJwksPath;
|
||||
std::vector<std::string> trustAnchors;
|
||||
std::vector<RpProviderConfig> providers;
|
||||
std::vector<Jwk> federationJwks;
|
||||
std::vector<Jwk> coreJwks;
|
||||
Json::Value trustMark;
|
||||
};
|
||||
|
||||
RpConfig loadRpConfig();
|
||||
Json::Value rpMetadataDocument(const RpConfig &config);
|
||||
Json::Value rpPublicJwksDocument(const RpConfig &config);
|
||||
Json::Value entityConfigurationDocument(const RpConfig &config);
|
||||
std::optional<RpProviderConfig> findProviderConfig(const RpConfig &config,
|
||||
const std::string &provider);
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "common/OidcUserClaims.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string claimString(const Json::Value &value,
|
||||
const char *primary,
|
||||
const char *secondary = nullptr)
|
||||
{
|
||||
const auto primaryValue = value.get(primary, "");
|
||||
if (primaryValue.isString() && !primaryValue.asString().empty())
|
||||
{
|
||||
return primaryValue.asString();
|
||||
}
|
||||
if (secondary)
|
||||
{
|
||||
const auto secondaryValue = value.get(secondary, "");
|
||||
if (secondaryValue.isString())
|
||||
{
|
||||
return secondaryValue.asString();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string trimFiscalPrefix(std::string value)
|
||||
{
|
||||
const std::string prefix = "TINIT-";
|
||||
if (value.rfind(prefix, 0) == 0)
|
||||
{
|
||||
value.erase(0, prefix.size());
|
||||
}
|
||||
std::transform(value.begin(),
|
||||
value.end(),
|
||||
value.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::optional<NormalizedUserClaims> normalizeIdTokenClaims(
|
||||
const std::string &issuer,
|
||||
const Json::Value &claims)
|
||||
{
|
||||
if (!claims.isObject())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto subject = claims.get("sub", "").asString();
|
||||
const auto fiscal = trimFiscalPrefix(
|
||||
claims.get("fiscal_number",
|
||||
claims.get("https://attributes.eid.gov.it/fiscal_number", ""))
|
||||
.asString());
|
||||
if (subject.empty() || fiscal.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NormalizedUserClaims user;
|
||||
user.issuer = issuer;
|
||||
user.subject = subject;
|
||||
user.firstName = claims.get("given_name", "").asString();
|
||||
user.lastName = claims.get("family_name", "").asString();
|
||||
user.email = claims.get("email", "").asString();
|
||||
user.fiscalCode = fiscal;
|
||||
user.rawClaims = claims;
|
||||
return user;
|
||||
}
|
||||
|
||||
std::optional<NormalizedUserClaims> mergeUserClaims(
|
||||
const std::string &issuer,
|
||||
const Json::Value &idTokenClaims,
|
||||
const Json::Value &userinfoClaims)
|
||||
{
|
||||
if (!idTokenClaims.isObject() || !userinfoClaims.isObject())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Json::Value merged = idTokenClaims;
|
||||
for (const auto &name : userinfoClaims.getMemberNames())
|
||||
{
|
||||
merged[name] = userinfoClaims[name];
|
||||
}
|
||||
|
||||
const auto subject = claimString(merged, "sub");
|
||||
const auto fiscal = trimFiscalPrefix(claimString(
|
||||
merged,
|
||||
"fiscal_number",
|
||||
"https://attributes.eid.gov.it/fiscal_number"));
|
||||
if (subject.empty() || fiscal.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
NormalizedUserClaims user;
|
||||
user.issuer = issuer;
|
||||
user.subject = subject;
|
||||
user.firstName = claimString(merged, "given_name");
|
||||
user.lastName = claimString(merged, "family_name");
|
||||
user.email = claimString(merged, "email");
|
||||
user.fiscalCode = fiscal;
|
||||
user.rawClaims = merged;
|
||||
return user;
|
||||
}
|
||||
} // namespace visione::oidc
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace visione::oidc
|
||||
{
|
||||
struct NormalizedUserClaims
|
||||
{
|
||||
std::string issuer;
|
||||
std::string subject;
|
||||
std::string firstName;
|
||||
std::string lastName;
|
||||
std::string email;
|
||||
std::string fiscalCode;
|
||||
Json::Value rawClaims;
|
||||
};
|
||||
|
||||
std::optional<NormalizedUserClaims> normalizeIdTokenClaims(
|
||||
const std::string &issuer,
|
||||
const Json::Value &claims);
|
||||
|
||||
std::optional<NormalizedUserClaims> mergeUserClaims(
|
||||
const std::string &issuer,
|
||||
const Json::Value &idTokenClaims,
|
||||
const Json::Value &userinfoClaims);
|
||||
} // namespace visione::oidc
|
||||
Reference in New Issue
Block a user