major feat: auth v1

This commit is contained in:
Matthew Toro 2026-04-03 21:39:59 -04:00
parent 0fa5ae3037
commit 1daae8e2a8
15 changed files with 627 additions and 100 deletions

View file

@ -2,9 +2,16 @@
#include "AuthScreen.h"
#include "Minecraft.h"
#include "User.h"
#include "..\Minecraft.World\AuthModule.h"
#include "..\Minecraft.World\HttpClient.h"
#include "..\Minecraft.World\StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
#include <fstream>
#include <shellapi.h>
using json = nlohmann::json;
static constexpr auto PROFILES_FILE = L"auth_profiles.dat";
static constexpr auto MS_CLIENT_ID = "00000000441cc96b";
vector<AuthProfile> AuthProfileManager::profiles;
int AuthProfileManager::selectedProfile = -1;
@ -41,8 +48,10 @@ void AuthProfileManager::load()
profiles.push_back(std::move(p));
}
int32_t savedIdx = 0;
file.read(reinterpret_cast<char *>(&savedIdx), sizeof(savedIdx));
if (!profiles.empty())
selectedProfile = 0;
selectedProfile = (savedIdx >= 0 && savedIdx < static_cast<int>(profiles.size())) ? savedIdx : 0;
}
void AuthProfileManager::save()
@ -67,11 +76,15 @@ void AuthProfileManager::save()
writeWstr(p.username);
writeWstr(p.token);
}
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)
void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token)
{
profiles.push_back({type, L"offline_" + username, username, {}});
wstring finalUid = uid.empty() ? L"offline_" + username : uid;
profiles.push_back({type, finalUid, username, token});
selectedProfile = static_cast<int>(profiles.size()) - 1;
save();
}
@ -99,5 +112,266 @@ bool AuthProfileManager::applySelectedProfile()
delete mc->user;
mc->user = new User(p.username, p.token);
// 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;
}
// --- AuthFlow ---
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;
void AuthFlow::reset()
{
cancelRequested = true;
if (workerThread.joinable())
workerThread.detach();
state = AuthFlowState::IDLE;
result = {};
userCode.clear();
verificationUri.clear();
cancelRequested = false;
}
void AuthFlow::startMicrosoft()
{
reset();
state = AuthFlowState::WAITING_FOR_USER;
workerThread = std::thread(microsoftFlowThread);
}
void AuthFlow::startElyBy(const wstring &username, const wstring &password)
{
reset();
state = AuthFlowState::EXCHANGING;
workerThread = std::thread(elybyFlowThread, narrowStr(username), narrowStr(password));
}
static void authFail(AuthResult &result, std::atomic<AuthFlowState> &state, const wchar_t *msg)
{
result = {false, {}, {}, {}, msg};
state = AuthFlowState::FAILED;
}
// parse json response body, return discarded json on bad status
static json parseResponse(const HttpResponse &resp, int expectedStatus = 200)
{
if (resp.statusCode != expectedStatus) return json::value_t::discarded;
return json::parse(resp.body, nullptr, false);
}
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;
for (int attempt = 0; attempt < 180; attempt++)
{
for (int ms = 0; ms < interval * 1000; ms += 250)
{
if (cancelRequested) return;
Sleep(250);
}
auto pollResp = HttpClient::post(
"https://login.live.com/oauth20_token.srf",
"client_id=" + string(MS_CLIENT_ID) + "&device_code=" + deviceCode + "&grant_type=urn:ietf:params:oauth:grant-type:device_code",
"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", "");
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)};
state = AuthFlowState::FAILED;
return;
}
}
if (msAccessToken.empty())
{
authFail(result, state, L"Timed out waiting for login");
return;
}
state = AuthFlowState::EXCHANGING;
if (cancelRequested) return;
// xbox live auth
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())
{
authFail(result, state, L"Xbox Live auth failed");
return;
}
string xblToken = xblJson.value("Token", "");
string userHash;
try { userHash = xblJson["DisplayClaims"]["xui"][0].value("uhs", ""); } catch (...) {}
if (xblToken.empty() || userHash.empty())
{
authFail(result, state, L"Bad Xbox Live response");
return;
}
// xsts auth
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())
{
authFail(result, state, L"XSTS auth failed");
return;
}
// minecraft login
auto mcResp = HttpClient::post("https://api.minecraftservices.com/authentication/login_with_xbox",
json({{"identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken}}).dump());
auto mcJson = parseResponse(mcResp);
string mcAccessToken = mcJson.is_discarded() ? "" : mcJson.value("access_token", "");
if (mcAccessToken.empty())
{
authFail(result, state, L"Minecraft auth failed");
return;
}
// get profile
auto profResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile",
{"Authorization: Bearer " + mcAccessToken});
auto profJson = parseResponse(profResp);
if (profJson.is_discarded())
{
authFail(result, state, L"Failed to get Minecraft profile");
return;
}
string profId = profJson.value("id", "");
string profName = profJson.value("name", "");
if (profId.empty() || profName.empty())
{
authFail(result, state, L"Profile missing id or name");
return;
}
result = {true, convStringToWstring(profName), convStringToWstring(profId), convStringToWstring(mcAccessToken), {}};
state = AuthFlowState::COMPLETE;
}
void AuthFlow::elybyFlowThread(const string &username, const string &password)
{
auto resp = HttpClient::post("https://authserver.ely.by/auth/authenticate", json({
{"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())
{
string msg = "Ely.by auth failed";
if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg);
result = {false, {}, {}, {}, convStringToWstring(msg)};
state = AuthFlowState::FAILED;
return;
}
string accessToken = respJson.value("accessToken", "");
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");
return;
}
result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), {}};
state = AuthFlowState::COMPLETE;
}

View file

@ -1,5 +1,7 @@
#pragma once
using namespace std;
#include <atomic>
#include <thread>
struct AuthProfile
{
@ -20,7 +22,7 @@ private:
public:
static void load();
static void save();
static void addProfile(AuthProfile::Type type, const wstring &username);
static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L"");
static void removeSelectedProfile();
static bool applySelectedProfile();
@ -28,3 +30,45 @@ public:
static int getSelectedIndex() { return selectedProfile; }
static void setSelectedIndex(int idx) { selectedProfile = idx; }
};
struct AuthResult
{
bool success;
wstring username;
wstring uuid;
wstring accessToken;
wstring error;
};
enum class AuthFlowState : uint8_t
{
IDLE,
WAITING_FOR_USER,
POLLING,
EXCHANGING,
COMPLETE,
FAILED
};
class AuthFlow
{
private:
static std::thread workerThread;
static std::atomic<AuthFlowState> state;
static std::atomic<bool> cancelRequested;
static AuthResult result;
static wstring userCode;
static wstring verificationUri;
static void microsoftFlowThread();
static void elybyFlowThread(const string &username, const string &password);
public:
static void startMicrosoft();
static void startElyBy(const wstring &username, const wstring &password);
static AuthFlowState getState() { return state.load(); }
static const AuthResult &getResult() { return result; }
static const wstring &getUserCode() { return userCode; }
static const wstring &getVerificationUri() { return verificationUri; }
static void reset();
};

View file

@ -56,6 +56,7 @@
#include "DLCTexturePack.h"
#include "..\Minecraft.World\HandshakeManager.h"
#include "..\Minecraft.World\AuthModule.h"
#include "AuthScreen.h"
#ifdef _WINDOWS64
#include "Xbox\Network\NetworkPlayerXbox.h"
@ -4143,6 +4144,17 @@ void ClientConnection::beginAuth()
handshakeManager->registerModule(new KeypairOfflineAuthModule());
handshakeManager->registerModule(new OfflineAuthModule());
const auto &profiles = AuthProfileManager::getProfiles();
int idx = AuthProfileManager::getSelectedIndex();
if (idx >= 0 && idx < static_cast<int>(profiles.size()))
{
const auto &p = profiles[idx];
wstring variation;
if (p.type == AuthProfile::MICROSOFT) variation = L"mojang";
else if (p.type == AuthProfile::ELYBY) variation = L"elyby";
handshakeManager->setCredentials(p.token, p.uid, p.username, variation);
}
auto initial = handshakeManager->createInitialPacket();
if (initial) send(initial);
}
@ -4155,6 +4167,8 @@ void ClientConnection::handleAuth(const shared_ptr<AuthPacket> &packet)
auto response = handshakeManager->handlePacket(packet);
if (response) send(response);
for (auto &p : handshakeManager->drainPendingPackets())
send(p);
if (handshakeManager->isComplete())
{
authComplete = true;

View file

@ -27,6 +27,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye
const wchar_t* defaultText = L"";
m_bPCMode = false;
m_eKeyboardMode = C_4JInput::EKeyboardMode_Default;
if (initData)
{
UIKeyboardInitData* kbData = static_cast<UIKeyboardInitData *>(initData);
@ -36,6 +37,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye
if (kbData->defaultText) defaultText = kbData->defaultText;
m_win64MaxChars = kbData->maxChars;
m_bPCMode = kbData->pcMode;
m_eKeyboardMode = kbData->keyboardMode;
}
m_win64TextBuffer = defaultText;
@ -276,7 +278,12 @@ void UIScene_Keyboard::tick()
}
if (changed)
m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/);
{
if (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password)
m_KeyboardTextInput.setLabel(wstring(m_win64TextBuffer.length(), L'*').c_str(), true);
else
m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/);
}
if (m_bPCMode)
{

View file

@ -13,6 +13,7 @@ private:
wstring m_win64TextBuffer;
int m_win64MaxChars;
bool m_bPCMode; // Hides on-screen keyboard buttons; physical keyboard only
C_4JInput::EKeyboardMode m_eKeyboardMode;
int m_iCursorPos;
#endif

View file

@ -33,6 +33,9 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
m_eAction=eAction_None;
m_bIgnorePress=false;
// auto-apply saved auth profile on startup
AuthProfileManager::load();
AuthProfileManager::applySelectedProfile();
m_buttons[static_cast<int>(eControl_PlayGame)].init(IDS_PLAY_GAME,eControl_PlayGame);
@ -455,6 +458,41 @@ void UIScene_MainMenu::RunAction(int iPad)
}
static int s_authPad = 0;
static void *s_authParam = nullptr;
static wstring s_elybyUsername;
static bool s_authFlowActive = false;
static wstring ReadKeyboardText(int maxLen)
{
vector<uint16_t> buf(maxLen, 0);
#ifdef _WINDOWS64
Win64_GetKeyboardText(buf.data(), maxLen);
#else
InputManager.GetText(buf.data());
#endif
wstring result;
for (int i = 0; i < maxLen && buf[i]; i++)
result += static_cast<wchar_t>(buf[i]);
return result;
}
static void ShowAuthMessageBox(int iPad, const wchar_t *title, const wchar_t *text,
const wchar_t **options, int optionCount,
int(*func)(LPVOID, int, C4JStorage::EMessageResult), void *lpParam, bool keepOpen = false)
{
MessageBoxInfo param = {};
param.uiOptionC = optionCount;
param.dwPad = iPad;
param.Func = func;
param.lpParam = lpParam;
param.rawTitle = title;
param.rawText = text;
param.rawOptions = options;
ui.NavigateToScene(iPad, eUIScene_MessageBox, &param, eUILayer_Alert, eUIGroup_Fullscreen);
if (keepOpen)
if (auto *scene = ui.FindScene(eUIScene_MessageBox))
static_cast<UIScene_MessageBox *>(scene)->setKeepOpen(true);
}
static const wchar_t *BuildAuthProfileText()
{
@ -484,34 +522,18 @@ static const wchar_t *BuildAuthProfileText()
void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass)
{
s_authPad = iPad;
s_authParam = pClass;
static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Back" };
MessageBoxInfo param = {};
param.uiOptionC = 4;
param.dwPad = iPad;
param.Func = &UIScene_MainMenu::AuthMenuReturned;
param.lpParam = pClass;
param.rawTitle = L"Authentication";
param.rawText = BuildAuthProfileText();
param.rawOptions = authOptions;
ui.NavigateToScene(iPad, eUIScene_MessageBox, &param, eUILayer_Alert, eUIGroup_Fullscreen);
UIScene *scene = ui.FindScene(eUIScene_MessageBox);
if (scene)
static_cast<UIScene_MessageBox *>(scene)->setKeepOpen(true);
ShowAuthMessageBox(iPad, L"Authentication", BuildAuthProfileText(),
authOptions, 4, &UIScene_MainMenu::AuthMenuReturned, pClass, true);
}
void UIScene_MainMenu::ShowAuthAddMenu(int iPad, void *pClass)
{
static const wchar_t *addOptions[] = { L"Microsoft Auth", L"Ely.by Auth", L"Add Offline User", L"Back" };
MessageBoxInfo param = {};
param.uiOptionC = 4;
param.dwPad = iPad;
param.Func = &UIScene_MainMenu::AuthAddMenuReturned;
param.lpParam = pClass;
param.rawTitle = L"Add Profile";
param.rawText = L"Select an auth type";
param.rawOptions = addOptions;
ui.NavigateToScene(iPad, eUIScene_MessageBox, &param, eUILayer_Alert, eUIGroup_Fullscreen);
ShowAuthMessageBox(iPad, L"Add Profile", L"Select an auth type",
addOptions, 4, &UIScene_MainMenu::AuthAddMenuReturned, pClass);
}
int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result)
@ -534,6 +556,7 @@ int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorag
{
ui.NavigateBack(iPad);
AuthProfileManager::applySelectedProfile();
AuthProfileManager::save();
break;
}
case C4JStorage::EMessage_ResultThirdOption:
@ -556,13 +579,31 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto
switch (result)
{
case C4JStorage::EMessage_ResultAccept:
AuthProfileManager::addProfile(AuthProfile::MICROSOFT, L"Player");
ShowAuthMenu(iPad, lpParam);
{
AuthFlow::startMicrosoft();
s_authFlowActive = true;
static const wchar_t *cancelOptions[] = { L"Cancel" };
ShowAuthMessageBox(iPad, L"Microsoft Login", L"Starting...",
cancelOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true);
break;
}
case C4JStorage::EMessage_ResultDecline:
AuthProfileManager::addProfile(AuthProfile::ELYBY, L"Player");
ShowAuthMenu(iPad, lpParam);
{
#ifdef _WINDOWS64
UIKeyboardInitData kbData;
kbData.title = L"Ely.by Username";
kbData.defaultText = L"";
kbData.maxChars = 64;
kbData.callback = &UIScene_MainMenu::ElyByUsernameReturned;
kbData.lpParam = lpParam;
kbData.pcMode = g_KBMInput.IsKBMActive();
ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData);
#else
InputManager.RequestKeyboard(L"Ely.by Username", L"", (DWORD)0, 64, &UIScene_MainMenu::ElyByUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default);
#endif
break;
}
case C4JStorage::EMessage_ResultThirdOption:
{
#ifdef _WINDOWS64
@ -586,24 +627,72 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto
return 0;
}
int UIScene_MainMenu::AuthMsFlowReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result)
{
s_authFlowActive = false;
AuthFlow::reset();
ui.NavigateBack(iPad);
ShowAuthMenu(iPad, lpParam);
return 0;
}
int UIScene_MainMenu::ElyByUsernameReturned(LPVOID lpParam, const bool bRes)
{
if (bRes)
{
wstring name = ReadKeyboardText(128);
if (!name.empty())
{
s_elybyUsername = std::move(name);
#ifdef _WINDOWS64
UIKeyboardInitData kbData;
kbData.title = L"Ely.by Password";
kbData.defaultText = L"";
kbData.maxChars = 128;
kbData.callback = &UIScene_MainMenu::ElyByPasswordReturned;
kbData.lpParam = lpParam;
kbData.pcMode = g_KBMInput.IsKBMActive();
kbData.keyboardMode = C_4JInput::EKeyboardMode_Password;
ui.NavigateToScene(s_authPad, eUIScene_Keyboard, &kbData);
#else
InputManager.RequestKeyboard(L"Ely.by Password", L"", (DWORD)0, 128, &UIScene_MainMenu::ElyByPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password);
#endif
return 0;
}
}
ShowAuthMenu(s_authPad, lpParam);
return 0;
}
int UIScene_MainMenu::ElyByPasswordReturned(LPVOID lpParam, const bool bRes)
{
if (bRes)
{
wstring pass = ReadKeyboardText(256);
if (!pass.empty())
{
AuthFlow::startElyBy(s_elybyUsername, pass);
s_authFlowActive = true;
static const wchar_t *waitOptions[] = { L"Cancel" };
ShowAuthMessageBox(s_authPad, L"Ely.by Login", L"Authenticating...",
waitOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true);
SecureZeroMemory(pass.data(), pass.size() * sizeof(wchar_t));
return 0;
}
}
ShowAuthMenu(s_authPad, lpParam);
return 0;
}
int UIScene_MainMenu::AuthKeyboardReturned(LPVOID lpParam, const bool bRes)
{
if (bRes)
{
uint16_t ui16Text[128];
ZeroMemory(ui16Text, 128 * sizeof(uint16_t));
#ifdef _WINDOWS64
Win64_GetKeyboardText(ui16Text, 128);
#else
InputManager.GetText(ui16Text);
#endif
if (ui16Text[0] != 0)
{
wchar_t wName[128] = {};
for (int k = 0; k < 127 && ui16Text[k]; k++)
wName[k] = static_cast<wchar_t>(ui16Text[k]);
AuthProfileManager::addProfile(AuthProfile::OFFLINE, wName);
}
wstring name = ReadKeyboardText(128);
if (!name.empty())
AuthProfileManager::addProfile(AuthProfile::OFFLINE, name);
}
ShowAuthMenu(s_authPad, lpParam);
return 0;
@ -2016,6 +2105,54 @@ void UIScene_MainMenu::RunUnlockOrDLC(int iPad)
void UIScene_MainMenu::tick()
{
UIScene::tick();
if (s_authFlowActive)
{
auto flowState = AuthFlow::getState();
auto *scene = ui.FindScene(eUIScene_MessageBox);
auto *msgBox = scene ? static_cast<UIScene_MessageBox *>(scene) : nullptr;
switch (flowState)
{
case AuthFlowState::WAITING_FOR_USER:
case AuthFlowState::POLLING:
{
if (msgBox && !AuthFlow::getUserCode().empty())
{
static wstring statusText;
statusText = L"Go to: " + AuthFlow::getVerificationUri() + L"\nEnter code: " + AuthFlow::getUserCode();
if (flowState == AuthFlowState::POLLING) statusText += L"\n\nWaiting for login...";
msgBox->updateContent(statusText.c_str());
}
break;
}
case AuthFlowState::EXCHANGING:
{
if (msgBox) msgBox->updateContent(L"Exchanging tokens...");
break;
}
case AuthFlowState::COMPLETE:
{
s_authFlowActive = false;
const auto &r = AuthFlow::getResult();
auto type = AuthFlow::getUserCode().empty() ? AuthProfile::ELYBY : AuthProfile::MICROSOFT;
AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken);
AuthFlow::reset();
if (scene) ui.NavigateBack(s_authPad);
ShowAuthMenu(s_authPad, s_authParam);
break;
}
case AuthFlowState::FAILED:
{
s_authFlowActive = false;
const auto &r = AuthFlow::getResult();
if (msgBox) msgBox->updateContent(r.error.empty() ? L"Authentication failed" : r.error.c_str());
AuthFlow::reset();
break;
}
default:
break;
}
}
if ( (eNavigateWhenReady >= 0) )
{

View file

@ -150,6 +150,9 @@ private:
static int AuthMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result);
static int AuthAddMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result);
static int AuthKeyboardReturned(LPVOID lpParam, const bool bRes);
static int AuthMsFlowReturned(void *pParam, int iPad, C4JStorage::EMessageResult result);
static int ElyByUsernameReturned(LPVOID lpParam, const bool bRes);
static int ElyByPasswordReturned(LPVOID lpParam, const bool bRes);
static void LoadTrial();

