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 ef07b66cec85de5a34a1d5c19d7d6b3030464b07
parent c7a19da80675ee4d81e52d2ff8df74d14935d255
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date:   Fri, 20 Sep 2024 22:15:01 +0200

implement basic token exchange endpoints

Diffstat:
Minclude/json.hpp | 9+++++++++
Minclude/matrix_coro.hpp | 12++++++++++--
Alinks.md | 2++
Msrc/matrix_coro.cpp | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
4 files changed, 205 insertions(+), 11 deletions(-)

diff --git a/include/json.hpp b/include/json.hpp @@ -73,3 +73,12 @@ struct ClientRegistrationResponse { std::string client_id; int client_id_issued_at; }; + + +struct TokenResponse { + std::string access_token; + std::string refresh_token; + std::string token_type; + int expires_in; + std::string scope; +}; diff --git a/include/matrix_coro.hpp b/include/matrix_coro.hpp @@ -55,11 +55,11 @@ private: * 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_endpoint The URL of the registration endpoint to register the client which can be optained from the openid-configuration. * \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, + [[nodiscard]] cppcoro::task<ClientRegistrationResponse> register_client(const std::string &registration_endpoint, const ClientRegistrationData & registration_data) const; @@ -83,4 +83,12 @@ private: "&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; } + + [[nodiscard]] cppcoro::task<TokenResponse> exchange_code_for_token( + const std::string &token_endpoint, const std::string &code, + const std::string &code_verifier, + const std::string &client_id, const std::string &redirect_url) const; + + [[nodiscard]] cppcoro::task<OpenIDConfiguration> fetch_openid_configration( + const std::string &auth_endpoint) const; }; diff --git a/links.md b/links.md @@ -0,0 +1 @@ +- https://github.com/element-hq/synapse/pull/17528 (Docker image for CI maybe) +\ No newline at end of file diff --git a/src/matrix_coro.cpp b/src/matrix_coro.cpp @@ -1,7 +1,6 @@ #include "matrix_coro.hpp" #include "spdlog/spdlog.h" -#include <iostream> #include <json/json.h> static size_t WriteCallback(void *contents, const size_t size, const size_t nmemb, void *userp) { @@ -85,23 +84,19 @@ cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(const std::string &c co_return response; } -cppcoro::task<ClientRegistrationResponse> Client::register_client(const std::string &auth_endpoint, +cppcoro::task<ClientRegistrationResponse> Client::register_client(const std::string &registration_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"); + if (registration_endpoint.find("https://") == std::string::npos) { + throw std::runtime_error("invalid registration 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_URL, registration_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); @@ -156,3 +151,183 @@ const { response.client_id_issued_at = resp_root["client_id_issued_at"].asInt(); co_return response; } + +cppcoro::task<TokenResponse> Client::exchange_code_for_token(const std::string &token_endpoint, + const std::string &code, + const std::string &code_verifier, + const std::string &client_id, + const std::string &redirect_url) const { + if (!curl) { + throw std::runtime_error("http client is not initialized"); + } + if (token_endpoint.find("https://") == std::string::npos) { + throw std::runtime_error("invalid token_endpoint"); + } + + std::string str_buffer; + curl_easy_setopt(curl, CURLOPT_URL, token_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); + + // Url encode the redirect URL + const auto url_encoded_redirect_url = url_encode(redirect_url); + + // Build the request body + const std::string post_fields = "grant_type=authorization_code&code=" + code + "&redirect_uri=" + + url_encoded_redirect_url + + "&client_id=" + client_id + "&code_verifier=" + code_verifier; + + // Set the POST data + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_fields.c_str()); + + // Set the Content-Type header + curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded"); + 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 exchange token 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 exchange token information"); + } + + TokenResponse response; + response.access_token = resp_root["access_token"].asString(); + response.expires_in = resp_root["expires_in"].asInt(); + response.refresh_token = resp_root["refresh_token"].asString(); + response.token_type = resp_root["token_type"].asString(); + response.scope = resp_root["scope"].asString(); + + co_return response; +} + +cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configration(const std::string &auth_endpoint) 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 + "/.well-known/openid-configuration"; + + 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 openid configuration 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 openid configuration information"); + } + + OpenIDConfiguration response; + response.issuer = root["issuer"].asString(); + response.authorization_endpoint = root["authorization_endpoint"].asString(); + response.token_endpoint = root["token_endpoint"].asString(); + response.jwks_uri = root["jwks_uri"].asString(); + response.registration_endpoint = root["registration_endpoint"].asString(); + + for (const auto &scope: root["scopes_supported"]) { + response.scopes_supported.push_back(scope.asString()); + } + for (const auto &response_type: root["response_types_supported"]) { + response.response_types_supported.push_back(response_type.asString()); + } + for (const auto &response_mode: root["response_modes_supported"]) { + response.response_modes_supported.push_back(response_mode.asString()); + } + for (const auto &grant_type: root["grant_types_supported"]) { + response.grant_types_supported.push_back(grant_type.asString()); + } + for (const auto &token_endpoint_auth_method: root["token_endpoint_auth_methods_supported"]) { + response.token_endpoint_auth_methods_supported.push_back(token_endpoint_auth_method.asString()); + } + for (const auto &token_endpoint_auth_signing_alg: root["token_endpoint_auth_signing_alg_values_supported"]) { + response.token_endpoint_auth_signing_alg_values_supported.push_back(token_endpoint_auth_signing_alg.asString()); + } + response.revocation_endpoint = root["revocation_endpoint"].asString(); + for (const auto &revocation_endpoint_auth_method: root["revocation_endpoint_auth_methods_supported"]) { + response.revocation_endpoint_auth_methods_supported.push_back(revocation_endpoint_auth_method.asString()); + } + for (const auto &revocation_endpoint_auth_signing_alg: root[ + "revocation_endpoint_auth_signing_alg_values_supported"]) { + response.revocation_endpoint_auth_signing_alg_values_supported.push_back( + revocation_endpoint_auth_signing_alg.asString()); + } + response.introspection_endpoint = root["introspection_endpoint"].asString(); + for (const auto &introspection_endpoint_auth_method: root["introspection_endpoint_auth_methods_supported"]) { + response.introspection_endpoint_auth_methods_supported.push_back(introspection_endpoint_auth_method.asString()); + } + for (const auto &introspection_endpoint_auth_signing_alg: root[ + "introspection_endpoint_auth_signing_alg_values_supported"]) { + response.introspection_endpoint_auth_signing_alg_values_supported.push_back( + introspection_endpoint_auth_signing_alg.asString()); + } + for (const auto &code_challenge_method: root["code_challenge_methods_supported"]) { + response.code_challenge_methods_supported.push_back(code_challenge_method.asString()); + } + response.userinfo_endpoint = root["userinfo_endpoint"].asString(); + for (const auto &subject_type: root["subject_types_supported"]) { + response.subject_types_supported.push_back(subject_type.asString()); + } + for (const auto &id_token_signing_alg: root["id_token_signing_alg_values_supported"]) { + response.id_token_signing_alg_values_supported.push_back(id_token_signing_alg.asString()); + } + for (const auto &userinfo_signing_alg: root["userinfo_signing_alg_values_supported"]) { + response.userinfo_signing_alg_values_supported.push_back(userinfo_signing_alg.asString()); + } + for (const auto &display_value: root["display_values_supported"]) { + response.display_values_supported.push_back(display_value.asString()); + } + for (const auto &claim_type: root["claim_types_supported"]) { + response.claim_types_supported.push_back(claim_type.asString()); + } + for (const auto &claim: root["claims_supported"]) { + response.claims_supported.push_back(claim.asString()); + } + response.claims_parameter_supported = root["claims_parameter_supported"].asBool(); + response.request_parameter_supported = root["request_parameter_supported"].asBool(); + response.request_uri_parameter_supported = root["request_uri_parameter_supported"].asBool(); + for (const auto &prompt_value: root["prompt_values_supported"]) { + response.prompt_values_supported.push_back(prompt_value.asString()); + } + response.device_authorization_endpoint = root["device_authorization_endpoint"].asString(); + response.org_matrix_matrix_authentication_service_graphql_endpoint = root[ + "org.matrix.matrix_authentication_service_graphql_endpoint"].asString(); + response.account_management_uri = root["account_management_uri"].asString(); + for (const auto &account_management_action: root["account_management_actions_supported"]) { + response.account_management_actions_supported.push_back(account_management_action.asString()); + } + + co_return response; +}