Skip to content

Commit

Permalink
2fa additions
Browse files Browse the repository at this point in the history
  • Loading branch information
Zentro committed Apr 22, 2022
1 parent 781cd9c commit 98f7b71
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 52 deletions.
2 changes: 1 addition & 1 deletion source/main/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ enum MsgType
MSG_NET_SSO_FAILURE,
MSG_NET_SSO_SUCCESS,
MSG_NET_SSO_2FA_REQUESTED,
//MSG_NET_SSO_2FA_FAILURE, may or may not be needed
MSG_NET_SSO_2FA_FAILURE,
// Simulation
MSG_SIM_PAUSE_REQUESTED,
MSG_SIM_UNPAUSE_REQUESTED,
Expand Down
236 changes: 218 additions & 18 deletions source/main/gui/panels/GUI_LoginBox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
# include <curl/easy.h>
#endif

#if defined(_MSC_VER) && defined(GetObject) // This MS Windows macro from <wingdi.h> (Windows Kit 8.1) clashes with RapidJSON
# undef GetObject
#endif

using namespace RoR;
using namespace GUI;

Expand All @@ -55,13 +59,15 @@ static size_t CurlWriteFunc(void* ptr, size_t size, size_t nmemb, std::string* d
return size * nmemb;
}

void PostAuth(std::string login, std::string passwd)
void PostAuthWithTfa(std::string login, std::string passwd, std::string provider, std::string code)
{
// !!!! DO NOT USE QUERY PARAMS - USE REQUEST BODY INSTEAD !!!!
rapidjson::Document j_request_body;
j_request_body.SetObject();
j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator());
j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("code", rapidjson::StringRef(code.c_str()), j_request_body.GetAllocator());
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
j_request_body.Accept(writer);
Expand All @@ -72,12 +78,18 @@ void PostAuth(std::string login, std::string passwd)
std::string response_header;
long response_code = 0;

struct curl_slist* slist;
slist = NULL;
slist = curl_slist_append(slist, "Content-Type: application/json");


CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://dev.api.rigsofrods.org/auth"); // todo api url + endpoint
curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/auth"); // todo api url + endpoint
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); // post request body
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);
Expand All @@ -88,13 +100,114 @@ void PostAuth(std::string login, std::string passwd)
curl_easy_cleanup(curl);
curl = nullptr;

// non-standard see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402
// we will use for it indicating that 2fa is required
if (response_code == 402) // not a failure, forward to 2fa prompt
if (response_code == 400) // a failure, bad tfa code
{
App::GetGameContext()->PushMessage(
Message(MSG_NET_SSO_2FA_FAILURE, _LC("Login", "The two-step verification value could not be confirmed. Please retry."))
);
return;
}
else if (response_code == 400) // a failure, user not found or bad pass combo
else if (response_code != 200) // a net failure, restart from beginning
{
App::GetGameContext()->PushMessage(
Message(MSG_NET_SSO_FAILURE, _LC("Login", "Connection error. Please check your connection and try again."))
);
return;
}

// if tfa success, then sso success
App::GetGameContext()->PushMessage(Message(MSG_NET_SSO_SUCCESS));
}

void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provider)
{
rapidjson::Document j_request_body;
j_request_body.SetObject();
j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("tfa_provider", rapidjson::StringRef(provider.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator());
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
j_request_body.Accept(writer);
std::string request_body = buffer.GetString();

std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
std::string response_payload;
std::string response_header;
long response_code = 0;

struct curl_slist* slist;
slist = NULL;
slist = curl_slist_append(slist, "Content-Type: application/json");

CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/auth"); // todo api url + endpoint
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); // post request body
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);

curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);

curl_easy_cleanup(curl);
curl = nullptr;

if (response_code != 200) // a net failure, restart from beginning
{
App::GetGameContext()->PushMessage(
Message(MSG_NET_SSO_FAILURE, _LC("Login", "Connection error. Please check your connection and try again."))
);
return;
}

App::GetGameContext()->PushMessage(Message(MSG_NET_SSO_2FA_REQUESTED)); // success
}

