matrix-coro

A WIP highlevel matrix c++ SDK with a MAS first approach
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-coro.git
Log | Files | Refs | README | LICENSE

commit c7a19da80675ee4d81e52d2ff8df74d14935d255
parent a23ef9e7ab640a0e71ba0fbb06176499486de8f4
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date:   Fri, 20 Sep 2024 21:42:17 +0200

Setup basics for the MAS oidc flows

Diffstat:
M.idea/misc.xml | 2+-
MCMakeLists.txt | 12+++++++++++-
Ainclude/json.hpp | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minclude/matrix_coro.hpp | 90++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Ainclude/utils.hpp | 10++++++++++
Msrc/matrix_coro.cpp | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
6 files changed, 268 insertions(+), 38 deletions(-)

diff --git a/.idea/misc.xml b/.idea/misc.xml @@ -4,7 +4,7 @@ <option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" /> <option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" /> <option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" /> - <option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="5" type="long" /> + <option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="7" type="long" /> <option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexRemoved" /> </component> <component name="CMakePythonSetting"> diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -29,6 +29,16 @@ if (NOT TARGET spdlog) find_package(spdlog REQUIRED) endif () +# include cthash using fetch content +FetchContent_Declare( + cthash + GIT_REPOSITORY https://github.com/hanickadot/cthash.git + GIT_TAG 52ed741c82c518f7c038c50d5f39bf943067d9a8 + GIT_SHALLOW ON + GIT_PROGRESS ON +) + +FetchContent_MakeAvailable(cthash) target_link_libraries( matrix_coro @@ -36,11 +46,11 @@ target_link_libraries( PkgConfig::JSONCPP CURL::libcurl cppcoro + cthash ) target_include_directories(matrix_coro PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include - PUBLIC ${Asyncpp_SOURCE_DIR}/include ) if (CMAKE_BUILD_TYPE MATCHES "Debug") diff --git a/include/json.hpp b/include/json.hpp @@ -0,0 +1,75 @@ +#pragma once +#include <optional> + +#include <json/json.h> + +struct WellKnownResponse { + std::string homeserver; + std::string identity_server; + Json::Value raw; +}; + +struct LoginResponse { + std::string access_token; + std::string device_id; + std::optional<int> expires_in_ms; + std::string home_server; + std::optional<std::string> refresh_token; + std::string user_id; + WellKnownResponse well_known; +}; + +struct AuthIssuerResponse { + std::string issuer; +}; + +struct OpenIDConfiguration { + std::string issuer; + std::string authorization_endpoint; + std::string token_endpoint; + std::string jwks_uri; + std::string registration_endpoint; + std::vector<std::string> scopes_supported; + std::vector<std::string> response_types_supported; + std::vector<std::string> response_modes_supported; + std::vector<std::string> grant_types_supported; + std::vector<std::string> token_endpoint_auth_methods_supported; + std::vector<std::string> token_endpoint_auth_signing_alg_values_supported; + std::string revocation_endpoint; + std::vector<std::string> revocation_endpoint_auth_methods_supported; + std::vector<std::string> revocation_endpoint_auth_signing_alg_values_supported; + std::string introspection_endpoint; + std::vector<std::string> introspection_endpoint_auth_methods_supported; + std::vector<std::string> introspection_endpoint_auth_signing_alg_values_supported; + std::vector<std::string> code_challenge_methods_supported; + std::string userinfo_endpoint; + std::vector<std::string> subject_types_supported; + std::vector<std::string> id_token_signing_alg_values_supported; + std::vector<std::string> userinfo_signing_alg_values_supported; + std::vector<std::string> display_values_supported; + std::vector<std::string> claim_types_supported; + std::vector<std::string> claims_supported; + bool claims_parameter_supported; + bool request_parameter_supported; + bool request_uri_parameter_supported; + std::vector<std::string> prompt_values_supported; + std::string device_authorization_endpoint; + std::string org_matrix_matrix_authentication_service_graphql_endpoint; + std::string account_management_uri; + std::vector<std::string> account_management_actions_supported; +}; + +struct ClientRegistrationData { + std::string application_type; + std::string client_name; + std::string client_uri; + std::string token_endpoint_auth_method; + std::vector<std::string> redirect_uris; + std::vector<std::string> response_types; + std::vector<std::string> grant_types; +}; + +struct ClientRegistrationResponse { + std::string client_id; + int client_id_issued_at; +}; diff --git a/include/matrix_coro.hpp b/include/matrix_coro.hpp @@ -1,39 +1,16 @@ #pragma once -#include <future> -#include <optional> +#include "json.hpp" +#include "utils.hpp" #include "cppcoro/task.hpp" #include <curl/curl.h> -#include <json/json.h> +#include <cthash/sha2/sha256.hpp> -struct WellKnownResponse { - std::string homeserver; - std::string identity_server; - Json::Value raw; -}; - -struct LoginResponse { - std::string access_token; - std::string device_id; - std::optional<int> expires_in_ms; - std::string home_server; - std::optional<std::string> refresh_token; - std::string user_id; - WellKnownResponse well_known; -}; - -struct PasswordLoginData { - std::string homeserver; - std::string mxid; - std::string password; - std::optional<std::string> initial_device_display_name; -}; class BaseClient { }; class LoggedInClient : public BaseClient { -private: LoginResponse login_data; public: @@ -49,14 +26,61 @@ public: curl_easy_cleanup(curl); } - cppcoro::task<LoggedInClient> password_login(PasswordLoginData data) const { - auto well_known_data = co_await fetch_wellknown(data.homeserver); - co_return LoggedInClient(LoginResponse{}); - } - private: CURL *curl = curl_easy_init(); - cppcoro::task<WellKnownResponse> fetch_wellknown(const std::string &homeserver) const; -}; + /** + * \brief Fetches the well-known configuration from the specified homeserver. + * + * This function sends a request to the given homeserver to retrieve the well-known configuration. + * + * \param homeserver The URL of the homeserver from which to fetch the well-known configuration. + * \return A cppcoro::task that resolves to a WellKnownResponse containing the well-known configuration. + */ + [[nodiscard]] cppcoro::task<WellKnownResponse> fetch_wellknown(const std::string &homeserver) const; + /** + * \brief Fetches the authentication issuer information from the specified client-server endpoint. + * + * This function sends a request to the given client-server endpoint to retrieve the authentication issuer information. + * + * \param cs_endpoint The URL of the client-server endpoint from which to fetch the authentication issuer information. + * \return A cppcoro::task that resolves to an AuthIssuerResponse containing the authentication issuer information. + */ + [[nodiscard]] cppcoro::task<AuthIssuerResponse> fetch_auth_issuer(const std::string &cs_endpoint) const; + + /** + * \brief Registers a client with the specified authentication endpoint (MSC2966). + * + * This function sends a registration request to the given authentication endpoint using the provided registration data. + * It allows the client to be registered as an OAuth2 client on the OIDC server side, enabling the client to let people log in. + * + * \param auth_endpoint The URL of the authentication endpoint to register the client. + * \param registration_data The data required for client registration. + * \return A cppcoro::task that resolves to a ClientRegistrationResponse containing the registration result. + */ + [[nodiscard]] cppcoro::task<ClientRegistrationResponse> register_client(const std::string &auth_endpoint, + const ClientRegistrationData & + registration_data) const; + + + // ReSharper disable once CppMemberFunctionMayBeStatic + // NOLINTNEXTLINE(*-convert-member-functions-to-static) + [[nodiscard]] constexpr std::string generate_authorize_url(const std::string &auth_endpoint, + const ClientRegistrationResponse &auth_data, + const std::string &redirect_url, + const std::string &state, + const std::string &code_verifier) const { + // URL encode the redirect URL + const auto url_encoded_redirect_url = url_encode(redirect_url); + + // Calculate the code challenge from the code_verifier by doing `BASE64URL(SHA256(code_verifier))` + const auto code_challenge = cthash::base64url_encode(cthash::simple<cthash::sha256>(code_verifier)).to_string(); + + + return auth_endpoint + "/authorize?response_type=code&response_mode=fragment&client_id=" + + auth_data.client_id + "&redirect_uri=" + url_encoded_redirect_url + + "&scope=urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*%20urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AABCDEFGHIJKL&state=" + + state + "&code_challenge_method=S256" + "&code_challenge=" + code_challenge; + } +}; diff --git a/include/utils.hpp b/include/utils.hpp @@ -0,0 +1,10 @@ +#pragma once +#include <string> +#include <curl/curl.h> + +inline std::string url_encode(const std::string &decoded) { + const auto encoded_value = curl_easy_escape(nullptr, decoded.c_str(), static_cast<int>(decoded.length())); + std::string result(encoded_value); + curl_free(encoded_value); + return result; +} diff --git a/src/matrix_coro.cpp b/src/matrix_coro.cpp @@ -2,8 +2,9 @@ #include "spdlog/spdlog.h" #include <iostream> +#include <json/json.h> -static size_t WriteCallback(void *contents, const size_t size, size_t nmemb, void *userp) { +static size_t WriteCallback(void *contents, const size_t size, const size_t nmemb, void *userp) { static_cast<std::string *>(userp)->append(static_cast<char *>(contents), size * nmemb); return size * nmemb; } @@ -31,13 +32,13 @@ cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &home //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); - if (CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { + if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { throw std::runtime_error("failed to find well_known: " + std::string(curl_easy_strerror(res))); } Json::Value root; Json::Reader reader; - if (bool parse_status = reader.parse(str_buffer, root); !parse_status) { + if (const bool parse_status = reader.parse(str_buffer, root); !parse_status) { throw std::runtime_error("failed to parse well_known"); } WellKnownResponse response; @@ -45,3 +46,113 @@ cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &home response.identity_server = root["m.identity_server"]["base_url"].asString(); co_return response; } + +cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(const std::string &cs_endpoint) const { + if (!curl) { + throw std::runtime_error("http client is not initialized"); + } + + // Throw if the cs_endpoint doesnt start with https:// or if it contains a trailing slash or if it is empty or it contains _matrix/client + if (cs_endpoint.find("https://") == std::string::npos || cs_endpoint.find("_matrix/client") != std::string::npos || + cs_endpoint.back() == '/') { + throw std::runtime_error("invalid cs_endpoint"); + } + + const auto endpoint = cs_endpoint + "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer"; + + std::string str_buffer; + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &str_buffer); + + /* enable all supported built-in compressions */ + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + + //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + + if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { + throw std::runtime_error("failed to find auth_issuer information: " + std::string(curl_easy_strerror(res))); + } + + Json::Value root; + Json::Reader reader; + if (const bool parse_status = reader.parse(str_buffer, root); !parse_status) { + throw std::runtime_error("failed to parse auth_issuer information"); + } + AuthIssuerResponse response; + response.issuer = root["issuer"].asString(); + co_return response; +} + +cppcoro::task<ClientRegistrationResponse> Client::register_client(const std::string &auth_endpoint, + const ClientRegistrationData &registration_data) +const { + if (!curl) { + throw std::runtime_error("http client is not initialized"); + } + + if (auth_endpoint.find("https://") == std::string::npos || auth_endpoint.find("_matrix/client") != std::string::npos + || + auth_endpoint.back() == '/') { + throw std::runtime_error("invalid auth_endpoint"); + } + + const auto endpoint = auth_endpoint + "/oauth2/registration"; + + std::string str_buffer; + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &str_buffer); + + /* enable all supported built-in compressions */ + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + + // Make it a POST request + curl_easy_setopt(curl, CURLOPT_POST, 1L); + + // Convert the registration data to a JSON string + Json::Value root; + root["application_type"] = registration_data.application_type; + root["client_name"] = registration_data.client_name; + root["redirect_uris"] = Json::arrayValue; + for (const auto &uri: registration_data.redirect_uris) { + root["redirect_uris"].append(uri); + } + root["response_types"] = Json::arrayValue; + for (const auto &response_type: registration_data.response_types) { + root["response_types"].append(response_type); + } + root["token_endpoint_auth_method"] = registration_data.token_endpoint_auth_method; + root["client_uri"] = registration_data.client_uri; + + // Convert the JSON to a string + Json::StreamWriterBuilder writer; + const std::string json_str = Json::writeString(writer, root); + + // Set the POST data + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_str.c_str()); + + // Set the Content-Type header + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + + //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + + if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { + throw std::runtime_error("failed to find auth_issuer information: " + std::string(curl_easy_strerror(res))); + } + + Json::Value resp_root; + Json::Reader reader; + if (const bool parse_status = reader.parse(str_buffer, resp_root); !parse_status) { + throw std::runtime_error("failed to parse auth_issuer information"); + } + ClientRegistrationResponse response; + response.client_id = resp_root["client_id"].asString(); + response.client_id_issued_at = resp_root["client_id_issued_at"].asInt(); + co_return response; +}