Authentication with OAuth2
authorOlivier Goffart <ogoffart@woboq.com>
Tue, 28 Mar 2017 08:31:38 +0000 (10:31 +0200)
committerMarkus Goetz <markus@woboq.com>
Thu, 1 Jun 2017 08:39:33 +0000 (10:39 +0200)
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

16 files changed:
src/gui/CMakeLists.txt
src/gui/creds/httpcredentialsgui.cpp
src/gui/creds/httpcredentialsgui.h
src/gui/creds/oauth.cpp [new file with mode: 0644]
src/gui/creds/oauth.h [new file with mode: 0644]
src/gui/owncloudsetupwizard.cpp
src/gui/wizard/owncloudoauthcredspage.cpp [new file with mode: 0644]
src/gui/wizard/owncloudoauthcredspage.h [new file with mode: 0644]
src/gui/wizard/owncloudsetuppage.cpp
src/gui/wizard/owncloudwizard.cpp
src/gui/wizard/owncloudwizard.h
src/gui/wizard/owncloudwizardcommon.h
src/libsync/creds/httpcredentials.cpp
src/libsync/creds/httpcredentials.h
src/libsync/theme.cpp
src/libsync/theme.h

index 603abea7ae32d65d9a274fe86a82d2310acae159..80b8b940a2aa6a7666c14b008a15ee7f2f62f011 100644 (file)
@@ -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
index 50d2a2367c7e696785e5f37f17f424f10723a06a..9c629ce654d838b932e66347184b157437033886 100644 (file)
 
 #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::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>"
@@ -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
index 235fd523ea3d13c08d0586fe49922b8ada72a37c..0eaeeee812229fd0f8bc1ac766fb25a582ac62d1 100644 (file)
@@ -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
diff --git a/src/gui/creds/oauth.cpp b/src/gui/creds/oauth.cpp
new file mode 100644 (file)
index 0000000..86b261b
--- /dev/null
@@ -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
diff --git a/src/gui/creds/oauth.h b/src/gui/creds/oauth.h
new file mode 100644 (file)
index 0000000..93e7ac2
--- /dev/null
@@ -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
index 9a7359f3ef97af6780080d2e68b0e2e022ee13e8..2f1d73b10155451bfb74c46c1e64c9d854d1e579 100644 (file)
@@ -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 (file)
index 0000000..50f498a
--- /dev/null
@@ -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
diff --git a/src/gui/wizard/owncloudoauthcredspage.h b/src/gui/wizard/owncloudoauthcredspage.h
new file mode 100644 (file)
index 0000000..2ef6365
--- /dev/null
@@ -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
index 39710e892379e54f50ee73eed08d2fc4f3a04224..271c943a2caa4367207f5abc228bd9679e05cbda 100644 (file)
@@ -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;
     }
index 84068ddd58ede20a5c883a7ef43994e96af97c8a..eb80a37c0bdada90ec1a8afcce980cda7fc677aa 100644 (file)
@@ -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();
index 4b9097f74e91e72aa957740d52e50cc38023aa90..78f5bb444f9854501a37546ce3603be1df77de52 100644 (file)
@@ -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
index bf6248c2c65052a5bd6499a8d93c67d696c23162..eaad0070463b198d700cedddf4f5de610ebb2439 100644 (file)
@@ -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
     };
index f5e3cc582068969198e454fd116e853badcacd5c..5601d9ae8397706a11e550fe7d566ae10b41818d 100644 (file)
@@ -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.
index 14118e924bb66d098e4c45f9bf9134e7abb4ff20..45b01c5ee5c4f48e35d762346ce954b355f6f71e 100644 (file)
@@ -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
index 9dd1c88a95f817177cd3eeded4ae2d675de5057a..55c44db817e2fd3008242e7f7b6a48650ff45b58 100644 (file)
@@ -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
index cc51251df5bfc26e41eb560a47d372270491d09d..bb5c858ae66500d4bd2e82a761c5dafecace12a1 100644 (file)
@@ -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