feat(CIE): Implement OIDC RP Service with login, callback, and session management

This commit is contained in:
gabdevele
2026-06-16 23:33:59 +02:00 Unverified
parent bb7ed18539
commit 5f81d2dcf7
31 changed files with 3218 additions and 4 deletions
+221
View File
@@ -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