diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 603abea7a..80b8b940a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/creds/httpcredentialsgui.cpp b/src/gui/creds/httpcredentialsgui.cpp index 50d2a2367..9c629ce65 100644 --- a/src/gui/creds/httpcredentialsgui.cpp +++ b/src/gui/creds/httpcredentialsgui.cpp @@ -15,9 +15,14 @@ #include #include +#include +#include +#include +#include #include "creds/httpcredentialsgui.h" #include "theme.h" #include "account.h" +#include 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:
" "
" @@ -87,6 +142,4 @@ QString HttpCredentialsGui::requestAppPasswordText(const Account *account) return tr("Click here to request an app password from the web interface.") .arg(account->url().toString() + path); } - - } // namespace OCC diff --git a/src/gui/creds/httpcredentialsgui.h b/src/gui/creds/httpcredentialsgui.h index 235fd523e..0eaeeee81 100644 --- a/src/gui/creds/httpcredentialsgui.h +++ b/src/gui/creds/httpcredentialsgui.h @@ -15,6 +15,9 @@ #pragma once #include "creds/httpcredentials.h" +#include "creds/oauth.h" +#include +#include 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> _asyncAuth; }; } // namespace OCC diff --git a/src/gui/creds/oauth.cpp b/src/gui/creds/oauth.cpp new file mode 100644 index 000000000..86b261bb3 --- /dev/null +++ b/src/gui/creds/oauth.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) by Olivier Goffart + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include +#include +#include +#include "account.h" +#include "creds/oauth.h" +#include +#include +#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", "404 Not Found

404 Not Found

"); + return; + } + + // TODO: add redirect to the page on the server + httpReplyAndClose(socket, "200 OK", "

Login Successfull

You can close this window.

"); + + 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 diff --git a/src/gui/creds/oauth.h b/src/gui/creds/oauth.h new file mode 100644 index 000000000..93e7ac209 --- /dev/null +++ b/src/gui/creds/oauth.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) by Olivier Goffart + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once +#include +#include + +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 diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index 9a7359f3e..2f1d73b10 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -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++; diff --git a/src/gui/wizard/owncloudoauthcredspage.cpp b/src/gui/wizard/owncloudoauthcredspage.cpp new file mode 100644 index 000000000..50f498a92 --- /dev/null +++ b/src/gui/wizard/owncloudoauthcredspage.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) by Olivier Goffart + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include + +#include "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(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(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(wizard()); + Q_ASSERT(ocWizard); + return new HttpCredentialsGui(_user, _token, _refreshToken, + ocWizard->_clientSslCertificate, ocWizard->_clientSslKey); +} + +} // namespace OCC diff --git a/src/gui/wizard/owncloudoauthcredspage.h b/src/gui/wizard/owncloudoauthcredspage.h new file mode 100644 index 000000000..2ef6365dd --- /dev/null +++ b/src/gui/wizard/owncloudoauthcredspage.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) by Olivier Goffart + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "wizard/abstractcredswizardpage.h" +#include "accountfwd.h" +#include "creds/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 _asyncAuth; +}; + +} // namespace OCC diff --git a/src/gui/wizard/owncloudsetuppage.cpp b/src/gui/wizard/owncloudsetuppage.cpp index 39710e892..271c943a2 100644 --- a/src/gui/wizard/owncloudsetuppage.cpp +++ b/src/gui/wizard/owncloudsetuppage.cpp @@ -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; } diff --git a/src/gui/wizard/owncloudwizard.cpp b/src/gui/wizard/owncloudwizard.cpp index 84068ddd5..eb80a37c0 100644 --- a/src/gui/wizard/owncloudwizard.cpp +++ b/src/gui/wizard/owncloudwizard.cpp @@ -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(); diff --git a/src/gui/wizard/owncloudwizard.h b/src/gui/wizard/owncloudwizard.h index 4b9097f74..78f5bb444 100644 --- a/src/gui/wizard/owncloudwizard.h +++ b/src/gui/wizard/owncloudwizard.h @@ -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 diff --git a/src/gui/wizard/owncloudwizardcommon.h b/src/gui/wizard/owncloudwizardcommon.h index bf6248c2c..eaad00704 100644 --- a/src/gui/wizard/owncloudwizardcommon.h +++ b/src/gui/wizard/owncloudwizardcommon.h @@ -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 }; diff --git a/src/libsync/creds/httpcredentials.cpp b/src/libsync/creds/httpcredentials.cpp index f5e3cc582..5601d9ae8 100644 --- a/src/libsync/creds/httpcredentials.cpp +++ b/src/libsync/creds/httpcredentials.cpp @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include @@ -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(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. diff --git a/src/libsync/creds/httpcredentials.h b/src/libsync/creds/httpcredentials.h index 14118e924..45b01c5ee 100644 --- a/src/libsync/creds/httpcredentials.h +++ b/src/libsync/creds/httpcredentials.h @@ -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 diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index 9dd1c88a9..55c44db81 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -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 diff --git a/src/libsync/theme.h b/src/libsync/theme.h index cc51251df..bb5c858ae 100644 --- a/src/libsync/theme.h +++ b/src/libsync/theme.h @@ -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