Merge pull request #1384 from nextcloud/login-flow-v2

Login flow v2
This commit is contained in:
Michael Schuster 2019-08-26 21:48:40 +02:00 committed by GitHub
commit 18404a128b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 909 additions and 29 deletions

View file

@ -38,6 +38,8 @@ set(client_UI_SRCS
wizard/owncloudconnectionmethoddialog.ui
wizard/owncloudhttpcredspage.ui
wizard/owncloudoauthcredspage.ui
wizard/flow2authcredspage.ui
wizard/flow2authwidget.ui
wizard/owncloudsetupnocredspage.ui
wizard/owncloudwizardresultpage.ui
wizard/webview.ui
@ -103,6 +105,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 +114,8 @@ set(client_SRCS
wizard/owncloudconnectionmethoddialog.cpp
wizard/owncloudhttpcredspage.cpp
wizard/owncloudoauthcredspage.cpp
wizard/flow2authcredspage.cpp
wizard/flow2authwidget.cpp
wizard/owncloudsetuppage.cpp
wizard/owncloudwizardcommon.cpp
wizard/owncloudwizard.cpp

176
src/gui/creds/flow2auth.cpp Normal file
View file

@ -0,0 +1,176 @@
/*
* 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 <QTimer>
#include <QBuffer>
#include "account.h"
#include "creds/flow2auth.h"
#include <QJsonObject>
#include <QJsonDocument>
#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()
{
// 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();
QString pollToken = json.value("poll").toObject().value("token").toString();
QString pollEndpoint = json.value("poll").toObject().value("endpoint").toString();
QUrl loginUrl = json["login"].toString();
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;
// Start polling
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();
// 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());
}
});
}
void Flow2Auth::slotPollTimerTimeout()
{
_pollTimer.stop();
// 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;
// Forget sensitive data
appPassword.clear();
loginName.clear();
// Failed: poll again
_pollTimer.start();
return;
}
// Success
qCInfo(lcFlow2auth) << "Success getting the appPassword for user: " << loginName << ", server: " << serverUrl.toString();
_account->setUrl(serverUrl);
emit result(LoggedIn, loginName, appPassword);
// Forget sensitive data
appPassword.clear();
loginName.clear();
});
}
} // namespace OCC

68
src/gui/creds/flow2auth.h Normal file
View file

@ -0,0 +1,68 @@
/*
* 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, grants and fetches the access token via Login Flow v2
*
* See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
*
*/
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();
void openBrowser();
QUrl authorisationLink() const;
signals:
/**
* The state has changed.
* when logged in, appPassword has the value of the app password.
*/
void result(Flow2Auth::Result result, const QString &user = QString(), const QString &appPassword = QString());
private slots:
void slotPollTimerTimeout();
private:
Account *_account;
QUrl _loginUrl;
QString _pollToken;
QString _pollEndpoint;
QTimer _pollTimer;
};
} // namespace OCC

View file

@ -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()) {
@ -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));
_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);
if (!_askDialog->isUsingFlow2()) {
QUrl url = _account->url();
QString path = url.path() + "/index.php/login/flow";
url.setPath(path);
_askDialog->setUrl(url);
}
return;
}

View file

@ -3,13 +3,21 @@
#include <QVBoxLayout>
#include <QLabel>
#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,27 +25,42 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(QWidget *parent)
_infoLabel = new QLabel();
_layout->addWidget(_infoLabel);
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);
WizardCommon::initErrorLabel(_errorLabel);
connect(_webView, &WebView::urlCatched, this, &WebFlowCredentialsDialog::urlCatched);
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
// deleted in the correct order.
delete _webView;
}
if (_flow2AuthWidget)
delete _flow2AuthWidget;
}
void WebFlowCredentialsDialog::setUrl(const QUrl &url) {
if (_webView)
_webView->setUrl(url);
}
@ -46,6 +69,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 {

View file

@ -4,23 +4,30 @@
#include <QDialog>
#include <QUrl>
#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);
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;

View file

@ -408,7 +408,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));

View file

@ -0,0 +1,148 @@
/*
* 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/credentialsfactory.h"
#include "creds/webflowcredentials.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();
// Don't hide the wizard (avoid user confusion)!
//wizard()->hide();
}
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 &appPassword)
{
switch (r) {
case Flow2Auth::NotSupported: {
/* 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<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: {
_user = user;
_appPassword = appPassword;
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 WebFlowCredentials(
_user,
_appPassword,
ocWizard->_clientSslCertificate,
ocWizard->_clientSslKey
);
}
bool Flow2AuthCredsPage::isComplete() const
{
return false; /* We can never go forward manually */
}
} // namespace OCC

View file

@ -0,0 +1,61 @@
/*
* 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 &appPassword);
signals:
void connectToOCUrl(const QString &);
public:
QString _user;
QString _appPassword;
QScopedPointer<Flow2Auth> _asyncAuth;
Ui_Flow2AuthCredsPage _ui;
};
} // namespace OCC

View file

@ -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>

View file

@ -0,0 +1,113 @@
/*
* 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 "flow2authwidget.h"
#include <QDesktopServices>
#include <QProgressBar>
#include <QLoggingCategory>
#include <QLocale>
#include <QMessageBox>
#include <QMenu>
#include <QClipboard>
#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();
}
}

View file

@ -0,0 +1,52 @@
/*
* 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.
*/
#ifndef FLOW2AUTHWIDGET_H
#define FLOW2AUTHWIDGET_H
#include <QUrl>
#include <QWidget>
#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<Flow2Auth> _asyncAuth;
Ui_Flow2AuthWidget _ui;
};
}
#endif // FLOW2AUTHWIDGET_H

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Flow2AuthWidget</class>
<widget class="QWidget" name="Flow2AuthWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>280</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>500</width>
<height>280</height>
</size>
</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>

View file

@ -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()

View 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:

View 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);

View file

@ -40,6 +40,7 @@ class OwncloudWizardResultPage;
class AbstractCredentials;
class AbstractCredentialsWizardPage;
class WebViewPage;
class Flow2AuthCredsPage;
/**
* @brief The OwncloudWizard class
@ -103,6 +104,7 @@ private:
#ifndef NO_SHIBBOLETH
OwncloudShibbolethCredsPage *_shibbolethCredsPage;
#endif
Flow2AuthCredsPage *_flow2CredsPage;
OwncloudAdvancedSetupPage *_advancedSetupPage;
OwncloudWizardResultPage *_resultPage;
AbstractCredentialsWizardPage *_credentialsPage;

View file

@ -38,6 +38,7 @@ namespace WizardCommon {
Page_HttpCreds,
Page_ShibbolethCreds,
Page_OAuthCreds,
Page_Flow2AuthCreds,
Page_WebView,
Page_AdvancedSetup,
Page_Result

View file

@ -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;

View 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();

View 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);