475d949a1e
- We now use the new endpoint for loggiong in via XBox tokens (/launcher/login) - We now check game entitlements instead of only relying on MC profile presence - Accounts can now be added even when they do not have a profile - The launcher will guide you through selecting a Minecraft name if you don't have one yet
350 lines
10 KiB
C++
350 lines
10 KiB
C++
/* Copyright 2013-2021 MultiMC Contributors
|
|
*
|
|
* Authors: Orochimarufan <orochimarufan.x3@gmail.com>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "MinecraftAccount.h"
|
|
#include "flows/AuthContext.h"
|
|
|
|
#include <QUuid>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QRegExp>
|
|
#include <QStringList>
|
|
#include <QJsonDocument>
|
|
|
|
#include <QDebug>
|
|
|
|
#include <QPainter>
|
|
#include <minecraft/auth/flows/MSASilent.h>
|
|
#include <minecraft/auth/flows/MSAInteractive.h>
|
|
|
|
#include <minecraft/auth/flows/MojangRefresh.h>
|
|
#include <minecraft/auth/flows/MojangLogin.h>
|
|
|
|
MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) {
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
if(account->data.resumeStateFromV2(json)) {
|
|
return account;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) {
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
if(account->data.resumeStateFromV3(json)) {
|
|
return account;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username)
|
|
{
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
account->data.type = AccountType::Mojang;
|
|
account->data.yggdrasilToken.extra["userName"] = username;
|
|
account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
|
return account;
|
|
}
|
|
|
|
MinecraftAccountPtr MinecraftAccount::createBlankMSA()
|
|
{
|
|
MinecraftAccountPtr account(new MinecraftAccount());
|
|
account->data.type = AccountType::MSA;
|
|
return account;
|
|
}
|
|
|
|
|
|
QJsonObject MinecraftAccount::saveToJson() const
|
|
{
|
|
return data.saveState();
|
|
}
|
|
|
|
AccountStatus MinecraftAccount::accountStatus() const {
|
|
if(data.type == AccountType::Mojang) {
|
|
if (data.accessToken().isEmpty()) {
|
|
return NotVerified;
|
|
}
|
|
else {
|
|
return Verified;
|
|
}
|
|
}
|
|
// MSA
|
|
// FIXME: this is extremely crude and probably wrong
|
|
if(data.msaToken.token.isEmpty()) {
|
|
return NotVerified;
|
|
}
|
|
else {
|
|
return Verified;
|
|
}
|
|
}
|
|
|
|
QPixmap MinecraftAccount::getFace() const {
|
|
QPixmap skinTexture;
|
|
if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) {
|
|
return QPixmap();
|
|
}
|
|
QPixmap skin = QPixmap(8, 8);
|
|
QPainter painter(&skin);
|
|
painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8));
|
|
painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8));
|
|
return skin.scaled(64, 64, Qt::KeepAspectRatio);
|
|
}
|
|
|
|
|
|
std::shared_ptr<AccountTask> MinecraftAccount::login(AuthSessionPtr session, QString password)
|
|
{
|
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
|
|
// take care of the true offline status
|
|
if (accountStatus() == NotVerified && password.isEmpty())
|
|
{
|
|
if (session)
|
|
{
|
|
session->status = AuthSession::RequiresPassword;
|
|
fillSession(session);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if(accountStatus() == Verified && !session->wants_online)
|
|
{
|
|
session->status = AuthSession::PlayableOffline;
|
|
session->auth_server_online = false;
|
|
fillSession(session);
|
|
return nullptr;
|
|
}
|
|
else
|
|
{
|
|
if (password.isEmpty())
|
|
{
|
|
m_currentTask.reset(new MojangRefresh(&data));
|
|
}
|
|
else
|
|
{
|
|
m_currentTask.reset(new MojangLogin(&data, password));
|
|
}
|
|
m_currentTask->assignSession(session);
|
|
|
|
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
|
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
|
}
|
|
return m_currentTask;
|
|
}
|
|
|
|
std::shared_ptr<AccountTask> MinecraftAccount::loginMSA(AuthSessionPtr session) {
|
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
|
|
if(accountStatus() == Verified && !session->wants_online)
|
|
{
|
|
session->status = AuthSession::PlayableOffline;
|
|
session->auth_server_online = false;
|
|
fillSession(session);
|
|
return nullptr;
|
|
}
|
|
else
|
|
{
|
|
m_currentTask.reset(new MSAInteractive(&data));
|
|
m_currentTask->assignSession(session);
|
|
|
|
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
|
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
|
}
|
|
return m_currentTask;
|
|
}
|
|
|
|
std::shared_ptr<AccountTask> MinecraftAccount::refresh(AuthSessionPtr session) {
|
|
Q_ASSERT(m_currentTask.get() == nullptr);
|
|
|
|
// take care of the true offline status
|
|
if (accountStatus() == NotVerified)
|
|
{
|
|
if (session)
|
|
{
|
|
if(data.type == AccountType::MSA) {
|
|
session->status = AuthSession::RequiresOAuth;
|
|
}
|
|
else {
|
|
session->status = AuthSession::RequiresPassword;
|
|
}
|
|
fillSession(session);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if(accountStatus() == Verified && !session->wants_online)
|
|
{
|
|
session->status = AuthSession::PlayableOffline;
|
|
session->auth_server_online = false;
|
|
fillSession(session);
|
|
return nullptr;
|
|
}
|
|
else
|
|
{
|
|
if(data.type == AccountType::MSA) {
|
|
m_currentTask.reset(new MSASilent(&data));
|
|
}
|
|
else {
|
|
m_currentTask.reset(new MojangRefresh(&data));
|
|
}
|
|
m_currentTask->assignSession(session);
|
|
|
|
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
|
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
|
}
|
|
return m_currentTask;
|
|
}
|
|
|
|
|
|
void MinecraftAccount::authSucceeded()
|
|
{
|
|
auto session = m_currentTask->getAssignedSession();
|
|
if (session)
|
|
{
|
|
/*
|
|
session->status = AuthSession::RequiresProfileSetup;
|
|
session->auth_server_online = true;
|
|
*/
|
|
if(data.profileId().size() == 0) {
|
|
session->status = AuthSession::RequiresProfileSetup;
|
|
}
|
|
else {
|
|
if(session->wants_online) {
|
|
session->status = AuthSession::PlayableOnline;
|
|
}
|
|
else {
|
|
session->status = AuthSession::PlayableOffline;
|
|
}
|
|
}
|
|
fillSession(session);
|
|
session->auth_server_online = true;
|
|
}
|
|
m_currentTask.reset();
|
|
emit changed();
|
|
}
|
|
|
|
void MinecraftAccount::authFailed(QString reason)
|
|
{
|
|
auto session = m_currentTask->getAssignedSession();
|
|
// This is emitted when the yggdrasil tasks time out or are cancelled.
|
|
// -> we treat the error as no-op
|
|
switch (m_currentTask->accountState()) {
|
|
case AccountTask::STATE_FAILED_SOFT: {
|
|
if (session)
|
|
{
|
|
if(accountStatus() == Verified) {
|
|
session->status = AuthSession::PlayableOffline;
|
|
}
|
|
else {
|
|
if(data.type == AccountType::MSA) {
|
|
session->status = AuthSession::RequiresOAuth;
|
|
}
|
|
else {
|
|
session->status = AuthSession::RequiresPassword;
|
|
}
|
|
}
|
|
session->auth_server_online = false;
|
|
fillSession(session);
|
|
}
|
|
}
|
|
break;
|
|
case AccountTask::STATE_FAILED_HARD: {
|
|
// FIXME: MSA data clearing
|
|
data.yggdrasilToken.token = QString();
|
|
data.yggdrasilToken.validity = Katabasis::Validity::None;
|
|
data.validity_ = Katabasis::Validity::None;
|
|
emit changed();
|
|
if (session)
|
|
{
|
|
if(data.type == AccountType::MSA) {
|
|
session->status = AuthSession::RequiresOAuth;
|
|
}
|
|
else {
|
|
session->status = AuthSession::RequiresPassword;
|
|
}
|
|
session->auth_server_online = true;
|
|
fillSession(session);
|
|
}
|
|
}
|
|
break;
|
|
case AccountTask::STATE_FAILED_GONE: {
|
|
data.validity_ = Katabasis::Validity::None;
|
|
emit changed();
|
|
if (session)
|
|
{
|
|
session->status = AuthSession::GoneOrMigrated;
|
|
session->auth_server_online = true;
|
|
fillSession(session);
|
|
}
|
|
}
|
|
break;
|
|
case AccountTask::STATE_CREATED:
|
|
case AccountTask::STATE_WORKING:
|
|
case AccountTask::STATE_SUCCEEDED: {
|
|
// Not reachable here, as they are not failures.
|
|
}
|
|
}
|
|
m_currentTask.reset();
|
|
}
|
|
|
|
void MinecraftAccount::fillSession(AuthSessionPtr session)
|
|
{
|
|
// the user name. you have to have an user name
|
|
// FIXME: not with MSA
|
|
session->username = data.userName();
|
|
// volatile auth token
|
|
session->access_token = data.accessToken();
|
|
// the semi-permanent client token
|
|
session->client_token = data.clientToken();
|
|
// profile name
|
|
session->player_name = data.profileName();
|
|
// profile ID
|
|
session->uuid = data.profileId();
|
|
// 'legacy' or 'mojang', depending on account type
|
|
session->user_type = typeString();
|
|
if (!session->access_token.isEmpty())
|
|
{
|
|
session->session = "token:" + data.accessToken() + ":" + data.profileId();
|
|
}
|
|
else
|
|
{
|
|
session->session = "-";
|
|
}
|
|
session->m_accountPtr = shared_from_this();
|
|
}
|
|
|
|
void MinecraftAccount::decrementUses()
|
|
{
|
|
Usable::decrementUses();
|
|
if(!isInUse())
|
|
{
|
|
emit changed();
|
|
// FIXME: we now need a better way to identify accounts...
|
|
qWarning() << "Profile" << data.profileId() << "is no longer in use.";
|
|
}
|
|
}
|
|
|
|
void MinecraftAccount::incrementUses()
|
|
{
|
|
bool wasInUse = isInUse();
|
|
Usable::incrementUses();
|
|
if(!wasInUse)
|
|
{
|
|
emit changed();
|
|
// FIXME: we now need a better way to identify accounts...
|
|
qWarning() << "Profile" << data.profileId() << "is now in use.";
|
|
}
|
|
}
|