Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion code/framework/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ if(WIN32)
src/external/epic/manifest.cpp
src/external/microsoft_store/package.cpp
src/external/rockstar/library.cpp
src/external/epic/auth.cpp
src/utils/hooking/hook_function.cpp
src/utils/hooking/hooking_patterns.cpp
src/utils/hooking/pattern_table.cpp
Expand Down Expand Up @@ -420,7 +421,8 @@ if(WIN32)
)

link_shared_deps(FrameworkLoader)
target_link_libraries(FrameworkLoader ${CLIENT_SHARED_LIBS} MafiaHubServices FrameworkLoaderData ntdll sfd)
# winhttp + crypt32: Epic account auth (external/epic/auth.cpp) — OAuth over HTTPS + DPAPI.
target_link_libraries(FrameworkLoader ${CLIENT_SHARED_LIBS} MafiaHubServices FrameworkLoaderData ntdll sfd winhttp crypt32)
target_link_libraries(FrameworkLoaderData udis86 minhook::minhook)
target_compile_definitions(FrameworkLoaderData PRIVATE _USE_MATH_DEFINES NOMINMAX)
target_link_options(FrameworkLoaderData PUBLIC /EXPORT:CoreRT_SetupSEHHandler /EXPORT:GetThreadLocalStorage /EXPORT:CoreSetExceptionOverride)
Expand Down Expand Up @@ -474,6 +476,26 @@ if(WIN32)
# clients need the subprocess exe at runtime; build it with every FrameworkClient consumer
add_dependencies(FrameworkClient cef_subprocess)

# Epic sign-in window, run by a launcher only when no stored sign-in refreshes, so launchers stay
# CEF-free. It compiles auth.cpp itself rather than taking FrameworkLoader's PE-loader link setup.
add_executable(epic_sign_in
src/external/epic/sign_in_main.cpp
src/external/epic/sign_in_view.cpp
src/external/epic/auth.cpp
)
target_link_libraries(epic_sign_in CEF Framework winhttp crypt32)
target_compile_definitions(epic_sign_in PRIVATE _USE_MATH_DEFINES NOMINMAX)
set_target_properties(epic_sign_in PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"
)
fw_set_binary_identity(epic_sign_in PRODUCT "MafiaHub Framework" VERSION ${FW_VERSION} COMPANY "MafiaHub" DESCRIPTION "MafiaHub Framework Epic Sign-in")
if(TARGET CEFCopyDlls)
add_dependencies(epic_sign_in CEFCopyDlls)
endif()
# launchers spawn it from their own directory; build it with every FrameworkLoader consumer
add_dependencies(FrameworkLoader epic_sign_in)

# Ensure runtime DLLs are copied to bin directory for all client/server targets
if(TARGET libnode_copy_dll)
add_dependencies(FrameworkClient libnode_copy_dll)
Expand Down
528 changes: 528 additions & 0 deletions code/framework/src/external/epic/auth.cpp

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions code/framework/src/external/epic/auth.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* MafiaHub OSS license
* Copyright (c) 2021-2023, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#pragma once

#include "utils/result.h"

#include <optional>
#include <string>

// Epic Games account auth for launching an EOS (Epic Store) game outside the Epic launcher.
// Mimics what the Epic launcher itself does: authenticate the user once (browser sign-in ->
// authorizationCode), keep a refresh token, and mint a short-lived single-use "exchange code"
// per launch that goes on the game's command line so its EOS ownership/entitlement check passes.
// Same approach as the open-source "Legendary" launcher. Store-generic — not tied to any game.
namespace Framework::External::Epic {
struct Tokens {
std::string accessToken;
std::string refreshToken;
std::string accountId;
std::string displayName;

bool Valid() const {
return !accessToken.empty();
}
};

enum class AuthError {
None,
NoStoredSignIn,
Rejected, // Epic refused the stored sign-in (HTTP 400/401): signed out, expired or revoked
Unreachable, // no usable answer from Epic
};
using AuthResult = Utils::Result<Tokens, AuthError>;

// Only a missing or rejected sign-in calls for a new one. When Epic can't be reached a new
// sign-in fails the same way, and the stored one still works once it can.
inline bool NeedsSignIn(AuthError error) {
return error == AuthError::NoStoredSignIn || error == AuthError::Rejected;
}

// A usable access token: the stored refresh token first, else, when NeedsSignIn, a browser +
// clipboard sign-in labelled with productName. The refresh token is persisted, DPAPI-encrypted,
// under %LOCALAPPDATA%\MafiaHub.
AuthResult EnsureAuthenticated(const std::wstring &productName = {});

// The silent half of EnsureAuthenticated: refresh the stored token, never show any UI. A rejected
// stored sign-in is forgotten; one Epic could not be asked about is kept.
AuthResult TryRefreshStoredAuth();

// Mint a fresh single-use exchange code from a valid access token (expires in ~5 min).
std::optional<std::string> GetExchangeCode(const Tokens &tokens);

// Embedded-webview sign-in: navigate to GetLoginUrl(), then hand the resulting redirect-page
// text (or a bare authorizationCode) to SignInWithAuthorizationCode to mint + persist tokens.
// It blocks on the network, so call it off any UI thread. False when nothing was persisted.
std::wstring GetLoginUrl();
bool SignInWithAuthorizationCode(const std::string &pageTextOrCode);

// The "-AUTH_TYPE=exchangecode ..." fragment (leading space) for the game's EOS init. The ids
// come from the Epic manifest and mint a fresh ownership token (.ovt), without which the game
// shows the "use the Epic launcher" gate; installDir's .egstore copy is the fallback.
std::wstring BuildLaunchArgs(const Tokens &tokens, const std::string &exchangeCode, const std::string &appName, const std::string &sandboxId, const std::string &catalogItemId, const std::string &installDir);

// Forget the stored credentials (e.g. after a hard auth failure so the next launch re-prompts).
void ClearStoredAuth();
} // namespace Framework::External::Epic
95 changes: 95 additions & 0 deletions code/framework/src/external/epic/sign_in_main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* MafiaHub OSS license
* Copyright (c) 2021-2026, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#include "sign_in_view.h"

