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:
Olivier Goffart 2017-03-28 10:31:38 +02:00 committed by Markus Goetz
parent 8a19f2ac65
commit 3d93527a8e
16 changed files with 606 additions and 20 deletions

View 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

View file

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

View 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

122
src/gui/creds/oauth.cpp Normal file
View 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
View 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

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

View 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

View 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

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

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

View 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

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

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

View 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

View 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

View 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