void PostAuth(std::string login, std::string passwd)
{
rapidjson::Document j_request_body;
j_request_body.SetObject();
j_request_body.AddMember("login", rapidjson::StringRef(login.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("password", rapidjson::StringRef(passwd.c_str()), j_request_body.GetAllocator());
j_request_body.AddMember("limit_ip", rapidjson::StringRef("1.1.1.1"), j_request_body.GetAllocator());
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
j_request_body.Accept(writer);
std::string request_body = buffer.GetString();

std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING);
std::string response_payload;
std::string response_header;
long response_code = 0;

struct curl_slist* slist;
slist = NULL;
slist = curl_slist_append(slist, "Content-Type: application/json");

CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/auth"); // todo api url + endpoint
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str()); // post request body
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);

curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);

curl_easy_cleanup(curl);
curl = nullptr;

if (response_code == 400) // a failure, user not found or bad pass combo
{
App::GetGameContext()->PushMessage(
Message(MSG_NET_SSO_FAILURE, _LC("Login", "You did not # correctly or your account is temporarily disabled. Please retry."))
Expand All @@ -108,7 +221,7 @@ void PostAuth(std::string login, std::string passwd)
);
return;
}
else if (response_code != 200) // a net failure
else if (response_code >= 300) // a net failure
{
Ogre::LogManager::getSingleton().stream()
<< "[RoR|User|SSO] Failed to sign user in; HTTP status code: " << response_code;
Expand All @@ -118,8 +231,7 @@ void PostAuth(std::string login, std::string passwd)
return;
}

// fetch user after, get token now
rapidjson::Document j_response_body;
/*rapidjson::Document j_response_body;
j_response_body.Parse(response_payload.c_str());
if (j_response_body.HasParseError() || !j_response_body.IsObject())
{
Expand All @@ -129,11 +241,28 @@ void PostAuth(std::string login, std::string passwd)
Message(MSG_NET_SSO_FAILURE, _LC("Login", "Received malformed data. Please try again."))
);
return;
}*/

// non-standard see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402
// we will use for it indicating that 2fa is required
if (response_code == 202)
{
// fetch providers
/*std::vector<std::string> tfa_providers;
rapidjson::Value& j_tfa_providers = j_response_body["tfa_providers"];
for (auto&& item : j_tfa_providers.GetArray())
{
tfa_providers.push_back(item.GetString());
}*/
App::GetGameContext()->PushMessage(Message(MSG_NET_SSO_2FA_REQUESTED/*,tfa_providers*/));
return;
}

App::sso_access_token->setStr(j_response_body["access_token"].GetString());
App::sso_refresh_token->setStr(j_response_body["refresh_token"].GetString());
App::sso_expiry_date->setStr(j_response_body["expiry_date"].GetString());
//App::sso_access_token->setStr(j_response_body["login_token"].GetString()); // todo rename
//App::sso_refresh_token->setStr(j_response_body["refresh_token"].GetString());
//App::sso_expiry_date->setStr(j_response_body["expiry_date"].GetString());

App::GetGameContext()->PushMessage(Message(MSG_NET_SSO_SUCCESS));
}

#endif
Expand All @@ -150,7 +279,7 @@ void LoginBox::Draw()

GUIManager::GuiTheme const& theme = App::GetGuiManager()->GetTheme();

ImGui::SetNextWindowContentWidth(300.f); // todo find better sizing
ImGui::SetNextWindowSize(ImVec2(400.f, 250.f), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPosCenter(ImGuiCond_Appearing);
ImGuiWindowFlags win_flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
bool keep_open = true;
Expand All @@ -164,11 +293,34 @@ void LoginBox::Draw()
ImGui::TextColored(App::GetGuiManager()->GetTheme().error_text_color, "%s", m_errors.c_str());
}