#include "logging/logger.h"

#include "include/cef_app.h"

#include <windows.h>

#include <filesystem>
#include <string>

// epic_sign_in.exe: the launcher runs it when no stored Epic sign-in refreshes. Exits 0 once a refresh
// token is persisted, 1 on cancel or failure. Its own process so launchers never host CEF.
namespace {
class SignInApp final
: public CefApp
, public CefBrowserProcessHandler {
public:
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
return this;
}

void OnBeforeCommandLineProcessing(const CefString &, CefRefPtr<CefCommandLine> commandLine) override {
commandLine->AppendSwitch("disable-spell-checking");
commandLine->AppendSwitch("disable-pdf-extension");
commandLine->AppendSwitch("disable-component-update");
}

void OnContextInitialized() override {
Framework::External::Epic::ShowSignInWindow([this](bool signedIn) {
_signedIn = signedIn;
CefQuitMessageLoop();
});
}

bool SignedIn() const {
return _signedIn;
}

private:
bool _signedIn = false;
IMPLEMENT_REFCOUNTING(SignInApp);
};

// A private profile keeps the Epic session cookie (signing in again stays one click) out of the
// shared %LOCALAPPDATA%\CEF\User Data, which other CEF apps also read.
std::wstring ProfileDirectory() {
wchar_t localAppData[MAX_PATH] = {};
if (!GetEnvironmentVariableW(L"LOCALAPPDATA", localAppData, MAX_PATH)) {
return {};
}
const auto dir = std::filesystem::path(localAppData) / L"MafiaHub" / L"epic_auth_cef";
std::error_code ec;
std::filesystem::create_directories(dir, ec);
return ec ? std::wstring {} : dir.wstring();
}
} // namespace

int main() {
CefMainArgs mainArgs(::GetModuleHandleW(nullptr));
CefRefPtr<SignInApp> app(new SignInApp());

// CEF re-launches this exe for its renderer and GPU subprocesses; those return here.
const int subprocessExit = CefExecuteProcess(mainArgs, app, nullptr);
if (subprocessExit >= 0) {
return subprocessExit;
}

// The waiting launcher still writes its own log in the same folder.
Framework::Logging::GetInstance()->SetLogName("epic_sign_in");

CefSettings settings;
settings.no_sandbox = true;
settings.multi_threaded_message_loop = false;
settings.log_severity = LOGSEVERITY_WARNING;
if (const std::wstring profile = ProfileDirectory(); !profile.empty()) {
CefString(&settings.root_cache_path) = profile;
CefString(&settings.cache_path) = profile;
}

if (!CefInitialize(mainArgs, settings, app, nullptr)) {
return 1;
}
CefRunMessageLoop();
CefShutdown();
return app->SignedIn() ? 0 : 1;
}
161 changes: 161 additions & 0 deletions code/framework/src/external/epic/sign_in_view.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* MafiaHub OSS license
* Copyright (c) 2021-2026, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#include "sign_in_view.h"

#include "auth.h"

#include "include/base/cef_callback.h"
#include "include/cef_browser.h"
#include "include/cef_client.h"
#include "include/cef_request_handler.h"
#include "include/cef_string_visitor.h"
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
#include "include/views/cef_window_delegate.h"
#include "include/wrapper/cef_closure_task.h"

#include <string>
#include <utility>

