major refactor: auth v3

This commit is contained in:
Matthew Toro 2026-04-06 02:33:39 -04:00
parent 8a8b4b2573
commit 179d21d7b6
18 changed files with 376 additions and 166 deletions

View file

@ -3,6 +3,7 @@
#include "Minecraft.h"
#include "User.h"
#include "..\Minecraft.World\AuthModule.h"
#include "..\Minecraft.World\SessionAuthModule.h"
#include "..\Minecraft.World\HttpClient.h"
#include "..\Minecraft.World\StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
@ -12,24 +13,33 @@
using json = nlohmann::json;
static constexpr auto PROFILES_FILE = L"auth_profiles.dat";
static constexpr auto MS_CLIENT_ID = "00000000441cc96b";
static constexpr auto MS_CLIENT_ID = "00000000402b5328";
static json parseResponse(const HttpResponse &resp, int expectedStatus = 200);
static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName);
static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken);
static bool elybyValidate(const string &accessToken, const string &clientToken);
static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken);
static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl);
static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl);
vector<AuthProfile> AuthProfileManager::profiles;
int AuthProfileManager::selectedProfile = -1;
static constexpr uint32_t PROFILE_MAGIC = 0x4D435032;
void AuthProfileManager::load()
{
profiles.clear();
std::ifstream file(PROFILES_FILE, std::ios::binary);
if (!file) return;
uint32_t header = 0;
file.read(reinterpret_cast<char *>(&header), sizeof(header));
bool hasVariation = (header == PROFILE_MAGIC);
uint32_t count = 0;
file.read(reinterpret_cast<char *>(&count), sizeof(count));
if (hasVariation)
file.read(reinterpret_cast<char *>(&count), sizeof(count));
else
count = header;
for (uint32_t i = 0; i < count && file.good(); i++)
{
@ -52,6 +62,10 @@ void AuthProfileManager::load()
p.username = readWstr();
p.token = readWstr();
p.clientToken = readWstr();
if (hasVariation)
p.variation = readWstr();
if (p.variation.empty() && p.type == AuthProfile::YGGDRASIL)
p.variation = L"elyby";
profiles.push_back(std::move(p));
}
@ -66,6 +80,9 @@ void AuthProfileManager::save()
std::ofstream file(PROFILES_FILE, std::ios::binary | std::ios::trunc);
if (!file) return;
uint32_t magic = PROFILE_MAGIC;
file.write(reinterpret_cast<const char *>(&magic), sizeof(magic));
uint32_t count = static_cast<uint32_t>(profiles.size());
file.write(reinterpret_cast<const char *>(&count), sizeof(count));
@ -83,16 +100,17 @@ void AuthProfileManager::save()
writeWstr(p.username);
writeWstr(p.token);
writeWstr(p.clientToken);
writeWstr(p.variation);
}
int32_t idx = static_cast<int32_t>(selectedProfile);
file.write(reinterpret_cast<const char *>(&idx), sizeof(idx));
}
void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken)
void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken, const wstring &variation)
{
wstring finalUid = uid.empty() ? L"offline_" + username : uid;
profiles.push_back({type, finalUid, username, token, clientToken});
profiles.push_back({type, finalUid, username, token, clientToken, variation});
selectedProfile = static_cast<int>(profiles.size()) - 1;
save();
}
@ -137,12 +155,14 @@ bool AuthProfileManager::applySelectedProfile()
}
}
}
else if (p.type == AuthProfile::ELYBY && !p.token.empty())
else if (p.type == AuthProfile::YGGDRASIL && !p.token.empty())
{
if (!elybyValidate(narrowStr(p.token), narrowStr(p.clientToken)))
auto *provider = YggdrasilRegistry::find(p.variation);
if (!provider) provider = &YggdrasilRegistry::defaultProvider();
if (!yggdrasilValidate(narrowStr(p.token), narrowStr(p.clientToken), provider->validateUrl()))
{
string newAccess, newClient;
if (elybyRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient))
if (yggdrasilRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient, provider->refreshUrl()))
{
p.token = convStringToWstring(newAccess);
if (!newClient.empty()) p.clientToken = convStringToWstring(newClient);
@ -169,7 +189,6 @@ bool AuthProfileManager::applySelectedProfile()
return true;
}
// --- AuthFlow ---
std::thread AuthFlow::workerThread;
std::atomic<AuthFlowState> AuthFlow::state{AuthFlowState::IDLE};
@ -177,6 +196,7 @@ std::atomic<bool> AuthFlow::cancelRequested{false};
AuthResult AuthFlow::result;
wstring AuthFlow::userCode;
wstring AuthFlow::verificationUri;
wstring AuthFlow::activeVariation;
void AuthFlow::reset()
{
@ -187,6 +207,7 @@ void AuthFlow::reset()
result = {};
userCode.clear();
verificationUri.clear();
activeVariation.clear();
cancelRequested = false;
}
@ -197,11 +218,15 @@ void AuthFlow::startMicrosoft()
workerThread = std::thread(microsoftFlowThread);
}
void AuthFlow::startElyBy(const wstring &username, const wstring &password)
void AuthFlow::startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName)
{
reset();
state = AuthFlowState::EXCHANGING;
workerThread = std::thread(elybyFlowThread, narrowStr(username), narrowStr(password));
wstring resolvedName = providerName.empty() ? YggdrasilRegistry::defaultProvider().name : providerName;
activeVariation = resolvedName;
auto *provider = YggdrasilRegistry::find(resolvedName);
string authUrl = provider ? provider->authenticateUrl() : YggdrasilRegistry::defaultProvider().authenticateUrl();
workerThread = std::thread(yggdrasilFlowThread, narrowStr(username), narrowStr(password), authUrl);
}
static void authFail(AuthResult &result, std::atomic<AuthFlowState> &state, const wchar_t *msg)
@ -210,7 +235,6 @@ static void authFail(AuthResult &result, std::atomic<AuthFlowState> &state, cons
state = AuthFlowState::FAILED;
}
// parse json response body, return discarded json on bad status
static json parseResponse(const HttpResponse &resp, int expectedStatus)
{
if (resp.statusCode != expectedStatus) return json::value_t::discarded;
@ -273,19 +297,15 @@ static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, s
newRefreshToken = j.value("refresh_token", "");
return !newAccessToken.empty();
}
// validate ely.by token via yggdrasil /validate endpoint
static bool elybyValidate(const string &accessToken, const string &clientToken)
static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl)
{
auto resp = HttpClient::post("https://authserver.ely.by/auth/validate",
auto resp = HttpClient::post(validateUrl,
json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump());
return resp.statusCode == 200;
}
// refresh ely.by token via yggdrasil /refresh endpoint
static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken)
static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl)
{
auto resp = HttpClient::post("https://authserver.ely.by/auth/refresh",
auto resp = HttpClient::post(refreshUrl,
json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump());
auto j = parseResponse(resp);
@ -403,9 +423,9 @@ void AuthFlow::microsoftFlowThread()
state = AuthFlowState::COMPLETE;
}
void AuthFlow::elybyFlowThread(const string &username, const string &password)
void AuthFlow::yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl)
{
auto resp = HttpClient::post("https://authserver.ely.by/auth/authenticate", json({
auto resp = HttpClient::post(authenticateUrl, json({
{"username", username},
{"password", password},
{"clientToken", "mcconsoles"},
@ -416,7 +436,7 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password)
if (resp.statusCode != 200 || respJson.is_discarded())
{
string msg = "Ely.by auth failed";
string msg = "Yggdrasil auth failed";
if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg);
result = {false, {}, {}, {}, {}, convStringToWstring(msg)};
state = AuthFlowState::FAILED;
@ -424,16 +444,16 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password)
}
string accessToken = respJson.value("accessToken", "");
string elyClientToken = respJson.value("clientToken", "");
string yggClientToken = respJson.value("clientToken", "");
string uuid, name;
try { uuid = respJson["selectedProfile"].value("id", ""); name = respJson["selectedProfile"].value("name", ""); } catch (...) {}
if (accessToken.empty() || uuid.empty() || name.empty())
{
authFail(result, state, L"Ely.by response missing profile");
authFail(result, state, L"Yggdrasil response missing profile");
return;
}
result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(elyClientToken), {}};
result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(yggClientToken), {}};
state = AuthFlowState::COMPLETE;
}