Merge pull request #6005 from nextcloud/work/remove-unused-oauth-theme-values

Remove dead OAuth code
This commit is contained in:
Claudio Cambra 2023-09-19 15:44:53 +08:00 committed by GitHub
commit 1a0a7ec7a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 30 additions and 1167 deletions

View File

@ -48,7 +48,6 @@ set(client_UI_SRCS
wizard/owncloudadvancedsetuppage.ui
wizard/owncloudconnectionmethoddialog.ui
wizard/owncloudhttpcredspage.ui
wizard/owncloudoauthcredspage.ui
wizard/owncloudsetupnocredspage.ui
wizard/webview.ui
wizard/welcomepage.ui
@ -224,8 +223,6 @@ set(client_SRCS
creds/credentialsfactory.cpp
creds/httpcredentialsgui.h
creds/httpcredentialsgui.cpp
creds/oauth.h
creds/oauth.cpp
creds/flow2auth.h
creds/flow2auth.cpp
creds/webflowcredentials.h
@ -242,8 +239,6 @@ set(client_SRCS
wizard/owncloudconnectionmethoddialog.cpp
wizard/owncloudhttpcredspage.h
wizard/owncloudhttpcredspage.cpp
wizard/owncloudoauthcredspage.h
wizard/owncloudoauthcredspage.cpp
wizard/flow2authcredspage.h
wizard/flow2authcredspage.cpp
wizard/flow2authwidget.h

View File

@ -1271,19 +1271,7 @@ void AccountSettings::slotAccountStateChanged()
showConnectionLabel(tr("Signed out from %1.").arg(serverWithUser));
break;
case AccountState::AskingCredentials: {
QUrl url;
if (const auto cred = qobject_cast<HttpCredentialsGui *>(account->credentials())) {
connect(cred, &HttpCredentialsGui::authorisationLinkChanged,
this, &AccountSettings::slotAccountStateChanged, Qt::UniqueConnection);
url = cred->authorisationLink();
}
if (url.isValid()) {
showConnectionLabel(tr("Obtaining authorization from the browser. "
"<a href='%1'>Click here</a> to re-open the browser.")
.arg(url.toString(QUrl::FullyEncoded)));
} else {
showConnectionLabel(tr("Connecting to %1 …").arg(serverWithUser));
}
showConnectionLabel(tr("Connecting to %1 …").arg(serverWithUser));
break;
}
case AccountState::NetworkError:

View File

@ -456,10 +456,6 @@ void AccountState::handleInvalidCredentials()
if (account()->credentials()->ready()) {
account()->credentials()->invalidateToken();
}
if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
if (creds->refreshAccessToken())
return;
}
account()->credentials()->askFromUser();
}

View File

@ -46,16 +46,7 @@ void HttpCredentialsGui::askFromUserAsync()
// First, we will check what kind of auth we need.
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
QObject::connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) {
if (type == DetermineAuthTypeJob::OAuth) {
_asyncAuth.reset(new OAuth(_account, this));
_asyncAuth->_expectedUser = _account->davUser();
connect(_asyncAuth.data(), &OAuth::result,
this, &HttpCredentialsGui::asyncAuthResult);
connect(_asyncAuth.data(), &OAuth::destroyed,
this, &HttpCredentialsGui::authorisationLinkChanged);
_asyncAuth->start();
emit authorisationLinkChanged();
} else if (type == DetermineAuthTypeJob::Basic) {
if (type == DetermineAuthTypeJob::Basic) {
showDialog();
} else {
// Shibboleth?
@ -66,32 +57,6 @@ void HttpCredentialsGui::askFromUserAsync()
job->start();
}
void HttpCredentialsGui::asyncAuthResult(OAuth::Result r, const QString &user,
const QString &token, const QString &refreshToken)
{
switch (r) {
case OAuth::NotSupported:
showDialog();
_asyncAuth.reset(nullptr);
return;
case OAuth::Error:
_asyncAuth.reset(nullptr);
emit asked();
return;
case OAuth::LoggedIn:
break;
}
ASSERT(_user == user); // ensured by _asyncAuth
_password = token;
_refreshToken = refreshToken;
_ready = true;
persist();
_asyncAuth.reset(nullptr);
emit asked();
}
void HttpCredentialsGui::showDialog()
{
QString msg = tr("Please enter %1 password:<br>"
@ -128,7 +93,6 @@ void HttpCredentialsGui::showDialog()
connect(dialog, &QDialog::finished, this, [this, dialog](int result) {
if (result == QDialog::Accepted) {
_password = dialog->textValue();
_refreshToken.clear();
_ready = true;
persist();
}

View File

@ -15,7 +15,6 @@
#pragma once
#include "creds/httpcredentials.h"
#include "creds/oauth.h"
#include <QPointer>
#include <QTcpServer>
@ -38,37 +37,13 @@ public:
: HttpCredentials(user, password, clientCertBundle, clientCertPassword)
{
}
HttpCredentialsGui(const QString &user, const QString &password, const QString &refreshToken,
const QByteArray &clientCertBundle, const QByteArray &clientCertPassword)
: HttpCredentials(user, password, clientCertBundle, clientCertPassword)
{
_refreshToken = refreshToken;
}
/**
* This will query the server and either uses OAuth via _asyncAuth->start()
* or call showDialog to ask the password
*/
void askFromUser() override;
/**
* In case of oauth, return an URL to the link to open the browser.
* An invalid URL otherwise
*/
[[nodiscard]] QUrl authorisationLink() const { return _asyncAuth ? _asyncAuth->authorisationLink() : QUrl(); }
static QString requestAppPasswordText(const Account *account);
private slots:
void asyncAuthResult(OAuth::Result, const QString &user, const QString &accessToken, const QString &refreshToken);
void showDialog();
void askFromUserAsync();
signals:
void authorisationLinkChanged();
private:
QScopedPointer<OAuth, QScopedPointerObjectDeleteLater<OAuth>> _asyncAuth;
};
} // namespace OCC

View File

@ -1,187 +0,0 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <QDesktopServices>
#include <QNetworkReply>
#include <QTimer>
#include <QBuffer>
#include "account.h"
#include "creds/oauth.h"
#include <QJsonObject>
#include <QJsonDocument>
#include "theme.h"
#include "networkjobs.h"
#include "creds/httpcredentials.h"
#include "guiutility.h"
namespace OCC {
Q_LOGGING_CATEGORY(lcOauth, "nextcloud.sync.credentials.oauth", QtInfoMsg)
OAuth::~OAuth() = default;
static void httpReplyAndClose(QTcpSocket *socket, const char *code, const char *html,
const char *moreHeaders = nullptr)
{
if (!socket)
return; // socket can have been deleted if the browser was closed
socket->write("HTTP/1.1 ");
socket->write(code);
socket->write("\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: ");
socket->write(QByteArray::number(qstrlen(html)));
if (moreHeaders) {
socket->write("\r\n");
socket->write(moreHeaders);
}
socket->write("\r\n\r\n");
socket->write(html);
socket->disconnectFromHost();
// We don't want that deleting the server too early prevent queued data to be sent on this socket.
// The socket will be deleted after disconnection because disconnected is connected to deleteLater
socket->setParent(nullptr);
}
void OAuth::start()
{
// Listen on the socket to get a port which will be used in the redirect_uri
if (!_server.listen(QHostAddress::LocalHost)) {
emit result(NotSupported, QString());
return;
}
if (!openBrowser())
return;
QObject::connect(&_server, &QTcpServer::newConnection, this, [this] {
while (QPointer<QTcpSocket> socket = _server.nextPendingConnection()) {
QObject::connect(socket.data(), &QTcpSocket::disconnected, socket.data(), &QTcpSocket::deleteLater);
QObject::connect(socket.data(), &QIODevice::readyRead, this, [this, socket] {
QByteArray peek = socket->peek(qMin(socket->bytesAvailable(), 4000LL)); //The code should always be within the first 4K
if (peek.indexOf('\n') < 0)
return; // wait until we find a \n
static const QRegularExpression rx("^GET /\\?code=([a-zA-Z0-9]+)[& ]"); // Match a /?code=... URL
const auto rxMatch = rx.match(peek);
if (!rxMatch.hasMatch()) {
httpReplyAndClose(socket, "404 Not Found", "<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html>");
return;
}
QString code = rxMatch.captured(1); // The 'code' is the first capture of the regexp
QUrl requestToken = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/apps/oauth2/api/v1/token"));
QNetworkRequest req;
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QString basicAuth = QString("%1:%2").arg(
Theme::instance()->oauthClientId(), Theme::instance()->oauthClientSecret());
req.setRawHeader("Authorization", "Basic " + basicAuth.toUtf8().toBase64());
// We just added the Authorization header, don't let HttpCredentialsAccessManager tamper with it
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
auto requestBody = new QBuffer;
QUrlQuery arguments(QString(
"grant_type=authorization_code&code=%1&redirect_uri=http://localhost:%2")
.arg(code, QString::number(_server.serverPort())));
requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1());
auto job = _account->sendRequest("POST", requestToken, req, requestBody);
job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec()));
QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this, socket](QNetworkReply *reply) {
auto jsonData = reply->readAll();
QJsonParseError jsonParseError{};
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
QString accessToken = json["access_token"].toString();
QString refreshToken = json["refresh_token"].toString();
QString user = json["user_id"].toString();
QUrl messageUrl = json["message_url"].toString();
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|| jsonData.isEmpty() || json.isEmpty() || refreshToken.isEmpty() || accessToken.isEmpty()
|| json["token_type"].toString() != QLatin1String("Bearer")) {
QString errorReason;
QString errorFromJson = json["error"].toString();
if (!errorFromJson.isEmpty()) {
errorReason = tr("Error returned from the server: <em>%1</em>")
.arg(errorFromJson.toHtmlEscaped());
} else if (reply->error() != QNetworkReply::NoError) {
errorReason = tr("There was an error accessing the \"token\" endpoint: <br><em>%1</em>")
.arg(reply->errorString().toHtmlEscaped());
} else if (jsonData.isEmpty()) {
// Can happen if a funky load balancer strips away POST data, e.g. BigIP APM my.policy
errorReason = tr("Empty JSON from OAuth2 redirect");
// We explicitly have this as error case since the json qcWarning output below is misleading,
// it will show a fake json will null values that actually never was received like this as
// soon as you access json["whatever"] the debug output json will claim to have "whatever":null
} else if (jsonParseError.error != QJsonParseError::NoError) {
errorReason = tr("Could not parse the JSON returned from the server: <br><em>%1</em>")
.arg(jsonParseError.errorString());
} else {
errorReason = tr("The reply from the server did not contain all expected fields");
}
qCWarning(lcOauth) << "Error when getting the accessToken" << json << errorReason;
httpReplyAndClose(socket, "500 Internal Server Error",
tr("<h1>Login Error</h1><p>%1</p>").arg(errorReason).toUtf8().constData());
emit result(Error);
return;
}
if (!_expectedUser.isNull() && user != _expectedUser) {
// Connected with the wrong user
QString message = tr("<h1>Wrong account</h1>"
"<p>You logged in with the account <em>%1</em>, but must log in with the account <em>%2</em>.<br>"
"Please log out of %3 in another tab, then <a href='%4'>click here</a> "
"and log in with %2.</p>")
.arg(user, _expectedUser, Theme::instance()->appNameGUI(),
authorisationLink().toString(QUrl::FullyEncoded));
httpReplyAndClose(socket, "200 OK", message.toUtf8().constData());
// We are still listening on the socket so we will get the new connection
return;
}
const char *loginSuccessfullHtml = "<h1>Login Successful</h1><p>You can close this window.</p>";
if (messageUrl.isValid()) {
httpReplyAndClose(socket, "303 See Other", loginSuccessfullHtml,
QByteArray("Location: " + messageUrl.toEncoded()).constData());
} else {
httpReplyAndClose(socket, "200 OK", loginSuccessfullHtml);
}
emit result(LoggedIn, user, accessToken, refreshToken);
});
});
}
});
}
QUrl OAuth::authorisationLink() const
{
Q_ASSERT(_server.isListening());
QUrlQuery query;
query.setQueryItems({ { QLatin1String("response_type"), QLatin1String("code") },
{ QLatin1String("client_id"), Theme::instance()->oauthClientId() },
{ QLatin1String("redirect_uri"), QLatin1String("http://localhost:") + QString::number(_server.serverPort()) } });
if (!_expectedUser.isNull())
query.addQueryItem("user", _expectedUser);
QUrl url = Utility::concatUrlPath(_account->url(), QLatin1String("/index.php/apps/oauth2/authorize"), query);
return url;
}
bool OAuth::openBrowser()
{
if (!Utility::openBrowser(authorisationLink())) {
// We cannot open the browser, then we claim we don't support OAuth.
emit result(NotSupported, QString());
return false;
}
return true;
}
} // namespace OCC

