From 2742411abd2de04441ac5d94a43c3a7e445a3739 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Sat, 17 Aug 2019 02:37:04 +0200 Subject: [PATCH 1/9] Login Flow V2: 1st test-implementation Signed-off-by: Michael Schuster --- src/gui/CMakeLists.txt | 3 + src/gui/connectionvalidator.cpp | 3 + src/gui/creds/flow2auth.cpp | 299 ++++++++++++++++++++++++++ src/gui/creds/flow2auth.h | 83 +++++++ src/gui/owncloudsetupwizard.cpp | 5 +- src/gui/wizard/flow2authcredspage.cpp | 135 ++++++++++++ src/gui/wizard/flow2authcredspage.h | 63 ++++++ src/gui/wizard/flow2authcredspage.ui | 87 ++++++++ src/gui/wizard/owncloudsetuppage.cpp | 2 + src/gui/wizard/owncloudwizard.cpp | 18 +- src/gui/wizard/owncloudwizard.h | 2 + src/gui/wizard/owncloudwizardcommon.h | 1 + src/libsync/networkjobs.cpp | 5 + src/libsync/networkjobs.h | 3 +- 14 files changed, 703 insertions(+), 6 deletions(-) create mode 100644 src/gui/creds/flow2auth.cpp create mode 100644 src/gui/creds/flow2auth.h create mode 100644 src/gui/wizard/flow2authcredspage.cpp create mode 100644 src/gui/wizard/flow2authcredspage.h create mode 100644 src/gui/wizard/flow2authcredspage.ui diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 167d1727f..0fb5970ad 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -38,6 +38,7 @@ set(client_UI_SRCS wizard/owncloudconnectionmethoddialog.ui wizard/owncloudhttpcredspage.ui wizard/owncloudoauthcredspage.ui + wizard/flow2authcredspage.ui wizard/owncloudsetupnocredspage.ui wizard/owncloudwizardresultpage.ui wizard/webview.ui @@ -103,6 +104,7 @@ set(client_SRCS creds/credentialsfactory.cpp creds/httpcredentialsgui.cpp creds/oauth.cpp + creds/flow2auth.cpp creds/webflowcredentials.cpp creds/webflowcredentialsdialog.cpp wizard/postfixlineedit.cpp @@ -111,6 +113,7 @@ set(client_SRCS wizard/owncloudconnectionmethoddialog.cpp wizard/owncloudhttpcredspage.cpp wizard/owncloudoauthcredspage.cpp + wizard/flow2authcredspage.cpp wizard/owncloudsetuppage.cpp wizard/owncloudwizardcommon.cpp wizard/owncloudwizard.cpp diff --git a/src/gui/connectionvalidator.cpp b/src/gui/connectionvalidator.cpp index b2f04d9b5..27b1a4b9a 100644 --- a/src/gui/connectionvalidator.cpp +++ b/src/gui/connectionvalidator.cpp @@ -233,6 +233,9 @@ void ConnectionValidator::checkServerCapabilities() QObject::connect(configJob, &JsonApiJob::jsonReceived, _account.data(), [=](const QJsonDocument &json) { ocsConfigReceived(json, account); + + /*int size = 0; + MessageBoxA(0, json.rawData(&size), "bla", MB_ICONINFORMATION);*/ }); configJob->start(); } diff --git a/src/gui/creds/flow2auth.cpp b/src/gui/creds/flow2auth.cpp new file mode 100644 index 000000000..0bf5041bf --- /dev/null +++ b/src/gui/creds/flow2auth.cpp @@ -0,0 +1,299 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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 +//#include +#include +#include +#include "account.h" +#include "creds/flow2auth.h" +#include +#include +#include +#include "theme.h" +#include "networkjobs.h" +#include "configfile.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcFlow2auth, "nextcloud.sync.credentials.flow2auth", QtInfoMsg) + +Flow2Auth::~Flow2Auth() +{ +} + +void Flow2Auth::start() +{ + // Step 1: Initiate a login, do an anonymous POST request + QUrl url = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/login/v2")); + + auto job = _account->sendRequest("POST", url); + 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(); + //MessageBoxA(0, jsonData.toStdString().c_str(), "Flow2Auth::start()", 0); + + QString pollToken = json.value("poll").toObject().value("token").toString(); + QString pollEndpoint = json.value("poll").toObject().value("endpoint").toString(); + QUrl loginUrl = json["login"].toString(); + /*MessageBoxA(0, pollToken.toStdString().c_str(), "pollToken", 0); + MessageBoxA(0, pollEndpoint.toStdString().c_str(), "pollEndpoint", 0); + MessageBoxA(0, loginUrl.toString().toStdString().c_str(), "loginUrl", 0);*/ + + if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError + || json.isEmpty() || pollToken.isEmpty() || pollEndpoint.isEmpty() || loginUrl.isEmpty()) { + QString errorReason; + QString errorFromJson = json["error"].toString(); + if (!errorFromJson.isEmpty()) { + errorReason = tr("Error returned from the server: %1") + .arg(errorFromJson.toHtmlEscaped()); + } else if (reply->error() != QNetworkReply::NoError) { + errorReason = tr("There was an error accessing the 'token' endpoint:
%1") + .arg(reply->errorString().toHtmlEscaped()); + } else if (jsonParseError.error != QJsonParseError::NoError) { + errorReason = tr("Could not parse the JSON returned from the server:
%1") + .arg(jsonParseError.errorString()); + } else { + errorReason = tr("The reply from the server did not contain all expected fields"); + } + qCWarning(lcFlow2auth) << "Error when getting the loginUrl" << json << errorReason; + emit result(Error); + return; + } + + + _loginUrl = loginUrl; + _pollToken = pollToken; + _pollEndpoint = pollEndpoint; + + + ConfigFile cfg; + std::chrono::milliseconds polltime = cfg.remotePollInterval(); + qCInfo(lcFlow2auth) << "setting remote poll timer interval to" << polltime.count() << "msec"; + _pollTimer.setInterval(polltime.count()); + QObject::connect(&_pollTimer, &QTimer::timeout, this, &Flow2Auth::slotPollTimerTimeout); + _pollTimer.start(); + + + if (!openBrowser()) + return; + + + /*if (!_expectedUser.isNull() && user != _expectedUser) { + // Connected with the wrong user + QString message = tr("

Wrong user

" + "

You logged-in with user %1, but must login with user %2.
" + "Please log out of %3 in another tab, then click here " + "and log in as user %2

") + .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 = "

Login Successful

You can close this window.

"; + 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);*/ + }); +#if 0 + return; + + + // 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 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 + QRegExp rx("^GET /\\?code=([a-zA-Z0-9]+)[& ]"); // Match a /?code=... URL + if (rx.indexIn(peek) != 0) { + httpReplyAndClose(socket, "404 Not Found", "404 Not Found

404 Not Found

"); + return; + } + + QString code = rx.cap(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()); + + 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 + || 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: %1") + .arg(errorFromJson.toHtmlEscaped()); + } else if (reply->error() != QNetworkReply::NoError) { + errorReason = tr("There was an error accessing the 'token' endpoint:
%1") + .arg(reply->errorString().toHtmlEscaped()); + } else if (jsonParseError.error != QJsonParseError::NoError) { + errorReason = tr("Could not parse the JSON returned from the server:
%1") + .arg(jsonParseError.errorString()); + } else { + errorReason = tr("The reply from the server did not contain all expected fields"); + } + qCWarning(lcFlow2auth) << "Error when getting the accessToken" << json << errorReason; + httpReplyAndClose(socket, "500 Internal Server Error", + tr("

Login Error

%1

").arg(errorReason).toUtf8().constData()); + emit result(Error); + return; + } + if (!_expectedUser.isNull() && user != _expectedUser) { + // Connected with the wrong user + QString message = tr("

Wrong user

" + "

You logged-in with user %1, but must login with user %2.
" + "Please log out of %3 in another tab, then click here " + "and log in as user %2

") + .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 = "

Login Successful

You can close this window.

"; + 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); + }); + }); + } + }); +#endif +} + +void Flow2Auth::slotPollTimerTimeout() +{ + _pollTimer.stop(); + +// qCInfo(lcFlow2auth) << "reached poll timout"; + + // Step 2: Poll + QNetworkRequest req; + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + auto requestBody = new QBuffer; + QUrlQuery arguments(QString("token=%1").arg(_pollToken)); + requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1()); + + auto job = _account->sendRequest("POST", _pollEndpoint, 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(); + + QUrl serverUrl = json["server"].toString(); + QString loginName = json["loginName"].toString(); + QString appPassword = json["appPassword"].toString(); + + if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError + || json.isEmpty() || serverUrl.isEmpty() || loginName.isEmpty() || appPassword.isEmpty()) { + QString errorReason; + QString errorFromJson = json["error"].toString(); + if (!errorFromJson.isEmpty()) { + errorReason = tr("Error returned from the server: %1") + .arg(errorFromJson.toHtmlEscaped()); + } else if (reply->error() != QNetworkReply::NoError) { + errorReason = tr("There was an error accessing the 'token' endpoint:
%1") + .arg(reply->errorString().toHtmlEscaped()); + } else if (jsonParseError.error != QJsonParseError::NoError) { + errorReason = tr("Could not parse the JSON returned from the server:
%1") + .arg(jsonParseError.errorString()); + } else { + errorReason = tr("The reply from the server did not contain all expected fields"); + } + qCDebug(lcFlow2auth) << "Error when polling for the appPassword" << json << errorReason; +// emit result(Error); + _pollTimer.start(); + return; + } + + qCInfo(lcFlow2auth) << "Success getting the appPassword for user: " << loginName << " on server: " << serverUrl; + + _account->setUrl(serverUrl); + + emit result(LoggedIn, loginName, appPassword); + }); +} + +QUrl Flow2Auth::authorisationLink() const +{ + return _loginUrl; + /*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 Flow2Auth::openBrowser() +{ + if (!QDesktopServices::openUrl(authorisationLink())) { + // We cannot open the browser, then we claim we don't support OAuth. + emit result(NotSupported, QString()); + return false; + } + return true; +} + +} // namespace OCC diff --git a/src/gui/creds/flow2auth.h b/src/gui/creds/flow2auth.h new file mode 100644 index 000000000..f979233c6 --- /dev/null +++ b/src/gui/creds/flow2auth.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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 +#include +#include +#include "accountfwd.h" + +namespace OCC { + +/** + * Job that does 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 Flow2Auth : public QObject +{ + Q_OBJECT +public: + Flow2Auth(Account *account, QObject *parent) + : QObject(parent) + , _account(account) + { + } + ~Flow2Auth(); + + enum Result { NotSupported, + LoggedIn, + Error }; + Q_ENUM(Result); + void start(); + bool openBrowser(); + QUrl authorisationLink() const; + +signals: + /** + * The state has changed. + * when logged in, token has the value of the token. + */ + void result(Flow2Auth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString()); + +private slots: + void slotPollTimerTimeout(); + +private: + Account *_account; + QUrl _loginUrl; + QString _pollToken; + QString _pollEndpoint; + QTimer _pollTimer; + +public: + QString _expectedUser; +}; + + +} // namespace OCC diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index 2a037ff84..653894874 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -105,9 +105,10 @@ void OwncloudSetupWizard::startWizard() { AccountPtr account = AccountManager::createAccount(); account->setCredentials(CredentialsFactory::create("dummy")); - account->setUrl(Theme::instance()->overrideServerUrl()); + account->setUrl(QUrl("https://127.0.0.1:8443/"));//Theme::instance()->overrideServerUrl()); _ocWizard->setAccount(account); _ocWizard->setOCUrl(account->url().toString()); +// _ocWizard->setOCUrl("https://127.0.0.1:8443/"); //account->url().toString()); _remoteFolder = Theme::instance()->defaultServerFolder(); // remoteFolder may be empty, which means / @@ -408,7 +409,7 @@ void OwncloudSetupWizard::slotAuthError() } _ocWizard->show(); - if (_ocWizard->currentId() == WizardCommon::Page_ShibbolethCreds || _ocWizard->currentId() == WizardCommon::Page_OAuthCreds) { + if (_ocWizard->currentId() == WizardCommon::Page_ShibbolethCreds || _ocWizard->currentId() == WizardCommon::Page_OAuthCreds || _ocWizard->currentId() == WizardCommon::Page_Flow2AuthCreds) { _ocWizard->back(); } _ocWizard->displayError(errorMsg, _ocWizard->currentId() == WizardCommon::Page_ServerSetup && checkDowngradeAdvised(reply)); diff --git a/src/gui/wizard/flow2authcredspage.cpp b/src/gui/wizard/flow2authcredspage.cpp new file mode 100644 index 000000000..2c0000ea4 --- /dev/null +++ b/src/gui/wizard/flow2authcredspage.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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 +#include +#include + +#include "wizard/flow2authcredspage.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 { + +Flow2AuthCredsPage::Flow2AuthCredsPage() + : 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 (Login Flow v2)"))); + + connect(_ui.openLinkButton, &QCommandLinkButton::clicked, [this] { + _ui.errorLabel->hide(); + if (_asyncAuth) + _asyncAuth->openBrowser(); + }); + _ui.openLinkButton->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(_ui.openLinkButton, &QWidget::customContextMenuRequested, [this](const QPoint &pos) { + auto menu = new QMenu(_ui.openLinkButton); + menu->addAction(tr("Copy link to clipboard"), this, [this] { + if (_asyncAuth) + QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded)); + }); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(_ui.openLinkButton->mapToGlobal(pos)); + }); +} + +void Flow2AuthCredsPage::initializePage() +{ + OwncloudWizard *ocWizard = qobject_cast(wizard()); + Q_ASSERT(ocWizard); + ocWizard->account()->setCredentials(CredentialsFactory::create("http")); + _asyncAuth.reset(new Flow2Auth(ocWizard->account().data(), this)); + connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthCredsPage::asyncAuthResult, Qt::QueuedConnection); + _asyncAuth->start(); + wizard()->hide(); +} + +void OCC::Flow2AuthCredsPage::cleanupPage() +{ + // The next or back button was activated, show the wizard again + wizard()->show(); + _asyncAuth.reset(); +} + +void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &user, + const QString &token, const QString &refreshToken) +{ + switch (r) { + case Flow2Auth::NotSupported: { + /* Flow2Auth not supported (can't open browser), fallback to HTTP credentials */ + OwncloudWizard *ocWizard = qobject_cast(wizard()); + ocWizard->back(); + ocWizard->setAuthType(DetermineAuthTypeJob::Basic); + break; + } + case Flow2Auth::Error: + /* Error while getting the access token. (Timeout, or the server did not accept our client credentials */ + _ui.errorLabel->show(); + wizard()->show(); + break; + case Flow2Auth::LoggedIn: { + _token = token; + _user = user; + _refreshToken = refreshToken; + OwncloudWizard *ocWizard = qobject_cast(wizard()); + Q_ASSERT(ocWizard); + emit connectToOCUrl(ocWizard->account()->url().toString()); + break; + } + } +} + +int Flow2AuthCredsPage::nextId() const +{ + return WizardCommon::Page_AdvancedSetup; +} + +void Flow2AuthCredsPage::setConnected() +{ + wizard()->show(); +} + +AbstractCredentials *Flow2AuthCredsPage::getCredentials() const +{ + OwncloudWizard *ocWizard = qobject_cast(wizard()); + Q_ASSERT(ocWizard); + return new HttpCredentialsGui(_user, _token, _refreshToken, + ocWizard->_clientSslCertificate, ocWizard->_clientSslKey); +} + +bool Flow2AuthCredsPage::isComplete() const +{ + return false; /* We can never go forward manually */ +} + +} // namespace OCC diff --git a/src/gui/wizard/flow2authcredspage.h b/src/gui/wizard/flow2authcredspage.h new file mode 100644 index 000000000..85f1a0a9f --- /dev/null +++ b/src/gui/wizard/flow2authcredspage.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Michael Schuster + * + * 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 +#include +#include +#include +#include + +#include "wizard/abstractcredswizardpage.h" +#include "accountfwd.h" +#include "creds/flow2auth.h" + +#include "ui_flow2authcredspage.h" + + +namespace OCC { + + +class Flow2AuthCredsPage : public AbstractCredentialsWizardPage +{ + Q_OBJECT +public: + Flow2AuthCredsPage(); + + AbstractCredentials *getCredentials() const override; + + void initializePage() override; + void cleanupPage() override; + int nextId() const override; + void setConnected(); + bool isComplete() const override; + +public Q_SLOTS: + void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &token, + const QString &reniewToken); + +signals: + void connectToOCUrl(const QString &); + +public: + QString _user; + QString _token; + QString _refreshToken; + QScopedPointer _asyncAuth; + Ui_Flow2AuthCredsPage _ui; +}; + +} // namespace OCC diff --git a/src/gui/wizard/flow2authcredspage.ui b/src/gui/wizard/flow2authcredspage.ui new file mode 100644 index 000000000..04c1d7217 --- /dev/null +++ b/src/gui/wizard/flow2authcredspage.ui @@ -0,0 +1,87 @@ + + + Flow2AuthCredsPage + + + + 0 + 0 + 424 + 373 + + + + Form + + + + + + TextLabel + + + Qt::RichText + + + Qt::AlignCenter + + + true + + + + + + + Please switch to your browser to proceed. + + + true + + + + + + + An error occurred while connecting. Please try again. + + + Qt::PlainText + + + + + + + Re-open Browser (or right-click to copy link) + + + + + + + Qt::Vertical + + + + 20 + 127 + + + + + + + + TextLabel + + + Qt::RichText + + + + + + + + diff --git a/src/gui/wizard/owncloudsetuppage.cpp b/src/gui/wizard/owncloudsetuppage.cpp index 4af0ca935..8cfa158d4 100644 --- a/src/gui/wizard/owncloudsetuppage.cpp +++ b/src/gui/wizard/owncloudsetuppage.cpp @@ -258,6 +258,8 @@ int OwncloudSetupPage::nextId() const return WizardCommon::Page_HttpCreds; case DetermineAuthTypeJob::OAuth: return WizardCommon::Page_OAuthCreds; + case DetermineAuthTypeJob::LoginFlowV2: + return WizardCommon::Page_Flow2AuthCreds; case DetermineAuthTypeJob::Shibboleth: return WizardCommon::Page_ShibbolethCreds; case DetermineAuthTypeJob::WebViewFlow: diff --git a/src/gui/wizard/owncloudwizard.cpp b/src/gui/wizard/owncloudwizard.cpp index 357b4e005..e4dda0fbd 100644 --- a/src/gui/wizard/owncloudwizard.cpp +++ b/src/gui/wizard/owncloudwizard.cpp @@ -27,6 +27,7 @@ #include "wizard/owncloudadvancedsetuppage.h" #include "wizard/owncloudwizardresultpage.h" #include "wizard/webviewpage.h" +#include "wizard/flow2authcredspage.h" #include "QProgressIndicator.h" @@ -45,6 +46,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) , _setupPage(new OwncloudSetupPage(this)) , _httpCredsPage(new OwncloudHttpCredsPage(this)) , _browserCredsPage(new OwncloudOAuthCredsPage) + , _flow2CredsPage(new Flow2AuthCredsPage) #ifndef NO_SHIBBOLETH , _shibbolethCredsPage(new OwncloudShibbolethCredsPage) #endif @@ -59,6 +61,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) setPage(WizardCommon::Page_ServerSetup, _setupPage); setPage(WizardCommon::Page_HttpCreds, _httpCredsPage); setPage(WizardCommon::Page_OAuthCreds, _browserCredsPage); + setPage(WizardCommon::Page_Flow2AuthCreds, _flow2CredsPage); #ifndef NO_SHIBBOLETH setPage(WizardCommon::Page_ShibbolethCreds, _shibbolethCredsPage); #endif @@ -76,6 +79,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent) 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); #ifndef NO_SHIBBOLETH connect(_shibbolethCredsPage, &OwncloudShibbolethCredsPage::connectToOCUrl, this, &OwncloudWizard::connectToOCUrl); #endif @@ -129,11 +133,13 @@ QString OwncloudWizard::ocUrl() const return url; } -bool OwncloudWizard::registration() { +bool OwncloudWizard::registration() +{ return _registration; } -void OwncloudWizard::setRegistration(bool registration) { +void OwncloudWizard::setRegistration(bool registration) +{ _registration = registration; } @@ -162,6 +168,10 @@ void OwncloudWizard::successfulStep() _browserCredsPage->setConnected(); break; + case WizardCommon::Page_Flow2AuthCreds: + _flow2CredsPage->setConnected(); + break; + #ifndef NO_SHIBBOLETH case WizardCommon::Page_ShibbolethCreds: _shibbolethCredsPage->setConnected(); @@ -195,6 +205,8 @@ void OwncloudWizard::setAuthType(DetermineAuthTypeJob::AuthType type) #endif if (type == DetermineAuthTypeJob::OAuth) { _credentialsPage = _browserCredsPage; + } else if (type == DetermineAuthTypeJob::LoginFlowV2) { + _credentialsPage = _flow2CredsPage; } else if (type == DetermineAuthTypeJob::WebViewFlow) { _credentialsPage = _webViewPage; } else { // try Basic auth even for "Unknown" @@ -221,7 +233,7 @@ void OwncloudWizard::slotCurrentPageChanged(int id) } setOption(QWizard::HaveCustomButton1, id == WizardCommon::Page_AdvancedSetup); - if (id == WizardCommon::Page_AdvancedSetup && _credentialsPage == _browserCredsPage) { + if (id == WizardCommon::Page_AdvancedSetup && (_credentialsPage == _browserCredsPage || _credentialsPage == _flow2CredsPage)) { // For OAuth, disable the back button in the Page_AdvancedSetup because we don't want // to re-open the browser. button(QWizard::BackButton)->setEnabled(false); diff --git a/src/gui/wizard/owncloudwizard.h b/src/gui/wizard/owncloudwizard.h index 1429709f1..0b7b715a8 100644 --- a/src/gui/wizard/owncloudwizard.h +++ b/src/gui/wizard/owncloudwizard.h @@ -40,6 +40,7 @@ class OwncloudWizardResultPage; class AbstractCredentials; class AbstractCredentialsWizardPage; class WebViewPage; +class Flow2AuthCredsPage; /** * @brief The OwncloudWizard class @@ -107,6 +108,7 @@ private: OwncloudWizardResultPage *_resultPage; AbstractCredentialsWizardPage *_credentialsPage; WebViewPage *_webViewPage; + Flow2AuthCredsPage *_flow2CredsPage; QStringList _setupLog; diff --git a/src/gui/wizard/owncloudwizardcommon.h b/src/gui/wizard/owncloudwizardcommon.h index c3174d15a..d1f7c08be 100644 --- a/src/gui/wizard/owncloudwizardcommon.h +++ b/src/gui/wizard/owncloudwizardcommon.h @@ -38,6 +38,7 @@ namespace WizardCommon { Page_HttpCreds, Page_ShibbolethCreds, Page_OAuthCreds, + Page_Flow2AuthCreds, Page_WebView, Page_AdvancedSetup, Page_Result diff --git a/src/libsync/networkjobs.cpp b/src/libsync/networkjobs.cpp index cf59b5ab6..c17889519 100644 --- a/src/libsync/networkjobs.cpp +++ b/src/libsync/networkjobs.cpp @@ -925,6 +925,11 @@ void DetermineAuthTypeJob::checkBothDone() result = WebViewFlow; } + // LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic + if (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)) { + result = LoginFlowV2; + } + qCInfo(lcDetermineAuthTypeJob) << "Auth type for" << _account->davUrl() << "is" << result; emit authType(result); deleteLater(); diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index c2fefd611..51d8df63d 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -412,7 +412,8 @@ public: Basic, // also the catch-all fallback for backwards compatibility reasons OAuth, Shibboleth, - WebViewFlow + WebViewFlow, + LoginFlowV2 }; explicit DetermineAuthTypeJob(AccountPtr account, QObject *parent = nullptr); From 8fa55b97b448203eae391e23879bc05054902ee4 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Mon, 19 Aug 2019 01:41:42 +0200 Subject: [PATCH 2/9] Login Flow V2: 1st implementation, cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first draft of the Login Flow V2 authorization method. See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 - Adds the Login Fĺow V2 auth method - Adds ability to reinitiate a new request via UI TODO: - Implement re-auth upon logout -> login - Improve UI - SSL: Client certificate login is possible at the first time only but missing after relaunch Signed-off-by: Michael Schuster --- src/gui/creds/flow2auth.cpp | 187 +++++------------------------------- src/gui/creds/flow2auth.h | 22 +---- 2 files changed, 30 insertions(+), 179 deletions(-) diff --git a/src/gui/creds/flow2auth.cpp b/src/gui/creds/flow2auth.cpp index 0bf5041bf..76ff7c4ac 100644 --- a/src/gui/creds/flow2auth.cpp +++ b/src/gui/creds/flow2auth.cpp @@ -14,14 +14,12 @@ */ #include -//#include #include #include #include "account.h" #include "creds/flow2auth.h" #include #include -#include #include "theme.h" #include "networkjobs.h" #include "configfile.h" @@ -36,23 +34,35 @@ Flow2Auth::~Flow2Auth() void Flow2Auth::start() { + // Note: All startup code is in openBrowser() to allow reinitiate a new request with + // fresh tokens. Opening the same pollEndpoint link twice triggers an expiration + // message by the server (security, intended design). + openBrowser(); +} + +QUrl Flow2Auth::authorisationLink() const +{ + return _loginUrl; +} + +void Flow2Auth::openBrowser() +{ + _pollTimer.stop(); + // Step 1: Initiate a login, do an anonymous POST request QUrl url = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/login/v2")); auto job = _account->sendRequest("POST", url); 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(); - //MessageBoxA(0, jsonData.toStdString().c_str(), "Flow2Auth::start()", 0); - QString pollToken = json.value("poll").toObject().value("token").toString(); + QString pollToken = json.value("poll").toObject().value("token").toString(); QString pollEndpoint = json.value("poll").toObject().value("endpoint").toString(); QUrl loginUrl = json["login"].toString(); - /*MessageBoxA(0, pollToken.toStdString().c_str(), "pollToken", 0); - MessageBoxA(0, pollEndpoint.toStdString().c_str(), "pollEndpoint", 0); - MessageBoxA(0, loginUrl.toString().toStdString().c_str(), "loginUrl", 0);*/ if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError || json.isEmpty() || pollToken.isEmpty() || pollEndpoint.isEmpty() || loginUrl.isEmpty()) { @@ -76,153 +86,32 @@ void Flow2Auth::start() } - _loginUrl = loginUrl; + _loginUrl = loginUrl; _pollToken = pollToken; _pollEndpoint = pollEndpoint; - ConfigFile cfg; + ConfigFile cfg; std::chrono::milliseconds polltime = cfg.remotePollInterval(); qCInfo(lcFlow2auth) << "setting remote poll timer interval to" << polltime.count() << "msec"; - _pollTimer.setInterval(polltime.count()); + _pollTimer.setInterval(polltime.count()); QObject::connect(&_pollTimer, &QTimer::timeout, this, &Flow2Auth::slotPollTimerTimeout); _pollTimer.start(); - if (!openBrowser()) - return; + if (!QDesktopServices::openUrl(authorisationLink())) { + // TODO: Ask the user to copy and open the link instead of failing here! - - /*if (!_expectedUser.isNull() && user != _expectedUser) { - // Connected with the wrong user - QString message = tr("

Wrong user

" - "

You logged-in with user %1, but must login with user %2.
" - "Please log out of %3 in another tab, then click here " - "and log in as user %2

") - .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 = "

Login Successful

You can close this window.

"; - 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);*/ - }); -#if 0 - return; - - - // 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 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 - QRegExp rx("^GET /\\?code=([a-zA-Z0-9]+)[& ]"); // Match a /?code=... URL - if (rx.indexIn(peek) != 0) { - httpReplyAndClose(socket, "404 Not Found", "404 Not Found

404 Not Found

"); - return; - } - - QString code = rx.cap(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()); - - 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 - || 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: %1") - .arg(errorFromJson.toHtmlEscaped()); - } else if (reply->error() != QNetworkReply::NoError) { - errorReason = tr("There was an error accessing the 'token' endpoint:
%1") - .arg(reply->errorString().toHtmlEscaped()); - } else if (jsonParseError.error != QJsonParseError::NoError) { - errorReason = tr("Could not parse the JSON returned from the server:
%1") - .arg(jsonParseError.errorString()); - } else { - errorReason = tr("The reply from the server did not contain all expected fields"); - } - qCWarning(lcFlow2auth) << "Error when getting the accessToken" << json << errorReason; - httpReplyAndClose(socket, "500 Internal Server Error", - tr("

Login Error

%1

").arg(errorReason).toUtf8().constData()); - emit result(Error); - return; - } - if (!_expectedUser.isNull() && user != _expectedUser) { - // Connected with the wrong user - QString message = tr("

Wrong user

" - "

You logged-in with user %1, but must login with user %2.
" - "Please log out of %3 in another tab, then click here " - "and log in as user %2

") - .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 = "

Login Successful

You can close this window.

"; - 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); - }); - }); + // We cannot open the browser, then we claim we don't support OAuth. + emit result(NotSupported, QString()); } }); -#endif } void Flow2Auth::slotPollTimerTimeout() { _pollTimer.stop(); -// qCInfo(lcFlow2auth) << "reached poll timout"; - // Step 2: Poll QNetworkRequest req; req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); @@ -259,8 +148,8 @@ void Flow2Auth::slotPollTimerTimeout() errorReason = tr("The reply from the server did not contain all expected fields"); } qCDebug(lcFlow2auth) << "Error when polling for the appPassword" << json << errorReason; -// emit result(Error); - _pollTimer.start(); + + _pollTimer.start(); return; } @@ -272,28 +161,4 @@ void Flow2Auth::slotPollTimerTimeout() }); } -QUrl Flow2Auth::authorisationLink() const -{ - return _loginUrl; - /*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 Flow2Auth::openBrowser() -{ - if (!QDesktopServices::openUrl(authorisationLink())) { - // We cannot open the browser, then we claim we don't support OAuth. - emit result(NotSupported, QString()); - return false; - } - return true; -} - } // namespace OCC diff --git a/src/gui/creds/flow2auth.h b/src/gui/creds/flow2auth.h index f979233c6..188865a0d 100644 --- a/src/gui/creds/flow2auth.h +++ b/src/gui/creds/flow2auth.h @@ -22,21 +22,9 @@ namespace OCC { /** - * Job that does the authorization grant and fetch the access token + * Job that does the authorization, grants and fetches the access token via Login Flow v2 * - * 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(...) + * See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 * */ class Flow2Auth : public QObject @@ -55,7 +43,7 @@ public: Error }; Q_ENUM(Result); void start(); - bool openBrowser(); + void openBrowser(); QUrl authorisationLink() const; signals: @@ -63,6 +51,7 @@ signals: * The state has changed. * when logged in, token has the value of the token. */ + // TODO: Remove refreshToken void result(Flow2Auth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString()); private slots: @@ -74,9 +63,6 @@ private: QString _pollToken; QString _pollEndpoint; QTimer _pollTimer; - -public: - QString _expectedUser; }; From 12f2ea672876d770bc3f5d8f6b3e39d888b07162 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Mon, 19 Aug 2019 20:40:30 +0200 Subject: [PATCH 3/9] Login Flow V2: remove static test url Signed-off-by: Michael Schuster --- src/gui/connectionvalidator.cpp | 3 --- src/gui/owncloudsetupwizard.cpp | 3 +-- src/libsync/networkjobs.h | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gui/connectionvalidator.cpp b/src/gui/connectionvalidator.cpp index 27b1a4b9a..b2f04d9b5 100644 --- a/src/gui/connectionvalidator.cpp +++ b/src/gui/connectionvalidator.cpp @@ -233,9 +233,6 @@ void ConnectionValidator::checkServerCapabilities() QObject::connect(configJob, &JsonApiJob::jsonReceived, _account.data(), [=](const QJsonDocument &json) { ocsConfigReceived(json, account); - - /*int size = 0; - MessageBoxA(0, json.rawData(&size), "bla", MB_ICONINFORMATION);*/ }); configJob->start(); } diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index 653894874..fbe07e8b2 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -105,10 +105,9 @@ void OwncloudSetupWizard::startWizard() { AccountPtr account = AccountManager::createAccount(); account->setCredentials(CredentialsFactory::create("dummy")); - account->setUrl(QUrl("https://127.0.0.1:8443/"));//Theme::instance()->overrideServerUrl()); + account->setUrl(Theme::instance()->overrideServerUrl()); _ocWizard->setAccount(account); _ocWizard->setOCUrl(account->url().toString()); -// _ocWizard->setOCUrl("https://127.0.0.1:8443/"); //account->url().toString()); _remoteFolder = Theme::instance()->defaultServerFolder(); // remoteFolder may be empty, which means / diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index 51d8df63d..d0829b463 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -413,7 +413,7 @@ public: OAuth, Shibboleth, WebViewFlow, - LoginFlowV2 + LoginFlowV2 }; explicit DetermineAuthTypeJob(AccountPtr account, QObject *parent = nullptr); From 628bab92c4424ea4d797d5adea179f02ca5b0ee7 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Fri, 23 Aug 2019 10:25:34 +0200 Subject: [PATCH 4/9] fix comment typo in webflowcredentials.cpp Signed-off-by: Michael Schuster --- src/gui/creds/webflowcredentials.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/creds/webflowcredentials.cpp b/src/gui/creds/webflowcredentials.cpp index 3ff15b26b..70a709470 100644 --- a/src/gui/creds/webflowcredentials.cpp +++ b/src/gui/creds/webflowcredentials.cpp @@ -102,7 +102,7 @@ bool WebFlowCredentials::ready() const { void WebFlowCredentials::fetchFromKeychain() { _wasFetched = true; - // Make sure we get the user fromt he config file + // Make sure we get the user from the config file fetchUser(); if (ready()) { From aa93a04fd69721a0d73810538093a1ec14a64107 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Fri, 23 Aug 2019 11:04:30 +0200 Subject: [PATCH 5/9] fix comment typo in httpcredentials.cpp Signed-off-by: Michael Schuster --- src/libsync/creds/httpcredentials.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libsync/creds/httpcredentials.cpp b/src/libsync/creds/httpcredentials.cpp index 3090bdb30..fbe01331e 100644 --- a/src/libsync/creds/httpcredentials.cpp +++ b/src/libsync/creds/httpcredentials.cpp @@ -173,7 +173,7 @@ void HttpCredentials::fetchFromKeychain() fetchUser(); if (!_ready && !_refreshToken.isEmpty()) { - // This happens if the credentials are still loaded from the keychain, bur we are called + // 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; From 7add98e9a316e42a28e55a5296fef30f557d2461 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Fri, 23 Aug 2019 11:42:12 +0200 Subject: [PATCH 6/9] UI: don't let Flow2 and OAuth hide the wizard Signed-off-by: Michael Schuster --- src/gui/wizard/flow2authcredspage.cpp | 4 +++- src/gui/wizard/owncloudoauthcredspage.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/gui/wizard/flow2authcredspage.cpp b/src/gui/wizard/flow2authcredspage.cpp index 2c0000ea4..e5914a10b 100644 --- a/src/gui/wizard/flow2authcredspage.cpp +++ b/src/gui/wizard/flow2authcredspage.cpp @@ -71,7 +71,9 @@ void Flow2AuthCredsPage::initializePage() _asyncAuth.reset(new Flow2Auth(ocWizard->account().data(), this)); connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthCredsPage::asyncAuthResult, Qt::QueuedConnection); _asyncAuth->start(); - wizard()->hide(); + + // Don't hide the wizard (avoid user confusion)! + //wizard()->hide(); } void OCC::Flow2AuthCredsPage::cleanupPage() diff --git a/src/gui/wizard/owncloudoauthcredspage.cpp b/src/gui/wizard/owncloudoauthcredspage.cpp index a32ccdb87..174aa05c4 100644 --- a/src/gui/wizard/owncloudoauthcredspage.cpp +++ b/src/gui/wizard/owncloudoauthcredspage.cpp @@ -70,7 +70,9 @@ void OwncloudOAuthCredsPage::initializePage() _asyncAuth.reset(new OAuth(ocWizard->account().data(), this)); connect(_asyncAuth.data(), &OAuth::result, this, &OwncloudOAuthCredsPage::asyncAuthResult, Qt::QueuedConnection); _asyncAuth->start(); - wizard()->hide(); + + // Don't hide the wizard (avoid user confusion)! + //wizard()->hide(); } void OCC::OwncloudOAuthCredsPage::cleanupPage() From fd8345ccbe623ebee624477d2abe002bad181298 Mon Sep 17 00:00:00 2001 From: Michael Schuster Date: Sat, 24 Aug 2019 16:21:44 +0200 Subject: [PATCH 7/9] Login Flow V2: adds re-auth upon logout, improvements - Implements re-auth upon logout -> login - Improves UI and security TODO: - SSL: Client certificate login is possible at the first time only but missing after relaunch Signed-off-by: Michael Schuster --- src/gui/CMakeLists.txt | 2 + src/gui/creds/flow2auth.cpp | 38 ++++--- src/gui/creds/flow2auth.h | 5 +- src/gui/creds/webflowcredentials.cpp | 25 +++-- src/gui/creds/webflowcredentialsdialog.cpp | 49 +++++++-- src/gui/creds/webflowcredentialsdialog.h | 13 ++- src/gui/wizard/flow2authcredspage.cpp | 21 ++-- src/gui/wizard/flow2authcredspage.h | 6 +- src/gui/wizard/flow2authwidget.cpp | 113 +++++++++++++++++++++ src/gui/wizard/flow2authwidget.h | 52 ++++++++++ src/gui/wizard/flow2authwidget.ui | 99 ++++++++++++++++++ 11 files changed, 376 insertions(+), 47 deletions(-) create mode 100644 src/gui/wizard/flow2authwidget.cpp create mode 100644 src/gui/wizard/flow2authwidget.h create mode 100644 src/gui/wizard/flow2authwidget.ui diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0fb5970ad..5db93a640 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -39,6 +39,7 @@ set(client_UI_SRCS wizard/owncloudhttpcredspage.ui wizard/owncloudoauthcredspage.ui wizard/flow2authcredspage.ui + wizard/flow2authwidget.ui wizard/owncloudsetupnocredspage.ui wizard/owncloudwizardresultpage.ui wizard/webview.ui @@ -114,6 +115,7 @@ set(client_SRCS wizard/owncloudhttpcredspage.cpp wizard/owncloudoauthcredspage.cpp wizard/flow2authcredspage.cpp + wizard/flow2authwidget.cpp wizard/owncloudsetuppage.cpp wizard/owncloudwizardcommon.cpp wizard/owncloudwizard.cpp diff --git a/src/gui/creds/flow2auth.cpp b/src/gui/creds/flow2auth.cpp index 76ff7c4ac..e570e01d4 100644 --- a/src/gui/creds/flow2auth.cpp +++ b/src/gui/creds/flow2auth.cpp @@ -34,9 +34,9 @@ Flow2Auth::~Flow2Auth() void Flow2Auth::start() { - // Note: All startup code is in openBrowser() to allow reinitiate a new request with - // fresh tokens. Opening the same pollEndpoint link twice triggers an expiration - // message by the server (security, intended design). + // Note: All startup code is in openBrowser() to allow reinitiate a new request with + // fresh tokens. Opening the same pollEndpoint link twice triggers an expiration + // message by the server (security, intended design). openBrowser(); } @@ -91,6 +91,7 @@ void Flow2Auth::openBrowser() _pollEndpoint = pollEndpoint; + // Start polling ConfigFile cfg; std::chrono::milliseconds polltime = cfg.remotePollInterval(); qCInfo(lcFlow2auth) << "setting remote poll timer interval to" << polltime.count() << "msec"; @@ -99,10 +100,10 @@ void Flow2Auth::openBrowser() _pollTimer.start(); - if (!QDesktopServices::openUrl(authorisationLink())) { - // TODO: Ask the user to copy and open the link instead of failing here! - - // We cannot open the browser, then we claim we don't support OAuth. + // Try to open Browser + if (!QDesktopServices::openUrl(authorisationLink())) { + // We cannot open the browser, then we claim we don't support Flow2Auth. + // Our UI callee should ask the user to copy and open the link. emit result(NotSupported, QString()); } }); @@ -110,7 +111,7 @@ void Flow2Auth::openBrowser() void Flow2Auth::slotPollTimerTimeout() { - _pollTimer.stop(); + _pollTimer.stop(); // Step 2: Poll QNetworkRequest req; @@ -122,6 +123,7 @@ void Flow2Auth::slotPollTimerTimeout() auto job = _account->sendRequest("POST", _pollEndpoint, req, requestBody); job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); + QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) { auto jsonData = reply->readAll(); QJsonParseError jsonParseError; @@ -149,15 +151,25 @@ void Flow2Auth::slotPollTimerTimeout() } qCDebug(lcFlow2auth) << "Error when polling for the appPassword" << json << errorReason; - _pollTimer.start(); + // Forget sensitive data + appPassword.clear(); + loginName.clear(); + + // Failed: poll again + _pollTimer.start(); return; } - qCInfo(lcFlow2auth) << "Success getting the appPassword for user: " << loginName << " on server: " << serverUrl; - - _account->setUrl(serverUrl); + // Success + qCInfo(lcFlow2auth) << "Success getting the appPassword for user: " << loginName << ", server: " << serverUrl.toString(); - emit result(LoggedIn, loginName, appPassword); + _account->setUrl(serverUrl); + + emit result(LoggedIn, loginName, appPassword); + + // Forget sensitive data + appPassword.clear(); + loginName.clear(); }); } diff --git a/src/gui/creds/flow2auth.h b/src/gui/creds/flow2auth.h index 188865a0d..b53834a11 100644 --- a/src/gui/creds/flow2auth.h +++ b/src/gui/creds/flow2auth.h @@ -49,10 +49,9 @@ public: signals: /** * The state has changed. - * when logged in, token has the value of the token. + * when logged in, appPassword has the value of the app password. */ - // TODO: Remove refreshToken - void result(Flow2Auth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString()); + void result(Flow2Auth::Result result, const QString &user = QString(), const QString &appPassword = QString()); private slots: void slotPollTimerTimeout(); diff --git a/src/gui/creds/webflowcredentials.cpp b/src/gui/creds/webflowcredentials.cpp index 70a709470..ecc0e61d9 100644 --- a/src/gui/creds/webflowcredentials.cpp +++ b/src/gui/creds/webflowcredentials.cpp @@ -114,12 +114,17 @@ void WebFlowCredentials::fetchFromKeychain() { } void WebFlowCredentials::askFromUser() { - _askDialog = new WebFlowCredentialsDialog(); + // LoginFlowV2 > WebViewFlow > OAuth > Shib > Basic + bool useFlow2 = (_account->serverVersionInt() >= Account::makeServerVersion(16, 0, 0)); - QUrl url = _account->url(); - QString path = url.path() + "/index.php/login/flow"; - url.setPath(path); - _askDialog->setUrl(url); + _askDialog = new WebFlowCredentialsDialog(_account, useFlow2); + + if (!useFlow2) { + QUrl url = _account->url(); + QString path = url.path() + "/index.php/login/flow"; + url.setPath(path); + _askDialog->setUrl(url); + } QString msg = tr("You have been logged out of %1 as user %2. Please login again") .arg(_account->displayName(), _user); @@ -142,10 +147,12 @@ void WebFlowCredentials::slotAskFromUserCredentialsProvided(const QString &user, .arg(_user); _askDialog->setError(msg); - QUrl url = _account->url(); - QString path = url.path() + "/index.php/login/flow"; - url.setPath(path); - _askDialog->setUrl(url); + if (!_askDialog->isUsingFlow2()) { + QUrl url = _account->url(); + QString path = url.path() + "/index.php/login/flow"; + url.setPath(path); + _askDialog->setUrl(url); + } return; } diff --git a/src/gui/creds/webflowcredentialsdialog.cpp b/src/gui/creds/webflowcredentialsdialog.cpp index 2d22ba06e..30a327afa 100644 --- a/src/gui/creds/webflowcredentialsdialog.cpp +++ b/src/gui/creds/webflowcredentialsdialog.cpp @@ -3,13 +3,21 @@ #include #include +#include "theme.h" +#include "wizard/owncloudwizardcommon.h" #include "wizard/webview.h" +#include "wizard/flow2authwidget.h" namespace OCC { -WebFlowCredentialsDialog::WebFlowCredentialsDialog(QWidget *parent) - : QDialog(parent) +WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlow2, QWidget *parent) + : QDialog(parent), + _useFlow2(useFlow2), + _flow2AuthWidget(nullptr), + _webView(nullptr) { + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + _layout = new QVBoxLayout(this); //QString msg = tr("You have been logged out of %1 as user %2, please login again") @@ -17,28 +25,44 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(QWidget *parent) _infoLabel = new QLabel(); _layout->addWidget(_infoLabel); - _webView = new WebView(); - _layout->addWidget(_webView); + if (_useFlow2) { + _flow2AuthWidget = new Flow2AuthWidget(account); + _layout->addWidget(_flow2AuthWidget); + + connect(_flow2AuthWidget, &Flow2AuthWidget::urlCatched, this, &WebFlowCredentialsDialog::urlCatched); + } else { + _webView = new WebView(); + _layout->addWidget(_webView); + + connect(_webView, &WebView::urlCatched, this, &WebFlowCredentialsDialog::urlCatched); + } _errorLabel = new QLabel(); _errorLabel->hide(); _layout->addWidget(_errorLabel); - setLayout(_layout); + Theme *theme = Theme::instance(); + WizardCommon::initErrorLabel(_errorLabel); - connect(_webView, &WebView::urlCatched, this, &WebFlowCredentialsDialog::urlCatched); + setLayout(_layout); } void WebFlowCredentialsDialog::closeEvent(QCloseEvent* e) { Q_UNUSED(e); - // Force calling WebView::~WebView() earlier so that _profile and _page are - // deleted in the correct order. - delete _webView; + if (_webView) { + // Force calling WebView::~WebView() earlier so that _profile and _page are + // deleted in the correct order. + delete _webView; + } + + if (_flow2AuthWidget) + delete _flow2AuthWidget; } void WebFlowCredentialsDialog::setUrl(const QUrl &url) { - _webView->setUrl(url); + if (_webView) + _webView->setUrl(url); } void WebFlowCredentialsDialog::setInfo(const QString &msg) { @@ -46,6 +70,11 @@ void WebFlowCredentialsDialog::setInfo(const QString &msg) { } void WebFlowCredentialsDialog::setError(const QString &error) { + if (_useFlow2 && _flow2AuthWidget) { + _flow2AuthWidget->setError(error); + return; + } + if (error.isEmpty()) { _errorLabel->hide(); } else { diff --git a/src/gui/creds/webflowcredentialsdialog.h b/src/gui/creds/webflowcredentialsdialog.h index 9849ee3a4..4e1f21c0d 100644 --- a/src/gui/creds/webflowcredentialsdialog.h +++ b/src/gui/creds/webflowcredentialsdialog.h @@ -4,23 +4,30 @@ #include #include +#include "accountfwd.h" + class QLabel; class QVBoxLayout; namespace OCC { class WebView; +class Flow2AuthWidget; class WebFlowCredentialsDialog : public QDialog { Q_OBJECT public: - WebFlowCredentialsDialog(QWidget *parent = nullptr); + WebFlowCredentialsDialog(Account *account, bool useFlow2, QWidget *parent = nullptr); void setUrl(const QUrl &url); void setInfo(const QString &msg); void setError(const QString &error); + const bool isUsingFlow2() const { + return _useFlow2; + } + protected: void closeEvent(QCloseEvent * e) override; @@ -28,7 +35,11 @@ signals: void urlCatched(const QString user, const QString pass, const QString host); private: + bool _useFlow2; + + Flow2AuthWidget *_flow2AuthWidget; WebView *_webView; + QLabel *_errorLabel; QLabel *_infoLabel; QVBoxLayout *_layout; diff --git a/src/gui/wizard/flow2authcredspage.cpp b/src/gui/wizard/flow2authcredspage.cpp index e5914a10b..91e5415bb 100644 --- a/src/gui/wizard/flow2authcredspage.cpp +++ b/src/gui/wizard/flow2authcredspage.cpp @@ -81,17 +81,25 @@ void OCC::Flow2AuthCredsPage::cleanupPage() // The next or back button was activated, show the wizard again wizard()->show(); _asyncAuth.reset(); + + // Forget sensitive data + _appPassword.clear(); + _user.clear(); } void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &user, - const QString &token, const QString &refreshToken) + const QString &appPassword) { switch (r) { case Flow2Auth::NotSupported: { - /* Flow2Auth not supported (can't open browser), fallback to HTTP credentials */ - OwncloudWizard *ocWizard = qobject_cast(wizard()); + /* Flow2Auth not supported (can't open browser) */ + _ui.errorLabel->setText(tr("Unable to open the Browser, please copy the link to your Browser.")); + _ui.errorLabel->show(); + + /* Don't fallback to HTTP credentials */ + /*OwncloudWizard *ocWizard = qobject_cast(wizard()); ocWizard->back(); - ocWizard->setAuthType(DetermineAuthTypeJob::Basic); + ocWizard->setAuthType(DetermineAuthTypeJob::Basic);*/ break; } case Flow2Auth::Error: @@ -100,9 +108,8 @@ void Flow2AuthCredsPage::asyncAuthResult(Flow2Auth::Result r, const QString &use wizard()->show(); break; case Flow2Auth::LoggedIn: { - _token = token; _user = user; - _refreshToken = refreshToken; + _appPassword = appPassword; OwncloudWizard *ocWizard = qobject_cast(wizard()); Q_ASSERT(ocWizard); emit connectToOCUrl(ocWizard->account()->url().toString()); @@ -125,7 +132,7 @@ AbstractCredentials *Flow2AuthCredsPage::getCredentials() const { OwncloudWizard *ocWizard = qobject_cast(wizard()); Q_ASSERT(ocWizard); - return new HttpCredentialsGui(_user, _token, _refreshToken, + return new HttpCredentialsGui(_user, _appPassword, QString(), ocWizard->_clientSslCertificate, ocWizard->_clientSslKey); } diff --git a/src/gui/wizard/flow2authcredspage.h b/src/gui/wizard/flow2authcredspage.h index 85f1a0a9f..bffcf68b3 100644 --- a/src/gui/wizard/flow2authcredspage.h +++ b/src/gui/wizard/flow2authcredspage.h @@ -46,16 +46,14 @@ public: bool isComplete() const override; public Q_SLOTS: - void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &token, - const QString &reniewToken); + void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &appPassword); signals: void connectToOCUrl(const QString &); public: QString _user; - QString _token; - QString _refreshToken; + QString _appPassword; QScopedPointer _asyncAuth; Ui_Flow2AuthCredsPage _ui; }; diff --git a/src/gui/wizard/flow2authwidget.cpp b/src/gui/wizard/flow2authwidget.cpp new file mode 100644 index 000000000..300b2e83d --- /dev/null +++ b/src/gui/wizard/flow2authwidget.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (C) by Michael Schuster + * + * 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 "flow2authwidget.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "common/utility.h" +#include "account.h" +#include "theme.h" +#include "wizard/owncloudwizardcommon.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcFlow2AuthWidget, "gui.wizard.flow2authwidget", QtInfoMsg) + + +Flow2AuthWidget::Flow2AuthWidget(Account *account, QWidget *parent) + : QWidget(parent), + _account(account), + _ui() +{ + _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); + + connect(_ui.openLinkButton, &QCommandLinkButton::clicked, [this] { + _ui.errorLabel->hide(); + if (_asyncAuth) + _asyncAuth->openBrowser(); + }); + _ui.openLinkButton->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(_ui.openLinkButton, &QWidget::customContextMenuRequested, [this](const QPoint &pos) { + auto menu = new QMenu(_ui.openLinkButton); + menu->addAction(tr("Copy link to clipboard"), this, [this] { + if (_asyncAuth) + QApplication::clipboard()->setText(_asyncAuth->authorisationLink().toString(QUrl::FullyEncoded)); + }); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(_ui.openLinkButton->mapToGlobal(pos)); + }); + + _asyncAuth.reset(new Flow2Auth(_account, this)); + connect(_asyncAuth.data(), &Flow2Auth::result, this, &Flow2AuthWidget::asyncAuthResult, Qt::QueuedConnection); + _asyncAuth->start(); +} + +void Flow2AuthWidget::asyncAuthResult(Flow2Auth::Result r, const QString &user, + const QString &appPassword) +{ + switch (r) { + case Flow2Auth::NotSupported: + /* Flow2Auth can't open browser */ + _ui.errorLabel->setText(tr("Unable to open the Browser, please copy the link to your Browser.")); + _ui.errorLabel->show(); + break; + case Flow2Auth::Error: + /* Error while getting the access token. (Timeout, or the server did not accept our client credentials */ + _ui.errorLabel->show(); + break; + case Flow2Auth::LoggedIn: { + _user = user; + _appPassword = appPassword; + emit urlCatched(_user, _appPassword, QString()); + break; + } + } +} + +void Flow2AuthWidget::setError(const QString &error) { + if (error.isEmpty()) { + _ui.errorLabel->hide(); + } else { + _ui.errorLabel->setText(error); + _ui.errorLabel->show(); + } +} + +Flow2AuthWidget::~Flow2AuthWidget() { + _asyncAuth.reset(); + + // Forget sensitive data + _appPassword.clear(); + _user.clear(); +} + +} diff --git a/src/gui/wizard/flow2authwidget.h b/src/gui/wizard/flow2authwidget.h new file mode 100644 index 000000000..cf04d9193 --- /dev/null +++ b/src/gui/wizard/flow2authwidget.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) by Michael Schuster + * + * 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. + */ + +#ifndef FLOW2AUTHWIDGET_H +#define FLOW2AUTHWIDGET_H + +#include +#include + +#include "creds/flow2auth.h" + +#include "ui_flow2authwidget.h" + +namespace OCC { + +class Flow2AuthWidget : public QWidget +{ + Q_OBJECT +public: + Flow2AuthWidget(Account *account, QWidget *parent = nullptr); + virtual ~Flow2AuthWidget(); + + void setError(const QString &error); + +public Q_SLOTS: + void asyncAuthResult(Flow2Auth::Result, const QString &user, const QString &appPassword); + +signals: + void urlCatched(const QString user, const QString pass, const QString host); + +private: + Account *_account; + QString _user; + QString _appPassword; + QScopedPointer _asyncAuth; + Ui_Flow2AuthWidget _ui; +}; + +} + +#endif // FLOW2AUTHWIDGET_H diff --git a/src/gui/wizard/flow2authwidget.ui b/src/gui/wizard/flow2authwidget.ui new file mode 100644 index 000000000..e73ae6a1d --- /dev/null +++ b/src/gui/wizard/flow2authwidget.ui @@ -0,0 +1,99 @@ + + + Flow2AuthWidget + + + + 0 + 0 + 500 + 280 + + + + + 0 + 0 + + + + + 500 + 280 + + + + Form + + + + + + TextLabel + + + Qt::RichText + + + Qt::AlignCenter + + + true + + + + + + + Please switch to your browser to proceed. + + + true + + + + + + + An error occurred while connecting. Please try again. + + + Qt::PlainText + + + + + + + Re-open Browser (or right-click to copy link) + + + + + + + Qt::Vertical + + + + 20 + 127 + + + + + + + + TextLabel + + + Qt::RichText + + + + + + + + From 50cd6af394f666065f386d1d55e7d6a90e0d9c9e Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Mon, 26 Aug 2019 20:04:23 +0200 Subject: [PATCH 8/9] Build a webflowcredentials Signed-off-by: Roeland Jago Douma --- src/gui/wizard/flow2authcredspage.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/gui/wizard/flow2authcredspage.cpp b/src/gui/wizard/flow2authcredspage.cpp index 91e5415bb..b52095f0d 100644 --- a/src/gui/wizard/flow2authcredspage.cpp +++ b/src/gui/wizard/flow2authcredspage.cpp @@ -23,8 +23,8 @@ #include "cookiejar.h" #include "wizard/owncloudwizardcommon.h" #include "wizard/owncloudwizard.h" -#include "creds/httpcredentialsgui.h" #include "creds/credentialsfactory.h" +#include "creds/webflowcredentials.h" namespace OCC { @@ -132,8 +132,12 @@ AbstractCredentials *Flow2AuthCredsPage::getCredentials() const { OwncloudWizard *ocWizard = qobject_cast(wizard()); Q_ASSERT(ocWizard); - return new HttpCredentialsGui(_user, _appPassword, QString(), - ocWizard->_clientSslCertificate, ocWizard->_clientSslKey); + return new WebFlowCredentials( + _user, + _appPassword, + ocWizard->_clientSslCertificate, + ocWizard->_clientSslKey + ); } bool Flow2AuthCredsPage::isComplete() const From 302ca0e04e4527d4eb08c78999605cffe7efacaa Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Mon, 26 Aug 2019 20:41:14 +0200 Subject: [PATCH 9/9] Fix some compiler warnings Signed-off-by: Roeland Jago Douma --- src/gui/creds/webflowcredentialsdialog.cpp | 3 +-- src/gui/creds/webflowcredentialsdialog.h | 2 +- src/gui/wizard/owncloudwizard.h | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/gui/creds/webflowcredentialsdialog.cpp b/src/gui/creds/webflowcredentialsdialog.cpp index 30a327afa..9971f8f34 100644 --- a/src/gui/creds/webflowcredentialsdialog.cpp +++ b/src/gui/creds/webflowcredentialsdialog.cpp @@ -41,14 +41,13 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo _errorLabel->hide(); _layout->addWidget(_errorLabel); - Theme *theme = Theme::instance(); WizardCommon::initErrorLabel(_errorLabel); setLayout(_layout); } void WebFlowCredentialsDialog::closeEvent(QCloseEvent* e) { - Q_UNUSED(e); + Q_UNUSED(e) if (_webView) { // Force calling WebView::~WebView() earlier so that _profile and _page are diff --git a/src/gui/creds/webflowcredentialsdialog.h b/src/gui/creds/webflowcredentialsdialog.h index 4e1f21c0d..a540f0edb 100644 --- a/src/gui/creds/webflowcredentialsdialog.h +++ b/src/gui/creds/webflowcredentialsdialog.h @@ -24,7 +24,7 @@ public: void setInfo(const QString &msg); void setError(const QString &error); - const bool isUsingFlow2() const { + bool isUsingFlow2() const { return _useFlow2; } diff --git a/src/gui/wizard/owncloudwizard.h b/src/gui/wizard/owncloudwizard.h index 0b7b715a8..c37bfd97e 100644 --- a/src/gui/wizard/owncloudwizard.h +++ b/src/gui/wizard/owncloudwizard.h @@ -104,11 +104,11 @@ private: #ifndef NO_SHIBBOLETH OwncloudShibbolethCredsPage *_shibbolethCredsPage; #endif + Flow2AuthCredsPage *_flow2CredsPage; OwncloudAdvancedSetupPage *_advancedSetupPage; OwncloudWizardResultPage *_resultPage; AbstractCredentialsWizardPage *_credentialsPage; WebViewPage *_webViewPage; - Flow2AuthCredsPage *_flow2CredsPage; QStringList _setupLog;