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
#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;
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::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::askFromUserAsync()
+void HttpCredentialsGui::showDialog()
{
QString msg = tr("Please enter %1 password:<br>"
"<br>"
return tr("<a href=\"%1\">Click here</a> to request an app password from the web interface.")
.arg(account->url().toString() + path);
}
-
-
} // namespace OCC
#pragma once
#include "creds/httpcredentials.h"
+#include "creds/oauth.h"
+#include <QPointer>
+#include <QTcpServer>
namespace OCC {
: 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
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++;
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
{
if (_authType == WizardCommon::HttpCreds) {
return WizardCommon::Page_HttpCreds;
+ } else if (_authType == WizardCommon::OAuth) {
+ return WizardCommon::Page_OAuthCreds;
} else {
return WizardCommon::Page_ShibbolethCreds;
}
#include "wizard/owncloudwizard.h"
#include "wizard/owncloudsetuppage.h"
#include "wizard/owncloudhttpcredspage.h"
+#include "wizard/owncloudoauthcredspage.h"
#ifndef NO_SHIBBOLETH
#include "wizard/owncloudshibbolethcredspage.h"
#endif
, _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()
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
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
_httpCredsPage->setConnected();
break;
+ case WizardCommon::Page_OAuthCreds:
+ _browserCredsPage->setConnected();
+ break;
+
#ifndef NO_SHIBBOLETH
case WizardCommon::Page_ShibbolethCreds:
_shibbolethCredsPage->setConnected();
_credentialsPage = _shibbolethCredsPage;
} else
#endif
- {
+ if (type == WizardCommon::OAuth) {
+ _credentialsPage = _browserCredsPage;
+ } else {
_credentialsPage = _httpCredsPage;
}
next();
class OwncloudSetupPage;
class OwncloudHttpCredsPage;
+class OwncloudOAuthCredsPage;
#ifndef NO_SHIBBOLETH
class OwncloudShibbolethCredsPage;
#endif
AccountPtr _account;
OwncloudSetupPage *_setupPage;
OwncloudHttpCredsPage *_httpCredsPage;
+ OwncloudOAuthCredsPage *_browserCredsPage;
#ifndef NO_SHIBBOLETH
OwncloudShibbolethCredsPage *_shibbolethCredsPage;
#endif
AbstractCredentialsWizardPage *_credentialsPage;
QStringList _setupLog;
+
+ friend class OwncloudSetupWizard;
};
} // namespace OCC
enum AuthType {
HttpCreds,
- Shibboleth
+ Shibboleth,
+ OAuth
};
enum SyncMode {
Page_ServerSetup,
Page_HttpCreds,
Page_ShibbolethCreds,
+ Page_OAuthCreds,
Page_AdvancedSetup,
Page_Result
};
#include <QNetworkReply>
#include <QSettings>
#include <QSslKey>
+#include <QJsonObject>
+#include <QJsonDocument>
#include <keychain.h>
namespace {
const char userC[] = "user";
+ const char isOAuthC[] = "oauth";
const char clientCertificatePEMC[] = "_clientCertificatePEM";
const char clientKeyPEMC[] = "_clientKeyPEM";
const char authenticationFailedC[] = "owncloud-authentication-failed";
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
// 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) {
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!";
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.
}
}
+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()) {
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);
void HttpCredentials::forgetSensitiveData()
{
+ // need to be done before invalidateToken, so it actually deletes the refresh_token from the keychain
+ _refreshToken.clear();
+
invalidateToken();
_previousPassword.clear();
}
}
_account->setCredentialSetting(QLatin1String(userC), _user);
+ _account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth());
// write cert
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
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();
}
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.
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
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 *);
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;
QSslCertificate _clientSslCertificate;
};
+
} // namespace OCC
#endif
return QLatin1String("/");
}
+QString Theme::oauthClientId() const
+{
+ return "xdXOt13JKxym1B1QcEncf2XDkLAexMBFwiT9j6EfhhHFJhs2KM9jbjTmf8JBXE69";
+}
+
+QString Theme::oauthClientSecret() const
+{
+ return "e4rAsNUSIUs0lF4nbv9FmCeUkTlV9GdgTLDH1b5uie7syb90SzEVrbN7HIpmWJeD";
+}
+
} // end namespace client
*/
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