namespace Framework::External::Epic {
namespace {
constexpr int kWindowWidth = 520;
constexpr int kWindowHeight = 760;

// The redirect page whose body carries the single-use authorizationCode.
constexpr const char *kRedirectMarker = "/id/api/redirect";

class SignInClient;

// Reads the redirect page's decoded text on the UI thread, then completes the sign-in.
class PageTextVisitor final: public CefStringVisitor {
public:
PageTextVisitor(CefRefPtr<SignInClient> client, CefRefPtr<CefBrowser> browser): _client(client), _browser(browser) {}
void Visit(const CefString &text) override;

private:
CefRefPtr<SignInClient> _client;
CefRefPtr<CefBrowser> _browser;
IMPLEMENT_REFCOUNTING(PageTextVisitor);
};

class SignInClient final
: public CefClient
, public CefLifeSpanHandler
, public CefLoadHandler
, public CefRequestHandler {
public:
explicit SignInClient(std::function<void(bool)> onDone): _onDone(std::move(onDone)) {}

CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override {
return this;
}
CefRefPtr<CefLoadHandler> GetLoadHandler() override {
return this;
}
CefRefPtr<CefRequestHandler> GetRequestHandler() override {
return this;
}

// The redirect page shows the authorization code, so hide the window as navigation to it
// starts, before it can paint. The page still has to load for the code to be read.
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame>, CefRefPtr<CefRequest> request, bool, bool) override {
if (request->GetURL().ToString().find(kRedirectMarker) != std::string::npos) {
if (CefRefPtr<CefBrowserView> view = CefBrowserView::GetForBrowser(browser)) {
if (CefRefPtr<CefWindow> window = view->GetWindow()) {
window->Hide();
}
}
}
return false;
}

void OnLoadEnd(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int) override {
if (_handled || !frame->IsMain()) {
return;
}
if (frame->GetURL().ToString().find(kRedirectMarker) == std::string::npos) {
return;
}
_handled = true;
frame->GetText(new PageTextVisitor(this, browser));
}

// The grant blocks on the network, so it runs off the UI thread; the window closes back on it.
void Complete(CefRefPtr<CefBrowser> browser, const std::string &pageText) {
CefPostTask(TID_FILE_USER_BLOCKING, base::BindOnce(&SignInClient::SignIn, this, browser, pageText));
}

void SignIn(CefRefPtr<CefBrowser> browser, const std::string &pageText) {
const bool signedIn = SignInWithAuthorizationCode(pageText);
CefPostTask(TID_UI, base::BindOnce(&SignInClient::Finish, this, browser, signedIn));
}

void Finish(CefRefPtr<CefBrowser> browser, bool signedIn) {
_result = signedIn;
if (CefRefPtr<CefBrowserView> view = CefBrowserView::GetForBrowser(browser)) {
if (CefRefPtr<CefWindow> window = view->GetWindow()) {
window->Close();
}
}
}

void OnBeforeClose(CefRefPtr<CefBrowser>) override {
if (_onDone) {
_onDone(_result);
_onDone = nullptr;
}
}

private:
std::function<void(bool)> _onDone;
bool _handled = false;
bool _result = false;
IMPLEMENT_REFCOUNTING(SignInClient);
};

void PageTextVisitor::Visit(const CefString &text) {
_client->Complete(_browser, text.ToString());
}

class SignInWindowDelegate final: public CefWindowDelegate {
public:
explicit SignInWindowDelegate(CefRefPtr<CefBrowserView> view): _view(view) {}

void OnWindowCreated(CefRefPtr<CefWindow> window) override {
window->AddChildView(_view);
window->CenterWindow(CefSize(kWindowWidth, kWindowHeight));
window->SetTitle("Sign in to Epic Games");
window->Show();
_view->RequestFocus();
}
void OnWindowDestroyed(CefRefPtr<CefWindow>) override {
_view = nullptr;
}
CefSize GetPreferredSize(CefRefPtr<CefView>) override {
return CefSize(kWindowWidth, kWindowHeight);
}
bool CanResize(CefRefPtr<CefWindow>) override {
return true;
}

private:
CefRefPtr<CefBrowserView> _view;
IMPLEMENT_REFCOUNTING(SignInWindowDelegate);
};
} // namespace

void ShowSignInWindow(std::function<void(bool)> onDone) {
CefRefPtr<SignInClient> client(new SignInClient(std::move(onDone)));
CefBrowserSettings settings;
const CefString url = GetLoginUrl();
CefRefPtr<CefBrowserView> view = CefBrowserView::CreateBrowserView(client, url, settings, nullptr, nullptr, nullptr);
CefWindow::CreateTopLevelWindow(new SignInWindowDelegate(view));
}
} // namespace Framework::External::Epic
18 changes: 18 additions & 0 deletions code/framework/src/external/epic/sign_in_view.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* MafiaHub OSS license
* Copyright (c) 2021-2026, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#pragma once

#include <functional>

namespace Framework::External::Epic {
// Opens Epic's sign-in page in a CEF window, reads the authorization code off the redirect page
// and persists the session. onDone(true) once signed in, false on failure or cancel. Call on the
// CEF UI thread while the message loop runs.
void ShowSignInWindow(std::function<void(bool)> onDone);
} // namespace Framework::External::Epic
Loading
Loading