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 d3bd2cbb53100b54a79c5efd8011d4c67d64a53d
parent 5f7d4259b61b2ce8f5f2955ae448e9adf7d4b058
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date:   Sat, 21 Sep 2024 15:44:22 +0200

Make auth fully work

Diffstat:
Adocs/example.png | 0
Mexamples/main.cpp | 11++++++++---
Minclude/json.hpp | 17++++++-----------
Minclude/matrix_coro.hpp | 23++++++++++++++++-------
Msrc/matrix_coro.cpp | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
5 files changed, 113 insertions(+), 30 deletions(-)

diff --git a/docs/example.png b/docs/example.png Binary files differ. diff --git a/examples/main.cpp b/examples/main.cpp @@ -4,7 +4,7 @@ #include "cppcoro/sync_wait.hpp" int main() { - spdlog::set_level(spdlog::level::debug); + spdlog::set_level(spdlog::level::info); spdlog::set_pattern("[%H:%M:%S %z] [%^%L%$] [thread %t] %v"); Client client; @@ -12,7 +12,8 @@ int main() { const auto homeserver = "https://synapse-oidc.element.dev"; auto redirect_url = "https://areweoidcyet.com/client-implementation-guide/callback"; const auto state = "state"; - const auto code_verifier = "code_verifier"; + const auto code_verifier = + "ahlae7FuMahCeeseip6Shooqu6aefai5xoocea5gav2"; ClientRegistrationData registration_data; registration_data.application_type = "web"; registration_data.client_name = "Test"; @@ -34,7 +35,11 @@ int main() { spdlog::info("Please paste the code here: "); std::cin >> code; - auto logged_in_client = sync_wait(client.exchange_token(code, redirect_url)); + const auto logged_in_client = sync_wait(client.exchange_token(code, redirect_url)); + + const auto whoami = sync_wait(logged_in_client.whoami()); + + spdlog::info("User ID: {}", whoami.user_id); return 0; } diff --git a/include/json.hpp b/include/json.hpp @@ -9,16 +9,6 @@ struct WellKnownResponse { 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; }; @@ -75,7 +65,6 @@ struct ClientRegistrationResponse { int client_id_issued_at; }; - struct TokenResponse { std::string access_token; std::string refresh_token; @@ -83,3 +72,9 @@ struct TokenResponse { int expires_in; std::string scope; }; + +struct WhoamiResponse { + std::string user_id; + std::string device_id; + bool is_guest; +}; diff --git a/include/matrix_coro.hpp b/include/matrix_coro.hpp @@ -5,27 +5,35 @@ #include "cppcoro/task.hpp" #include <curl/curl.h> #include <cthash/sha2/sha256.hpp> +#include <spdlog/spdlog.h> class BaseClient { +protected: + CURL *curl = curl_easy_init(); + +public: + ~BaseClient() { + curl_easy_cleanup(curl); + } }; class LoggedInClient : public BaseClient { TokenResponse token_data; + WellKnownResponse well_known; public: - explicit LoggedInClient(TokenResponse token_data) : token_data(std::move(token_data)) { + LoggedInClient(TokenResponse token_data, WellKnownResponse well_known): token_data(std::move(token_data)), + well_known(std::move(well_known)) { } + + [[nodiscard]] cppcoro::task<WhoamiResponse> whoami() const; }; class Client : public BaseClient { friend class ClientTest; // Declare the test class as a friend public: - ~Client() { - curl_easy_cleanup(curl); - } - [[nodiscard]] cppcoro::task<std::string> get_auth_url(std::string homeserver, std::string redirect_url, std::string state, std::string code_verifier, const ClientRegistrationData &registration_data); @@ -34,7 +42,6 @@ public: const std::string &redirect_url) const; private: - CURL *curl = curl_easy_init(); WellKnownResponse well_known; AuthIssuerResponse auth_issuer; ClientRegistrationResponse client_registration; @@ -84,11 +91,13 @@ private: const std::string &redirect_url, const std::string &state, const std::string &code_verifier) const { + spdlog::debug("Code verifier: {}", code_verifier); // 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(); + const auto sha256_code_verifier = cthash::simple<cthash::sha256>(code_verifier); + const auto code_challenge = cthash::base64url_encode(sha256_code_verifier).to_string(); return auth_endpoint + "?response_type=code&response_mode=fragment&client_id=" + auth_data.client_id + "&redirect_uri=" + url_encoded_redirect_url + diff --git a/src/matrix_coro.cpp b/src/matrix_coro.cpp @@ -11,6 +11,63 @@ static size_t WriteCallback(void *contents, const size_t size, const size_t nmem return size * nmemb; } +cppcoro::task<WhoamiResponse> LoggedInClient::whoami() const { + CURL *curl = curl_easy_init(); + if (!curl) { + throw std::runtime_error("http client is not initialized"); + } + + if (token_data.access_token.empty()) { + throw std::runtime_error("access token is empty"); + } + + auto homeserver = well_known.homeserver; + + // Add https as needed to the homeserver address + std::string homeserver_https = homeserver; + if (homeserver_https.find("https://") == std::string::npos) { + homeserver_https = "https://" + homeserver_https; + } + spdlog::info("Fetching whoami from homeserver: {}", homeserver); + auto endpoint = homeserver_https + "_matrix/client/v3/account/whoami"; + + // Remove double slashes and make them single + std::regex re("([^:])(//+)"); + endpoint = std::regex_replace(endpoint, re, "$1/"); + spdlog::debug("Whoami endpoint: {}", endpoint); + + std::string str_buffer; + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); + 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, ""); + + // Add Authorization header + auto access_token = token_data.access_token; + curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER, access_token.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + + //curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + + if (const CURLcode res = curl_easy_perform(curl); res != CURLE_OK) { + throw std::runtime_error("failed to fetch whoami: " + 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 whoami"); + } + WhoamiResponse response; + response.user_id = root["user_id"].asString(); + response.device_id = root["device_id"].asString(); + response.is_guest = root["is_guest"].asBool(); + + co_return response; +} + cppcoro::task<std::string> Client::get_auth_url(std::string homeserver, std::string redirect_url, std::string state, std::string code_verifier, const ClientRegistrationData &registration_data) { @@ -19,18 +76,20 @@ cppcoro::task<std::string> Client::get_auth_url(std::string homeserver, std::str spdlog::debug("Fetched well-known from homeserver: {}", well_known.homeserver); // Get the auth issuer information - const auto auth_issuer = co_await fetch_auth_issuer(well_known.homeserver); - spdlog::debug("Fetched auth issuer from homeserver: {}", auth_issuer.issuer); + const auto [issuer] = co_await fetch_auth_issuer(well_known.homeserver); + spdlog::debug("Fetched auth issuer from homeserver: {}", issuer); // Get the openid configuration - const auto openid_configuration = co_await fetch_openid_configuration(auth_issuer.issuer); + const auto openid_configuration = co_await fetch_openid_configuration(issuer); // Register the client const auto client_registration = co_await register_client(openid_configuration.registration_endpoint, registration_data); - spdlog::debug("Registered client with client_id: {}", client_registration.client_id); - spdlog::debug("Authorization endpoint: {}", openid_configuration.authorization_endpoint); + spdlog::info("Registered client with client_id: {}", client_registration.client_id); + + this->code_verifier = code_verifier; + // Build the auth URL co_return this->generate_authorize_url(openid_configuration.authorization_endpoint, client_registration, redirect_url, state, @@ -41,8 +100,15 @@ cppcoro::task<LoggedInClient> Client::exchange_token(const std::string &code, co const auto token_resp = co_await exchange_code_for_token(this->openid_configuration.token_endpoint, code, this->code_verifier, this->client_registration.client_id, redirect_url); + + if (token_resp.access_token.empty()) { + throw std::runtime_error("access token is empty after exchange"); + } + spdlog::info("Successfully exchanged code for token"); - co_return LoggedInClient(token_resp); + + const auto logged_in_client = LoggedInClient(token_resp, this->well_known); + co_return logged_in_client; } cppcoro::task<WellKnownResponse> Client::fetch_wellknown(std::string homeserver) { @@ -56,11 +122,14 @@ cppcoro::task<WellKnownResponse> Client::fetch_wellknown(std::string homeserver) if (homeserver_https.find("https://") == std::string::npos) { homeserver_https = "https://" + homeserver_https; } - auto well_known_url = homeserver_https + "/.well-known/matrix/client"; + auto endpoint = homeserver_https + "/.well-known/matrix/client"; + + // Remove double slashes and make them single + std::regex re("([^:])(//+)"); + endpoint = std::regex_replace(endpoint, re, "$1/"); std::string str_buffer; - curl_easy_setopt(curl, CURLOPT_URL, well_known_url.c_str()); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &str_buffer); @@ -255,6 +324,11 @@ cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_e if (const bool parse_status = reader.parse(str_buffer, resp_root); !parse_status) { throw std::runtime_error("failed to parse exchange token information"); } + spdlog::debug("Token response: {}", str_buffer); + if (resp_root.isMember("error")) { + throw std::runtime_error("error: " + resp_root["error"].asString() + ", error_description: " + + resp_root["error_description"].asString()); + } TokenResponse response; response.access_token = resp_root["access_token"].asString();