mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-28 19:58:56 +03:00
Remove OAuth authentication method as it is now unused
Signed-off-by: Claudio Cambra <claudio.cambra@nextcloud.com>
This commit is contained in:
parent
9e74f5c508
commit
6cf04d660b
26 changed files with 30 additions and 1167 deletions
|
@ -48,7 +48,6 @@ set(client_UI_SRCS
|
||||||
wizard/owncloudadvancedsetuppage.ui
|
wizard/owncloudadvancedsetuppage.ui
|
||||||
wizard/owncloudconnectionmethoddialog.ui
|
wizard/owncloudconnectionmethoddialog.ui
|
||||||
wizard/owncloudhttpcredspage.ui
|
wizard/owncloudhttpcredspage.ui
|
||||||
wizard/owncloudoauthcredspage.ui
|
|
||||||
wizard/owncloudsetupnocredspage.ui
|
wizard/owncloudsetupnocredspage.ui
|
||||||
wizard/webview.ui
|
wizard/webview.ui
|
||||||
wizard/welcomepage.ui
|
wizard/welcomepage.ui
|
||||||
|
@ -224,8 +223,6 @@ set(client_SRCS
|
||||||
creds/credentialsfactory.cpp
|
creds/credentialsfactory.cpp
|
||||||
creds/httpcredentialsgui.h
|
creds/httpcredentialsgui.h
|
||||||
creds/httpcredentialsgui.cpp
|
creds/httpcredentialsgui.cpp
|
||||||
creds/oauth.h
|
|
||||||
creds/oauth.cpp
|
|
||||||
creds/flow2auth.h
|
creds/flow2auth.h
|
||||||
creds/flow2auth.cpp
|
creds/flow2auth.cpp
|
||||||
creds/webflowcredentials.h
|
creds/webflowcredentials.h
|
||||||
|
@ -242,8 +239,6 @@ set(client_SRCS
|
||||||
wizard/owncloudconnectionmethoddialog.cpp
|
wizard/owncloudconnectionmethoddialog.cpp
|
||||||
wizard/owncloudhttpcredspage.h
|
wizard/owncloudhttpcredspage.h
|
||||||
wizard/owncloudhttpcredspage.cpp
|
wizard/owncloudhttpcredspage.cpp
|
||||||
wizard/owncloudoauthcredspage.h
|
|
||||||
wizard/owncloudoauthcredspage.cpp
|
|
||||||
wizard/flow2authcredspage.h
|
wizard/flow2authcredspage.h
|
||||||
wizard/flow2authcredspage.cpp
|
wizard/flow2authcredspage.cpp
|
||||||
wizard/flow2authwidget.h
|
wizard/flow2authwidget.h
|
||||||
|
|
|
@ -1271,19 +1271,7 @@ void AccountSettings::slotAccountStateChanged()
|
||||||
showConnectionLabel(tr("Signed out from %1.").arg(serverWithUser));
|
showConnectionLabel(tr("Signed out from %1.").arg(serverWithUser));
|
||||||
break;
|
break;
|
||||||
case AccountState::AskingCredentials: {
|
case AccountState::AskingCredentials: {
|
||||||
QUrl url;
|
showConnectionLabel(tr("Connecting to %1 …").arg(serverWithUser));
|
||||||
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));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AccountState::NetworkError:
|
case AccountState::NetworkError:
|
||||||
|
|
|
@ -456,10 +456,6 @@ void AccountState::handleInvalidCredentials()
|
||||||
if (account()->credentials()->ready()) {
|
if (account()->credentials()->ready()) {
|
||||||
account()->credentials()->invalidateToken();
|
account()->credentials()->invalidateToken();
|
||||||
}
|
}
|
||||||
if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
|
|
||||||
if (creds->refreshAccessToken())
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
account()->credentials()->askFromUser();
|
account()->credentials()->askFromUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,16 +46,7 @@ void HttpCredentialsGui::askFromUserAsync()
|
||||||
// First, we will check what kind of auth we need.
|
// First, we will check what kind of auth we need.
|
||||||
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
||||||
QObject::connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) {
|
QObject::connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) {
|
||||||
if (type == DetermineAuthTypeJob::OAuth) {
|
if (type == DetermineAuthTypeJob::Basic) {
|
||||||
_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) {
|
|
||||||
showDialog();
|
showDialog();
|
||||||
} else {
|
} else {
|
||||||
// Shibboleth?
|
// Shibboleth?
|
||||||
|
@ -66,32 +57,6 @@ void HttpCredentialsGui::askFromUserAsync()
|
||||||
job->start();
|
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()
|
void HttpCredentialsGui::showDialog()
|
||||||
{
|
{
|
||||||
QString msg = tr("Please enter %1 password:<br>"
|
QString msg = tr("Please enter %1 password:<br>"
|
||||||
|
@ -128,7 +93,6 @@ void HttpCredentialsGui::showDialog()
|
||||||
connect(dialog, &QDialog::finished, this, [this, dialog](int result) {
|
connect(dialog, &QDialog::finished, this, [this, dialog](int result) {
|
||||||
if (result == QDialog::Accepted) {
|
if (result == QDialog::Accepted) {
|
||||||
_password = dialog->textValue();
|
_password = dialog->textValue();
|
||||||
_refreshToken.clear();
|
|
||||||
_ready = true;
|
_ready = true;
|
||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "creds/httpcredentials.h"
|
#include "creds/httpcredentials.h"
|
||||||
#include "creds/oauth.h"
|
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
#include <QTcpServer>
|
#include <QTcpServer>
|
||||||
|
|
||||||
|
@ -38,37 +37,13 @@ public:
|
||||||
: HttpCredentials(user, password, clientCertBundle, clientCertPassword)
|
: 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;
|
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);
|
static QString requestAppPasswordText(const Account *account);
|
||||||
private slots:
|
private slots:
|
||||||
void asyncAuthResult(OAuth::Result, const QString &user, const QString &accessToken, const QString &refreshToken);
|
|
||||||
void showDialog();
|
void showDialog();
|
||||||
void askFromUserAsync();
|
void askFromUserAsync();
|
||||||
|
|
||||||
signals:
|
|
||||||
void authorisationLinkChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
QScopedPointer<OAuth, QScopedPointerObjectDeleteLater<OAuth>> _asyncAuth;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace OCC
|
} // namespace OCC
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -146,7 +146,7 @@ void WebFlowCredentials::askFromUser() {
|
||||||
// Do a DetermineAuthTypeJob to make sure that the server is still using Flow2
|
// Do a DetermineAuthTypeJob to make sure that the server is still using Flow2
|
||||||
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
|
||||||
connect(job, &DetermineAuthTypeJob::authType, [this](DetermineAuthTypeJob::AuthType type) {
|
connect(job, &DetermineAuthTypeJob::authType, [this](DetermineAuthTypeJob::AuthType type) {
|
||||||
// LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic
|
// LoginFlowV2 > WebViewFlow > Shib > Basic
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
bool useFlow2 = (type != DetermineAuthTypeJob::WebViewFlow);
|
bool useFlow2 = (type != DetermineAuthTypeJob::WebViewFlow);
|
||||||
#else // WITH_WEBENGINE
|
#else // WITH_WEBENGINE
|
||||||
|
|
|
@ -31,7 +31,6 @@ bool Utility::openBrowser(const QUrl &url, QWidget *errorWidgetParent)
|
||||||
const QStringList allowedUrlSchemes = {
|
const QStringList allowedUrlSchemes = {
|
||||||
"http",
|
"http",
|
||||||
"https",
|
"https",
|
||||||
"oauthtest"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!allowedUrlSchemes.contains(url.scheme())) {
|
if (!allowedUrlSchemes.contains(url.scheme())) {
|
||||||
|
|
|
@ -430,7 +430,7 @@ void OwncloudSetupWizard::slotAuthError()
|
||||||
|
|
||||||
// bring wizard to top
|
// bring wizard to top
|
||||||
_ocWizard->bringToTop();
|
_ocWizard->bringToTop();
|
||||||
if (_ocWizard->currentId() == WizardCommon::Page_OAuthCreds || _ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) {
|
if (_ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) {
|
||||||
_ocWizard->back();
|
_ocWizard->back();
|
||||||
}
|
}
|
||||||
_ocWizard->displayError(errorMsg, _ocWizard->currentId() == WizardCommon::Page_ServerSetup && checkDowngradeAdvised(reply));
|
_ocWizard->displayError(errorMsg, _ocWizard->currentId() == WizardCommon::Page_ServerSetup && checkDowngradeAdvised(reply));
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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>
|
|
|
@ -209,8 +209,6 @@ int OwncloudSetupPage::nextId() const
|
||||||
switch (_authType) {
|
switch (_authType) {
|
||||||
case DetermineAuthTypeJob::Basic:
|
case DetermineAuthTypeJob::Basic:
|
||||||
return WizardCommon::Page_HttpCreds;
|
return WizardCommon::Page_HttpCreds;
|
||||||
case DetermineAuthTypeJob::OAuth:
|
|
||||||
return WizardCommon::Page_OAuthCreds;
|
|
||||||
case DetermineAuthTypeJob::LoginFlowV2:
|
case DetermineAuthTypeJob::LoginFlowV2:
|
||||||
return WizardCommon::Page_Flow2AuthCreds;
|
return WizardCommon::Page_Flow2AuthCreds;
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
#include "wizard/welcomepage.h"
|
#include "wizard/welcomepage.h"
|
||||||
#include "wizard/owncloudsetuppage.h"
|
#include "wizard/owncloudsetuppage.h"
|
||||||
#include "wizard/owncloudhttpcredspage.h"
|
#include "wizard/owncloudhttpcredspage.h"
|
||||||
#include "wizard/owncloudoauthcredspage.h"
|
|
||||||
#include "wizard/owncloudadvancedsetuppage.h"
|
#include "wizard/owncloudadvancedsetuppage.h"
|
||||||
#include "wizard/webviewpage.h"
|
#include "wizard/webviewpage.h"
|
||||||
#include "wizard/flow2authcredspage.h"
|
#include "wizard/flow2authcredspage.h"
|
||||||
|
@ -49,7 +48,6 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
|
||||||
, _welcomePage(new WelcomePage(this))
|
, _welcomePage(new WelcomePage(this))
|
||||||
, _setupPage(new OwncloudSetupPage(this))
|
, _setupPage(new OwncloudSetupPage(this))
|
||||||
, _httpCredsPage(new OwncloudHttpCredsPage(this))
|
, _httpCredsPage(new OwncloudHttpCredsPage(this))
|
||||||
, _browserCredsPage(new OwncloudOAuthCredsPage)
|
|
||||||
, _flow2CredsPage(new Flow2AuthCredsPage)
|
, _flow2CredsPage(new Flow2AuthCredsPage)
|
||||||
, _advancedSetupPage(new OwncloudAdvancedSetupPage(this))
|
, _advancedSetupPage(new OwncloudAdvancedSetupPage(this))
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
|
@ -64,7 +62,6 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
|
||||||
setPage(WizardCommon::Page_Welcome, _welcomePage);
|
setPage(WizardCommon::Page_Welcome, _welcomePage);
|
||||||
setPage(WizardCommon::Page_ServerSetup, _setupPage);
|
setPage(WizardCommon::Page_ServerSetup, _setupPage);
|
||||||
setPage(WizardCommon::Page_HttpCreds, _httpCredsPage);
|
setPage(WizardCommon::Page_HttpCreds, _httpCredsPage);
|
||||||
setPage(WizardCommon::Page_OAuthCreds, _browserCredsPage);
|
|
||||||
setPage(WizardCommon::Page_Flow2AuthCreds, _flow2CredsPage);
|
setPage(WizardCommon::Page_Flow2AuthCreds, _flow2CredsPage);
|
||||||
setPage(WizardCommon::Page_AdvancedSetup, _advancedSetupPage);
|
setPage(WizardCommon::Page_AdvancedSetup, _advancedSetupPage);
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
|
@ -79,7 +76,6 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
|
||||||
connect(this, &QWizard::currentIdChanged, this, &OwncloudWizard::slotCurrentPageChanged);
|
connect(this, &QWizard::currentIdChanged, this, &OwncloudWizard::slotCurrentPageChanged);
|
||||||
connect(_setupPage, &OwncloudSetupPage::determineAuthType, this, &OwncloudWizard::determineAuthType);
|
connect(_setupPage, &OwncloudSetupPage::determineAuthType, this, &OwncloudWizard::determineAuthType);
|
||||||
connect(_httpCredsPage, &OwncloudHttpCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
connect(_httpCredsPage, &OwncloudHttpCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
||||||
connect(_browserCredsPage, &OwncloudOAuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
|
||||||
connect(_flow2CredsPage, &Flow2AuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
connect(_flow2CredsPage, &Flow2AuthCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
connect(_webViewPage, &WebViewPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
connect(_webViewPage, &WebViewPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl);
|
||||||
|
@ -233,10 +229,6 @@ void OwncloudWizard::successfulStep()
|
||||||
_httpCredsPage->setConnected();
|
_httpCredsPage->setConnected();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WizardCommon::Page_OAuthCreds:
|
|
||||||
_browserCredsPage->setConnected();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WizardCommon::Page_Flow2AuthCreds:
|
case WizardCommon::Page_Flow2AuthCreds:
|
||||||
_flow2CredsPage->setConnected();
|
_flow2CredsPage->setConnected();
|
||||||
break;
|
break;
|
||||||
|
@ -280,9 +272,7 @@ void OwncloudWizard::setAuthType(DetermineAuthTypeJob::AuthType type)
|
||||||
{
|
{
|
||||||
_setupPage->setAuthType(type);
|
_setupPage->setAuthType(type);
|
||||||
|
|
||||||
if (type == DetermineAuthTypeJob::OAuth) {
|
if (type == DetermineAuthTypeJob::LoginFlowV2) {
|
||||||
_credentialsPage = _browserCredsPage;
|
|
||||||
} else if (type == DetermineAuthTypeJob::LoginFlowV2) {
|
|
||||||
_credentialsPage = _flow2CredsPage;
|
_credentialsPage = _flow2CredsPage;
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
} else if (type == DetermineAuthTypeJob::WebViewFlow) {
|
} else if (type == DetermineAuthTypeJob::WebViewFlow) {
|
||||||
|
@ -329,8 +319,8 @@ void OwncloudWizard::slotCurrentPageChanged(int id)
|
||||||
emit clearPendingRequests();
|
emit clearPendingRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id == WizardCommon::Page_AdvancedSetup && (_credentialsPage == _browserCredsPage || _credentialsPage == _flow2CredsPage)) {
|
if (id == WizardCommon::Page_AdvancedSetup && _credentialsPage == _flow2CredsPage) {
|
||||||
// For OAuth, disable the back button in the Page_AdvancedSetup because we don't want
|
// Disable the back button in the Page_AdvancedSetup because we don't want
|
||||||
// to re-open the browser.
|
// to re-open the browser.
|
||||||
button(QWizard::BackButton)->setEnabled(false);
|
button(QWizard::BackButton)->setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ Q_DECLARE_LOGGING_CATEGORY(lcWizard)
|
||||||
class WelcomePage;
|
class WelcomePage;
|
||||||
class OwncloudSetupPage;
|
class OwncloudSetupPage;
|
||||||
class OwncloudHttpCredsPage;
|
class OwncloudHttpCredsPage;
|
||||||
class OwncloudOAuthCredsPage;
|
|
||||||
class OwncloudAdvancedSetupPage;
|
class OwncloudAdvancedSetupPage;
|
||||||
class OwncloudWizardResultPage;
|
class OwncloudWizardResultPage;
|
||||||
class AbstractCredentials;
|
class AbstractCredentials;
|
||||||
|
@ -122,7 +121,6 @@ private:
|
||||||
WelcomePage *_welcomePage;
|
WelcomePage *_welcomePage;
|
||||||
OwncloudSetupPage *_setupPage;
|
OwncloudSetupPage *_setupPage;
|
||||||
OwncloudHttpCredsPage *_httpCredsPage;
|
OwncloudHttpCredsPage *_httpCredsPage;
|
||||||
OwncloudOAuthCredsPage *_browserCredsPage;
|
|
||||||
Flow2AuthCredsPage *_flow2CredsPage;
|
Flow2AuthCredsPage *_flow2CredsPage;
|
||||||
OwncloudAdvancedSetupPage *_advancedSetupPage;
|
OwncloudAdvancedSetupPage *_advancedSetupPage;
|
||||||
OwncloudWizardResultPage *_resultPage = nullptr;
|
OwncloudWizardResultPage *_resultPage = nullptr;
|
||||||
|
|
|
@ -44,7 +44,6 @@ namespace WizardCommon {
|
||||||
Page_Welcome,
|
Page_Welcome,
|
||||||
Page_ServerSetup,
|
Page_ServerSetup,
|
||||||
Page_HttpCreds,
|
Page_HttpCreds,
|
||||||
Page_OAuthCreds,
|
|
||||||
Page_Flow2AuthCreds,
|
Page_Flow2AuthCreds,
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
Page_WebView,
|
Page_WebView,
|
||||||
|
|
|
@ -39,7 +39,6 @@ Q_LOGGING_CATEGORY(lcHttpCredentials, "nextcloud.sync.credentials.http", QtInfoM
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const char userC[] = "user";
|
const char userC[] = "user";
|
||||||
const char isOAuthC[] = "oauth";
|
|
||||||
const char clientCertBundleC[] = "clientCertPkcs12";
|
const char clientCertBundleC[] = "clientCertPkcs12";
|
||||||
const char clientCertPasswordC[] = "_clientCertPassword";
|
const char clientCertPasswordC[] = "_clientCertPassword";
|
||||||
const char clientCertificatePEMC[] = "_clientCertificatePEM";
|
const char clientCertificatePEMC[] = "_clientCertificatePEM";
|
||||||
|
@ -63,14 +62,10 @@ protected:
|
||||||
QNetworkRequest req(request);
|
QNetworkRequest req(request);
|
||||||
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
|
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
|
||||||
if (_cred && !_cred->password().isEmpty()) {
|
if (_cred && !_cred->password().isEmpty()) {
|
||||||
if (_cred->isUsingOAuth()) {
|
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
|
||||||
req.setRawHeader("Authorization", "Bearer " + _cred->password().toUtf8());
|
req.setRawHeader("Authorization", "Basic " + credHash);
|
||||||
} else {
|
|
||||||
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
|
|
||||||
req.setRawHeader("Authorization", "Basic " + credHash);
|
|
||||||
}
|
|
||||||
} else if (!request.url().password().isEmpty()) {
|
} 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.
|
// credentials are put in the URL from the code making the request.
|
||||||
QByteArray credHash = request.url().userInfo().toUtf8().toBase64();
|
QByteArray credHash = request.url().userInfo().toUtf8().toBase64();
|
||||||
req.setRawHeader("Authorization", "Basic " + credHash);
|
req.setRawHeader("Authorization", "Basic " + credHash);
|
||||||
|
@ -85,15 +80,7 @@ protected:
|
||||||
req.setSslConfiguration(sslConfiguration);
|
req.setSslConfiguration(sslConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *reply = AccessManager::createRequest(op, req, outgoingData);
|
return 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -178,13 +165,6 @@ void HttpCredentials::fetchFromKeychain()
|
||||||
// User must be fetched from config file
|
// User must be fetched from config file
|
||||||
fetchUser();
|
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) {
|
if (_ready) {
|
||||||
Q_EMIT fetched();
|
Q_EMIT fetched();
|
||||||
} else {
|
} else {
|
||||||
|
@ -370,20 +350,13 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incoming)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isOauth = _account->credentialSetting(QLatin1String(isOAuthC)).toBool();
|
_password = job->textData();
|
||||||
if (isOauth) {
|
|
||||||
_refreshToken = job->textData();
|
|
||||||
} else {
|
|
||||||
_password = job->textData();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_user.isEmpty()) {
|
if (_user.isEmpty()) {
|
||||||
qCWarning(lcHttpCredentials) << "Strange: User is empty!";
|
qCWarning(lcHttpCredentials) << "Strange: User is empty!";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_refreshToken.isEmpty() && error == QKeychain::NoError) {
|
if (!_password.isEmpty() && error == QKeychain::NoError) {
|
||||||
refreshAccessToken();
|
|
||||||
} else if (!_password.isEmpty() && error == QKeychain::NoError) {
|
|
||||||
// All cool, the keychain did not come back with error.
|
// All cool, the keychain did not come back with error.
|
||||||
// Still, the password can be empty which indicates a problem and
|
// Still, the password can be empty which indicates a problem and
|
||||||
// the password dialog has to be opened.
|
// 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()
|
void HttpCredentials::invalidateToken()
|
||||||
{
|
{
|
||||||
if (!_password.isEmpty()) {
|
if (!_password.isEmpty()) {
|
||||||
|
@ -480,12 +401,6 @@ void HttpCredentials::invalidateToken()
|
||||||
// clear the session cookie.
|
// clear the session cookie.
|
||||||
_account->clearCookieJar();
|
_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());
|
auto *job = new QKeychain::DeletePasswordJob(Theme::instance()->appName());
|
||||||
addSettingsToJob(_account, job);
|
addSettingsToJob(_account, job);
|
||||||
job->setInsecureFallback(true);
|
job->setInsecureFallback(true);
|
||||||
|
@ -502,9 +417,6 @@ void HttpCredentials::invalidateToken()
|
||||||
|
|
||||||
void HttpCredentials::forgetSensitiveData()
|
void HttpCredentials::forgetSensitiveData()
|
||||||
{
|
{
|
||||||
// need to be done before invalidateToken, so it actually deletes the refresh_token from the keychain
|
|
||||||
_refreshToken.clear();
|
|
||||||
|
|
||||||
invalidateToken();
|
invalidateToken();
|
||||||
_previousPassword.clear();
|
_previousPassword.clear();
|
||||||
}
|
}
|
||||||
|
@ -517,7 +429,6 @@ void HttpCredentials::persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
_account->setCredentialSetting(QLatin1String(userC), _user);
|
_account->setCredentialSetting(QLatin1String(userC), _user);
|
||||||
_account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth());
|
|
||||||
if (!_clientCertBundle.isEmpty()) {
|
if (!_clientCertBundle.isEmpty()) {
|
||||||
// Note that the _clientCertBundle will often be cleared after usage,
|
// Note that the _clientCertBundle will often be cleared after usage,
|
||||||
// it's just written if it gets passed into the constructor.
|
// it's just written if it gets passed into the constructor.
|
||||||
|
@ -605,7 +516,7 @@ void HttpCredentials::slotWritePasswordToKeychain()
|
||||||
job->setInsecureFallback(false);
|
job->setInsecureFallback(false);
|
||||||
connect(job, &QKeychain::Job::finished, this, &HttpCredentials::slotWriteJobDone);
|
connect(job, &QKeychain::Job::finished, this, &HttpCredentials::slotWriteJobDone);
|
||||||
job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
|
job->setKey(keychainKey(_account->url().toString(), _user, _account->id()));
|
||||||
job->setTextData(isUsingOAuth() ? _refreshToken : _password);
|
job->setTextData(_password);
|
||||||
job->start();
|
job->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -621,19 +532,12 @@ void HttpCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *a
|
||||||
{
|
{
|
||||||
if (!_ready)
|
if (!_ready)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Q_UNUSED(authenticator)
|
Q_UNUSED(authenticator)
|
||||||
// Because of issue #4326, we need to set the login and password manually at every requests
|
// 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.
|
// Thus, if we reach this signal, those credentials were invalid and we terminate.
|
||||||
qCWarning(lcHttpCredentials) << "Stop request: Authentication failed for " << reply->url().toString();
|
qCWarning(lcHttpCredentials) << "Stop request: Authentication failed for " << reply->url().toString();
|
||||||
reply->setProperty(authenticationFailedC, true);
|
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)
|
bool HttpCredentials::retryIfNeeded(AbstractNetworkJob *job)
|
||||||
|
@ -641,11 +545,8 @@ bool HttpCredentials::retryIfNeeded(AbstractNetworkJob *job)
|
||||||
auto *reply = job->reply();
|
auto *reply = job->reply();
|
||||||
if (!reply || !reply->property(needRetryC).toBool())
|
if (!reply || !reply->property(needRetryC).toBool())
|
||||||
return false;
|
return false;
|
||||||
if (_isRenewingOAuthToken) {
|
|
||||||
_retryQueue.append(job);
|
job->retry();
|
||||||
} else {
|
|
||||||
job->retry();
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace OCC {
|
||||||
|
|
||||||
HttpCredentials is then split in HttpCredentials and HttpCredentialsGui.
|
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.
|
is in HttpCredentialsGui.
|
||||||
|
|
||||||
The authentication mechanism looks like this.
|
The authentication mechanism looks like this.
|
||||||
|
@ -52,13 +52,13 @@ namespace OCC {
|
||||||
v }
|
v }
|
||||||
slotReadClientCertPEMJobDone } There are first 3 QtKeychain jobs to fetch
|
slotReadClientCertPEMJobDone } There are first 3 QtKeychain jobs to fetch
|
||||||
| } the TLS client keys, if any, and the password
|
| } the TLS client keys, if any, and the password
|
||||||
v } (or refresh token
|
v }
|
||||||
slotReadClientKeyPEMJobDone }
|
slotReadClientKeyPEMJobDone }
|
||||||
| }
|
| }
|
||||||
v
|
v
|
||||||
slotReadJobDone
|
slotReadJobDone
|
||||||
| |
|
| |
|
||||||
| +-------> emit fetched() if OAuth is not used
|
| +-------> emit fetched()
|
||||||
|
|
|
|
||||||
v
|
v
|
||||||
refreshAccessToken()
|
refreshAccessToken()
|
||||||
|
@ -97,17 +97,9 @@ public:
|
||||||
QString fetchUser();
|
QString fetchUser();
|
||||||
virtual bool sslIsTrusted() { return false; }
|
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
|
// To fetch the user name as early as possible
|
||||||
void setAccount(Account *account) override;
|
void setAccount(Account *account) override;
|
||||||
|
|
||||||
// Whether we are using OAuth
|
|
||||||
[[nodiscard]] bool isUsingOAuth() const { return !_refreshToken.isNull(); }
|
|
||||||
|
|
||||||
bool retryIfNeeded(AbstractNetworkJob *) override;
|
bool retryIfNeeded(AbstractNetworkJob *) override;
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
|
@ -157,21 +149,18 @@ protected:
|
||||||
bool unpackClientCertBundle();
|
bool unpackClientCertBundle();
|
||||||
|
|
||||||
QString _user;
|
QString _user;
|
||||||
QString _password; // user's password, or access_token for OAuth
|
QString _password; // user's password
|
||||||
QString _refreshToken; // OAuth _refreshToken, set if OAuth is used.
|
|
||||||
QString _previousPassword;
|
QString _previousPassword;
|
||||||
|
|
||||||
QString _fetchErrorString;
|
QString _fetchErrorString;
|
||||||
bool _ready = false;
|
bool _ready = false;
|
||||||
bool _isRenewingOAuthToken = false;
|
|
||||||
QByteArray _clientCertBundle;
|
QByteArray _clientCertBundle;
|
||||||
QByteArray _clientCertPassword;
|
QByteArray _clientCertPassword;
|
||||||
QSslKey _clientSslKey;
|
QSslKey _clientSslKey;
|
||||||
QSslCertificate _clientSslCertificate;
|
QSslCertificate _clientSslCertificate;
|
||||||
bool _keychainMigration = false;
|
bool _keychainMigration = false;
|
||||||
bool _retryOnKeyChainError = true; // true if we haven't done yet any reading from keychain
|
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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1056,16 +1056,13 @@ void DetermineAuthTypeJob::start()
|
||||||
});
|
});
|
||||||
connect(propfind, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
|
connect(propfind, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
|
||||||
auto authChallenge = reply->rawHeader("WWW-Authenticate").toLower();
|
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 {
|
} else {
|
||||||
if (authChallenge.isEmpty()) {
|
qCWarning(lcDetermineAuthTypeJob) << "Unknown WWW-Authenticate reply to auth-test PROPFIND:" << authChallenge;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
_resultPropfind = Basic;
|
||||||
_propfindDone = true;
|
_propfindDone = true;
|
||||||
checkAllDone();
|
checkAllDone();
|
||||||
});
|
});
|
||||||
|
@ -1111,13 +1108,13 @@ void DetermineAuthTypeJob::checkAllDone()
|
||||||
auto result = _resultPropfind;
|
auto result = _resultPropfind;
|
||||||
|
|
||||||
#ifdef WITH_WEBENGINE
|
#ifdef WITH_WEBENGINE
|
||||||
// WebViewFlow > OAuth > Basic
|
// WebViewFlow > Basic
|
||||||
if (_account->serverVersionInt() >= Account::makeServerVersion(12, 0, 0)) {
|
if (_account->serverVersionInt() >= Account::makeServerVersion(12, 0, 0)) {
|
||||||
result = WebViewFlow;
|
result = WebViewFlow;
|
||||||
}
|
}
|
||||||
#endif // WITH_WEBENGINE
|
#endif // WITH_WEBENGINE
|
||||||
|
|
||||||
// LoginFlowV2 > WebViewFlow > OAuth > Basic
|
// LoginFlowV2 > WebViewFlow > Basic
|
||||||
if (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)) {
|
if (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)) {
|
||||||
result = LoginFlowV2;
|
result = LoginFlowV2;
|
||||||
}
|
}
|
||||||
|
|
|
@ -523,7 +523,6 @@ public:
|
||||||
WebViewFlow,
|
WebViewFlow,
|
||||||
#endif // WITH_WEBENGINE
|
#endif // WITH_WEBENGINE
|
||||||
Basic, // also the catch-all fallback for backwards compatibility reasons
|
Basic, // also the catch-all fallback for backwards compatibility reasons
|
||||||
OAuth,
|
|
||||||
LoginFlowV2
|
LoginFlowV2
|
||||||
};
|
};
|
||||||
Q_ENUM(AuthType)
|
Q_ENUM(AuthType)
|
||||||
|
|
|
@ -163,7 +163,7 @@ void GETFileJob::slotMetaDataChanged()
|
||||||
|
|
||||||
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307
|
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307
|
||||||
|| httpStatus == 308 || httpStatus == 401) {
|
|| 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
|
// 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.
|
// request. newReplyHook() will reestablish signal connections for the follow-up request.
|
||||||
bool ok = disconnect(reply(), &QNetworkReply::finished, this, &GETFileJob::slotReadyRead)
|
bool ok = disconnect(reply(), &QNetworkReply::finished, this, &GETFileJob::slotReadyRead)
|
||||||
|
|
|
@ -783,16 +783,6 @@ QString Theme::quotaBaseFolder() const
|
||||||
return QLatin1String("/");
|
return QLatin1String("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Theme::oauthClientId() const
|
|
||||||
{
|
|
||||||
return "xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Theme::oauthClientSecret() const
|
|
||||||
{
|
|
||||||
return "UBntmLjC2yYCeHwsyj73Uwo9TAaecAetRwMw0xYcvNL9yRdLSUi0hUAHfvCHFeFh";
|
|
||||||
}
|
|
||||||
|
|
||||||
QString Theme::versionSwitchOutput() const
|
QString Theme::versionSwitchOutput() const
|
||||||
{
|
{
|
||||||
QString helpText;
|
QString helpText;
|
||||||
|
|
|
@ -466,13 +466,6 @@ public:
|
||||||
*/
|
*/
|
||||||
[[nodiscard]] QString quotaBaseFolder() const;
|
[[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.
|
* @brief What should be output for the --version command line switch.
|
||||||
*
|
*
|
||||||
|
|
|
@ -115,8 +115,6 @@ nextcloud_add_test(Account)
|
||||||
nextcloud_add_test(FolderMan)
|
nextcloud_add_test(FolderMan)
|
||||||
nextcloud_add_test(RemoteWipe)
|
nextcloud_add_test(RemoteWipe)
|
||||||
|
|
||||||
nextcloud_add_test(OAuth)
|
|
||||||
|
|
||||||
configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY)
|
configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY)
|
||||||
|
|
||||||
find_package(CMocka)
|
find_package(CMocka)
|
||||||
|
|
|
@ -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"
|
|
Loading…
Reference in a new issue