View File

@ -1,76 +0,0 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QPointer>
#include <QTcpServer>
#include <QUrl>
#include "accountfwd.h"
namespace OCC {
/**
* Job that do the authorization grant and fetch the access token
*
* Normal workflow:
*
* --> start()
* |
* +----> openBrowser() open the browser to the login page, redirects to http://localhost:xxx
* |
* +----> _server starts listening on a TCP port waiting for an HTTP request with a 'code'
* |
* v
* request the access_token and the refresh_token via 'apps/oauth2/api/v1/token'
* |
* v
* emit result(...)
*
*/
class OAuth : public QObject
{
Q_OBJECT
public:
OAuth(Account *account, QObject *parent)
: QObject(parent)
, _account(account)
{
}
~OAuth() override;
enum Result { NotSupported,
LoggedIn,
Error };
Q_ENUM(Result);
void start();
bool openBrowser();
[[nodiscard]] QUrl authorisationLink() const;
signals:
/**
* The state has changed.
* when logged in, token has the value of the token.
*/
void result(OAuth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString());
private:
Account *_account;
QTcpServer _server;
public:
QString _expectedUser;
};
} // namespace OCC

View File

@ -146,7 +146,7 @@ void WebFlowCredentials::askFromUser() {
// Do a DetermineAuthTypeJob to make sure that the server is still using Flow2
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
connect(job, &DetermineAuthTypeJob::authType, [this](DetermineAuthTypeJob::AuthType type) {
// LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic
// LoginFlowV2 > WebViewFlow > Shib > Basic
#ifdef WITH_WEBENGINE
bool useFlow2 = (type != DetermineAuthTypeJob::WebViewFlow);
#else // WITH_WEBENGINE

View File

@ -31,7 +31,6 @@ bool Utility::openBrowser(const QUrl &url, QWidget *errorWidgetParent)
const QStringList allowedUrlSchemes = {
"http",
"https",
"oauthtest"
};
if (!allowedUrlSchemes.contains(url.scheme())) {

View File

@ -430,7 +430,7 @@ void OwncloudSetupWizard::slotAuthError()
// bring wizard to top
_ocWizard->bringToTop();
if (_ocWizard->currentId() == WizardCommon::Page_OAuthCreds || _ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) {
if (_ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) {
_ocWizard->back();
}
_ocWizard->displayError(errorMsg, _ocWizard->currentId() == WizardCommon::Page_ServerSetup && checkDowngradeAdvised(reply));

View File

@ -1,140 +0,0 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include <QVariant>
#include <QMenu>
#include <QClipboard>
#include "wizard/owncloudoauthcredspage.h"
#include "theme.h"
#include "account.h"
#include "cookiejar.h"
#include "wizard/owncloudwizardcommon.h"
#include "wizard/owncloudwizard.h"
#include "creds/httpcredentialsgui.h"
#include "creds/credentialsfactory.h"
namespace OCC {
OwncloudOAuthCredsPage::OwncloudOAuthCredsPage()
: AbstractCredentialsWizardPage()
{
_ui.setupUi(this);
Theme *theme = Theme::instance();
_ui.topLabel->hide();
_ui.bottomLabel->hide();
QVariant variant = theme->customMedia(Theme::oCSetupTop);
WizardCommon::setupCustomMedia(variant, _ui.topLabel);
variant = theme->customMedia(Theme::oCSetupBottom);
WizardCommon::setupCustomMedia(variant, _ui.bottomLabel);
WizardCommon::initErrorLabel(_ui.errorLabel);
setTitle(WizardCommon::titleTemplate().arg(tr("Connect to %1").arg(Theme::instance()->appNameGUI())));
setSubTitle(WizardCommon::subTitleTemplate().arg(tr("Login in your browser")));
connect(_ui.openLinkButton, &QCommandLinkButton::clicked, this, &OwncloudOAuthCredsPage::slotOpenBrowser);
connect(_ui.copyLinkButton, &QCommandLinkButton::clicked, this, &OwncloudOAuthCredsPage::slotCopyLinkToClipboard);
}
void OwncloudOAuthCredsPage::initializePage()
{
auto *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
Q_ASSERT(ocWizard);
ocWizard->account()->setCredentials(CredentialsFactory::create("http"));
_asyncAuth.reset(new OAuth(ocWizard->account().data(), this));
connect(_asyncAuth.data(), &OAuth::result, this, &OwncloudOAuthCredsPage::asyncAuthResult, Qt::QueuedConnection);
_asyncAuth->start();
// Don't hide the wizard (avoid user confusion)!
//wizard()->hide();
}
void OCC::OwncloudOAuthCredsPage::cleanupPage()
{
// The next or back button was activated, show the wizard again
wizard()->show();
_asyncAuth.reset();
}
void OwncloudOAuthCredsPage::asyncAuthResult(OAuth::Result r, const QString &user,
const QString &token, const QString &refreshToken)
{
switch (r) {
case OAuth::NotSupported: {
/* OAuth not supported (can't open browser), fallback to HTTP credentials */
auto *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
ocWizard->back();
ocWizard->setAuthType(DetermineAuthTypeJob::Basic);
break;
}
case OAuth::Error:
/* Error while getting the access token. (Timeout, or the server did not accept our client credentials */
_ui.errorLabel->show();
wizard()->show();
break;
case OAuth::LoggedIn: {
_token = token;
_user = user;
_refreshToken = refreshToken;
auto *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
Q_ASSERT(ocWizard);
emit connectToOCUrl(ocWizard->account()->url().toString());
break;
}
}
}
int OwncloudOAuthCredsPage::nextId() const
{
return WizardCommon::Page_AdvancedSetup;
}
void OwncloudOAuthCredsPage::setConnected()
{
wizard()->show();
}
AbstractCredentials *OwncloudOAuthCredsPage::getCredentials() const
{
auto *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
Q_ASSERT(ocWizard);
return new HttpCredentialsGui(_user, _token, _refreshToken,
ocWizard->_clientCertBundle, ocWizard->_clientCertPassword);
}
bool OwncloudOAuthCredsPage::isComplete() const
{
return false; /* We can never go forward manually */
}
void OwncloudOAuthCredsPage::slotOpenBrowser()
{
if (_ui.errorLabel)
_ui.errorLabel->hide();
qobject_cast<OwncloudWizard *>(wizard())->account()->clearCookieJar(); // #6574
if (_asyncAuth)
_asyncAuth->openBrowser();
}
void OwncloudOAuthCredsPage::slotCopyLinkToClipboard()
{
if (_asyncAuth)
QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded));
}
} // namespace OCC

View File

@ -1,66 +0,0 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QList>
#include <QMap>
#include <QNetworkCookie>
#include <QUrl>
#include <QPointer>
#include "wizard/abstractcredswizardpage.h"
#include "accountfwd.h"
#include "creds/oauth.h"
#include "ui_owncloudoauthcredspage.h"
namespace OCC {
class OwncloudOAuthCredsPage : public AbstractCredentialsWizardPage
{
Q_OBJECT
public:
OwncloudOAuthCredsPage();
[[nodiscard]] AbstractCredentials *getCredentials() const override;
void initializePage() override;
void cleanupPage() override;
[[nodiscard]] int nextId() const override;
void setConnected();
[[nodiscard]] bool isComplete() const override;
public Q_SLOTS:
void asyncAuthResult(OAuth::Result, const QString &user, const QString &token,
const QString &reniewToken);
signals:
void connectToOCUrl(const QString &);
public:
QString _user;
QString _token;
QString _refreshToken;
QScopedPointer<OAuth> _asyncAuth;
Ui_OwncloudOAuthCredsPage _ui{};
protected slots:
void slotOpenBrowser();
void slotCopyLinkToClipboard();
};
} // namespace OCC

View File

@ -1,100 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OwncloudOAuthCredsPage</class>
<widget class="QWidget" name="OwncloudOAuthCredsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>424</width>
<height>373</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="topLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Please switch to your browser to proceed.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="errorLabel">
<property name="text">
<string>An error occurred while connecting. Please try again.</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item>
<widget class="QCommandLinkButton" name="openLinkButton">
<property name="text">
<string>Re-open Browser</string>
</property>
</widget>
</item>
<item>
<widget class="QCommandLinkButton" name="copyLinkButton">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Copy link</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>127</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="bottomLabel">
<property name="text">
<string notr="true">TextLabel</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -209,8 +209,6 @@ int OwncloudSetupPage::nextId() const
switch (_authType) {
case DetermineAuthTypeJob::Basic:
return WizardCommon::Page_HttpCreds;
case DetermineAuthTypeJob::OAuth:
return WizardCommon::Page_OAuthCreds;
case DetermineAuthTypeJob::LoginFlowV2:
return WizardCommon::Page_Flow2AuthCreds;
#ifdef WITH_WEBENGINE

View File

@ -23,7 +23,6 @@
#include "wizard/welcomepage.h"
#include "wizard/owncloudsetuppage.h"
#include "wizard/owncloudhttpcredspage.h"
#include "wizard/owncloudoauthcredspage.h"
#include "wizard/owncloudadvancedsetuppage.h"
#include "wizard/webviewpage.h"
#include "wizard/flow2authcredspage.h"
@ -49,7 +48,6 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
, _welcomePage(new WelcomePage(this))
, _setupPage(new OwncloudSetupPage(this))
, _httpCredsPage(new OwncloudHttpCredsPage(this))
, _browserCredsPage(new OwncloudOAuthCredsPage)
, _flow2CredsPage(new Flow2AuthCredsPage)
, _advancedSetupPage(new OwncloudAdvancedSetupPage(this))
#ifdef WITH_WEBENGINE
@ -64,7 +62,6 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
setPage(WizardCommon::Page_Welcome, _welcomePage);
setPage(WizardCommon::Page_ServerSetup, _setupPage);
setPage(WizardCommon::Page_HttpCreds, _httpCredsPage);
setPage(WizardCommon::Page_OAuthCreds, _browserCredsPage);
setPage(WizardCommon::Page_Flow2AuthCreds, _flow2CredsPage);
setPage(WizardCommon::Page_AdvancedSetup, _advancedSetupPage);
#ifdef WITH_WEBENGINE
@ -79,7 +76,6 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
connect(this, &QWizard::currentIdChanged, this, &OwncloudWizard::slotCurrentPageChanged);
connect(_setupPage, &OwncloudSetupPage::determineAuthType, this, &OwncloudWizard::determineAuthType);
connect(_httpCredsPage, &OwncloudHttpCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
connect(_browserCredsPage, &OwncloudOAuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
connect(_flow2CredsPage, &Flow2AuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
#ifdef WITH_WEBENGINE
connect(_webViewPage, &WebViewPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
@ -233,10 +229,6 @@ void OwncloudWizard::successfulStep()
_httpCredsPage->setConnected();
break;
case WizardCommon::Page_OAuthCreds:
_browserCredsPage->setConnected();
break;
case WizardCommon::Page_Flow2AuthCreds:
_flow2CredsPage->setConnected();
break;
@ -280,9 +272,7 @@ void OwncloudWizard::setAuthType(DetermineAuthTypeJob::AuthType type)
{
_setupPage->setAuthType(type);
if (type == DetermineAuthTypeJob::OAuth) {
_credentialsPage = _browserCredsPage;
} else if (type == DetermineAuthTypeJob::LoginFlowV2) {
if (type == DetermineAuthTypeJob::LoginFlowV2) {
_credentialsPage = _flow2CredsPage;
#ifdef WITH_WEBENGINE
} else if (type == DetermineAuthTypeJob::WebViewFlow) {
@ -329,8 +319,8 @@ void OwncloudWizard::slotCurrentPageChanged(int id)
emit clearPendingRequests();
}
if (id == WizardCommon::Page_AdvancedSetup && (_credentialsPage == _browserCredsPage || _credentialsPage == _flow2CredsPage)) {
// For OAuth, disable the back button in the Page_AdvancedSetup because we don't want
if (id == WizardCommon::Page_AdvancedSetup && _credentialsPage == _flow2CredsPage) {
// Disable the back button in the Page_AdvancedSetup because we don't want
// to re-open the browser.
button(QWizard::BackButton)->setEnabled(false);
}

View File

@ -32,7 +32,6 @@ Q_DECLARE_LOGGING_CATEGORY(lcWizard)
class WelcomePage;
class OwncloudSetupPage;
class OwncloudHttpCredsPage;
class OwncloudOAuthCredsPage;
class OwncloudAdvancedSetupPage;
class OwncloudWizardResultPage;
class AbstractCredentials;
@ -122,7 +121,6 @@ private:
WelcomePage *_welcomePage;
OwncloudSetupPage *_setupPage;
OwncloudHttpCredsPage *_httpCredsPage;
OwncloudOAuthCredsPage *_browserCredsPage;
Flow2AuthCredsPage *_flow2CredsPage;
OwncloudAdvancedSetupPage *_advancedSetupPage;
OwncloudWizardResultPage *_resultPage = nullptr;

View File

@ -44,7 +44,6 @@ namespace WizardCommon {
Page_Welcome,
Page_ServerSetup,
Page_HttpCreds,
Page_OAuthCreds,
Page_Flow2AuthCreds,
#ifdef WITH_WEBENGINE
Page_WebView,

View File

@ -39,7 +39,6 @@ Q_LOGGING_CATEGORY(lcHttpCredentials, "nextcloud.sync.credentials.http", QtInfoM
namespace {
const char userC[] = "user";
const char isOAuthC[] = "oauth";
const char clientCertBundleC[] = "clientCertPkcs12";
const char clientCertPasswordC[] = "_clientCertPassword";
const char clientCertificatePEMC[] = "_clientCertificatePEM";
@ -63,14 +62,10 @@ protected:
QNetworkRequest req(request);
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
if (_cred && !_cred->password().isEmpty()) {
if (_cred->isUsingOAuth()) {
req.setRawHeader("Authorization", "Bearer " + _cred->password().toUtf8());
} else {
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
req.setRawHeader("Authorization", "Basic " + credHash);
}
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
req.setRawHeader("Authorization", "Basic " + credHash);
} else if (!request.url().password().isEmpty()) {
// Typically the requests to get or refresh the OAuth access token. The client
// Typically the requests to get or refresh the access token. The client
// credentials are put in the URL from the code making the request.
QByteArray credHash = request.url().userInfo().toUtf8().toBase64();
req.setRawHeader("Authorization", "Basic " + credHash);
@ -85,15 +80,7 @@ protected:
req.setSslConfiguration(sslConfiguration);
}
auto *reply = AccessManager::createRequest(op, req, outgoingData);
if (_cred->_isRenewingOAuthToken) {
// We know this is going to fail, but we have no way to queue it there, so we will
// simply restart the job after the failure.
reply->setProperty(needRetryC, true);
}
return reply;
return AccessManager::createRequest(op, req, outgoingData);
}
private:
@ -178,13 +165,6 @@ void HttpCredentials::fetchFromKeychain()
// User must be fetched from config file
fetchUser();
if (!_ready && !_refreshToken.isEmpty()) {
// This happens if the credentials are still loaded from the keychain, but we are called
// here because the auth is invalid, so this means we simply need to refresh the credentials
refreshAccessToken();
return;
}
if (_ready) {
Q_EMIT fetched();
} else {
@ -370,20 +350,13 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incoming)
return;
}
bool isOauth = _account->credentialSetting(QLatin1String(isOAuthC)).toBool();
if (isOauth) {
_refreshToken = job->textData();
} else {
_password = job->textData();
}
_password = job->textData();
if (_user.isEmpty()) {
qCWarning(lcHttpCredentials) << "Strange: User is empty!";
}
if (!_refreshToken.isEmpty() && error == QKeychain::NoError) {
refreshAccessToken();
} else if (!_password.isEmpty() && error == QKeychain::NoError) {
if (!_password.isEmpty() && error == QKeychain::NoError) {
// All cool, the keychain did not come back with error.
// Still, the password can be empty which indicates a problem and
// the password dialog has to be opened.
@ -408,58 +381,6 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incoming)
}
}
bool HttpCredentials::refreshAccessToken()
{
if (_refreshToken.isEmpty())
return false;
QUrl requestToken = Utility::concatUrlPath(_account->url(), QLatin1String("/index.php/apps/oauth2/api/v1/token"));
QNetworkRequest req;
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QString basicAuth = QString("%1:%2").arg(
Theme::instance()->oauthClientId(), Theme::instance()->oauthClientSecret());
req.setRawHeader("Authorization", "Basic " + basicAuth.toUtf8().toBase64());
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
auto requestBody = new QBuffer;
QUrlQuery arguments(QString("grant_type=refresh_token&refresh_token=%1").arg(_refreshToken));
requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1());
auto job = _account->sendRequest("POST", requestToken, req, requestBody);
job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec()));
QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
auto jsonData = reply->readAll();
QJsonParseError jsonParseError{};
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
QString accessToken = json["access_token"].toString();
if (jsonParseError.error != QJsonParseError::NoError || json.isEmpty()) {
// Invalid or empty JSON: Network error maybe?
qCWarning(lcHttpCredentials) << "Error while refreshing the token" << reply->errorString() << jsonData << jsonParseError.errorString();
} else if (accessToken.isEmpty()) {
// If the json was valid, but the reply did not contain an access token, the token
// is considered expired. (Usually the HTTP reply code is 400)
qCDebug(lcHttpCredentials) << "Expired refresh token. Logging out";
_refreshToken.clear();
} else {
_ready = true;
_password = accessToken;
_refreshToken = json["refresh_token"].toString();
persist();
}
_isRenewingOAuthToken = false;
for (const auto &job : qAsConst(_retryQueue)) {
if (job)
job->retry();
}
_retryQueue.clear();
emit fetched();
});
_isRenewingOAuthToken = true;
return true;
}
void HttpCredentials::invalidateToken()
{
if (!_password.isEmpty()) {
@ -480,12 +401,6 @@ void HttpCredentials::invalidateToken()
// clear the session cookie.
_account->clearCookieJar();
if (!_refreshToken.isEmpty()) {
// Only invalidate the access_token (_password) but keep the _refreshToken in the keychain
// (when coming from forgetSensitiveData, the _refreshToken is cleared)
return;
}
auto *job = new QKeychain::DeletePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job);
job->setInsecureFallback(true);
@ -502,9 +417,6 @@ void HttpCredentials::invalidateToken()
void HttpCredentials::forgetSensitiveData()
{
// need to be done before invalidateToken, so it actually deletes the refresh_token from the keychain
_refreshToken.clear();
invalidateToken();
_previousPassword.clear();
}
@ -517,7 +429,6 @@ void HttpCredentials::persist()
}
_account->setCredentialSetting(QLatin1String(userC), _user);
_account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth());
if (!_clientCertBundle.isEmpty()) {
// Note that the _clientCertBundle will often be cleared after usage,
// it's just written if it gets passed into the constructor.
@ -605,7 +516,7 @@ void HttpCredentials::slotWritePasswordToKeychain()
job->setInsecureFallback(false);
connect(job, &QKeychain::Job::finished, this, &HttpCredentials::slotWriteJobDone);
job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
job->setTextData(isUsingOAuth() ? _refreshToken : _password);
job->setTextData(_password);
job->start();
}
@ -621,19 +532,12 @@ void HttpCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *a
{
if (!_ready)
return;
Q_UNUSED(authenticator)
// Because of issue #4326, we need to set the login and password manually at every requests
// Thus, if we reach this signal, those credentials were invalid and we terminate.
qCWarning(lcHttpCredentials) << "Stop request: Authentication failed for " << reply->url().toString();
reply->setProperty(authenticationFailedC, true);
if (_isRenewingOAuthToken) {
reply->setProperty(needRetryC, true);
} else if (isUsingOAuth() && !reply->property(needRetryC).toBool()) {
reply->setProperty(needRetryC, true);
qCInfo(lcHttpCredentials) << "Refreshing token";
refreshAccessToken();
}
}
bool HttpCredentials::retryIfNeeded(AbstractNetworkJob *job)
@ -641,11 +545,8 @@ bool HttpCredentials::retryIfNeeded(AbstractNetworkJob *job)
auto *reply = job->reply();
if (!reply || !reply->property(needRetryC).toBool())
return false;
if (_isRenewingOAuthToken) {
_retryQueue.append(job);
} else {
job->retry();
}
job->retry();
return true;
}

View File

@ -40,7 +40,7 @@ namespace OCC {
HttpCredentials is then split in HttpCredentials and HttpCredentialsGui.
This class handle both HTTP Basic Auth and OAuth. But anything that needs GUI to ask the user
This class handles HTTP Basic Auth. But anything that needs GUI to ask the user
is in HttpCredentialsGui.
The authentication mechanism looks like this.
@ -52,13 +52,13 @@ namespace OCC {
v }
slotReadClientCertPEMJobDone } There are first 3 QtKeychain jobs to fetch
| } the TLS client keys, if any, and the password
v } (or refresh token
v }
slotReadClientKeyPEMJobDone }
| }
v
slotReadJobDone
| |
| +-------> emit fetched() if OAuth is not used
| +-------> emit fetched()
|
v
refreshAccessToken()
@ -97,17 +97,9 @@ public:
QString fetchUser();
virtual bool sslIsTrusted() { return false; }
/* If we still have a valid refresh token, try to refresh it asynchronously and emit fetched()
* otherwise return false
*/
bool refreshAccessToken();
// To fetch the user name as early as possible
void setAccount(Account *account) override;
// Whether we are using OAuth
[[nodiscard]] bool isUsingOAuth() const { return !_refreshToken.isNull(); }
bool retryIfNeeded(AbstractNetworkJob *) override;
private Q_SLOTS:
@ -157,21 +149,18 @@ protected:
bool unpackClientCertBundle();
QString _user;
QString _password; // user's password, or access_token for OAuth
QString _refreshToken; // OAuth _refreshToken, set if OAuth is used.
QString _password; // user's password
QString _previousPassword;
QString _fetchErrorString;
bool _ready = false;
bool _isRenewingOAuthToken = false;
QByteArray _clientCertBundle;
QByteArray _clientCertPassword;
QSslKey _clientSslKey;
QSslCertificate _clientSslCertificate;
bool _keychainMigration = false;
bool _retryOnKeyChainError = true; // true if we haven't done yet any reading from keychain
QVector<QPointer<AbstractNetworkJob>> _retryQueue; // Jobs we need to retry once the auth token is fetched
};

