Login Flow V2: 1st test-implementation
authorMichael Schuster <michael@schuster.ms>
Sat, 17 Aug 2019 00:37:04 +0000 (02:37 +0200)
committerRoeland Jago Douma <roeland@famdouma.nl>
Mon, 26 Aug 2019 18:03:15 +0000 (20:03 +0200)
Signed-off-by: Michael Schuster <michael@schuster.ms>
14 files changed:
src/gui/CMakeLists.txt
src/gui/connectionvalidator.cpp
src/gui/creds/flow2auth.cpp [new file with mode: 0644]
src/gui/creds/flow2auth.h [new file with mode: 0644]
src/gui/owncloudsetupwizard.cpp
src/gui/wizard/flow2authcredspage.cpp [new file with mode: 0644]
src/gui/wizard/flow2authcredspage.h [new file with mode: 0644]
src/gui/wizard/flow2authcredspage.ui [new file with mode: 0644]
src/gui/wizard/owncloudsetuppage.cpp
src/gui/wizard/owncloudwizard.cpp
src/gui/wizard/owncloudwizard.h
src/gui/wizard/owncloudwizardcommon.h
src/libsync/networkjobs.cpp
src/libsync/networkjobs.h

index 167d1727fbdc7dfaef8827b376da73dfb6cbfe51..0fb5970adb8d38d0bb2a5d3b837e095d46909061 100644 (file)
@@ -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
index b2f04d9b5fcd9eedb47850f15291229fadcebe71..27b1a4b9aa738c94ce78bda36bf4cf74e55db98b 100644 (file)
@@ -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 (file)
index 0000000..0bf5041
--- /dev/null
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
+ * Copyright (C) by Michael Schuster <michael@nextcloud.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/flow2auth.h"
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QJsonArray>
+#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: <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 (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(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("<h1>Wrong user</h1>"
+                                 "<p>You logged-in with user <em>%1</em>, but must login with user <em>%2</em>.<br>"
+                                 "Please log out of %3 in another tab, then <a href='%4'>click here</a> "
+                                 "and log in as user %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);*/
+    });
+#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<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
+                QRegExp rx("^GET /\\?code=([a-zA-Z0-9]+)[& ]"); // Match a  /?code=...  URL
+                if (rx.indexIn(peek) != 0) {
+                    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 = 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: <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 (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(lcFlow2auth) << "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 user</h1>"
+                                             "<p>You logged-in with user <em>%1</em>, but must login with user <em>%2</em>.<br>"
+                                             "Please log out of %3 in another tab, then <a href='%4'>click here</a> "
+                                             "and log in as user %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);
+                });
+            });
+        }
+    });
+#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: <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 (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");
+            }
+            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 (file)
index 0000000..f979233
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
+ * Copyright (C) by Michael Schuster <michael@nextcloud.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 <QUrl>
+#include <QTimer>
+#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
index 2a037ff844575418ffdc18b6e241c8b782d4cc9d..653894874c7c04a704ec2e566c471b290de6b14c 100644 (file)
@@ -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 (file)
index 0000000..2c0000e
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
+ * Copyright (C) by Michael Schuster <michael@nextcloud.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/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<OwncloudWizard *>(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<OwncloudWizard *>(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<OwncloudWizard *>(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<OwncloudWizard *>(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 (file)
index 0000000..85f1a0a
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
+ * Copyright (C) by Michael Schuster <michael@nextcloud.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/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<Flow2Auth> _asyncAuth;
+    Ui_Flow2AuthCredsPage _ui;
+};
+
+} // namespace OCC
diff --git a/src/gui/wizard/flow2authcredspage.ui b/src/gui/wizard/flow2authcredspage.ui
new file mode 100644 (file)
index 0000000..04c1d72
--- /dev/null
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Flow2AuthCredsPage</class>
+ <widget class="QWidget" name="Flow2AuthCredsPage">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>424</width>
+    <height>373</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>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 (or right-click to 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>
index 4af0ca93525aa80b1c8fe8cc91c76c621464ce75..8cfa158d4c8564fb1997db4928ad435765cf4559 100644 (file)
@@ -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:
index 357b4e005192731e295df562c8a0b4fcebbb32a9..e4dda0fbd242a9121d53f9563b935872ac03e8ae 100644 (file)
@@ -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);
index 1429709f14ec08167fbc18d393bcf98b9eb5301e..0b7b715a8d9ebc05ecce3fc2173b6f58f14f76ce 100644 (file)
@@ -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;
 
index c3174d15ab6a6ef0ad771dc17406f9703148a9dc..d1f7c08be07a04ee21bb5a1315d05c2c4510c787 100644 (file)
@@ -38,6 +38,7 @@ namespace WizardCommon {
         Page_HttpCreds,
         Page_ShibbolethCreds,
         Page_OAuthCreds,
+        Page_Flow2AuthCreds,
         Page_WebView,
         Page_AdvancedSetup,
         Page_Result
index cf59b5ab6f12aa1cbe8ac559f34aad2aac508805..c1788951918d43ae2e164abff76aff757619e603 100644 (file)
@@ -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();
index c2fefd6113b64bb049c6375723f5d202dff39bc5..51d8df63dc6fd418a8e18030ce7d5171345d4435 100644 (file)
@@ -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);