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