View File

@ -1056,16 +1056,13 @@ void DetermineAuthTypeJob::start()
});
connect(propfind, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
auto authChallenge = reply->rawHeader("WWW-Authenticate").toLower();
if (authChallenge.contains("bearer ")) {
_resultPropfind = OAuth;
if (authChallenge.isEmpty()) {
qCWarning(lcDetermineAuthTypeJob) << "Did not receive WWW-Authenticate reply to auth-test PROPFIND";
} else {
if (authChallenge.isEmpty()) {
qCWarning(lcDetermineAuthTypeJob) << "Did not receive WWW-Authenticate reply to auth-test PROPFIND";
} else {
qCWarning(lcDetermineAuthTypeJob) << "Unknown WWW-Authenticate reply to auth-test PROPFIND:" << authChallenge;
}
_resultPropfind = Basic;
qCWarning(lcDetermineAuthTypeJob) << "Unknown WWW-Authenticate reply to auth-test PROPFIND:" << authChallenge;
}
_resultPropfind = Basic;
_propfindDone = true;
checkAllDone();
});
@ -1111,13 +1108,13 @@ void DetermineAuthTypeJob::checkAllDone()
auto result = _resultPropfind;
#ifdef WITH_WEBENGINE
// WebViewFlow > OAuth > Basic
// WebViewFlow > Basic
if (_account->serverVersionInt() >= Account::makeServerVersion(12, 0, 0)) {
result = WebViewFlow;
}
#endif // WITH_WEBENGINE
// LoginFlowV2 > WebViewFlow > OAuth > Basic
// LoginFlowV2 > WebViewFlow > Basic
if (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)) {
result = LoginFlowV2;
}

