commit 5f7d4259b61b2ce8f5f2955ae448e9adf7d4b058
parent abe5b14296532739d21d441a1e7ba9cd296221ba
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date: Sat, 21 Sep 2024 12:02:40 +0200
Fix issues and make an example
Diffstat:
7 files changed, 169 insertions(+), 45 deletions(-)
diff --git a/.idea/misc.xml b/.idea/misc.xml
@@ -13,6 +13,7 @@
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
<component name="CidrRootsConfiguration">
<sourceRoots>
+ <file path="$PROJECT_DIR$/examples" />
<file path="$PROJECT_DIR$/include" />
<file path="$PROJECT_DIR$/src" />
<file path="$PROJECT_DIR$/tests" />
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -65,4 +65,7 @@ if (CMAKE_BUILD_TYPE MATCHES "Debug")
#)
endif ()
+# Examples
+add_subdirectory(examples)
+
add_subdirectory(tests)
\ No newline at end of file
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
@@ -0,0 +1,11 @@
+add_executable(
+ matrix_coro_example
+ main.cpp
+)
+
+target_link_libraries(
+ matrix_coro_example
+ matrix_coro
+ cppcoro
+ spdlog::spdlog
+)
+\ No newline at end of file
diff --git a/examples/main.cpp b/examples/main.cpp
@@ -0,0 +1,40 @@
+#include <spdlog/spdlog.h>
+
+#include "matrix_coro.hpp"
+#include "cppcoro/sync_wait.hpp"
+
+int main() {
+ spdlog::set_level(spdlog::level::debug);
+ spdlog::set_pattern("[%H:%M:%S %z] [%^%L%$] [thread %t] %v");
+
+ Client client;
+
+ 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";
+ ClientRegistrationData registration_data;
+ registration_data.application_type = "web";
+ registration_data.client_name = "Test";
+ registration_data.client_uri = "https://areweoidcyet.com/";
+ registration_data.token_endpoint_auth_method = "none";
+ registration_data.redirect_uris = {redirect_url};
+ registration_data.response_types = {"code"};
+ registration_data.grant_types = {"authorization_code", "refresh_token"};
+ registration_data.contacts = {"mailto:hello@localhost"};
+
+ auto auth_url_task = client.get_auth_url(homeserver, redirect_url, state, code_verifier, registration_data);
+
+ auto auth_url = sync_wait(auth_url_task);
+
+ spdlog::info("Please login via the Auth URL: {}", auth_url);
+
+ // Wait for the user to paste the code here
+ std::string code;
+ spdlog::info("Please paste the code here: ");
+ std::cin >> code;
+
+ auto logged_in_client = sync_wait(client.exchange_token(code, redirect_url));
+
+ return 0;
+}
diff --git a/include/matrix_coro.hpp b/include/matrix_coro.hpp
@@ -11,10 +11,10 @@ class BaseClient {
};
class LoggedInClient : public BaseClient {
- LoginResponse login_data;
+ TokenResponse token_data;
public:
- explicit LoggedInClient(LoginResponse login_data) : login_data(std::move(login_data)) {
+ explicit LoggedInClient(TokenResponse token_data) : token_data(std::move(token_data)) {
}
};
@@ -26,8 +26,21 @@ public:
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 ®istration_data);
+
+ [[nodiscard]] cppcoro::task<LoggedInClient> exchange_token(const std::string &code,
+ const std::string &redirect_url) const;
+
private:
CURL *curl = curl_easy_init();
+ WellKnownResponse well_known;
+ AuthIssuerResponse auth_issuer;
+ ClientRegistrationResponse client_registration;
+ OpenIDConfiguration openid_configuration;
+ std::string state;
+ std::string code_verifier;
/**
* \brief Fetches the well-known configuration from the specified homeserver.
@@ -37,7 +50,7 @@ private:
* \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;
+ [[nodiscard]] cppcoro::task<WellKnownResponse> fetch_wellknown(std::string homeserver);
/**
* \brief Fetches the authentication issuer information from the specified client-server endpoint.
@@ -47,7 +60,7 @@ private:
* \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(std::string cs_endpoint) const;
+ [[nodiscard]] cppcoro::task<AuthIssuerResponse> fetch_auth_issuer(std::string cs_endpoint);
/**
* \brief Registers a client with the specified authentication endpoint (MSC2966).
@@ -61,7 +74,7 @@ private:
*/
[[nodiscard]] cppcoro::task<ClientRegistrationResponse> register_client(std::string registration_endpoint,
const ClientRegistrationData &
- registration_data) const;
+ registration_data);
// ReSharper disable once CppMemberFunctionMayBeStatic
@@ -77,8 +90,7 @@ private:
// 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=" +
+ return auth_endpoint + "?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;
@@ -92,5 +104,5 @@ private:
const std::string &redirect_url) const;
[[nodiscard]] cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(
- std::string auth_endpoint) const;
+ std::string auth_endpoint);
};
diff --git a/src/matrix_coro.cpp b/src/matrix_coro.cpp
@@ -1,4 +1,7 @@
#include "matrix_coro.hpp"
+
+#include <regex>
+
#include "spdlog/spdlog.h"
#include <json/json.h>
@@ -8,10 +11,45 @@ static size_t WriteCallback(void *contents, const size_t size, const size_t nmem
return size * nmemb;
}
-cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &homeserver) const {
+cppcoro::task<std::string> Client::get_auth_url(std::string homeserver, std::string redirect_url,
+ std::string state, std::string code_verifier,
+ const ClientRegistrationData ®istration_data) {
+ // Get the well-known configuration
+ const auto well_known = co_await fetch_wellknown(homeserver);
+ 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);
+
+ // Get the openid configuration
+ const auto openid_configuration = co_await fetch_openid_configuration(auth_issuer.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);
+ // Build the auth URL
+ co_return this->generate_authorize_url(openid_configuration.authorization_endpoint, client_registration,
+ redirect_url, state,
+ code_verifier);
+}
+
+cppcoro::task<LoggedInClient> Client::exchange_token(const std::string &code, const std::string &redirect_url) const {
+ 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);
+ spdlog::info("Successfully exchanged code for token");
+ co_return LoggedInClient(token_resp);
+}
+
+cppcoro::task<WellKnownResponse> Client::fetch_wellknown(std::string homeserver) {
if (!curl) {
throw std::runtime_error("http client is not initialized");
}
+ spdlog::info("Fetching well-known from homeserver: {}", homeserver);
// Add https as needed to the homeserver address
std::string homeserver_https = homeserver;
@@ -43,21 +81,24 @@ cppcoro::task<WellKnownResponse> Client::fetch_wellknown(const std::string &home
WellKnownResponse response;
response.homeserver = root["m.homeserver"]["base_url"].asString();
response.identity_server = root["m.identity_server"]["base_url"].asString();
+ this->well_known = response;
co_return response;
}
-cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpoint) const {
+cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpoint) {
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() == '/') {
+ if (cs_endpoint.find("https://") == std::string::npos || cs_endpoint.find("_matrix/client") != std::string::npos) {
throw std::runtime_error("invalid cs_endpoint");
}
- const auto endpoint = cs_endpoint + "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer";
+ auto endpoint = cs_endpoint + "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer";
+
+ // 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, endpoint.c_str());
@@ -76,17 +117,18 @@ cppcoro::task<AuthIssuerResponse> Client::fetch_auth_issuer(std::string cs_endpo
Json::Value root;
Json::Reader reader;
+ spdlog::debug("Auth issuer response: {}", str_buffer);
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();
+ this->auth_issuer = response;
co_return response;
}
cppcoro::task<ClientRegistrationResponse> Client::register_client(std::string registration_endpoint,
- const ClientRegistrationData ®istration_data)
-const {
+ const ClientRegistrationData ®istration_data) {
if (!curl) {
throw std::runtime_error("http client is not initialized");
}
@@ -129,6 +171,7 @@ const {
// Convert the JSON to a string
Json::StreamWriterBuilder writer;
const std::string json_str = Json::writeString(writer, root);
+ spdlog::debug("Registration request: {}", json_str);
// Set the POST data
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_str.c_str());
@@ -142,17 +185,20 @@ const {
//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 registration information: " + std::string(curl_easy_strerror(res)));
+ throw std::runtime_error(
+ "failed to find registration information: " + std::string(curl_easy_strerror(res)));
}
Json::Value resp_root;
Json::Reader reader;
+ spdlog::debug("Registration response: {}", str_buffer);
if (const bool parse_status = reader.parse(str_buffer, resp_root); !parse_status) {
throw std::runtime_error("failed to parse registration information");
}
ClientRegistrationResponse response;
response.client_id = resp_root["client_id"].asString();
response.client_id_issued_at = resp_root["client_id_issued_at"].asInt();
+ this->client_registration = response;
co_return response;
}
@@ -200,7 +246,8 @@ cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_e
//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)));
+ throw std::runtime_error(
+ "failed to find exchange token information: " + std::string(curl_easy_strerror(res)));
}
Json::Value resp_root;
@@ -219,18 +266,22 @@ cppcoro::task<TokenResponse> Client::exchange_code_for_token(std::string token_e
co_return response;
}
-cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::string auth_endpoint) const {
+cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::string auth_endpoint) {
if (!curl) {
throw std::runtime_error("http client is not initialized");
}
+ spdlog::info("Fetching openid configuration from auth_endpoint: {}", auth_endpoint);
- if (auth_endpoint.find("https://") == std::string::npos || auth_endpoint.find("_matrix/client") != std::string::npos
- ||
- auth_endpoint.back() == '/') {
+ if (auth_endpoint.find("https://") == std::string::npos || auth_endpoint.find("_matrix/client") !=
+ std::string::npos) {
throw std::runtime_error("invalid auth_endpoint");
}
- const auto endpoint = auth_endpoint + "/.well-known/openid-configuration";
+ auto endpoint = auth_endpoint + "/.well-known/openid-configuration";
+
+ // 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, endpoint.c_str());
@@ -277,7 +328,8 @@ cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::strin
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.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"]) {
@@ -290,7 +342,8 @@ cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::strin
}
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());
+ 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"]) {
@@ -333,5 +386,7 @@ cppcoro::task<OpenIDConfiguration> Client::fetch_openid_configuration(std::strin
response.account_management_actions_supported.push_back(account_management_action.asString());
}
+ this->openid_configuration = response;
+
co_return response;
}
diff --git a/tests/test.cpp b/tests/test.cpp
@@ -8,23 +8,23 @@
class ClientTest {
public:
- static cppcoro::task<WellKnownResponse> test_fetch_wellknown(const Client &client, const std::string &homeserver) {
+ static cppcoro::task<WellKnownResponse> test_fetch_wellknown(Client &client, const std::string &homeserver) {
return client.fetch_wellknown(homeserver);
}
- static cppcoro::task<AuthIssuerResponse> test_fetch_auth_issuer(const Client &client,
+ static cppcoro::task<AuthIssuerResponse> test_fetch_auth_issuer(Client &client,
const std::string &cs_endpoint) {
return client.fetch_auth_issuer(cs_endpoint);
}
- static cppcoro::task<ClientRegistrationResponse> test_register_client(const Client &client,
+ static cppcoro::task<ClientRegistrationResponse> test_register_client(Client &client,
const std::string ®istration_endpoint,
const ClientRegistrationData &
registration_data) {
return client.register_client(registration_endpoint, registration_data);
}
- static std::string test_generate_authorize_url(const Client &client,
+ static std::string test_generate_authorize_url(Client &client,
const std::string &auth_endpoint,
const ClientRegistrationResponse &auth_data,
const std::string &redirect_url,
@@ -33,7 +33,7 @@ public:
return client.generate_authorize_url(auth_endpoint, auth_data, redirect_url, state, code_verifier);
}
- static cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(const Client &client,
+ static cppcoro::task<OpenIDConfiguration> fetch_openid_configuration(Client &client,
const std::string &auth_endpoint) {
return client.fetch_openid_configuration(auth_endpoint);
}
@@ -58,7 +58,7 @@ SCENARIO("fetch_wellknown can find and parse well-known at matrix.org") {
initLogging();
GIVEN("A Client instance") {
WHEN("fetch_wellknown is called with matrix.org") {
- const Client client;
+ Client client;
auto task = ClientTest::test_fetch_wellknown(client, "matrix.org");
auto [homeserver, identity_server, raw] = sync_wait(task);
THEN("A valid WellKnownResponse should be returned") {
@@ -74,7 +74,7 @@ SCENARIO("fetch_wellknown throws runtime_error if curl_easy_perform fails") {
GIVEN("A Client instance with an invalid URL") {
WHEN("fetch_wellknown is called with an invalid URL") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
REQUIRE_THROWS_AS(sync_wait(ClientTest::test_fetch_wellknown(client,"invalid_url")),
std::runtime_error);
}
@@ -87,7 +87,7 @@ SCENARIO("fetch_wellknown throws runtime_error if JSON parsing fails") {
GIVEN("A Client instance with a URL returning invalid JSON") {
WHEN("fetch_wellknown is called with a URL returning invalid JSON") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
REQUIRE_THROWS_AS(
sync_wait(ClientTest::test_fetch_wellknown(client,"https://example.com/invalid-json")),
std::runtime_error);
@@ -100,7 +100,7 @@ SCENARIO("fetch_auth_issuer can find and parse auth issuer at https://synapse-oi
initLogging();
GIVEN("A Client instance") {
WHEN("fetch_auth_issuer is called with https://synapse-oidc.element.dev") {
- const Client client;
+ Client client;
auto task = ClientTest::test_fetch_auth_issuer(client, "https://synapse-oidc.element.dev");
auto [issuer] = sync_wait(task);
THEN("A valid AuthIssuerResponse should be returned") {
@@ -115,7 +115,7 @@ SCENARIO("fetch_auth_issuer throws runtime_error if curl_easy_perform fails") {
GIVEN("A Client instance with an invalid URL") {
WHEN("fetch_auth_issuer is called with an invalid URL") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
REQUIRE_THROWS_AS(sync_wait(ClientTest::test_fetch_auth_issuer(client,"invalid_url")),
std::runtime_error);
}
@@ -128,7 +128,7 @@ SCENARIO("fetch_auth_issuer throws runtime_error if JSON parsing fails") {
GIVEN("A Client instance with a URL returning invalid JSON") {
WHEN("fetch_auth_issuer is called with a URL returning invalid JSON") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
REQUIRE_THROWS_AS(
sync_wait(ClientTest::test_fetch_auth_issuer(client,"https://example.com/invalid-json")),
std::runtime_error);
@@ -141,7 +141,7 @@ SCENARIO("register_client can register a client at https://synapse-oidc.element.
initLogging();
GIVEN("A Client instance") {
WHEN("register_client is called with https://synapse-oidc.element.dev") {
- const Client client;
+ Client client;
ClientRegistrationData registration_data;
registration_data.application_type = "web";
registration_data.client_name = "Test Client";
@@ -167,7 +167,7 @@ SCENARIO("register_client throws runtime_error if curl_easy_perform fails") {
GIVEN("A Client instance with an invalid URL") {
WHEN("register_client is called with an invalid URL") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
ClientRegistrationData registration_data;
registration_data.application_type = "web";
registration_data.client_name = "Test Client";
@@ -189,7 +189,7 @@ SCENARIO("register_client throws runtime_error if JSON parsing fails") {
GIVEN("A Client instance with a URL returning invalid JSON") {
WHEN("register_client is called with a URL returning invalid JSON") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
ClientRegistrationData registration_data;
registration_data.application_type = "web";
registration_data.client_name = "Test Client";
@@ -212,15 +212,16 @@ SCENARIO("generate_authorize_url can generate a valid authorize URL") {
initLogging();
GIVEN("A Client") {
WHEN("generate_authorize_url is called") {
- const Client client;
+ Client client;
ClientRegistrationResponse auth_data;
auth_data.client_id = "test_client_id";
auth_data.client_id_issued_at = 1630000000;
std::string redirect_url = "https://example.com";
std::string state = "test_state";
std::string code_verifier = "test_code_verifier";
- auto authorize_url = ClientTest::test_generate_authorize_url(client, "https://auth-oidc.element.dev",
- auth_data, redirect_url, state, code_verifier);
+ auto authorize_url = ClientTest::test_generate_authorize_url(
+ client, "https://auth-oidc.element.dev/authorize",
+ auth_data, redirect_url, state, code_verifier);
THEN("A valid authorize URL should be returned") {
REQUIRE(
authorize_url ==
@@ -236,7 +237,7 @@ SCENARIO("fetch_openid_configuration can find and parse openid configuration at
initLogging();
GIVEN("A Client instance") {
WHEN("fetch_openid_configuration is called with https://auth-oidc.element.dev") {
- const Client client;
+ Client client;
auto task = ClientTest::fetch_openid_configuration(client, "https://auth-oidc.element.dev");
auto resp = sync_wait(task);
THEN("A valid OpenIDConfiguration should be returned") {
@@ -260,7 +261,7 @@ SCENARIO("fetch_openid_configuration throws runtime_error if curl_easy_perform f
GIVEN("A Client instance with an invalid URL") {
WHEN("fetch_openid_configuration is called with an invalid URL") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
REQUIRE_THROWS_AS(sync_wait(ClientTest::fetch_openid_configuration(client,"invalid_url")),
std::runtime_error);
}
@@ -273,7 +274,7 @@ SCENARIO("fetch_openid_configuration throws runtime_error if JSON parsing fails"
GIVEN("A Client instance with a URL returning invalid JSON") {
WHEN("fetch_openid_configuration is called with a URL returning invalid JSON") {
THEN("A runtime_error should be thrown") {
- const Client client;
+ Client client;
REQUIRE_THROWS_AS(
sync_wait(ClientTest::fetch_openid_configuration(client,"https://example.com/invalid-json")),
std::runtime_error);