diff --git a/README.md b/README.md index da714fc..1c40902 100644 --- a/README.md +++ b/README.md @@ -346,7 +346,7 @@ This allows you to: - [ ] Material color application to models - [ ] Texture loading and mapping -### Phase 3: Network & Protocol (In Progress) +### Phase 3: Network & Protocol ✅ MOSTLY COMPLETE - [x] Domain connection via UDP - [x] NLPacket protocol implementation - [x] DomainConnectRequest / DomainList handshake @@ -356,9 +356,10 @@ This allows you to: - [x] Session UUID generation - [x] Protocol signature verification (MD5) - [x] Domain address parsing (host:port/position/orientation) -- [ ] OAuth 2.0 authentication (infrastructure ready, needs web flow) ⏭️ NEXT +- [x] **OAuth 2.0 authentication with browser flow** 🎉 +- [x] Token persistence and refresh - [ ] Assignment client direct connections -- [ ] Token persistence and refresh +- [ ] Authenticated EntityServer queries ### Phase 4: Entity System (Current Focus) - [ ] Apply entity colors to model materials diff --git a/src/OverteAuth.cpp.backup b/src/OverteAuth.cpp.backup new file mode 100644 index 0000000..b508a3b --- /dev/null +++ b/src/OverteAuth.cpp.backup @@ -0,0 +1,142 @@ +// OverteAuth.cpp +#include "OverteAuth.hpp" + +#include +#include +#include +#include +#include + +using namespace std::chrono; + +OverteAuth::OverteAuth() { + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +OverteAuth::~OverteAuth() { + curl_global_cleanup(); +} + +size_t OverteAuth::writeCallback(void* contents, size_t size, size_t nmemb, void* userp) { + size_t totalSize = size * nmemb; + std::string* response = static_cast(userp); + response->append(static_cast(contents), totalSize); + return totalSize; +} + +std::string OverteAuth::extractJsonString(const std::string& json, const std::string& key) { + // Simple JSON string extraction (not a full parser, but works for our needs) + std::string searchKey = "\"" + key + "\""; + size_t keyPos = json.find(searchKey); + if (keyPos == std::string::npos) { + return ""; + } + + // Find the opening quote after the colon + size_t colonPos = json.find(':', keyPos); + if (colonPos == std::string::npos) { + return ""; + } + + size_t quoteStart = json.find('"', colonPos); + if (quoteStart == std::string::npos) { + return ""; + } + + // Find the closing quote + size_t quoteEnd = json.find('"', quoteStart + 1); + if (quoteEnd == std::string::npos) { + return ""; + } + + return json.substr(quoteStart + 1, quoteEnd - quoteStart - 1); +} + +bool OverteAuth::login(const std::string& username, const std::string& password, const std::string& metaverseUrl) { + m_metaverseUrl = metaverseUrl; + m_username = username; + + CURL* curl = curl_easy_init(); + if (!curl) { + std::cerr << "[OverteAuth] Failed to initialize CURL" << std::endl; + return false; + } + + // Construct OAuth token endpoint + // From Overte AccountManager: /oauth/token path under metaverse server + std::string tokenUrl = m_metaverseUrl; + if (tokenUrl.back() == '/') { + tokenUrl.pop_back(); + } + tokenUrl += "/api/v1/token"; // Try API v1 endpoint + + std::cout << "[OverteAuth] Token URL: " << tokenUrl << std::endl; + + // Construct POST data + std::ostringstream postData; + postData << "grant_type=password"; + postData << "&username=" << username; // Should URL-encode but simple for now + postData << "&password=" << password; + postData << "&scope=owner"; + + std::string responseData; + + curl_easy_setopt(curl, CURLOPT_URL, tokenUrl.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.str().c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "Starworld/1.0"); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + + // Set Content-Type header + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + std::cerr << "[OverteAuth] Login failed: " << curl_easy_strerror(res) << std::endl; + return false; + } + + if (httpCode != 200) { + std::cerr << "[OverteAuth] Login failed with HTTP " << httpCode << std::endl; + std::cerr << "[OverteAuth] Response: " << responseData << std::endl; + return false; + } + + // Parse JSON response + m_accessToken = extractJsonString(responseData, "access_token"); + m_refreshToken = extractJsonString(responseData, "refresh_token"); + + if (m_accessToken.empty()) { + std::cerr << "[OverteAuth] No access token in response" << std::endl; + return false; + } + + // Set expiration time (default to 1 hour if not specified) + auto now = duration_cast(system_clock::now().time_since_epoch()).count(); + m_tokenExpiresAt = now + 3600; + + std::cout << "[OverteAuth] Successfully authenticated as " << username << std::endl; + std::cout << "[OverteAuth] Access token: " << m_accessToken.substr(0, 20) << "..." << std::endl; + + return true; +} + +void OverteAuth::logout() { + m_accessToken.clear(); + m_refreshToken.clear(); + m_username.clear(); + m_tokenExpiresAt = 0; + + std::cout << "[OverteAuth] Logged out" << std::endl; +}