Skip to content

Commit 4b3e531

Browse files
authored
feat(auth): improve custom auth flow (#138)
2 parents 8437e33 + 3a85567 commit 4b3e531

File tree

9 files changed

+224
-2
lines changed

9 files changed

+224
-2
lines changed

launcher/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ set(MINECRAFT_SOURCES
269269

270270
minecraft/auth/custom/steps/CustomAuthStep.cpp
271271
minecraft/auth/custom/steps/CustomAuthStep.h
272+
minecraft/auth/custom/steps/CustomGetSkinStep.cpp
273+
minecraft/auth/custom/steps/CustomGetSkinStep.h
272274

273275
minecraft/gameoptions/GameOptions.h
274276
minecraft/gameoptions/GameOptions.cpp

launcher/minecraft/auth/AuthFlow.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
// Custom
2525
#include "custom/steps/CustomAuthStep.h"
26+
#include "custom/steps/CustomGetSkinStep.h"
2627

2728
#include "tasks/Task.h"
2829

@@ -70,6 +71,7 @@ AuthFlow::AuthFlow(AccountData* data, Action action, QString password) : Task(),
7071
} break;
7172
case AccountType::Custom: {
7273
m_steps.append(makeShared<CustomAuthStep>(m_data, action, std::move(password)));
74+
m_steps.append(makeShared<CustomGetSkinStep>(m_data));
7375
} break;
7476
default:
7577
break;

launcher/minecraft/auth/custom/steps/CustomAuthStep.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "Application.h"
2222
#include "Logging.h"
2323
#include "net/NetUtils.h"
24+
#include "net/RawHeaderProxy.h"
2425

2526
#include <utility>
2627

@@ -36,6 +37,11 @@ void CustomAuthStep::perform()
3637
m_response.reset(new QByteArray());
3738
m_request = Net::Upload::makeByteArray(url, m_response, requestData.toUtf8());
3839

40+
const auto headerProxy =
41+
new Net::RawHeaderProxy(QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } });
42+
m_request->addHeaderProxy(headerProxy);
43+
// RawHeaderProxy::addHeaderProxy takes ownership of the proxy, so no cleanup is required
44+
3945
m_task.reset(new NetJob(authType() + "AuthStep", APPLICATION->network()));
4046
m_task->setAskRetry(false);
4147
m_task->addNetAction(m_request);
@@ -59,7 +65,11 @@ QString CustomAuthStep::requestTemplate()
5965
"username": "%1",
6066
"password": "%2",
6167
"clientToken": "%3",
62-
"requestUser": false
68+
"requestUser": false,
69+
"agent": {
70+
"name":"Minecraft",
71+
"version":1
72+
}
6373
}
6474
)XXX";
6575
} else {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
/*
3+
* Freesm Launcher - Minecraft Launcher
4+
* Copyright (C) 2025 so5iso4ka <so5iso4ka@icloud.com>
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, version 3.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
#include <QJsonDocument>
20+
#include <QJsonObject>
21+
#include <QJsonParseError>
22+
#include <QUrl>
23+
24+
#include "Application.h"
25+
#include "QObjectPtr.h"
26+
#include "net/Download.h"
27+
#include "net/RawHeaderProxy.h"
28+
29+
#include "CustomGetSkinStep.h"
30+
31+
namespace {
32+
QString getSkinUrlFromProperties(const QString& encodedProps)
33+
{
34+
QJsonParseError err;
35+
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromBase64(encodedProps.toUtf8()), &err);
36+
if (err.error != QJsonParseError::NoError) {
37+
return {};
38+
}
39+
40+
auto json = doc.object();
41+
auto url = json["textures"].toObject()["SKIN"].toObject()["url"].toString();
42+
43+
return { url };
44+
}
45+
} // namespace
46+
47+
CustomGetSkinStep::CustomGetSkinStep(AccountData* data) : GetSkinStep(data) {}
48+
49+
CustomGetSkinStep::~CustomGetSkinStep() = default;
50+
51+
void CustomGetSkinStep::perform()
52+
{
53+
const QUrl url(m_data->authUrl + QString("/sessionserver/session/minecraft/profile/%1").arg(m_data->minecraftProfile.id));
54+
55+
m_response = std::make_shared<QByteArray>();
56+
m_request = Net::Download::makeByteArray(url, m_response);
57+
58+
const auto headerProxy =
59+
new Net::RawHeaderProxy(QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } });
60+
m_request->addHeaderProxy(headerProxy);
61+
// RawHeaderProxy::addHeaderProxy takes ownership of the proxy, so no cleanup is required
62+
63+
m_task = makeShared<NetJob>("CustomGetSkinStep", APPLICATION->network());
64+
m_task->setAskRetry(false);
65+
m_task->addNetAction(m_request);
66+
67+
connect(m_task.get(), &Task::finished, this, &CustomGetSkinStep::onPropertiesFetchDone);
68+
69+
m_task->start();
70+
}
71+
72+
QString CustomGetSkinStep::describe()
73+
{
74+
return tr("Getting custom account skin");
75+
}
76+
77+
void CustomGetSkinStep::onPropertiesFetchDone()
78+
{
79+
if (m_request->error() != QNetworkReply::NoError) {
80+
qWarning() << "Reply error:" << m_request->error();
81+
emit finished(AccountTaskState::STATE_WORKING, tr("Failed to get skin for account: %1").arg(m_request->errorString()));
82+
return;
83+
}
84+
85+
QJsonParseError err;
86+
auto jsonResponse = QJsonDocument::fromJson(*m_response, &err);
87+
if (err.error != QJsonParseError::NoError) {
88+
emit finished(AccountTaskState::STATE_WORKING, tr("Failed to get skin for account: %1").arg(err.error));
89+
return;
90+
}
91+
92+
auto props = jsonResponse["properties"].toArray();
93+
94+
for (auto prop : props) {
95+
auto obj = prop.toObject();
96+
if (obj["name"].toString() == "textures") {
97+
QString url = getSkinUrlFromProperties(obj["value"].toString());
98+
if (url.isEmpty()) {
99+
break;
100+
}
101+
m_data->minecraftProfile.skin.url = url;
102+
return GetSkinStep::perform();
103+
}
104+
}
105+
106+
emit finished(AccountTaskState::STATE_WORKING, tr("Failed to get skin for account: %1").arg(err.error));
107+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
/*
3+
* Freesm Launcher - Minecraft Launcher
4+
* Copyright (C) 2025 so5iso4ka <so5iso4ka@icloud.com>
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, version 3.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
#pragma once
20+
21+
#include <QObject>
22+
#include <QString>
23+
#include <memory>
24+
25+
#include "minecraft/auth/msa/steps/GetSkinStep.h"
26+
#include "net/Download.h"
27+
#include "net/NetJob.h"
28+
29+
class CustomGetSkinStep : public GetSkinStep {
30+
Q_OBJECT
31+
32+
public:
33+
explicit CustomGetSkinStep(AccountData* data);
34+
~CustomGetSkinStep() override;
35+
36+
void perform() override;
37+
38+
QString describe() override;
39+
40+
private slots:
41+
void onPropertiesFetchDone();
42+
43+
private:
44+
std::shared_ptr<QByteArray> m_response;
45+
Net::Download::Ptr m_request;
46+
NetJob::Ptr m_task;
47+
};

launcher/net/NetRequest.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ QNetworkReply::NetworkError NetRequest::error() const
343343
return m_reply ? m_reply->error() : QNetworkReply::NoError;
344344
}
345345

346+
QList<QNetworkReply::RawHeaderPair> NetRequest::getRawHeaders() const
347+
{
348+
if (!m_reply.isNull()) {
349+
return m_reply->rawHeaderPairs();
350+
}
351+
return {};
352+
}
353+
346354
QUrl NetRequest::url() const
347355
{
348356
return m_url;

launcher/net/NetRequest.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ class NetRequest : public Task {
7272
void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
7373
void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr<Net::HeaderProxy>(proxy)); }
7474

75+
QList<QNetworkReply::RawHeaderPair> getRawHeaders() const;
76+
7577
QUrl url() const;
7678
void setUrl(QUrl url) { m_url = url; }
7779
int replyStatusCode() const;

launcher/ui/dialogs/CustomLoginDialog.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
#include <QPushButton>
2020
#include <QUrl>
2121

22+
#include "Application.h"
23+
#include "net/Download.h"
24+
2225
CustomLoginDialog::CustomLoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::CustomLoginDialog)
2326
{
2427
ui->setupUi(this);
@@ -45,7 +48,7 @@ CustomLoginDialog::~CustomLoginDialog()
4548
// Stage 1: User interaction
4649
void CustomLoginDialog::accept()
4750
{
48-
QUrl url = ui->authUrlTextBox->text();
51+
const QUrl url = ui->authUrlTextBox->text();
4952
if (!url.isValid()) {
5053
emit onTaskFailed(tr("Provided URL isn't valid"));
5154
return;
@@ -58,6 +61,42 @@ void CustomLoginDialog::accept()
5861
setUserInputsEnabled(false);
5962
ui->progressBar->setVisible(true);
6063

64+
m_response = std::make_shared<QByteArray>();
65+
66+
m_requestTask = Net::Download::makeByteArray(url, m_response);
67+
m_requestTask->setNetwork(APPLICATION->network());
68+
69+
connect(m_requestTask.get(), &Task::finished, this, &CustomLoginDialog::onUrlResolving);
70+
71+
m_requestTask->start();
72+
}
73+
74+
void CustomLoginDialog::onUrlResolving()
75+
{
76+
disconnect(m_requestTask.get(), &Task::finished, this, &CustomLoginDialog::onUrlResolving);
77+
78+
if (m_requestTask->error() != QNetworkReply::NoError) {
79+
emit onTaskFailed(m_requestTask->errorString());
80+
return;
81+
}
82+
83+
// modify url if header say so
84+
QUrl url;
85+
auto headers = m_requestTask->getRawHeaders();
86+
if (const auto it =
87+
std::find_if(headers.begin(), headers.end(),
88+
[](const auto& pair) { return QString::fromUtf8(pair.first).toLower() == "x-authlib-injector-api-location"; });
89+
it != headers.end()) {
90+
const QUrl location = QString::fromUtf8(it->second);
91+
if (location.isRelative()) {
92+
url = m_requestTask->url().resolved(location);
93+
} else {
94+
url = location;
95+
}
96+
} else {
97+
url = m_requestTask->url();
98+
}
99+
61100
// Setup the login task and start it
62101
m_account = CustomAccount::createCustom(ui->userTextBox->text(), url.toString(QUrl::StripTrailingSlash), ui->loginUrlTextBox->text(),
63102
ui->refreshUrlTextBox->text());

launcher/ui/dialogs/CustomLoginDialog.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <QDialog>
1919

2020
#include "minecraft/auth/custom/CustomAccount.h"
21+
#include "net/Download.h"
2122
#include "tasks/Task.h"
2223

2324
namespace Ui {
@@ -40,6 +41,8 @@ class CustomLoginDialog : public QDialog {
4041
protected slots:
4142
void accept();
4243

44+
void onUrlResolving();
45+
4346
void onTaskFailed(const QString& reason);
4447
void onTaskSucceeded();
4548
void onTaskStatus(const QString& status);
@@ -53,4 +56,6 @@ class CustomLoginDialog : public QDialog {
5356
Ui::CustomLoginDialog* ui;
5457
CustomAccountPtr m_account;
5558
Task::Ptr m_loginTask;
59+
Net::Download::Ptr m_requestTask;
60+
std::shared_ptr<QByteArray> m_response;
5661
};

0 commit comments

Comments
 (0)