MinecraftConsoles/Minecraft.Client/AuthScreen.cpp

460 lines
15 KiB
C++
Raw Normal View History

#include "stdafx.h"
#include "AuthScreen.h"
#include "Minecraft.h"
#include "User.h"
2026-04-03 21:39:59 -04:00
#include "..\Minecraft.World\AuthModule.h"
2026-04-06 02:33:39 -04:00
#include "..\Minecraft.World\SessionAuthModule.h"
2026-04-03 21:39:59 -04:00
#include "..\Minecraft.World\HttpClient.h"
#include "..\Minecraft.World\StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
#include <chrono>
#include <fstream>
2026-04-03 21:39:59 -04:00
#include <shellapi.h>
2026-04-03 21:39:59 -04:00
using json = nlohmann::json;
static constexpr auto PROFILES_FILE = L"auth_profiles.dat";
2026-04-06 02:33:39 -04:00
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);
2026-04-06 02:33:39 -04:00
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;
2026-04-06 02:33:39 -04:00
static constexpr uint32_t PROFILE_MAGIC = 0x4D435032;
void AuthProfileManager::load()
{
profiles.clear();
std::ifstream file(PROFILES_FILE, std::ios::binary);
if (!file) return;
2026-04-06 02:33:39 -04:00
uint32_t header = 0;
file.read(reinterpret_cast<char *>(&header), sizeof(header));
bool hasVariation = (header == PROFILE_MAGIC);
uint32_t count = 0;
2026-04-06 02:33:39 -04:00
if (hasVariation)
file.read(reinterpret_cast<char *>(&count), sizeof(count));
else
count = header;
for (uint32_t i = 0; i < count && file.good(); i++)
{
AuthProfile p;
uint8_t type;
file.read(reinterpret_cast<char *>(&type), sizeof(type));
p.type = static_cast<AuthProfile::Type>(type);
auto readWstr = [&file]() -> wstring {
uint16_t len = 0;
file.read(reinterpret_cast<char *>(&len), sizeof(len));
2026-03-29 16:39:12 -04:00
if (!file || len > 4096) return {};
wstring s(len, L'\0');
file.read(reinterpret_cast<char *>(s.data()), len * sizeof(wchar_t));
2026-03-29 16:39:12 -04:00
if (!file) return {};
return s;
};
p.uid = readWstr();
p.username = readWstr();
p.token = readWstr();
p.clientToken = readWstr();
2026-04-06 02:33:39 -04:00
if (hasVariation)
p.variation = readWstr();
if (p.variation.empty() && p.type == AuthProfile::YGGDRASIL)
p.variation = L"elyby";
profiles.push_back(std::move(p));
}
2026-04-03 21:39:59 -04:00
int32_t savedIdx = 0;
file.read(reinterpret_cast<char *>(&savedIdx), sizeof(savedIdx));
if (!profiles.empty())
2026-04-03 21:39:59 -04:00
selectedProfile = (savedIdx >= 0 && savedIdx < static_cast<int>(profiles.size())) ? savedIdx : 0;
}
void AuthProfileManager::save()
{
std::ofstream file(PROFILES_FILE, std::ios::binary | std::ios::trunc);
if (!file) return;
2026-04-06 02:33:39 -04:00
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));
auto writeWstr = [&file](const wstring &s) {
uint16_t len = static_cast<uint16_t>(s.length());
file.write(reinterpret_cast<const char *>(&len), sizeof(len));
file.write(reinterpret_cast<const char *>(s.data()), len * sizeof(wchar_t));
};
for (const auto &p : profiles)
{
uint8_t type = static_cast<uint8_t>(p.type);
file.write(reinterpret_cast<const char *>(&type), sizeof(type));
writeWstr(p.uid);
writeWstr(p.username);
writeWstr(p.token);
writeWstr(p.clientToken);
2026-04-06 02:33:39 -04:00
writeWstr(p.variation);
}
2026-04-03 21:39:59 -04:00
int32_t idx = static_cast<int32_t>(selectedProfile);
file.write(reinterpret_cast<const char *>(&idx), sizeof(idx));
}
2026-04-06 02:33:39 -04:00
void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken, const wstring &variation)
{
2026-04-03 21:39:59 -04:00
wstring finalUid = uid.empty() ? L"offline_" + username : uid;
2026-04-06 02:33:39 -04:00
profiles.push_back({type, finalUid, username, token, clientToken, variation});
selectedProfile = static_cast<int>(profiles.size()) - 1;
save();
}
void AuthProfileManager::removeSelectedProfile()
{
if (selectedProfile < 0 || selectedProfile >= static_cast<int>(profiles.size()))
return;
profiles.erase(profiles.begin() + selectedProfile);
if (selectedProfile >= static_cast<int>(profiles.size()))
selectedProfile = static_cast<int>(profiles.size()) - 1;
save();
}
bool AuthProfileManager::applySelectedProfile()
{
if (selectedProfile < 0 || selectedProfile >= static_cast<int>(profiles.size()))
return false;
auto &p = profiles[selectedProfile];
if (p.type == AuthProfile::MICROSOFT && !p.clientToken.empty())
{
auto checkResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile",
{"Authorization: Bearer " + narrowStr(p.token)});
if (checkResp.statusCode != 200)
{
string newMsAccess, newMsRefresh;
if (msRefreshOAuth(narrowStr(p.clientToken), newMsAccess, newMsRefresh))
{
string mcToken, profId, profName;
if (msTokenExchange(newMsAccess, mcToken, profId, profName))
{
p.token = convStringToWstring(mcToken);
p.clientToken = convStringToWstring(newMsRefresh);
p.username = convStringToWstring(profName);
p.uid = convStringToWstring(profId);
save();
}
}
}
}
2026-04-06 02:33:39 -04:00
else if (p.type == AuthProfile::YGGDRASIL && !p.token.empty())
{
2026-04-06 02:33:39 -04:00
auto *provider = YggdrasilRegistry::find(p.variation);
if (!provider) provider = &YggdrasilRegistry::defaultProvider();
if (!yggdrasilValidate(narrowStr(p.token), narrowStr(p.clientToken), provider->validateUrl()))
{
string newAccess, newClient;
2026-04-06 02:33:39 -04:00
if (yggdrasilRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient, provider->refreshUrl()))
{
p.token = convStringToWstring(newAccess);
if (!newClient.empty()) p.clientToken = convStringToWstring(newClient);
save();
}
}
}
auto *mc = Minecraft::GetInstance();
if (mc->user)
delete mc->user;
mc->user = new User(p.username, p.token);
2026-04-03 21:39:59 -04:00
// push auth name into the platform globals so ProfileManager.GetGamertag() picks it up
// instead of returning the default "Player"
extern char g_Win64Username[17];
extern wchar_t g_Win64UsernameW[17];
string narrow = narrowStr(p.username);
strncpy_s(g_Win64Username, sizeof(g_Win64Username), narrow.c_str(), _TRUNCATE);
wcsncpy_s(g_Win64UsernameW, 17, p.username.c_str(), _TRUNCATE);
return true;
}
2026-04-03 21:39:59 -04:00
std::thread AuthFlow::workerThread;
std::atomic<AuthFlowState> AuthFlow::state{AuthFlowState::IDLE};
std::atomic<bool> AuthFlow::cancelRequested{false};
AuthResult AuthFlow::result;
wstring AuthFlow::userCode;
wstring AuthFlow::verificationUri;
2026-04-06 02:33:39 -04:00
wstring AuthFlow::activeVariation;
2026-04-03 21:39:59 -04:00
void AuthFlow::reset()
{
cancelRequested = true;
if (workerThread.joinable())
workerThread.detach();
state = AuthFlowState::IDLE;
result = {};
userCode.clear();
verificationUri.clear();
2026-04-06 02:33:39 -04:00
activeVariation.clear();
2026-04-03 21:39:59 -04:00
cancelRequested = false;
}
void AuthFlow::startMicrosoft()
{
reset();
state = AuthFlowState::WAITING_FOR_USER;
workerThread = std::thread(microsoftFlowThread);
}
2026-04-06 02:33:39 -04:00
void AuthFlow::startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName)
2026-04-03 21:39:59 -04:00
{
reset();
state = AuthFlowState::EXCHANGING;
2026-04-06 02:33:39 -04:00
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);
2026-04-03 21:39:59 -04:00
}
static void authFail(AuthResult &result, std::atomic<AuthFlowState> &state, const wchar_t *msg)
{
result = {false, {}, {}, {}, {}, msg};
2026-04-03 21:39:59 -04:00
state = AuthFlowState::FAILED;
}
static json parseResponse(const HttpResponse &resp, int expectedStatus)
2026-04-03 21:39:59 -04:00
{
if (resp.statusCode != expectedStatus) return json::value_t::discarded;
return json::parse(resp.body, nullptr, false);
}
static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName)
{
auto xblResp = HttpClient::post("https://user.auth.xboxlive.com/user/authenticate", json({
{"Properties", {{"AuthMethod", "RPS"}, {"SiteName", "user.auth.xboxlive.com"}, {"RpsTicket", msAccessToken}}},
{"RelyingParty", "http://auth.xboxlive.com"},
{"TokenType", "JWT"}
}).dump());
auto xblJson = parseResponse(xblResp);
if (xblJson.is_discarded()) return false;
string xblToken = xblJson.value("Token", "");
string userHash;
try { userHash = xblJson["DisplayClaims"]["xui"][0].value("uhs", ""); } catch (...) {}
if (xblToken.empty() || userHash.empty()) return false;
auto xstsResp = HttpClient::post("https://xsts.auth.xboxlive.com/xsts/authorize", json({
{"Properties", {{"SandboxId", "RETAIL"}, {"UserTokens", {xblToken}}}},
{"RelyingParty", "rp://api.minecraftservices.com/"},
{"TokenType", "JWT"}
}).dump());
auto xstsJson = parseResponse(xstsResp);
string xstsToken = xstsJson.is_discarded() ? "" : xstsJson.value("Token", "");
if (xstsToken.empty()) return false;
auto mcResp = HttpClient::post("https://api.minecraftservices.com/authentication/login_with_xbox",
json({{"identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken}}).dump());
auto mcJson = parseResponse(mcResp);
mcToken = mcJson.is_discarded() ? "" : mcJson.value("access_token", "");
if (mcToken.empty()) return false;
auto profResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile",
{"Authorization: Bearer " + mcToken});
auto profJson = parseResponse(profResp);
if (profJson.is_discarded()) return false;
profId = profJson.value("id", "");
profName = profJson.value("name", "");
return !profId.empty() && !profName.empty();
}
static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken)
{
auto resp = HttpClient::post("https://login.live.com/oauth20_token.srf",
"client_id=" + string(MS_CLIENT_ID) + "&refresh_token=" + refreshToken + "&grant_type=refresh_token&scope=service::user.auth.xboxlive.com::MBI_SSL",
"application/x-www-form-urlencoded");
auto j = parseResponse(resp);
if (j.is_discarded()) return false;
newAccessToken = j.value("access_token", "");
newRefreshToken = j.value("refresh_token", "");
return !newAccessToken.empty();
}
2026-04-06 02:33:39 -04:00
static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl)
{
2026-04-06 02:33:39 -04:00
auto resp = HttpClient::post(validateUrl,
json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump());
return resp.statusCode == 200;
}
2026-04-06 02:33:39 -04:00
static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl)
{
2026-04-06 02:33:39 -04:00
auto resp = HttpClient::post(refreshUrl,
json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump());
auto j = parseResponse(resp);
if (j.is_discarded()) return false;
newAccessToken = j.value("accessToken", "");
newClientToken = j.value("clientToken", "");
return !newAccessToken.empty();
}
2026-04-03 21:39:59 -04:00
void AuthFlow::microsoftFlowThread()
{
auto dcResp = HttpClient::post(
"https://login.live.com/oauth20_connect.srf",
"client_id=" + string(MS_CLIENT_ID) + "&scope=service::user.auth.xboxlive.com::MBI_SSL&response_type=device_code",
"application/x-www-form-urlencoded"
);
auto dcJson = parseResponse(dcResp);
if (dcJson.is_discarded())
{
authFail(result, state, L"Failed to get device code");
return;
}
string deviceCode = dcJson.value("device_code", "");
string uCode = dcJson.value("user_code", "");
string vUri = dcJson.value("verification_uri", "");
int interval = dcJson.value("interval", 5);
if (deviceCode.empty() || uCode.empty())
{
authFail(result, state, L"Missing device code fields");
return;
}
userCode = convStringToWstring(uCode);
verificationUri = convStringToWstring(vUri);
// copy code to clipboard so the user can just paste it
if (OpenClipboard(nullptr))
{
EmptyClipboard();
size_t bytes = (uCode.size() + 1) * sizeof(char);
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (hMem)
{
memcpy(GlobalLock(hMem), uCode.c_str(), bytes);
GlobalUnlock(hMem);
SetClipboardData(CF_TEXT, hMem);
}
CloseClipboard();
}
if (!vUri.empty())
ShellExecuteW(nullptr, L"open", verificationUri.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
state = AuthFlowState::POLLING;
string msAccessToken;
string msRefreshToken;
const string pollBody = "client_id=" + string(MS_CLIENT_ID) + "&device_code=" + deviceCode + "&grant_type=urn:ietf:params:oauth:grant-type:device_code";
2026-04-03 21:39:59 -04:00
for (int attempt = 0; attempt < 180; attempt++)
{
for (int ms = 0; ms < interval * 1000; ms += 250)
{
if (cancelRequested) return;
std::this_thread::sleep_for(std::chrono::milliseconds(250));
2026-04-03 21:39:59 -04:00
}
auto pollResp = HttpClient::post(
"https://login.live.com/oauth20_token.srf",
pollBody,
2026-04-03 21:39:59 -04:00
"application/x-www-form-urlencoded"
);
auto pollJson = json::parse(pollResp.body, nullptr, false);
if (pollJson.is_discarded()) continue;
if (pollResp.statusCode == 200)
{
msAccessToken = pollJson.value("access_token", "");
msRefreshToken = pollJson.value("refresh_token", "");
2026-04-03 21:39:59 -04:00
if (!msAccessToken.empty()) break;
}
string err = pollJson.value("error", "");
if (err == "authorization_pending") continue;
if (err == "slow_down") { interval += 5; continue; }
if (!err.empty())
{
result = {false, {}, {}, {}, {}, convStringToWstring("Auth error: " + err)};
2026-04-03 21:39:59 -04:00
state = AuthFlowState::FAILED;
return;
}
}
if (msAccessToken.empty())
{
authFail(result, state, L"Timed out waiting for login");
return;
}
state = AuthFlowState::EXCHANGING;
if (cancelRequested) return;
string mcAccessToken, profId, profName;
if (!msTokenExchange(msAccessToken, mcAccessToken, profId, profName))
2026-04-03 21:39:59 -04:00
{
authFail(result, state, L"Token exchange failed");
2026-04-03 21:39:59 -04:00
return;
}
result = {true, convStringToWstring(profName), convStringToWstring(profId), convStringToWstring(mcAccessToken), convStringToWstring(msRefreshToken), {}};
2026-04-03 21:39:59 -04:00
state = AuthFlowState::COMPLETE;
}
2026-04-06 02:33:39 -04:00
void AuthFlow::yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl)
2026-04-03 21:39:59 -04:00
{
2026-04-06 02:33:39 -04:00
auto resp = HttpClient::post(authenticateUrl, json({
2026-04-03 21:39:59 -04:00
{"username", username},
{"password", password},
{"clientToken", "mcconsoles"},
{"agent", {{"name", "Minecraft"}, {"version", 1}}}
}).dump());
auto respJson = json::parse(resp.body, nullptr, false);
if (resp.statusCode != 200 || respJson.is_discarded())
{
2026-04-06 02:33:39 -04:00
string msg = "Yggdrasil auth failed";
2026-04-03 21:39:59 -04:00
if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg);
result = {false, {}, {}, {}, {}, convStringToWstring(msg)};
2026-04-03 21:39:59 -04:00
state = AuthFlowState::FAILED;
return;
}
string accessToken = respJson.value("accessToken", "");
2026-04-06 02:33:39 -04:00
string yggClientToken = respJson.value("clientToken", "");
2026-04-03 21:39:59 -04:00
string uuid, name;
try { uuid = respJson["selectedProfile"].value("id", ""); name = respJson["selectedProfile"].value("name", ""); } catch (...) {}
if (accessToken.empty() || uuid.empty() || name.empty())
{
2026-04-06 02:33:39 -04:00
authFail(result, state, L"Yggdrasil response missing profile");
2026-04-03 21:39:59 -04:00
return;
}
2026-04-06 02:33:39 -04:00
result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(yggClientToken), {}};
2026-04-03 21:39:59 -04:00
state = AuthFlowState::COMPLETE;
}