Verify access tokens before launching Minecraft

Kind of an important thing to do... Heh...
This commit is contained in:
Andrew 2013-11-28 20:45:52 -06:00
parent 1f150dcb78
commit bfc9e1e5d5
12 changed files with 440 additions and 73 deletions

View File

@ -217,6 +217,8 @@ gui/dialogs/EditNotesDialog.h
gui/dialogs/EditNotesDialog.cpp gui/dialogs/EditNotesDialog.cpp
gui/dialogs/CustomMessageBox.h gui/dialogs/CustomMessageBox.h
gui/dialogs/CustomMessageBox.cpp gui/dialogs/CustomMessageBox.cpp
gui/dialogs/PasswordDialog.h
gui/dialogs/PasswordDialog.cpp
gui/dialogs/AccountListDialog.h gui/dialogs/AccountListDialog.h
gui/dialogs/AccountListDialog.cpp gui/dialogs/AccountListDialog.cpp
gui/dialogs/AccountSelectDialog.h gui/dialogs/AccountSelectDialog.h
@ -280,6 +282,8 @@ logic/auth/YggdrasilTask.h
logic/auth/YggdrasilTask.cpp logic/auth/YggdrasilTask.cpp
logic/auth/AuthenticateTask.h logic/auth/AuthenticateTask.h
logic/auth/AuthenticateTask.cpp logic/auth/AuthenticateTask.cpp
logic/auth/ValidateTask.h
logic/auth/ValidateTask.cpp
# legacy instances # legacy instances
@ -366,6 +370,7 @@ gui/dialogs/SettingsDialog.ui
gui/dialogs/CopyInstanceDialog.ui gui/dialogs/CopyInstanceDialog.ui
gui/dialogs/NewInstanceDialog.ui gui/dialogs/NewInstanceDialog.ui
gui/dialogs/LoginDialog.ui gui/dialogs/LoginDialog.ui
gui/dialogs/PasswordDialog.ui
gui/dialogs/AboutDialog.ui gui/dialogs/AboutDialog.ui
gui/dialogs/VersionSelectDialog.ui gui/dialogs/VersionSelectDialog.ui
gui/dialogs/LwjglSelectDialog.ui gui/dialogs/LwjglSelectDialog.ui

View File