View file

@ -144,6 +144,8 @@ void UIScene_MessageBox::handlePress(F64 controlId, F64 childId)
void UIScene_MessageBox::updateContent(const wchar_t *text)
{
m_labelContent.init(text);
IggyDataValue result;
IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcAutoResize, 0, nullptr);
}
bool UIScene_MessageBox::hasFocus(int iPad)

View file

@ -295,8 +295,9 @@ typedef struct _UIKeyboardInitData
int(*callback)(LPVOID, const bool);
LPVOID lpParam;
bool pcMode; // When true, disables on-screen keyboard buttons (PC keyboard users only need the text field)
C_4JInput::EKeyboardMode keyboardMode;
_UIKeyboardInitData() : title(nullptr), defaultText(nullptr), maxChars(25), callback(nullptr), lpParam(nullptr), pcMode(false) {}
_UIKeyboardInitData() : title(nullptr), defaultText(nullptr), maxChars(25), callback(nullptr), lpParam(nullptr), pcMode(false), keyboardMode(C_4JInput::EKeyboardMode_Default) {}
} UIKeyboardInitData;
// Stores the text typed in UIScene_Keyboard so callbacks can retrieve it

View file

@ -5,11 +5,6 @@
#include "Common/vendor/nlohmann/json.hpp"
#include <random>
static string narrowStr(const wstring &w)
{
return string(w.begin(), w.end());
}
static wstring generateServerId()
{
static constexpr wchar_t hex[] = L"0123456789abcdef";

View file

@ -6,6 +6,11 @@ using namespace std;
#include <utility>
#include <unordered_map>
inline string narrowStr(const wstring &w)
{
return string(w.begin(), w.end());
}
class AuthModule
{
public:

View file

@ -1,9 +1,19 @@
#include "stdafx.h"
#include "HandshakeManager.h"
#include "AuthModule.h"
#include "HttpClient.h"
#include "StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
static constexpr auto PROTOCOL_VERSION = L"1.0";
static wstring getField(const vector<pair<wstring, wstring>> &fields, const wchar_t *key)
{
for (const auto &[k, v] : fields)
if (k == key) return v;
return {};
}
HandshakeManager::HandshakeManager(bool isServer)
: isServer(isServer), state(HandshakeState::IDLE), activeModule(nullptr)
{
@ -20,6 +30,21 @@ void HandshakeManager::registerModule(AuthModule *module)
modules[module->schemeName()] = module;
}
void HandshakeManager::setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation)
{
accessToken = token;
clientUid = uid;
clientUsername = username;
preferredVariation = variation;
}
vector<shared_ptr<AuthPacket>> HandshakeManager::drainPendingPackets()
{
vector<shared_ptr<AuthPacket>> out;
out.swap(pendingPackets);
return out;
}
shared_ptr<AuthPacket> HandshakeManager::handlePacket(const shared_ptr<AuthPacket> &packet)
{
return isServer ? handleServer(packet) : handleClient(packet);
@ -37,10 +62,7 @@ shared_ptr<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacke
{
case AuthStage::ANNOUNCE_VERSION:
{
protocolVersion = L"";
for (const auto &[k, v] : packet->fields)
if (k == L"version") protocolVersion = v;
protocolVersion = getField(packet->fields, L"version");
if (protocolVersion != PROTOCOL_VERSION)
return fail();
@ -58,11 +80,7 @@ shared_ptr<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacke
case AuthStage::ACCEPT_SCHEME:
{
wstring variation;
for (const auto &[k, v] : packet->fields)
if (k == L"variation") variation = v;
activeVariation = variation;
activeVariation = getField(packet->fields, L"variation");
state = HandshakeState::SETTINGS_SENT;
auto settings = activeModule->getSettings(activeVariation);
return makePacket(AuthStage::SCHEME_SETTINGS, std::move(settings));
@ -87,14 +105,7 @@ shared_ptr<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacke
case AuthStage::AUTH_DONE:
{
wstring uid, username;
for (const auto &[k, v] : packet->fields)
{
if (k == L"uid") uid = v;
else if (k == L"username") username = v;
}
if (uid != finalUid || username != finalUsername)
if (getField(packet->fields, L"uid") != finalUid || getField(packet->fields, L"username") != finalUsername)
return fail();
state = HandshakeState::IDENTITY_ASSIGNED;
@ -106,14 +117,7 @@ shared_ptr<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacke
case AuthStage::CONFIRM_IDENTITY:
{
wstring uid, username;
for (const auto &[k, v] : packet->fields)
{
if (k == L"uid") uid = v;
else if (k == L"username") username = v;
}
if (uid != finalUid || username != finalUsername)
if (getField(packet->fields, L"uid") != finalUid || getField(packet->fields, L"username") != finalUsername)
return fail();
state = HandshakeState::COMPLETE;
@ -131,12 +135,8 @@ shared_ptr<AuthPacket> HandshakeManager::handleClient(const shared_ptr<AuthPacke
{
case AuthStage::DECLARE_SCHEME:
{
wstring scheme;
for (const auto &[k, v] : packet->fields)
{
if (k == L"version") protocolVersion = v;
else if (k == L"scheme") scheme = v;
}
protocolVersion = getField(packet->fields, L"version");
wstring scheme = getField(packet->fields, L"scheme");
if (protocolVersion != PROTOCOL_VERSION)
return fail();
@ -148,7 +148,11 @@ shared_ptr<AuthPacket> HandshakeManager::handleClient(const shared_ptr<AuthPacke
activeModule = it->second;
auto variations = activeModule->supportedVariations();
activeVariation = variations.empty() ? L"" : variations[0];
if (!preferredVariation.empty() &&
std::find(variations.begin(), variations.end(), preferredVariation) != variations.end())
activeVariation = preferredVariation;
else
activeVariation = variations.empty() ? L"" : variations[0];
state = HandshakeState::SCHEME_ACCEPTED;
return makePacket(AuthStage::ACCEPT_SCHEME, {{L"variation", activeVariation}});
@ -156,17 +160,38 @@ shared_ptr<AuthPacket> HandshakeManager::handleClient(const shared_ptr<AuthPacke
case AuthStage::SCHEME_SETTINGS:
{
wstring serverId = getField(packet->fields, L"serverId");
wstring sessionEndpoint = getField(packet->fields, L"sessionEndpoint");
wstring scheme(activeModule->schemeName());
if (scheme == L"mcconsoles:session" && !accessToken.empty())
{
nlohmann::json body = {
{"accessToken", narrowStr(accessToken)},
{"selectedProfile", narrowStr(clientUid)},
{"serverId", narrowStr(serverId)}
};
auto resp = HttpClient::post(narrowStr(sessionEndpoint) + "/session/minecraft/join", body.dump());
if (resp.statusCode != 204)
return fail();
}
state = HandshakeState::AUTH_IN_PROGRESS;
return makePacket(AuthStage::BEGIN_AUTH);
pendingPackets.push_back(makePacket(AuthStage::BEGIN_AUTH));
pendingPackets.push_back(makePacket(AuthStage::AUTH_DATA, {
{L"uid", clientUid},
{L"username", clientUsername}
}));
pendingPackets.push_back(makePacket(AuthStage::AUTH_DONE, {
{L"uid", clientUid},
{L"username", clientUsername}
}));
return nullptr;
}
case AuthStage::ASSIGN_IDENTITY:
{
for (const auto &[k, v] : packet->fields)
{
if (k == L"uid") finalUid = v;
else if (k == L"username") finalUsername = v;
}
finalUid = getField(packet->fields, L"uid");
finalUsername = getField(packet->fields, L"username");
state = HandshakeState::IDENTITY_CONFIRMED;
return makePacket(AuthStage::CONFIRM_IDENTITY, {

View file

@ -33,6 +33,13 @@ private:
wstring activeVariation;
wstring protocolVersion;
wstring accessToken;
wstring clientUid;
wstring clientUsername;
wstring preferredVariation;
vector<shared_ptr<AuthPacket>> pendingPackets;
public:
wstring finalUid;
wstring finalUsername;
@ -41,8 +48,10 @@ public:
~HandshakeManager();
void registerModule(AuthModule *module);
void setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation = L"");
shared_ptr<AuthPacket> handlePacket(const shared_ptr<AuthPacket> &packet);
shared_ptr<AuthPacket> createInitialPacket();
vector<shared_ptr<AuthPacket>> drainPendingPackets();
bool isComplete() const { return state == HandshakeState::COMPLETE; }
bool isFailed() const { return state == HandshakeState::FAILED; }

View file

@ -9,13 +9,15 @@ static size_t writeCallback(char *data, size_t size, size_t nmemb, void *userdat
return size * nmemb;
}
static HttpResponse performRequest(CURL *curl)
static HttpResponse performRequest(CURL *curl, struct curl_slist *extraHeaders = nullptr)
{
std::string responseBody;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
if (extraHeaders)
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, extraHeaders);
CURLcode res = curl_easy_perform(curl);
@ -24,20 +26,30 @@ static HttpResponse performRequest(CURL *curl)
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
curl_easy_cleanup(curl);
if (extraHeaders)
curl_slist_free_all(extraHeaders);
return {statusCode, std::move(responseBody)};
}
HttpResponse HttpClient::get(const std::string &url)
static struct curl_slist *buildHeaders(const std::vector<std::string> &headers)
{
struct curl_slist *list = nullptr;
for (const auto &h : headers)
list = curl_slist_append(list, h.c_str());
return list;
}
HttpResponse HttpClient::get(const std::string &url, const std::vector<std::string> &headers)
{
CURL *curl = curl_easy_init();
if (!curl)
return {0, ""};
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
return performRequest(curl);
return performRequest(curl, headers.empty() ? nullptr : buildHeaders(headers));
}
HttpResponse HttpClient::post(const std::string &url, const std::string &body, const std::string &contentType)
HttpResponse HttpClient::post(const std::string &url, const std::string &body, const std::string &contentType, const std::vector<std::string> &headers)
{
CURL *curl = curl_easy_init();
if (!curl)
@ -47,11 +59,8 @@ HttpResponse HttpClient::post(const std::string &url, const std::string &body, c
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
struct curl_slist *headers = nullptr;
headers = curl_slist_append(headers, ("Content-Type: " + contentType).c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
auto *headerList = buildHeaders(headers);
headerList = curl_slist_append(headerList, ("Content-Type: " + contentType).c_str());
HttpResponse resp = performRequest(curl);
curl_slist_free_all(headers);
return resp;
return performRequest(curl, headerList);
}

View file

@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <vector>
struct HttpResponse
{
@ -11,6 +12,6 @@ struct HttpResponse
class HttpClient
{
public:
static HttpResponse get(const std::string &url);
static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json");
static HttpResponse get(const std::string &url, const std::vector<std::string> &headers = {});
static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json", const std::vector<std::string> &headers = {});
};