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:
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 ®istration_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;
+}