if (m_needs_2fa) // we got 402, 2fa time
if (m_needs_tfa) // we got 202, 2fa time
{
ImGui::Text(_LC("Login", "Please enter your two-factor authentication code"));
ImGui::InputText("##2fa", m_2fa_code.GetBuffer(), m_2fa_code.GetCapacity());
ImGui::Button("Confirm"); // todo complete 2fa implementation
ImGui::BeginTabBar("TfaOptTab");
if (ImGui::BeginTabItem(_LC("Login", "Verification code via app")))
{
m_tfa_provider = "totp";
ImGui::TextWrapped(_LC("Login", "Please enter the verification code generated by the app on your phone."));
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem(_LC("Login", "Email confirmation")))
{
m_tfa_provider = "email";
/*if (m_tfa_trigger) // do not trigger again, default true until TriggerTfa() envoked
{
this->TriggerTfa();
}*/
m_tfa_trigger = true; // switching between tabs
ImGui::TextWrapped(_LC("Login", "An email has been sent with a single-use code. Please enter that code to continue."));
ImGui::EndTabItem();
}
ImGui::EndTabBar();
ImGui::InputText("##2fa", m_tfa_code.GetBuffer(), m_tfa_code.GetCapacity());
if (ImGui::Button("Confirm"))
{
this->ConfirmTfa();
}
ImGui::Separator();
ImGui::TextWrapped(_LC("Login", "A backup code can be used when you don't have access to an alternative verification method. To do so, you must login using a web browser."));
}
else
{
Expand Down Expand Up @@ -222,8 +374,56 @@ void LoginBox::ShowError(std::string const& msg)
{
m_is_processing = false;
m_errors = msg;
//m_needs_tfa = false; // restart from beginning
}

void LoginBox::ConfirmTfa()
{
#if defined(USE_CURL)
m_is_processing = true;

if (m_tfa_code.IsEmpty())
{
App::GetGameContext()->PushMessage(
Message(MSG_NET_SSO_FAILURE, _LC("Login", "There must not be any empty fields."))
);
return;
}

std::string login(m_login);
std::string passwd(m_passwd);
std::string tfa_code(m_tfa_code);

std::packaged_task<void(std::string, std::string, std::string, std::string)> task(PostAuthWithTfa);
std::thread(std::move(task), login, passwd, m_tfa_provider, tfa_code).detach();
#endif
}

void LoginBox::TriggerTfa()
{
#if defined(USE_CURL)
// check if the provider is present
m_is_processing = true; // don't let them switch tabs
m_tfa_trigger = false;

std::string login(m_login);
std::string passwd(m_passwd);

std::packaged_task<void(std::string, std::string, std::string)> task(PostAuthTriggerTfa);
std::thread(std::move(task), login, passwd, m_tfa_provider).detach();
#endif
}

void LoginBox::NeedsTfa(/*tfa_providers*/)
{
// prefer auth app, alternative method is email
m_is_processing = false;
m_needs_tfa = true;
//m_tfa_providers = tfa_providers;
}

//void LoginBox::

void LoginBox::SetVisible(bool visible)
{
m_is_visible = visible;
Expand Down
42 changes: 9 additions & 33 deletions source/main/gui/panels/GUI_LoginBox.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,6 @@
namespace RoR {
namespace GUI {

struct Token
{
std::string access_token; //<- bearer token
std::string expiry_date;
//std::string refresh_token; //<- remember me if token expires
};

struct UserItem
{
std::string email;
int uid; //<- uid for fetching purposes
std::string username; //<- this will replace MP usernames
bool use_tfa; //<- for indication purposes
//std::vector<UserAvatarCollection> avatar_urls;
//std::vector<UserProfileBannerCollection> profile_banner_urls;
};

struct UserAvatarCollection {
std::string o;
std::string h;
std::string l;
std::string m;
std::string s;
};

struct UserProfileBannerCollection {
std::string l;
std::string m;
};

class LoginBox {
public:
LoginBox();
Expand All @@ -73,18 +43,24 @@ class LoginBox {
void SetVisible(bool visible);
bool IsVisible() const { return m_is_visible; }
void ShowError(std::string const& msg);
void ConfirmTfa();
void TriggerTfa();
void NeedsTfa();
void Login();
void Draw();

private:
bool m_is_visible = false;
Str<1000> m_login;
Str<1000> m_passwd;
Str<1000> m_2fa_code;
Str<1000> m_tfa_code;
bool m_remember = true;
std::string m_errors; // wrong pw combo or bad 2fa code, allow for retries
bool m_needs_2fa = false;
std::string m_errors;
bool m_needs_tfa = false;
bool m_is_processing = false;
std::vector<std::string> m_tfa_providers;
std::string m_tfa_provider;
bool m_tfa_trigger = true;
};

}
Expand Down
Loading

0 comments on commit 98f7b71

Please # to comment.