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