commit d3bd2cbb53100b54a79c5efd8011d4c67d64a53d
parent 5f7d4259b61b2ce8f5f2955ae448e9adf7d4b058
Author: Marcel Radzio <mtrnord@nordgedanken.dev>
Date: Sat, 21 Sep 2024 15:44:22 +0200
Make auth fully work
Diffstat:
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 ®istration_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 ®istration_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();