View File

@ -523,7 +523,6 @@ public:
WebViewFlow,
#endif // WITH_WEBENGINE
Basic, // also the catch-all fallback for backwards compatibility reasons
OAuth,
LoginFlowV2
};
Q_ENUM(AuthType)

View File

@ -163,7 +163,7 @@ void GETFileJob::slotMetaDataChanged()
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307
|| httpStatus == 308 || httpStatus == 401) {
// Redirects and auth failures (oauth token renew) are handled by AbstractNetworkJob and
// Redirects and auth failures (token renew) are handled by AbstractNetworkJob and
// will end up restarting the job. We do not want to process further data from the initial
// request. newReplyHook() will reestablish signal connections for the follow-up request.
bool ok = disconnect(reply(), &QNetworkReply::finished, this, &GETFileJob::slotReadyRead)

View File

@ -783,16 +783,6 @@ QString Theme::quotaBaseFolder() const
return QLatin1String("/");
}
QString Theme::oauthClientId() const
{
return "xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69";
}
QString Theme::oauthClientSecret() const
{
return "UBntmLjC2yYCeHwsyj73Uwo9TAaecAetRwMw0xYcvNL9yRdLSUi0hUAHfvCHFeFh";
}
QString Theme::versionSwitchOutput() const
{
QString helpText;

View File

@ -466,13 +466,6 @@ public:
*/
[[nodiscard]] QString quotaBaseFolder() const;
/**
* The OAuth client_id, secret pair.
* Note that client that change these value cannot connect to un-branded owncloud servers.
*/
[[nodiscard]] QString oauthClientId() const;
[[nodiscard]] QString oauthClientSecret() const;
/**
* @brief What should be output for the --version command line switch.
*

View File

@ -115,8 +115,6 @@ nextcloud_add_test(Account)
nextcloud_add_test(FolderMan)
nextcloud_add_test(RemoteWipe)
nextcloud_add_test(OAuth)
configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY)
find_package(CMocka)

View File

@ -1,337 +0,0 @@
/*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*
*/
#include <QtTest/QtTest>
#include <QDesktopServices>
#include "gui/creds/oauth.h"
#include "syncenginetestutils.h"
#include "theme.h"
#include "common/asserts.h"
using namespace OCC;
class DesktopServiceHook : public QObject
{
Q_OBJECT
signals:
void hooked(const QUrl &);
public:
DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); }
};
static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud");
class FakePostReply : public QNetworkReply
{
Q_OBJECT
public:
std::unique_ptr<QIODevice> payload;
bool aborted = false;
bool redirectToPolicy = false;
bool redirectToToken = false;
FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
std::unique_ptr<QIODevice> payload_, QObject *parent)
: QNetworkReply{parent}, payload{std::move(payload_)}
{
setRequest(request);
setUrl(request.url());
setOperation(op);
open(QIODevice::ReadOnly);
payload->open(QIODevice::ReadOnly);
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
}
Q_INVOKABLE virtual void respond() {
if (aborted) {
setError(OperationCanceledError, "Operation Canceled");
emit metaDataChanged();
emit finished();
return;
} else if (redirectToPolicy) {
setHeader(QNetworkRequest::LocationHeader, "/my.policy");
setAttribute(QNetworkRequest::RedirectionTargetAttribute, "/my.policy");
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 302); // 302 might or might not lose POST data in rfc
setHeader(QNetworkRequest::ContentLengthHeader, 0);
emit metaDataChanged();
emit finished();
return;
} else if (redirectToToken) {
// Redirect to self
QVariant destination = QVariant(sOAuthTestServer.toString()+QLatin1String("/index.php/apps/oauth2/api/v1/token"));
setHeader(QNetworkRequest::LocationHeader, destination);
setAttribute(QNetworkRequest::RedirectionTargetAttribute, destination);
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 307); // 307 explicitly in rfc says to not lose POST data
setHeader(QNetworkRequest::ContentLengthHeader, 0);
emit metaDataChanged();
emit finished();
return;
}
setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
emit metaDataChanged();
if (bytesAvailable())
emit readyRead();
emit finished();
}
void abort() override {
aborted = true;
}
[[nodiscard]] qint64 bytesAvailable() const override {
if (aborted)
return 0;
return payload->bytesAvailable();
}
qint64 readData(char *data, qint64 maxlen) override {
return payload->read(data, maxlen);
}
};
// Reply with a small delay
class SlowFakePostReply : public FakePostReply {
Q_OBJECT
public:
using FakePostReply::FakePostReply;
void respond() override {
// override of FakePostReply::respond, will call the real one with a delay.
QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
}
};
class OAuthTestCase : public QObject
{
Q_OBJECT
DesktopServiceHook desktopServiceHook;
public:
enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
Q_ENUM(State);
bool replyToBrowserOk = false;
bool gotAuthOk = false;
[[nodiscard]] virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
FakeQNAM *fakeQnam = nullptr;
QNetworkAccessManager realQNAM;
QPointer<QNetworkReply> browserReply = nullptr;
QString code = generateEtag();
OCC::AccountPtr account;
QScopedPointer<OAuth> oauth;
virtual void test() {
fakeQnam = new FakeQNAM({});
account = OCC::Account::create();
account->setUrl(sOAuthTestServer);
account->setCredentials(new FakeCredentials{fakeQnam});
fakeQnam->setParent(this);
fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
ASSERT(device);
ASSERT(device->bytesAvailable()>0); // OAuth2 always sends around POST data.
return this->tokenReply(op, req);
});
QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
this, &OAuthTestCase::openBrowserHook);
oauth.reset(new OAuth(account.data(), nullptr));
QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
oauth->start();
QTRY_VERIFY(done());
}
virtual void openBrowserHook(const QUrl &url) {
QCOMPARE(state, StartState);
state = BrowserOpened;
QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
QUrlQuery query(url);
QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
redirectUri.setQuery("code=" + code);
createBrowserReply(QNetworkRequest(redirectUri));
}
virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
browserReply = realQNAM.get(request);
QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
return browserReply;
}
virtual void browserReplyFinished() {
QCOMPARE(sender(), browserReply.data());
QCOMPARE(state, TokenAsked);
browserReply->deleteLater();
QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
replyToBrowserOk = true;
};
virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
{
ASSERT(state == BrowserOpened);
state = TokenAsked;
ASSERT(op == QNetworkAccessManager::PostOperation);
ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
std::unique_ptr<QBuffer> payload(new QBuffer());
payload->setData(tokenReplyPayload());
return new FakePostReply(op, req, std::move(payload), fakeQnam);
}
[[nodiscard]] virtual QByteArray tokenReplyPayload() const {
QJsonDocument jsondata(QJsonObject{
{ "access_token", "123" },
{ "refresh_token" , "456" },
{ "message_url", "owncloud://success"},
{ "user_id", "789" },
{ "token_type", "Bearer" }
});
return jsondata.toJson();
}
virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
QCOMPARE(state, TokenAsked);
QCOMPARE(result, OAuth::LoggedIn);
QCOMPARE(user, QString("789"));
QCOMPARE(token, QString("123"));
QCOMPARE(refreshToken, QString("456"));
gotAuthOk = true;
}
};
class TestOAuth: public QObject
{
Q_OBJECT
private slots:
void testBasic()
{
OAuthTestCase test;
test.test();
}
// Test for https://github.com/owncloud/client/pull/6057
void testCloseBrowserDontCrash()
{
struct Test : OAuthTestCase {
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
{
ASSERT(browserReply);
// simulate the fact that the browser is closing the connection
browserReply->abort();
QCoreApplication::processEvents();
ASSERT(state == BrowserOpened);
state = TokenAsked;
std::unique_ptr<QBuffer> payload(new QBuffer);
payload->setData(tokenReplyPayload());
return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
}
void browserReplyFinished() override
{
QCOMPARE(sender(), browserReply.data());
QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
replyToBrowserOk = true;
}
} test;
test.test();
}
void testRandomConnections()
{
// Test that we can send random garbage to the litening socket and it does not prevent the connection
struct Test : OAuthTestCase {
QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
QTimer::singleShot(0, this, [this, request] {
auto port = request.url().port();
state = CustomState;
QVector<QByteArray> payloads = {
"GET FOFOFO HTTP 1/1\n\n",
"GET /?code=invalie HTTP 1/1\n\n",
"GET /?code=xxxxx&bar=fff",
QByteArray("\0\0\0", 3),
QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
QByteArray("\n\n\n\n"),
};
foreach (const auto &x, payloads) {
auto socket = new QTcpSocket(this);
socket->connectToHost("localhost", port);
QVERIFY(socket->waitForConnected());
socket->write(x);
}
// Do the actual request a bit later
QTimer::singleShot(100, this, [this, request] {
QCOMPARE(state, CustomState);
state = BrowserOpened;
this->OAuthTestCase::createBrowserReply(request);
});
});
return nullptr;
}
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
if (state == CustomState)
return new FakeErrorReply{op, req, this, 500};
return OAuthTestCase::tokenReply(op, req);
}
void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
const QString &refreshToken) override {
if (state != CustomState)
return OAuthTestCase::oauthResult(result, user, token, refreshToken);
QCOMPARE(result, OAuth::Error);
}
} test;
test.test();
}
void testTokenUrlHasRedirect()
{
struct Test : OAuthTestCase {
int redirectsDone = 0;
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & request) override
{
ASSERT(browserReply);
// Kind of reproduces what we had in https://github.com/owncloud/enterprise/issues/2951 (not 1:1)
if (redirectsDone == 0) {
std::unique_ptr<QBuffer> payload(new QBuffer());
payload->setData("");
auto *reply = new SlowFakePostReply(op, request, std::move(payload), this);
reply->redirectToPolicy = true;
redirectsDone++;
return reply;
} else if (redirectsDone == 1) {
std::unique_ptr<QBuffer> payload(new QBuffer());
payload->setData("");
auto *reply = new SlowFakePostReply(op, request, std::move(payload), this);
reply->redirectToToken = true;
redirectsDone++;
return reply;
} else {
// ^^ This is with a custom reply and not actually HTTP, so we're testing the HTTP redirect code
// we have in AbstractNetworkJob::slotFinished()
redirectsDone++;
return OAuthTestCase::tokenReply(op, request);
}
}
} test;
test.test();
}
};
QTEST_GUILESS_MAIN(TestOAuth)
#include "testoauth.moc"