feat(CIE): Implement OIDC RP Service with login, callback, and session management
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user