mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-24 05:55:59 +03:00
Authentication with OAuth2
When the OAuth2 app (https://github.com/owncloud/oauth2) is enabled, We will open a browser and perform the OAuth2 authentication Issue: #4798 and https://github.com/owncloud/platform/issues/17
This commit is contained in:
parent
8a19f2ac65
commit
3d93527a8e
16 changed files with 606 additions and 20 deletions
|
@ -93,11 +93,13 @@ set(client_SRCS
|
|||
servernotificationhandler.cpp
|
||||
creds/credentialsfactory.cpp
|
||||
creds/httpcredentialsgui.cpp
|
||||
creds/oauth.cpp
|
||||
wizard/postfixlineedit.cpp
|
||||
wizard/abstractcredswizardpage.cpp
|
||||
wizard/owncloudadvancedsetuppage.cpp
|
||||
wizard/owncloudconnectionmethoddialog.cpp
|
||||
wizard/owncloudhttpcredspage.cpp
|
||||
wizard/owncloudoauthcredspage.cpp
|
||||
wizard/owncloudsetuppage.cpp
|
||||
wizard/owncloudwizardcommon.cpp
|
||||
wizard/owncloudwizard.cpp
|
||||
|
|
|
@ -15,9 +15,14 @@
|
|||
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QDesktopServices>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QBuffer>
|
||||
#include "creds/httpcredentialsgui.h"
|
||||
#include "theme.h"
|
||||
#include "account.h"
|
||||
#include <QMessageBox>
|
||||
|
||||
using namespace QKeychain;
|
||||
|
||||
|
@ -25,11 +30,61 @@ namespace OCC {
|
|||
|
||||
void HttpCredentialsGui::askFromUser()
|
||||
{
|
||||
// The rest of the code assumes that this will be done asynchronously
|
||||
QMetaObject::invokeMethod(this, "askFromUserAsync", Qt::QueuedConnection);
|
||||
_password = QString(); // So our QNAM does not add any auth
|
||||
|
||||
// First, we will send a call to the webdav endpoint to check what kind of auth we need.
|
||||
auto reply = _account->sendRequest("GET", _account->davUrl());
|
||||
QTimer::singleShot(30 * 1000, reply, &QNetworkReply::abort);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
||||
reply->deleteLater();
|
||||
if (reply->rawHeader("WWW-Authenticate").contains("Bearer ")) {
|
||||
// OAuth
|
||||
_asyncAuth.reset(new OAuth(_account, this));
|
||||
connect(_asyncAuth.data(), &OAuth::result,
|
||||
this, &HttpCredentialsGui::asyncAuthResult);
|
||||
_asyncAuth->start();
|
||||
} else if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||
// Show the dialog
|
||||
// We will re-enter the event loop, so better wait the next iteration
|
||||
QMetaObject::invokeMethod(this, "showDialog", Qt::QueuedConnection);
|
||||
} else {
|
||||
// Network error?
|
||||
emit asked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void HttpCredentialsGui::askFromUserAsync()
|
||||
void HttpCredentialsGui::asyncAuthResult(OAuth::Result r, const QString &user,
|
||||
const QString &token, const QString &refreshToken)
|
||||
{
|
||||
switch (r) {
|
||||
case OAuth::NotSupported:
|
||||
// We will re-enter the event loop, so better wait the next iteration
|
||||
QMetaObject::invokeMethod(this, "showDialog", Qt::QueuedConnection);
|
||||
_asyncAuth.reset(0);
|
||||
return;
|
||||
case OAuth::Error:
|
||||
_asyncAuth.reset(0);
|
||||
emit asked();
|
||||
return;
|
||||
case OAuth::LoggedIn:
|
||||
break;
|
||||
}
|
||||
|
||||
if (_user != user) {
|
||||
QMessageBox::warning(nullptr, tr("Login Error"), tr("You must sign in as user %1").arg(_user));
|
||||
_asyncAuth->openBrowser();
|
||||
return;
|
||||
}
|
||||
_password = token;
|
||||
_refreshToken = refreshToken;
|
||||
_ready = true;
|
||||
persist();
|
||||
_asyncAuth.reset(0);
|
||||
emit asked();
|
||||
}
|
||||
|
||||
void HttpCredentialsGui::showDialog()
|
||||
{
|
||||
QString msg = tr("Please enter %1 password:<br>"
|
||||
"<br>"
|
||||
|
@ -87,6 +142,4 @@ QString HttpCredentialsGui::requestAppPasswordText(const Account *account)
|
|||
return tr("<a href=\"%1\">Click here</a> to request an app password from the web interface.")
|
||||
.arg(account->url().toString() + path);
|
||||
}
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
#pragma once
|
||||
#include "creds/httpcredentials.h"
|
||||
#include "creds/oauth.h"
|
||||
#include <QPointer>
|
||||
#include <QTcpServer>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
@ -34,10 +37,26 @@ public:
|
|||
: HttpCredentials(user, password, certificate, key)
|
||||
{
|
||||
}
|
||||
void askFromUser() Q_DECL_OVERRIDE;
|
||||
Q_INVOKABLE void askFromUserAsync();
|
||||
HttpCredentialsGui(const QString &user, const QString &password, const QString &refreshToken,
|
||||
const QSslCertificate &certificate, const QSslKey &key)
|
||||
: HttpCredentials(user, password, certificate, key)
|
||||
{
|
||||
_refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will query the server and either uses OAuth via _asyncAuth->start()
|
||||
* or call showDialog to ask the password
|
||||
*/
|
||||
Q_INVOKABLE void askFromUser() Q_DECL_OVERRIDE;
|
||||
|
||||
static QString requestAppPasswordText(const Account *account);
|
||||
private slots:
|
||||
void asyncAuthResult(OAuth::Result, const QString &user, const QString &accessToken, const QString &refreshToken);
|
||||
void showDialog();
|
||||
|
||||
private:
|
||||
QScopedPointer<OAuth, QScopedPointerObjectDeleteLater<OAuth>> _asyncAuth;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
|
122
src/gui/creds/oauth.cpp
Normal file
122
src/gui/creds/oauth.cpp
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include "account.h"
|
||||
#include "creds/oauth.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include "theme.h"
|
||||
|
||||
|
||||
namespace OCC {
|
||||
|
||||
OAuth::~OAuth()
|
||||
{
|
||||
}
|
||||
|
||||
static void httpReplyAndClose(QTcpSocket *socket, const char *code, const char *html)
|
||||
{
|
||||
socket->write("HTTP/1.1 ");
|
||||
socket->write(code);
|
||||
socket->write("\r\nContent-Type: text/html\r\nConnection: close\r\nContent-Length: ");
|
||||
socket->write(QByteArray::number(qstrlen(html)));
|
||||
socket->write("\r\n\r\n");
|
||||
socket->write(html);
|
||||
socket->disconnectFromHost();
|
||||
}
|
||||
|
||||
void OAuth::start()
|
||||
{
|
||||
// Listen on the socket to get a port which will be used in the redirect_uri
|
||||
if (!_server.listen(QHostAddress::LocalHost)) {
|
||||
emit result(NotSupported, QString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!openBrowser())
|
||||
return;
|
||||
|
||||
QObject::connect(&_server, &QTcpServer::newConnection, this, [this] {
|
||||
while (QTcpSocket *socket = _server.nextPendingConnection()) {
|
||||
QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
|
||||
QObject::connect(socket, &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;
|
||||
}
|
||||
|
||||
// TODO: add redirect to the page on the server
|
||||
httpReplyAndClose(socket, "200 OK", "<h1>Login Successfull</h1><p>You can close this window.</p>");
|
||||
|
||||
QString code = rx.cap(1); // The 'code' is the first capture of the regexp
|
||||
|
||||
QUrl requestToken(_account->url().toString()
|
||||
+ QLatin1String("/index.php/apps/oauth2/api/v1/token?grant_type=authorization_code&code=")
|
||||
+ code
|
||||
+ QLatin1String("&redirect_uri=http://localhost:") + QString::number(_server.serverPort()));
|
||||
requestToken.setUserName(Theme::instance()->oauthClientId());
|
||||
requestToken.setPassword(Theme::instance()->oauthClientSecret());
|
||||
QNetworkRequest req;
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
auto reply = _account->sendRequest("POST", requestToken, req);
|
||||
QTimer::singleShot(30 * 1000, reply, &QNetworkReply::abort);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, 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();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|
||||
|| json.isEmpty() || refreshToken.isEmpty() || accessToken.isEmpty()
|
||||
|| json["token_type"].toString() != QLatin1String("Bearer")) {
|
||||
qDebug() << "Error when getting the accessToken" << reply->error() << json << jsonParseError.errorString();
|
||||
emit result(Error);
|
||||
return;
|
||||
}
|
||||
emit result(LoggedIn, user, accessToken, refreshToken);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
QTimer::singleShot(5 * 60 * 1000, this, [this] { result(Error); });
|
||||
}
|
||||
|
||||
|
||||
bool OAuth::openBrowser()
|
||||
{
|
||||
Q_ASSERT(_server.isListening());
|
||||
auto url = QUrl(_account->url().toString()
|
||||
+ QLatin1String("/index.php/apps/oauth2/authorize?response_type=code&client_id=")
|
||||
+ Theme::instance()->oauthClientId()
|
||||
+ QLatin1String("&redirect_uri=http://localhost:") + QString::number(_server.serverPort()));
|
||||
|
||||
|
||||
if (!QDesktopServices::openUrl(url)) {
|
||||
// We cannot open the browser, then we claim we don't support OAuth.
|
||||
emit result(NotSupported, QString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
69
src/gui/creds/oauth.h
Normal file
69
src/gui/creds/oauth.h
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <QPointer>
|
||||
#include <QTcpServer>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* Job that do the authorization grant and fetch the access token
|
||||
*
|
||||
* Normal workflow:
|
||||
*
|
||||
* --> start()
|
||||
* |
|
||||
* +----> openBrowser() open the browser to the login page, redirects to http://localhost:xxx
|
||||
* |
|
||||
* +----> _server starts listening on a TCP port waiting for an HTTP request with a 'code'
|
||||
* |
|
||||
* v
|
||||
* request the access_token and the refresh_token via 'apps/oauth2/api/v1/token'
|
||||
* |
|
||||
* v
|
||||
* emit result(...)
|
||||
*
|
||||
*/
|
||||
class OAuth : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
OAuth(Account *account, QObject *parent)
|
||||
: QObject(parent)
|
||||
, _account(account)
|
||||
{
|
||||
}
|
||||
~OAuth();
|
||||
|
||||
enum Result { NotSupported,
|
||||
LoggedIn,
|
||||
Error };
|
||||
void start();
|
||||
bool openBrowser();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* The state has changed.
|
||||
* when logged in, token has the value of the token.
|
||||
*/
|
||||
void result(OAuth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString());
|
||||
|
||||
private:
|
||||
Account *_account;
|
||||
QTcpServer _server;
|
||||
};
|
||||
|
||||
|
||||
} // namespace OCC
|
|
@ -625,7 +625,11 @@ bool DetermineAuthTypeJob::finished()
|
|||
redirection.clear();
|
||||
}
|
||||
if ((reply()->error() == QNetworkReply::AuthenticationRequiredError) || redirection.isEmpty()) {
|
||||
emit authType(WizardCommon::HttpCreds);
|
||||
if (reply()->rawHeader("WWW-Authenticate").contains("Bearer ")) {
|
||||
emit authType(WizardCommon::OAuth);
|
||||
} else {
|
||||
emit authType(WizardCommon::HttpCreds);
|
||||
}
|
||||
} else if (redirection.toString().endsWith(account()->davPath())) {
|
||||
// do a new run
|
||||
_redirects++;
|
||||
|
|
103
src/gui/wizard/owncloudoauthcredspage.cpp
Normal file
103
src/gui/wizard/owncloudoauthcredspage.cpp
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include <QVariant>
|
||||
|
||||
#include "wizard/owncloudoauthcredspage.h"
|
||||
#include "theme.h"
|
||||
#include "account.h"
|
||||
#include "cookiejar.h"
|
||||
#include "wizard/owncloudwizardcommon.h"
|
||||
#include "wizard/owncloudwizard.h"
|
||||
#include "creds/httpcredentialsgui.h"
|
||||
#include "creds/credentialsfactory.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
OwncloudOAuthCredsPage::OwncloudOAuthCredsPage()
|
||||
: AbstractCredentialsWizardPage()
|
||||
, _afterInitialSetup(false)
|
||||
|
||||
{
|
||||
}
|
||||
|
||||
void OwncloudOAuthCredsPage::setVisible(bool visible)
|
||||
{
|
||||
if (!_afterInitialSetup) {
|
||||
QWizardPage::setVisible(visible);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible() == visible) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
|
||||
Q_ASSERT(ocWizard);
|
||||
ocWizard->account()->setCredentials(CredentialsFactory::create("http"));
|
||||
_asyncAuth.reset(new OAuth(ocWizard->account().data(), this));
|
||||
connect(_asyncAuth.data(), SIGNAL(result(OAuth::Result, QString, QString, QString)),
|
||||
this, SLOT(asyncAuthResult(OAuth::Result, QString, QString, QString)));
|
||||
_asyncAuth->start();
|
||||
wizard()->hide();
|
||||
} else {
|
||||
// The next or back button was activated, show the wizard again
|
||||
wizard()->show();
|
||||
}
|
||||
}
|
||||
|
||||
void OwncloudOAuthCredsPage::asyncAuthResult(OAuth::Result r, const QString &user,
|
||||
const QString &token, const QString &refreshToken)
|
||||
{
|
||||
switch (r) {
|
||||
case OAuth::NotSupported:
|
||||
case OAuth::Error:
|
||||
qWarning() << "FIXME!!!";
|
||||
break;
|
||||
case OAuth::LoggedIn: {
|
||||
_token = token;
|
||||
_user = user;
|
||||
_refreshToken = refreshToken;
|
||||
OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
|
||||
Q_ASSERT(ocWizard);
|
||||
emit connectToOCUrl(ocWizard->account()->url().toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OwncloudOAuthCredsPage::initializePage()
|
||||
{
|
||||
_afterInitialSetup = true;
|
||||
}
|
||||
|
||||
int OwncloudOAuthCredsPage::nextId() const
|
||||
{
|
||||
return WizardCommon::Page_AdvancedSetup;
|
||||
}
|
||||
|
||||
void OwncloudOAuthCredsPage::setConnected()
|
||||
{
|
||||
wizard()->show();
|
||||
}
|
||||
|
||||
AbstractCredentials *OwncloudOAuthCredsPage::getCredentials() const
|
||||
{
|
||||
OwncloudWizard *ocWizard = qobject_cast<OwncloudWizard *>(wizard());
|
||||
Q_ASSERT(ocWizard);
|
||||
return new HttpCredentialsGui(_user, _token, _refreshToken,
|
||||
ocWizard->_clientSslCertificate, ocWizard->_clientSslKey);
|
||||
}
|
||||
|
||||
} // namespace OCC
|
60
src/gui/wizard/owncloudoauthcredspage.h
Normal file
60
src/gui/wizard/owncloudoauthcredspage.h
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QNetworkCookie>
|
||||
#include <QUrl>
|
||||
#include <QPointer>
|
||||
|
||||
#include "wizard/abstractcredswizardpage.h"
|
||||
#include "accountfwd.h"
|
||||
#include "creds/oauth.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
||||
class OwncloudOAuthCredsPage : public AbstractCredentialsWizardPage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
OwncloudOAuthCredsPage();
|
||||
|
||||
AbstractCredentials *getCredentials() const Q_DECL_OVERRIDE;
|
||||
|
||||
void initializePage() Q_DECL_OVERRIDE;
|
||||
int nextId() const Q_DECL_OVERRIDE;
|
||||
void setConnected();
|
||||
|
||||
public Q_SLOTS:
|
||||
void setVisible(bool visible) Q_DECL_OVERRIDE;
|
||||
void asyncAuthResult(OAuth::Result, const QString &user, const QString &token,
|
||||
const QString &reniewToken);
|
||||
|
||||
signals:
|
||||
void connectToOCUrl(const QString &);
|
||||
|
||||
private:
|
||||
bool _afterInitialSetup;
|
||||
|
||||
public:
|
||||
QString _user;
|
||||
QString _token;
|
||||
QString _refreshToken;
|
||||
QScopedPointer<OAuth> _asyncAuth;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
|
@ -203,6 +203,8 @@ int OwncloudSetupPage::nextId() const
|
|||
{
|
||||
if (_authType == WizardCommon::HttpCreds) {
|
||||
return WizardCommon::Page_HttpCreds;
|
||||
} else if (_authType == WizardCommon::OAuth) {
|
||||
return WizardCommon::Page_OAuthCreds;
|
||||
} else {
|
||||
return WizardCommon::Page_ShibbolethCreds;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "wizard/owncloudwizard.h"
|
||||
#include "wizard/owncloudsetuppage.h"
|
||||
#include "wizard/owncloudhttpcredspage.h"
|
||||
#include "wizard/owncloudoauthcredspage.h"
|
||||
#ifndef NO_SHIBBOLETH
|
||||
#include "wizard/owncloudshibbolethcredspage.h"
|
||||
#endif
|
||||
|
@ -42,12 +43,11 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
|
|||
, _account(0)
|
||||
, _setupPage(new OwncloudSetupPage(this))
|
||||
, _httpCredsPage(new OwncloudHttpCredsPage(this))
|
||||
,
|
||||
, _browserCredsPage(new OwncloudOAuthCredsPage)
|
||||
#ifndef NO_SHIBBOLETH
|
||||
_shibbolethCredsPage(new OwncloudShibbolethCredsPage)
|
||||
,
|
||||
, _shibbolethCredsPage(new OwncloudShibbolethCredsPage)
|
||||
#endif
|
||||
_advancedSetupPage(new OwncloudAdvancedSetupPage)
|
||||
, _advancedSetupPage(new OwncloudAdvancedSetupPage)
|
||||
, _resultPage(new OwncloudWizardResultPage)
|
||||
, _credentialsPage(0)
|
||||
, _setupLog()
|
||||
|
@ -55,6 +55,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
|
|||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
setPage(WizardCommon::Page_ServerSetup, _setupPage);
|
||||
setPage(WizardCommon::Page_HttpCreds, _httpCredsPage);
|
||||
setPage(WizardCommon::Page_OAuthCreds, _browserCredsPage);
|
||||
#ifndef NO_SHIBBOLETH
|
||||
setPage(WizardCommon::Page_ShibbolethCreds, _shibbolethCredsPage);
|
||||
#endif
|
||||
|
@ -70,6 +71,7 @@ OwncloudWizard::OwncloudWizard(QWidget *parent)
|
|||
connect(this, SIGNAL(currentIdChanged(int)), SLOT(slotCurrentPageChanged(int)));
|
||||
connect(_setupPage, SIGNAL(determineAuthType(QString)), SIGNAL(determineAuthType(QString)));
|
||||
connect(_httpCredsPage, SIGNAL(connectToOCUrl(QString)), SIGNAL(connectToOCUrl(QString)));
|
||||
connect(_browserCredsPage, SIGNAL(connectToOCUrl(QString)), SIGNAL(connectToOCUrl(QString)));
|
||||
#ifndef NO_SHIBBOLETH
|
||||
connect(_shibbolethCredsPage, SIGNAL(connectToOCUrl(QString)), SIGNAL(connectToOCUrl(QString)));
|
||||
#endif
|
||||
|
@ -142,6 +144,10 @@ void OwncloudWizard::successfulStep()
|
|||
_httpCredsPage->setConnected();
|
||||
break;
|
||||
|
||||
case WizardCommon::Page_OAuthCreds:
|
||||
_browserCredsPage->setConnected();
|
||||
break;
|
||||
|
||||
#ifndef NO_SHIBBOLETH
|
||||
case WizardCommon::Page_ShibbolethCreds:
|
||||
_shibbolethCredsPage->setConnected();
|
||||
|
@ -169,7 +175,9 @@ void OwncloudWizard::setAuthType(WizardCommon::AuthType type)
|
|||
_credentialsPage = _shibbolethCredsPage;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (type == WizardCommon::OAuth) {
|
||||
_credentialsPage = _browserCredsPage;
|
||||
} else {
|
||||
_credentialsPage = _httpCredsPage;
|
||||
}
|
||||
next();
|
||||
|
|
|
@ -30,6 +30,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcWizard)
|
|||
|
||||
class OwncloudSetupPage;
|
||||
class OwncloudHttpCredsPage;
|
||||
class OwncloudOAuthCredsPage;
|
||||
#ifndef NO_SHIBBOLETH
|
||||
class OwncloudShibbolethCredsPage;
|
||||
#endif
|
||||
|
@ -94,6 +95,7 @@ private:
|
|||
AccountPtr _account;
|
||||
OwncloudSetupPage *_setupPage;
|
||||
OwncloudHttpCredsPage *_httpCredsPage;
|
||||
OwncloudOAuthCredsPage *_browserCredsPage;
|
||||
#ifndef NO_SHIBBOLETH
|
||||
OwncloudShibbolethCredsPage *_shibbolethCredsPage;
|
||||
#endif
|
||||
|
@ -102,6 +104,8 @@ private:
|
|||
AbstractCredentialsWizardPage *_credentialsPage;
|
||||
|
||||
QStringList _setupLog;
|
||||
|
||||
friend class OwncloudSetupWizard;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -30,7 +30,8 @@ namespace WizardCommon {
|
|||
|
||||
enum AuthType {
|
||||
HttpCreds,
|
||||
Shibboleth
|
||||
Shibboleth,
|
||||
OAuth
|
||||
};
|
||||
|
||||
enum SyncMode {
|
||||
|
@ -42,6 +43,7 @@ namespace WizardCommon {
|
|||
Page_ServerSetup,
|
||||
Page_HttpCreds,
|
||||
Page_ShibbolethCreds,
|
||||
Page_OAuthCreds,
|
||||
Page_AdvancedSetup,
|
||||
Page_Result
|
||||
};
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
#include <QSslKey>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <keychain.h>
|
||||
|
||||
|
@ -37,6 +39,7 @@ Q_LOGGING_CATEGORY(lcHttpCredentials, "sync.credentials.http", QtInfoMsg)
|
|||
|
||||
namespace {
|
||||
const char userC[] = "user";
|
||||
const char isOAuthC[] = "oauth";
|
||||
const char clientCertificatePEMC[] = "_clientCertificatePEM";
|
||||
const char clientKeyPEMC[] = "_clientKeyPEM";
|
||||
const char authenticationFailedC[] = "owncloud-authentication-failed";
|
||||
|
@ -54,9 +57,20 @@ public:
|
|||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) Q_DECL_OVERRIDE
|
||||
{
|
||||
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
|
||||
QNetworkRequest req(request);
|
||||
req.setRawHeader(QByteArray("Authorization"), QByteArray("Basic ") + credHash);
|
||||
if (!_cred->password().isEmpty()) {
|
||||
if (_cred->isUsingOAuth()) {
|
||||
req.setRawHeader("Authorization", "Bearer " + _cred->password().toUtf8());
|
||||
} else {
|
||||
QByteArray credHash = QByteArray(_cred->user().toUtf8() + ":" + _cred->password().toUtf8()).toBase64();
|
||||
req.setRawHeader("Authorization", "Basic " + credHash);
|
||||
}
|
||||
} else if (!request.url().password().isEmpty()) {
|
||||
// Typically the requests to get or refresh the OAuth access token. The client
|
||||
// credentials are put in the URL from the code making the request.
|
||||
QByteArray credHash = request.url().userInfo().toUtf8().toBase64();
|
||||
req.setRawHeader("Authorization", "Basic " + credHash);
|
||||
}
|
||||
|
||||
if (!_cred->_clientSslKey.isNull() && !_cred->_clientSslCertificate.isNull()) {
|
||||
// SSL configuration
|
||||
|
@ -149,6 +163,13 @@ void HttpCredentials::fetchFromKeychain()
|
|||
// User must be fetched from config file
|
||||
fetchUser();
|
||||
|
||||
if (!_ready && !_refreshToken.isEmpty()) {
|
||||
// This happens if the credentials are still loaded from the keychain, bur we are called
|
||||
// here because the auth is invalid, so this means we simply need to refresh the credentials
|
||||
refreshAccessToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString kck = keychainKey(_account->url().toString(), _user);
|
||||
|
||||
if (_ready) {
|
||||
|
@ -236,7 +257,13 @@ bool HttpCredentials::stillValid(QNetworkReply *reply)
|
|||
void HttpCredentials::slotReadJobDone(QKeychain::Job *incomingJob)
|
||||
{
|
||||
QKeychain::ReadPasswordJob *job = static_cast<ReadPasswordJob *>(incomingJob);
|
||||
_password = job->textData();
|
||||
|
||||
bool isOauth = _account->credentialSetting(QLatin1String(isOAuthC)).toBool();
|
||||
if (isOauth) {
|
||||
_refreshToken = job->textData();
|
||||
} else {
|
||||
_password = job->textData();
|
||||
}
|
||||
|
||||
if (_user.isEmpty()) {
|
||||
qCWarning(lcHttpCredentials) << "Strange: User is empty!";
|
||||
|
@ -244,7 +271,9 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incomingJob)
|
|||
|
||||
QKeychain::Error error = job->error();
|
||||
|
||||
if (!_password.isEmpty() && error == NoError) {
|
||||
if (!_refreshToken.isEmpty() && error == NoError) {
|
||||
refreshAccessToken();
|
||||
} else if (!_password.isEmpty() && error == NoError) {
|
||||
// All cool, the keychain did not come back with error.
|
||||
// Still, the password can be empty which indicates a problem and
|
||||
// the password dialog has to be opened.
|
||||
|
@ -262,6 +291,41 @@ void HttpCredentials::slotReadJobDone(QKeychain::Job *incomingJob)
|
|||
}
|
||||
}
|
||||
|
||||
void HttpCredentials::refreshAccessToken()
|
||||
{
|
||||
QUrl requestToken(_account->url().toString()
|
||||
+ QLatin1String("/index.php/apps/oauth2/api/v1/token?grant_type=refresh_token&refresh_token=")
|
||||
+ _refreshToken);
|
||||
requestToken.setUserName(Theme::instance()->oauthClientId());
|
||||
requestToken.setPassword(Theme::instance()->oauthClientSecret());
|
||||
QNetworkRequest req;
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
auto reply = _account->sendRequest("POST", requestToken, req);
|
||||
QTimer::singleShot(30 * 1000, reply, &QNetworkReply::abort);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
||||
reply->deleteLater();
|
||||
auto jsonData = reply->readAll();
|
||||
QJsonParseError jsonParseError;
|
||||
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
|
||||
QString accessToken = json["access_token"].toString();
|
||||
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError || json.isEmpty()) {
|
||||
// Network error maybe?
|
||||
qDebug() << "Error while refreshing the token" << reply->errorString() << jsonData << jsonParseError.errorString();
|
||||
} else if (accessToken.isEmpty()) {
|
||||
// The token is no longer valid.
|
||||
qDebug() << "Expired refresh token. Logging out";
|
||||
_refreshToken.clear();
|
||||
} else {
|
||||
_ready = true;
|
||||
_password = accessToken;
|
||||
_refreshToken = json["refresh_token"].toString();
|
||||
persist();
|
||||
}
|
||||
emit fetched();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void HttpCredentials::invalidateToken()
|
||||
{
|
||||
if (!_password.isEmpty()) {
|
||||
|
@ -279,6 +343,12 @@ void HttpCredentials::invalidateToken()
|
|||
return;
|
||||
}
|
||||
|
||||
if (!_refreshToken.isEmpty()) {
|
||||
// Only invalidate the access_token (_password) but keep the _refreshToken in the keychain
|
||||
// (when coming from forgetSensitiveData, the _refreshToken is cleared)
|
||||
return;
|
||||
}
|
||||
|
||||
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
||||
addSettingsToJob(_account, job);
|
||||
job->setInsecureFallback(true);
|
||||
|
@ -315,6 +385,9 @@ void HttpCredentials::clearQNAMCache()
|
|||
|
||||
void HttpCredentials::forgetSensitiveData()
|
||||
{
|
||||
// need to be done before invalidateToken, so it actually deletes the refresh_token from the keychain
|
||||
_refreshToken.clear();
|
||||
|
||||
invalidateToken();
|
||||
_previousPassword.clear();
|
||||
}
|
||||
|
@ -327,6 +400,7 @@ void HttpCredentials::persist()
|
|||
}
|
||||
|
||||
_account->setCredentialSetting(QLatin1String(userC), _user);
|
||||
_account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth());
|
||||
|
||||
// write cert
|
||||
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
||||
|
@ -359,7 +433,7 @@ void HttpCredentials::slotWriteClientKeyPEMJobDone(Job *incomingJob)
|
|||
job->setInsecureFallback(false);
|
||||
connect(job, SIGNAL(finished(QKeychain::Job *)), SLOT(slotWriteJobDone(QKeychain::Job *)));
|
||||
job->setKey(keychainKey(_account->url().toString(), _user));
|
||||
job->setTextData(_password);
|
||||
job->setTextData(isUsingOAuth() ? _refreshToken : _password);
|
||||
job->start();
|
||||
}
|
||||
|
||||
|
@ -378,6 +452,8 @@ void HttpCredentials::slotWriteJobDone(QKeychain::Job *job)
|
|||
|
||||
void HttpCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *authenticator)
|
||||
{
|
||||
if (!_ready)
|
||||
return;
|
||||
Q_UNUSED(authenticator)
|
||||
// Because of issue #4326, we need to set the login and password manually at every requests
|
||||
// Thus, if we reach this signal, those credentials were invalid and we terminate.
|
||||
|
|
|
@ -32,6 +32,43 @@ class ReadPasswordJob;
|
|||
|
||||
namespace OCC {
|
||||
|
||||
/*
|
||||
The authentication system is this way because of Shibboleth.
|
||||
There used to be two different ways to authenticate: Shibboleth and HTTP Basic Auth.
|
||||
AbstractCredentials can be inherited from both ShibbolethCrendentials and HttpCredentials.
|
||||
|
||||
HttpCredentials is then split in HttpCredentials and HttpCredentialsGui.
|
||||
|
||||
This class handle both HTTP Basic Auth and OAuth. But anything that needs GUI to ask the user
|
||||
is in HttpCredentialsGui.
|
||||
|
||||
The authentication mechanism looks like this.
|
||||
|
||||
1) First, AccountState will attempt to load the certificate from the keychain
|
||||
|
||||
----> fetchFromKeychain ------------------------> shortcut to refreshAccessToken if the cached
|
||||
| } information is still valid
|
||||
v }
|
||||
slotReadClientCertPEMJobDone } There are first 3 QtKeychain jobs to fetch
|
||||
| } the TLS client keys, if any, and the password
|
||||
v } (or refresh token
|
||||
slotReadClientKeyPEMJobDone }
|
||||
| }
|
||||
v
|
||||
slotReadJobDone
|
||||
| |
|
||||
| +-------> emit fetched() if OAuth is not used
|
||||
|
|
||||
v
|
||||
refreshAccessToken()
|
||||
|
|
||||
v
|
||||
emit fetched()
|
||||
|
||||
2) If the credentials is still not valid when fetched() is emitted, the ui, will call askFromUser()
|
||||
which is implemented in HttpCredentialsGui
|
||||
|
||||
*/
|
||||
class OWNCLOUDSYNC_EXPORT HttpCredentials : public AbstractCredentials
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -48,15 +85,21 @@ public:
|
|||
bool stillValid(QNetworkReply *reply) Q_DECL_OVERRIDE;
|
||||
void persist() Q_DECL_OVERRIDE;
|
||||
QString user() const Q_DECL_OVERRIDE;
|
||||
// the password or token
|
||||
QString password() const;
|
||||
void invalidateToken() Q_DECL_OVERRIDE;
|
||||
void forgetSensitiveData() Q_DECL_OVERRIDE;
|
||||
QString fetchUser();
|
||||
virtual bool sslIsTrusted() { return false; }
|
||||
|
||||
void refreshAccessToken();
|
||||
|
||||
// To fetch the user name as early as possible
|
||||
void setAccount(Account *account) Q_DECL_OVERRIDE;
|
||||
|
||||
// Whether we are using OAuth
|
||||
bool isUsingOAuth() const { return !_refreshToken.isNull(); }
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotAuthentication(QNetworkReply *, QAuthenticator *);
|
||||
|
||||
|
@ -71,7 +114,8 @@ private Q_SLOTS:
|
|||
|
||||
protected:
|
||||
QString _user;
|
||||
QString _password;
|
||||
QString _password; // user's password, or access_token for OAuth
|
||||
QString _refreshToken; // OAuth _refreshToken, set if OAuth is used.
|
||||
QString _previousPassword;
|
||||
|
||||
QString _fetchErrorString;
|
||||
|
@ -80,6 +124,7 @@ protected:
|
|||
QSslCertificate _clientSslCertificate;
|
||||
};
|
||||
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
#endif
|
||||
|
|
|
@ -503,5 +503,15 @@ QString Theme::quotaBaseFolder() const
|
|||
return QLatin1String("/");
|
||||
}
|
||||
|
||||
QString Theme::oauthClientId() const
|
||||
{
|
||||
return "xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69";
|
||||
}
|
||||
|
||||
QString Theme::oauthClientSecret() const
|
||||
{
|
||||
return "e4rAsNUSIUs0lF4nbv9FmCeUkTlV9GdgTLDH1b5uie7syb90SzEVrbN7HIpmWJeD";
|
||||
}
|
||||
|
||||
|
||||
} // end namespace client
|
||||
|
|
|
@ -320,6 +320,13 @@ public:
|
|||
*/
|
||||
virtual QString quotaBaseFolder() const;
|
||||
|
||||
/**
|
||||
* The OAuth client_id, secret pair.
|
||||
* Note that client that change these value cannot connect to un-branded owncloud servers.
|
||||
*/
|
||||
virtual QString oauthClientId() const;
|
||||
virtual QString oauthClientSecret() const;
|
||||
|
||||
|
||||
protected:
|
||||
#ifndef TOKEN_AUTH_ONLY
|
||||
|
|
Loading…
Reference in a new issue