Verified Commit e656592a authored by Daniel Vrátil's avatar Daniel Vrátil 🤖
Browse files

Remove the AuthWidget, have AuthJob open real browser instead

I've lately seen issues with Google claiming that the app of the
browser is not secure. As such it is safer to just use a real
web browser. It also means we can get rid of the awful webengine
dependency in KGAPI.

AS of now, the AuthJob will simply launch a browser, wait for user
to click through the authentication there and wait for the tokens
to be sent by the browser via a socket. Implementations are encouraged
to not launch the AuthJob silently as that might confuse users as to
what is happening, but instead do so upon a user action.
parent 4b320bfc
......@@ -3,10 +3,9 @@ ecm_create_qm_loader(QM_LOADER libkgapi_qt)
set(kgapicore_SRCS
accountinfo/accountinfo.cpp
accountinfo/accountinfofetchjob.cpp
private/fullauthenticationjob.cpp
private/newtokensfetchjob.cpp
ui/authwidget.cpp
ui/authwidget_p.cpp
ui/authwidgetfactory.cpp
private/refreshtokensjob.cpp
account.cpp
accountmanager.cpp
accountstorage.cpp
......@@ -50,13 +49,6 @@ ecm_generate_headers(kgapicore_accountinfo_CamelCase_HEADERS
RELATIVE accountinfo
)
ecm_generate_headers(kgapicore_ui_CamelCase_HEADERS
HEADER_NAMES
AuthWidget
REQUIRED_HEADERS kgapicore_ui_HEADERS
RELATIVE ui
)
add_library(KPimGAPICore
${kgapicore_SRCS}
)
......@@ -73,9 +65,6 @@ PRIVATE
KF5::KIOWidgets
KF5::WindowSystem
KF5::Wallet
Qt5::WebEngineWidgets
PUBLIC
Qt5::Widgets
)
set_target_properties(KPimGAPICore PROPERTIES
......
......@@ -10,124 +10,43 @@
#include "account.h"
#include "../debug.h"
#include "job_p.h"
#include "ui/authwidget.h"
#include "ui/authwidget_p.h"
#include "ui/authwidgetfactory_p.h"
#include "private/newtokensfetchjob_p.h"
#include "private/fullauthenticationjob_p.h"
#include "private/refreshtokensjob_p.h"
#include <QWidget>
#include <QDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QJsonDocument>
#include <QPointer>
#include <QPushButton>
#include <QNetworkCookieJar>
#include <QUrlQuery>
#include <KWindowSystem>
using namespace KGAPI2;
KGAPICORE_EXPORT uint16_t kgapiTcpAuthServerPort = 0;
class Q_DECL_HIDDEN AuthJob::Private
{
public:
Private(AuthJob *parent);
QWidget* fullAuthentication();
void refreshTokens();
public:
Private(AuthJob *qq)
: q(qq)
{}
template<typename JobType>
void jobFinished(Job *job)
{
if (job->error()) {
q->setError(job->error());
q->setErrorString(job->errorString());
} else {
account = static_cast<JobType*>(job)->account();
}
void _k_fullAuthenticationFinished(const KGAPI2::AccountPtr& account);
void _k_fullAuthenticationFailed(KGAPI2::Error errorCode, const QString &errorMessage);
void _k_destructDelayed();
q->emitFinished();
}
AccountPtr account;
QString apiKey;
QString secretKey;
QWidget* widget = nullptr;
QString username;
QString password;
QPointer<QDialog> dialog;
private:
AuthJob *const q;
private:
AuthJob * const q;
};
AuthJob::Private::Private(AuthJob *parent):
q(parent)
{
}
QWidget* AuthJob::Private::fullAuthentication()
{
AuthWidget* authWidget = AuthWidgetFactory::instance()->create(widget);
// FIXME: Find a better way to pass the keys
authWidget->d->apiKey = apiKey;
authWidget->d->secretKey = secretKey;
authWidget->setUsername(username);
authWidget->setPassword(password);
authWidget->setAccount(account);
return authWidget;
}
void AuthJob::Private::refreshTokens()
{
static_cast<Job*>(q)->d->accessManager->setCookieJar(new QNetworkCookieJar);
QNetworkRequest request;
request.setUrl(QUrl(QStringLiteral("https://accounts.google.com/o/oauth2/token")));
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QUrlQuery params;
params.addQueryItem(QStringLiteral("client_id"), apiKey);
params.addQueryItem(QStringLiteral("client_secret"), secretKey);
params.addQueryItem(QStringLiteral("refresh_token"), account->refreshToken());
params.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token"));
qCDebug(KGAPIDebug) << "Requesting token refresh.";
q->enqueueRequest(request, params.toString(QUrl::FullyEncoded).toLatin1());
}
void AuthJob::Private::_k_fullAuthenticationFailed(Error errorCode, const QString &errorMessage)
{
q->setError(errorCode);
q->setErrorString(errorMessage);
q->emitFinished();
}
void AuthJob::Private::_k_fullAuthenticationFinished( const AccountPtr &account_ )
{
account = account_;
q->emitFinished();
}
void AuthJob::Private::_k_destructDelayed()
{
if (!dialog)
return;
if (dialog->isVisible())
dialog->hide();
dialog->deleteLater();
dialog = nullptr;
}
AuthJob::AuthJob(const AccountPtr& account, const QString &apiKey, const QString &secretKey, QWidget* parent):
Job(parent),
d(new Private(this))
{
d->account = account;
d->apiKey = apiKey;
d->secretKey = secretKey;
d->widget = parent;
}
AuthJob::AuthJob(const AccountPtr& account, const QString &apiKey, const QString &secretKey, QObject* parent):
Job(parent),
d(new Private(this))
......@@ -137,123 +56,53 @@ AuthJob::AuthJob(const AccountPtr& account, const QString &apiKey, const QString
d->secretKey = secretKey;
}
AuthJob::~AuthJob()
{
delete d;
}
AuthJob::~AuthJob() = default;
AccountPtr AuthJob::account() const
{
return d->account;
}
void AuthJob::setUsername(const QString& username)
void AuthJob::setUsername(const QString& /*username*/)
{
d->username = username;
}
void AuthJob::setPassword(const QString& password)
void AuthJob::setPassword(const QString& /*password*/)
{
d->password = password;
}
void AuthJob::handleReply(const QNetworkReply *reply, const QByteArray& rawData)
void AuthJob::handleReply(const QNetworkReply * /*reply*/, const QByteArray& /*rawData*/)
{
Q_UNUSED(reply);
QJsonDocument document = QJsonDocument::fromJson(rawData);
if (document.isNull()) {
setError(KGAPI2::InvalidResponse);
setErrorString(tr("Failed to parse newly fetched tokens"));
emitFinished();
return;
}
QVariantMap map = document.toVariant().toMap();
/* Expected structure:
* {
* "access_token": "the_access_token",
* "token_type":"Bearer",
* "expires_in":3600
* }
*/
const qlonglong expiresIn = map.value(QStringLiteral("expires_in")).toLongLong();
d->account->setExpireDateTime(QDateTime::currentDateTime().addSecs(expiresIn));
d->account->setAccessToken(map.value(QStringLiteral("access_token")).toString());
emitFinished();
// Should never be called.
Q_UNREACHABLE();
}
void AuthJob::dispatchRequest(QNetworkAccessManager* accessManager, const QNetworkRequest& request, const QByteArray& data, const QString& contentType)
void AuthJob::dispatchRequest(QNetworkAccessManager* /*accessManager*/, const QNetworkRequest& /*request*/,
const QByteArray& /*data*/, const QString& /*contentType*/)
{
Q_UNUSED(contentType);
accessManager->post(request, data);
// Should never be called.
Q_UNREACHABLE();
}
void AuthJob::start()
{
AuthWidget *widget = nullptr;
if (d->account->refreshToken().isEmpty() || (d->account->m_scopesChanged == true)) {
d->account->addScope(Account::accountInfoEmailScopeUrl());
/* Pre-fill the username in the dialog so that user knows what account
* (s)he is re-authenticating for */
if (!d->account->accountName().isEmpty() && d->username.isEmpty()) {
d->username = d->account->accountName();
}
widget = qobject_cast<AuthWidget*>(d->fullAuthentication());
auto *job = new FullAuthenticationJob(d->account, d->apiKey, d->secretKey, this);
job->setServerPort(kgapiTcpAuthServerPort);
connect(job, &Job::finished, this, [this](Job *job) { d->jobFinished<FullAuthenticationJob>(job); });
} else {
if (d->account->accountName().isEmpty()) {
if (d->account->accountName().isEmpty()) {
setError(KGAPI2::InvalidAccount);
setErrorString(tr("Account name is empty"));
emitFinished();
return;
}
d->refreshTokens();
}
if (widget) {
d->dialog = new QDialog();
d->dialog->setModal(true);
d->dialog->resize(840, 760);
d->dialog->setAttribute(Qt::WA_NativeWindow, true);
KWindowSystem::setMainWindow(d->dialog->windowHandle(), KWindowSystem::activeWindow());
QVBoxLayout *layout = new QVBoxLayout(d->dialog);
layout->addWidget(widget, 2);
QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, Qt::Horizontal, d->dialog);
layout->addWidget(buttons, 0);
connect(buttons, &QDialogButtonBox::rejected,
this, [this]() {
d->_k_destructDelayed();
d->_k_fullAuthenticationFailed(AuthCancelled, tr("Authentication canceled"));
});
connect(widget, &AuthWidget::authenticated,
this, [this](const KGAPI2::AccountPtr &account) {
d->_k_destructDelayed();
d->_k_fullAuthenticationFinished(account);
});
connect(widget, &AuthWidget::error,
this, [this](KGAPI2::Error error, const QString &str) {
d->_k_destructDelayed();
d->_k_fullAuthenticationFailed(error, str);
});
d->dialog->show();
buttons->button(QDialogButtonBox::Cancel)->setDefault(false); // QTBUG-66109
widget->authenticate();
auto *job = new RefreshTokensJob(d->account, d->apiKey, d->secretKey, this);
connect(job, &Job::finished, this, [this](Job *job) { d->jobFinished<RefreshTokensJob>(job); });
}
}
#include "moc_authjob.cpp"
......@@ -23,9 +23,9 @@ namespace KGAPI2 {
* This job can be either used to refresh expired tokens (this is usually done
* automatically by Job implementation), or to request tokens for a new account.
*
* In the latter case, the AuthJob will automatically show a dialog where user
* has to provide Google account credentials and grant access to all requested
* scopes (@see Account::scopes).
* In the latter case, the AuthJob will automatically open a browser window
* where user has to provide Google account credentials and grant access to all
* requested scopes (@see Account::scopes).
*
* @author Daniel Vrátil <dvratil@redhat.com>
* @since 2.0
......@@ -35,36 +35,6 @@ class KGAPICORE_EXPORT AuthJob : public KGAPI2::Job
Q_OBJECT
public:
/**
* @brief Creates a new authentication job that will use @p parent as parent
* for the authentication dialog.
*
* When constructed with a widget parent, AuthJob will place the
* authentication widget on the @p parent instead of displaying a dialog.
* This allows embedding the authentication process into a wizard for
* instance.
*
* @param account Account to authenticate. When only scopes are set, a full
* authentication process will run (including showing the auth
* widget) and the rest will be filled by the job.
* @par
* Passing an Account with account name, scopes and both
* tokens filled will only refresh the access token. If however
* the scopes have been changed a full authentication will be
* started.
* @par
* Any other Account will be considered invalid and the job
* will finish immediately.
*
* @param apiKey Application API key
* @param secretKey Application secret API key
* @param parent Parent widget on which auth widget should be constructed if
* necessary.
*/
explicit AuthJob(const AccountPtr &account, const QString &apiKey,
const QString &secretKey, QWidget* parent);
/**
* @brief Creates a new authentication job
*
......@@ -72,8 +42,6 @@ class KGAPICORE_EXPORT AuthJob : public KGAPI2::Job
* job might pop up the authentication dialog.
*
* @param account Account to authenticate.
* See AuthJob(AccountPtr,QString,QString,QWidget) for
* detailed description of @p account content.
* @param apiKey Application API key
* @param secretKey Application secret API key
* @param parent
......@@ -98,33 +66,22 @@ class KGAPICORE_EXPORT AuthJob : public KGAPI2::Job
/**
* Sets the username that will be used when authenticate is called
*
* The username will be automatically filled in the Google login
* form in the authentication widget.
*
* Be aware that the username will be set every time \sa authenticate is
* called so if you want to change or remove it call \sa setUsername again
* with empty string or \sa clearCredentials.
*
* @param username username to use
* @deprecated
*/
QT_DEPRECATED_X("It's no longer possible to prefill username")
void setUsername(const QString &username);
/**
* Sets the password that will be used when authenticate is called
*
* The password will be automatically filled in the Google login
* form in the authentication widget.
*
* Be aware that the password will be set every time \sa authenticate is
* called so if you want to change or remove it call \sa setPassword again
* with empty string or \sa clearCredentials.
*
* @param password password to use
* @deprecated
*/
QT_DEPRECATED_X("It's no longer possible to prefill password")
void setPassword(const QString &password);
protected:
/**
* @brief KGAPI2::Job::handleReply implementation
*
......@@ -153,7 +110,7 @@ class KGAPICORE_EXPORT AuthJob : public KGAPI2::Job
private:
class Private;
Private * const d;
QScopedPointer<Private> const d;
friend class Private;
};
......
/*
* This file is part of LibKGAPI
*
* SPDX-FileCopyrightText: 2020 Daniel Vrátil <dvratil@kde.org>
*
* SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include "fullauthenticationjob_p.h"
#include "newtokensfetchjob_p.h"
#include "accountinfo/accountinfofetchjob.h"
#include "accountinfo/accountinfo.h"
#include "account.h"
#include "../../debug.h"
#include <QAbstractSocket>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDesktopServices>
#include <QUrlQuery>
#include <QUrl>
#include <QDateTime>
using namespace KGAPI2;
namespace KGAPI2
{
class Q_DECL_HIDDEN FullAuthenticationJob::Private
{
public:
Private(const AccountPtr &account, const QString &apiKey, const QString &secretKey, FullAuthenticationJob *qq)
: mAccount(account)
, mApiKey(apiKey)
, mSecretKey(secretKey)
, q(qq)
{}
void emitError(Error error, const QString &text)
{
q->setError(error);
q->setErrorString(text);
q->emitFinished();
}
void socketError(QAbstractSocket::SocketError error)
{
if (mConnection) {
mConnection->deleteLater();
}
qCDebug(KGAPIDebug) << "Socket error when receiving response:" << error;
emitError(InvalidResponse, tr("Error receiving response: %1").arg(error));
}
void socketReady()
{
Q_ASSERT(mConnection);
const QByteArray data = mConnection->readLine();
mConnection->write("HTTP/1.1 200 OK\n");
mConnection->flush();
mConnection->deleteLater();
qCDebug(KGAPIDebug) << "Got connection on socket";
const auto line = data.split(' ');
if (line.size() != 3 || line.at(0) != QByteArray("GET") || !line.at(2).startsWith(QByteArray("HTTP/1.1"))) {
qCDebug(KGAPIDebug) << "Token response invalid";
emitError(InvalidResponse, tr("Token response invalid"));
return;
}
//qCDebug(KGAPIDebug) << "Receiving data on socket: " << data;
const QUrl url(QString::fromLatin1(line.at(1)));
const QUrlQuery query(url);
const QString code = query.queryItemValue(QStringLiteral("code"));
if (code.isEmpty()) {
const QString error = query.queryItemValue(QStringLiteral("error"));
if (!error.isEmpty()) {
qCDebug(KGAPIDebug) << "Google has returned an error response:" << error;
emitError(UnknownError, error);
} else {
qCDebug(KGAPIDebug) << "Could not extract token from HTTP answer";
emitError(InvalidAccount, tr("Could not extract token from HTTP answer"));
}
return;
}
Q_ASSERT(mServerPort != -1);
auto fetch = new KGAPI2::NewTokensFetchJob(code, mApiKey, mSecretKey, mServerPort);
q->connect(fetch, &Job::finished, q, [this](Job *job) { tokensReceived(job); });
}
void tokensReceived(Job *job)
{
auto *tokensFetchJob = qobject_cast<NewTokensFetchJob*>(job);
if (tokensFetchJob->error()) {
qCDebug(KGAPIDebug) << "Error when retrieving tokens:" << job->errorString();
emitError(static_cast<Error>(job->error()), job->errorString());
return;
}
mAccount->setAccessToken(tokensFetchJob->accessToken());
mAccount->setRefreshToken(tokensFetchJob->refreshToken());
mAccount->setExpireDateTime(QDateTime::currentDateTime().addSecs(tokensFetchJob->expiresIn()));
tokensFetchJob->deleteLater();
auto *fetchJob = new KGAPI2::AccountInfoFetchJob(mAccount, q);
q->connect(fetchJob, &Job::finished, q, [this](Job *job) { accountInfoReceived(job); });
qCDebug(KGAPIDebug) << "Requesting AccountInfo";
}
void accountInfoReceived(Job *job)
{
if (job->error()) {
qCDebug(KGAPIDebug) << "Error when retrieving AccountInfo:" << job->errorString();
emitError(static_cast<Error>(job->error()), job->errorString());
return;
}
const auto objects = qobject_cast<AccountInfoFetchJob*>(job)->items();
Q_ASSERT(!objects.isEmpty());
const auto accountInfo = objects.first().staticCast<AccountInfo>();
mAccount->setAccountName(accountInfo->email());
job->deleteLater();
q->emitFinished();
}
public:
AccountPtr mAccount;
QString mApiKey;
QString mSecretKey;
std::unique_ptr<QTcpServer> mServer;
QTcpSocket *mConnection = nullptr;
uint16_t mServerPort = 0;
private:
FullAuthenticationJob * const q;
};
} // namespace KGAPI2
FullAuthenticationJob::FullAuthenticationJob(const AccountPtr &account, const QString &apiKey, const QString &secretKey, QObject *parent)
: Job(parent)
, d(new Private(account, apiKey, secretKey, this))
{
};
FullAuthenticationJob::~FullAuthenticationJob() = default;
void FullAuthenticationJob::setServerPort(uint16_t port)
{
d->mServerPort = port;
}
AccountPtr FullAuthenticationJob::account() const
{
return d->mAccount;
}
void FullAuthenticationJob::start()
{
if (d->mAccount.isNull()) {
d->emitError(InvalidAccount, tr("Invalid account"));
return;
}
if (d->mAccount->scopes().isEmpty()) {
d->emitError(InvalidAccount, tr("No scopes to authenticate for"));
return;
}
QStringList scopes;
scopes.reserve(d->mAccount->scopes().size());
Q_FOREACH(const QUrl & scope, d->mAccount->scopes()) {
scopes << scope.toString();
}
d->mServer = std::unique_ptr<QTcpServer>(new QTcpServer(this));
if (!d->mServer->listen(QHostAddress::LocalHost, d->mServerPort)) {
d->emitError(InvalidAccount, tr("Could not start OAuth HTTP server"));
return;
}
d->mServerPort = d->mServer->serverPort();
connect(d->mServer.get(), &QTcpServer::acceptError, this, [this](QAbstractSocket::SocketError e) { d->socketError(e); });
connect(d->mServer.get(), &QTcpServer::newConnection, this, [this]() {
d->mConnection = d->mServer->nextPendingConnection();