@ -60,6 +60,7 @@
#include "gui/dialogs/CopyInstanceDialog.h" #include "gui/dialogs/CopyInstanceDialog.h"
#include "gui/dialogs/AccountListDialog.h" #include "gui/dialogs/AccountListDialog.h"
#include "gui/dialogs/AccountSelectDialog.h" #include "gui/dialogs/AccountSelectDialog.h"
#include "gui/dialogs/PasswordDialog.h"
#include "gui/ConsoleWindow.h" #include "gui/ConsoleWindow.h"
@ -69,6 +70,9 @@
#include "logic/lists/IconList.h" #include "logic/lists/IconList.h"
#include "logic/lists/JavaVersionList.h" #include "logic/lists/JavaVersionList.h"
#include "logic/auth/AuthenticateTask.h"
#include "logic/auth/ValidateTask.h"
#include "logic/net/LoginTask.h" #include "logic/net/LoginTask.h"
#include "logic/BaseInstance.h" #include "logic/BaseInstance.h"
@ -709,7 +713,7 @@ void MainWindow::instanceActivated(QModelIndex index)
NagUtils::checkJVMArgs(inst->settings().get("JvmArgs").toString(), this); NagUtils::checkJVMArgs(inst->settings().get("JvmArgs").toString(), this);
doLogin(); doLaunch();
} }
void MainWindow::on_actionLaunchInstance_triggered() void MainWindow::on_actionLaunchInstance_triggered()
@ -717,11 +721,11 @@ void MainWindow::on_actionLaunchInstance_triggered()
if (m_selectedInstance) if (m_selectedInstance)
{ {
NagUtils::checkJVMArgs(m_selectedInstance->settings().get("JvmArgs").toString(), this); NagUtils::checkJVMArgs(m_selectedInstance->settings().get("JvmArgs").toString(), this);
doLogin(); doLaunch();
} }
} }
void MainWindow::doLogin(const QString &errorMsg) void MainWindow::doLaunch()
{ {
if (!m_selectedInstance) if (!m_selectedInstance)
return; return;
@ -761,11 +765,69 @@ void MainWindow::doLogin(const QString &errorMsg)
if (account.get() != nullptr) if (account.get() != nullptr)
{ {
// We'll need to validate the access token to make sure the account is still logged in. doLaunchInst(m_selectedInstance, account);
// TODO: Do that ^ }
}
void MainWindow::doLaunchInst(BaseInstance* instance, MojangAccountPtr account)
{
// We'll need to validate the access token to make sure the account is still logged in.
ProgressDialog progDialog(this);
ValidateTask validateTask(account, &progDialog);
progDialog.exec(&validateTask);
if (validateTask.successful())
{
prepareLaunch(m_selectedInstance, account); prepareLaunch(m_selectedInstance, account);
} }
else
{
YggdrasilTask::Error* error = validateTask.getError();
if (error != nullptr)
{
if (error->getErrorMessage().contains("invalid token", Qt::CaseInsensitive))
{
// TODO: Allow the user to enter their password and "refresh" their access token.
if (doRefreshToken(account, tr("Your account's access token is invalid. Please enter your password to log in again.")))
doLaunchInst(instance, account);
}
else
{
CustomMessageBox::selectable(this, tr("Access Token Validation Error"),
tr("There was an error when trying to validate your access token.\n"
"Details: %s").arg(error->getDisplayMessage()),
QMessageBox::Warning, QMessageBox::Ok)->exec();
}
}
else
{
CustomMessageBox::selectable(this, tr("Access Token Validation Error"),
tr("There was an unknown error when trying to validate your access token."
"The authentication server might be down, or you might not be connected to the Internet."),
QMessageBox::Warning, QMessageBox::Ok)->exec();
}
}
}
bool MainWindow::doRefreshToken(MojangAccountPtr account, const QString& errorMsg)
{
PasswordDialog passDialog(errorMsg, this);
if (passDialog.exec() == QDialog::Accepted)
{
// To refresh the token, we just create an authenticate task with the given account and the user's password.
ProgressDialog progDialog(this);
AuthenticateTask authTask(account, passDialog.password(), &progDialog);
progDialog.exec(&authTask);
if (authTask.successful())
return true;
else
{
// If the authentication task failed, recurse with the task's error message.
return doRefreshToken(account, authTask.failReason());
}
}
else return false;
} }
void MainWindow::prepareLaunch(BaseInstance* instance, MojangAccountPtr account) void MainWindow::prepareLaunch(BaseInstance* instance, MojangAccountPtr account)

View File

@ -106,7 +106,23 @@ slots:
void on_actionEditInstNotes_triggered(); void on_actionEditInstNotes_triggered();
void doLogin(const QString &errorMsg = ""); /*!
* Launches the currently selected instance with the default account.
* If no default account is selected, prompts the user to pick an account.
*/
void doLaunch();
/*!
* Launches the given instance with the given account.
*/
void doLaunchInst(BaseInstance* instance, MojangAccountPtr account);
/*!
* Opens an input dialog, allowing the user to input their password and refresh its access token.
* This function will execute the proper Yggdrasil task to refresh the access token.
* Returns true if successful. False if the user cancelled.
*/
bool doRefreshToken(MojangAccountPtr account, const QString& errorMsg="");
/*! /*!
* Launches the given instance with the given account. * Launches the given instance with the given account.

View File

@ -0,0 +1,38 @@
/* Copyright 2013 MultiMC Contributors
*
* 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 "PasswordDialog.h"
#include "ui_PasswordDialog.h"
PasswordDialog::PasswordDialog(const QString& errorMsg, QWidget *parent) :
QDialog(parent),
ui(new Ui::PasswordDialog)
{
ui->setupUi(this);
ui->errorLabel->setText(errorMsg);
ui->errorLabel->setVisible(!errorMsg.isEmpty());
}
PasswordDialog::~PasswordDialog()
{
delete ui;
}
QString PasswordDialog::password() const
{
return ui->passTextBox->text();
}

View File

@ -0,0 +1,40 @@
/* Copyright 2013 MultiMC Contributors
*
* 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.
*/
#pragma once
#include <QDialog>
namespace Ui {
class PasswordDialog;
}
class PasswordDialog : public QDialog
{
Q_OBJECT
public:
explicit PasswordDialog(const QString& errorMsg="", QWidget *parent = 0);
~PasswordDialog();
/*!
* Gets the text entered in the dialog's password field.
*/
QString password() const;
private:
Ui::PasswordDialog *ui;
};

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PasswordDialog</class>
<widget class="QDialog" name="PasswordDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>94</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="errorLabel">
<property name="text">
<string>Error message here...</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PasswordDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PasswordDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,66 @@
/* Copyright 2013 MultiMC Contributors
*
* 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 <logic/auth/ValidateTask.h>
#include <logic/auth/MojangAccount.h>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QVariant>
#include <QDebug>
#include "logger/QsLog.h"
ValidateTask::ValidateTask(MojangAccountPtr account, QObject* parent) :
YggdrasilTask(account, parent)
{
}
QJsonObject ValidateTask::getRequestContent() const
{
QJsonObject req;
req.insert("accessToken", getMojangAccount()->accessToken());
return req;
}
bool ValidateTask::processResponse(QJsonObject responseData)
{
// Assume that if processError wasn't called, then the request was successful.
emitSucceeded();
return true;
}
QString ValidateTask::getEndpoint() const
{
return "validate";
}
QString ValidateTask::getStateMessage(const YggdrasilTask::State state) const
{
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Validating Access Token: Sending request.");
case STATE_PROCESSING_RESPONSE:
return tr("Validating Access Token: Processing response.");
default:
return YggdrasilTask::getStateMessage(state);
}
}

44
logic/auth/ValidateTask.h Normal file
View File

@ -0,0 +1,44 @@
/* Copyright 2013 MultiMC Contributors
*
* 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.
*/
#pragma once
#include <logic/auth/YggdrasilTask.h>
#include <QObject>
#include <QString>
#include <QJsonObject>
/**
* The validate task takes a MojangAccount and checks to make sure its access token is valid.
*/
class ValidateTask : public YggdrasilTask
{
Q_OBJECT
public:
ValidateTask(MojangAccountPtr account, QObject* parent=0);
protected:
virtual QJsonObject getRequestContent() const;
virtual QString getEndpoint() const;
virtual bool processResponse(QJsonObject responseData);
QString getStateMessage(const YggdrasilTask::State state) const;
private:
};

View File

@ -64,78 +64,65 @@ void YggdrasilTask::processReply(QNetworkReply* reply)
// Wrong reply for some reason... // Wrong reply for some reason...
return; return;
// Check for errors. if (reply->error() == QNetworkReply::OperationCanceledError)
switch (reply->error())
{ {
case QNetworkReply::NoError: emitFailed("Yggdrasil task cancelled.");
return;
}
else
{
// Try to parse the response regardless of the response code.
// Sometimes the auth server will give more information and an error code.
QJsonParseError jsonError;
QByteArray replyData = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
// Check the response code.
int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (responseCode == 200)
{ {
// Try to parse the response regardless of the response code. // If the response code was 200, then there shouldn't be an error. Make sure anyways.
// Sometimes the auth server will give more information and an error code. // Also, sometimes an empty reply indicates success. If there was no data received,
QJsonParseError jsonError; // pass an empty json object to the processResponse function.
QByteArray replyData = reply->readAll(); if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
// Check the response code.
int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
switch (responseCode)
{ {
case 200: if (!processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()))
{ {
// If the response code was 200, then there shouldn't be an error. Make sure anyways. YggdrasilTask::Error* err = getError();
switch (jsonError.error) if (err)
{ emitFailed(err->getErrorMessage());
case QJsonParseError::NoError: else
if (!processResponse(doc.object())) emitFailed(tr("An unknown error occurred when processing the response from the authentication server."));
{ }
YggdrasilTask::Error* err = getError(); else
if (err) {
emitFailed(err->getErrorMessage()); emitSucceeded();
else
emitFailed(tr("An unknown error occurred when processing the response from the authentication server."));
}
else
{
emitSucceeded();
}
break;
default:
emitFailed(tr("Failed to parse Yggdrasil JSON response: \"%1\".").arg(jsonError.errorString()));
break;
}
break;
} }
default:
// If the response code was something else, then Yggdrasil may have given us information about the error.
// If we can parse the response, then get information from it. Otherwise just say there was an unknown error.
switch (jsonError.error)
{
case QJsonParseError::NoError:
// We were able to parse the server's response. Woo!
// Call processError. If a subclass has overridden it then they'll handle their stuff there.
processError(doc.object());
break;
default:
// The server didn't say anything regarding the error. Give the user an unknown error.
emitFailed(tr("Login failed: Unknown HTTP code %1 encountered.").arg(responseCode));
break;
}
break;
} }
else
break; {
emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset));
}
}
else
{
// If the response code was not 200, then Yggdrasil may have given us information about the error.
// If we can parse the response, then get information from it. Otherwise just say there was an unknown error.
if (jsonError.error == QJsonParseError::NoError)
{
// We were able to parse the server's response. Woo!
// Call processError. If a subclass has overridden it then they'll handle their stuff there.
QLOG_DEBUG() << "The request failed, but the server gave us an error message. Processing error.";
emitFailed(processError(doc.object()));
}
else
{
// The server didn't say anything regarding the error. Give the user an unknown error.
QLOG_DEBUG() << "The request failed and the server gave no error message. Unknown error.";
emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(reply->errorString()));
}
} }
case QNetworkReply::OperationCanceledError:
emitFailed(tr("Login canceled."));
break;
default:
emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server."));
break;
} }
} }
@ -145,7 +132,7 @@ QString YggdrasilTask::processError(QJsonObject responseData)
QJsonValue msgVal = responseData.value("errorMessage"); QJsonValue msgVal = responseData.value("errorMessage");
QJsonValue causeVal = responseData.value("cause"); QJsonValue causeVal = responseData.value("cause");
if (errorVal.isString() && msgVal.isString() && causeVal.isString()) if (errorVal.isString() && msgVal.isString())
{ {
m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString("")); m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString(""));
return m_error->getDisplayMessage(); return m_error->getDisplayMessage();

