diff --git a/include/auth/auth_handler.hpp b/include/auth/auth_handler.hpp index 14bfe0bd..8f382915 100644 --- a/include/auth/auth_handler.hpp +++ b/include/auth/auth_handler.hpp @@ -45,7 +45,9 @@ public: // Authentication void authenticate(const std::string& username, const std::string& password); + void authenticate(const std::string& username, const std::string& password, const std::string& pin); void authenticateWithHash(const std::string& username, const std::vector& authHash); + void authenticateWithHash(const std::string& username, const std::vector& authHash, const std::string& pin); // Optional: when the auth server requires a PIN (securityFlags & 0x01), call this to continue. // PIN must be 4-10 digits. void submitPin(const std::string& pin); diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index 6c7ebc79..65e943aa 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -59,6 +59,7 @@ private: int expansionIndex = 0; // Index into expansion registry profiles bool authenticating = false; bool showPassword = false; + bool pinAutoSubmitted_ = false; // Status std::string statusMessage; diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index 5deaa32d..5bed9777 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -81,6 +81,10 @@ void AuthHandler::requestRealmList() { } void AuthHandler::authenticate(const std::string& user, const std::string& pass) { + authenticate(user, pass, std::string()); +} + +void AuthHandler::authenticate(const std::string& user, const std::string& pass, const std::string& pin) { if (!isConnected()) { LOG_ERROR("Cannot authenticate: not connected to auth server"); fail("Not connected"); @@ -97,7 +101,7 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass) username = user; password = pass; - pendingPin_.clear(); + pendingPin_ = pin; securityFlags_ = 0; pinGridSeed_ = 0; pinServerSalt_ = {}; @@ -111,6 +115,10 @@ void AuthHandler::authenticate(const std::string& user, const std::string& pass) } void AuthHandler::authenticateWithHash(const std::string& user, const std::vector& authHash) { + authenticateWithHash(user, authHash, std::string()); +} + +void AuthHandler::authenticateWithHash(const std::string& user, const std::vector& authHash, const std::string& pin) { if (!isConnected()) { LOG_ERROR("Cannot authenticate: not connected to auth server"); fail("Not connected"); @@ -127,7 +135,7 @@ void AuthHandler::authenticateWithHash(const std::string& user, const std::vecto username = user; password.clear(); - pendingPin_.clear(); + pendingPin_ = pin; securityFlags_ = 0; pinGridSeed_ = 0; pinServerSalt_ = {}; @@ -341,7 +349,21 @@ void AuthHandler::handlePacket(network::Packet& packet) { if (state == AuthState::CHALLENGE_SENT) { handleLogonChallengeResponse(packet); } else { - LOG_WARNING("Unexpected LOGON_CHALLENGE response in state: ", (int)state); + // Some servers send a short LOGON_CHALLENGE failure packet if auth times out while we wait for 2FA/PIN. + LogonChallengeResponse response; + if (LogonChallengeResponseParser::parse(packet, response) && !response.isSuccess()) { + std::ostringstream ss; + ss << "Server cancelled authentication"; + if (state == AuthState::PIN_REQUIRED) { + ss << " while waiting for 2FA/PIN code"; + } + ss << ": " << getAuthResultString(response.result) + << " (code 0x" << std::hex << std::setw(2) << std::setfill('0') + << static_cast(response.result) << std::dec << ")"; + fail(ss.str()); + } else { + LOG_WARNING("Unexpected LOGON_CHALLENGE response in state: ", (int)state); + } } break; diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 0bea71b1..c0431774 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -336,6 +336,18 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { // Checkbox state changed } + // Optional 2FA / PIN field (some servers require this; e.g. Turtle WoW uses Google Authenticator). + // Keep it visible pre-connect so we can send LOGON_PROOF immediately after the SRP challenge. + { + ImGuiInputTextFlags pinFlags = ImGuiInputTextFlags_Password; + if (authHandler.getState() == auth::AuthState::PIN_REQUIRED) { + pinFlags |= ImGuiInputTextFlags_EnterReturnsTrue; + } + ImGui::InputText("2FA / PIN", pinCode, sizeof(pinCode), pinFlags); + ImGui::SameLine(); + ImGui::TextDisabled("(optional)"); + } + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -356,6 +368,7 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { if (authenticating) { auto state = authHandler.getState(); if (state != auth::AuthState::PIN_REQUIRED) { + pinAutoSubmitted_ = false; authTimer += ImGui::GetIO().DeltaTime; // Show progress with elapsed time @@ -363,13 +376,28 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { snprintf(progressBuf, sizeof(progressBuf), "Authenticating... (%.0fs)", authTimer); ImGui::Text("%s", progressBuf); } else { - ImGui::TextWrapped("This server requires a PIN. Enter your PIN to continue."); - ImGui::InputText("PIN", pinCode, sizeof(pinCode), ImGuiInputTextFlags_Password); + ImGui::TextWrapped("This server requires a 2FA / PIN code. Enter it and submit quickly (the server may time out)."); + + // If the user already typed a code before clicking Connect, submit immediately once. + if (!pinAutoSubmitted_) { + bool digitsOnly = true; + size_t len = std::strlen(pinCode); + for (size_t i = 0; i < len; ++i) { + if (pinCode[i] < '0' || pinCode[i] > '9') { digitsOnly = false; break; } + } + if (digitsOnly && len >= 4 && len <= 10) { + authHandler.submitPin(pinCode); + pinCode[0] = '\0'; + pinAutoSubmitted_ = true; + } + } + ImGui::SameLine(); - if (ImGui::Button("Submit PIN")) { + if (ImGui::Button("Submit 2FA/PIN")) { authHandler.submitPin(pinCode); - // Don't keep the PIN around longer than needed. + // Don't keep the code around longer than needed. pinCode[0] = '\0'; + pinAutoSubmitted_ = true; } } @@ -489,18 +517,24 @@ void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) { authenticating = true; authTimer = 0.0f; setStatus("Connected, authenticating...", false); + pinAutoSubmitted_ = false; // Save login info for next session saveLoginInfo(false); + const std::string pinStr = trimAscii(pinCode); + // Send authentication credentials if (useHash) { auto hashBytes = hexDecode(savedPasswordHash); - authHandler.authenticateWithHash(username, hashBytes); + authHandler.authenticateWithHash(username, hashBytes, pinStr); } else { usingStoredHash = false; - authHandler.authenticate(username, password); + authHandler.authenticate(username, password, pinStr); } + + // Don't keep the code around longer than needed. + pinCode[0] = '\0'; } else { std::stringstream errSs; errSs << "Failed to connect to " << hostname << ":" << port