View File

@ -99,6 +99,8 @@ protected:
* If an error occurred, this should emit a failed signal and return false. * If an error occurred, this should emit a failed signal and return false.
* If Yggdrasil gave an error response, it should call setError() first, and then return false. * If Yggdrasil gave an error response, it should call setError() first, and then return false.
* Otherwise, it should return true. * Otherwise, it should return true.
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
* an empty QJsonObject.
*/ */
virtual bool processResponse(QJsonObject responseData) = 0; virtual bool processResponse(QJsonObject responseData) = 0;

View File

@ -53,6 +53,8 @@ void Task::start()
void Task::emitFailed(QString reason) void Task::emitFailed(QString reason)
{ {
m_running = false; m_running = false;
m_succeeded = false;
m_failReason = reason;
QLOG_ERROR() << "Task failed: " << reason; QLOG_ERROR() << "Task failed: " << reason;
emit failed(reason); emit failed(reason);
} }
@ -60,6 +62,8 @@ void Task::emitFailed(QString reason)
void Task::emitSucceeded() void Task::emitSucceeded()
{ {
m_running = false; m_running = false;
m_succeeded = true;
QLOG_INFO() << "Task succeeded";
emit succeeded(); emit succeeded();
} }
@ -67,3 +71,14 @@ bool Task::isRunning() const
{ {
return m_running; return m_running;
} }
bool Task::successful() const
{
return m_succeeded;
}
QString Task::failReason() const
{
return m_failReason;
}

View File

@ -29,6 +29,18 @@ public:
virtual void getProgress(qint64 &current, qint64 &total); virtual void getProgress(qint64 &current, qint64 &total);
virtual bool isRunning() const; virtual bool isRunning() const;
/*!
* True if this task was successful.
* If the task failed or is still running, returns false.
*/
virtual bool successful() const;
/*!
* Returns the string that was passed to emitFailed as the error message when the task failed.
* If the task hasn't failed, returns an empty string.
*/
virtual QString failReason() const;
public public
slots: slots:
virtual void start(); virtual void start();
@ -48,4 +60,6 @@ protected:
QString m_status; QString m_status;
int m_progress = 0; int m_progress = 0;
bool m_running = false; bool m_running = false;
bool m_succeeded = false;
QString m_failReason = "";
}; };