Skip to content

Commit d56ed8a

Browse files
committed
OAuth2: Refresh the token without aborting the sync
OAuth2 access token typically only has a token valid for 1 hour. Before this patch, when the token was timing out during the sync, the sync was aborted, and the ConnectionValidator was then requesting a new token, so the sync can be started over. If the discovery takes longer than the oauth2 validity, this means that the sync can never proceed, as it would be always restarted from scratch. With this patch, we try to transparently renew the OAuth2 token and restart the jobs that failed because the access token was invalid. Note that some changes were required in the GETFile job because it handled the error itself and so it was erroring the jobs before its too late. Issue #6814
1 parent 542b6ef commit d56ed8a

7 files changed

+80
-5
lines changed

src/libsync/abstractnetworkjob.cpp

+21
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <QAuthenticator>
3030
#include <QMetaEnum>
3131

32+
#include "common/asserts.h"
3233
#include "networkjobs.h"
3334
#include "account.h"
3435
#include "owncloudpropagator.h"
@@ -161,6 +162,10 @@ void AbstractNetworkJob::slotFinished()
161162
}
162163

163164
if (_reply->error() != QNetworkReply::NoError) {
165+
166+
if (_account->credentials()->retryIfNeeded(this))
167+
return;
168+
164169
if (!_ignoreCredentialFailure || _reply->error() != QNetworkReply::AuthenticationRequiredError) {
165170
qCWarning(lcNetworkJob) << _reply->error() << errorString()
166171
<< _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
@@ -408,4 +413,20 @@ QString networkReplyErrorString(const QNetworkReply &reply)
408413
return AbstractNetworkJob::tr("Server replied \"%1 %2\" to \"%3 %4\"").arg(QString::number(httpStatus), httpReason, requestVerb(reply), reply.request().url().toDisplayString());
409414
}
410415

416+
void AbstractNetworkJob::retry()
417+
{
418+
ENFORCE(_reply);
419+
auto req = _reply->request();
420+
QUrl requestedUrl = req.url();
421+
QByteArray verb = requestVerb(*_reply);
422+
qCInfo(lcNetworkJob) << "Restarting" << verb << requestedUrl;
423+
resetTimeout();
424+
if (_requestBody) {
425+
_requestBody->seek(0);
426+
}
427+
// The cookie will be added automatically, we don't want AccessManager::createRequest to duplicate them
428+
req.setRawHeader("cookie", QByteArray());
429+
sendRequest(verb, requestedUrl, req, _requestBody);
430+
}
431+
411432
} // namespace OCC

src/libsync/abstractnetworkjob.h

+3
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ class OWNCLOUDSYNC_EXPORT AbstractNetworkJob : public QObject
9090
*/
9191
QString errorStringParsingBody(QByteArray *body = 0);
9292

93+
/** Make a new request */
94+
void retry();
95+
9396
/** static variable the HTTP timeout (in seconds). If set to 0, the default will be used
9497
*/
9598
static int httpTimeout;

src/libsync/creds/abstractcredentials.h

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class QNetworkAccessManager;
2525
class QNetworkReply;
2626
namespace OCC {
2727

28+
class AbstractNetworkJob;
29+
2830
class OWNCLOUDSYNC_EXPORT AbstractCredentials : public QObject
2931
{
3032
Q_OBJECT
@@ -87,6 +89,9 @@ class OWNCLOUDSYNC_EXPORT AbstractCredentials : public QObject
8789

8890
static QString keychainKey(const QString &url, const QString &user, const QString &accountId);
8991

92+
/** If the job need to be restarted or queue, this does it and returns true. */
93+
virtual bool retryIfNeeded(AbstractNetworkJob *) { return false; }
94+
9095
Q_SIGNALS:
9196
/** Emitted when fetchFromKeychain() is done.
9297
*

src/libsync/creds/httpcredentials.cpp

+38-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ namespace {
4545
const char clientCertificatePEMC[] = "_clientCertificatePEM";
4646
const char clientKeyPEMC[] = "_clientKeyPEM";
4747
const char authenticationFailedC[] = "owncloud-authentication-failed";
48+
const char needRetryC[] = "owncloud-need-retry";
4849
} // ns
4950

5051
class HttpCredentialsAccessManager : public AccessManager
@@ -84,8 +85,15 @@ class HttpCredentialsAccessManager : public AccessManager
8485
req.setSslConfiguration(sslConfiguration);
8586
}
8687

88+
auto *reply = AccessManager::createRequest(op, req, outgoingData);
8789

88-
return AccessManager::createRequest(op, req, outgoingData);
90+
if (_cred->_isRenewingOAuthToken) {
91+
// We know this is going to fail, but we have no way to queue it there, so we will
92+
// simply restart the job after the failure.
93+
reply->setProperty(needRetryC, true);
94+
}
95+
96+
return reply;
8997
}
9098

9199
private:
@@ -361,6 +369,7 @@ bool HttpCredentials::refreshAccessToken()
361369
QString basicAuth = QString("%1:%2").arg(
362370
Theme::instance()->oauthClientId(), Theme::instance()->oauthClientSecret());
363371
req.setRawHeader("Authorization", "Basic " + basicAuth.toUtf8().toBase64());
372+
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
364373

365374
auto requestBody = new QBuffer;
366375
QUrlQuery arguments(QString("grant_type=refresh_token&refresh_token=%1").arg(_refreshToken));
@@ -387,8 +396,15 @@ bool HttpCredentials::refreshAccessToken()
387396
_refreshToken = json["refresh_token"].toString();
388397
persist();
389398
}
399+
_isRenewingOAuthToken = false;
400+
for (const auto &job : _retryQueue) {
401+
if (job)
402+
job->retry();
403+
}
404+
_retryQueue.clear();
390405
emit fetched();
391406
});
407+
_isRenewingOAuthToken = true;
392408
return true;
393409
}
394410

@@ -516,7 +532,27 @@ void HttpCredentials::slotAuthentication(QNetworkReply *reply, QAuthenticator *a
516532
// Thus, if we reach this signal, those credentials were invalid and we terminate.
517533
qCWarning(lcHttpCredentials) << "Stop request: Authentication failed for " << reply->url().toString();
518534
reply->setProperty(authenticationFailedC, true);
519-
reply->close();
535+
536+
if (_isRenewingOAuthToken) {
537+
reply->setProperty(needRetryC, true);
538+
} else if (isUsingOAuth() && !reply->property(needRetryC).toBool()) {
539+
reply->setProperty(needRetryC, true);
540+
qCInfo(lcHttpCredentials) << "Refreshing token";
541+
refreshAccessToken();
542+
}
543+
}
544+
545+
bool HttpCredentials::retryIfNeeded(AbstractNetworkJob *job)
546+
{
547+
auto *reply = job->reply();
548+
if (!reply || !reply->property(needRetryC).toBool())
549+
return false;
550+
if (_isRenewingOAuthToken) {
551+
_retryQueue.append(job);
552+
} else {
553+
job->retry();
554+
}
555+
return true;
520556
}
521557

522558
} // namespace OCC

src/libsync/creds/httpcredentials.h

+5
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ class OWNCLOUDSYNC_EXPORT HttpCredentials : public AbstractCredentials
107107
// Whether we are using OAuth
108108
bool isUsingOAuth() const { return !_refreshToken.isNull(); }
109109

110+
bool retryIfNeeded(AbstractNetworkJob *) override;
111+
110112
private Q_SLOTS:
111113
void slotAuthentication(QNetworkReply *, QAuthenticator *);
112114

@@ -138,10 +140,13 @@ private Q_SLOTS:
138140

139141
QString _fetchErrorString;
140142
bool _ready = false;
143+
bool _isRenewingOAuthToken = false;
141144
QSslKey _clientSslKey;
142145
QSslCertificate _clientSslCertificate;
143146
bool _keychainMigration = false;
144147
bool _retryOnKeyChainError = true; // true if we haven't done yet any reading from keychain
148+
149+
QVector<QPointer<AbstractNetworkJob>> _retryQueue; // Jobs we need to retry once the auth token is fetched
145150
};
146151

147152

src/libsync/propagatedownload.cpp

+7-2
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,14 @@ void GETFileJob::slotMetaDataChanged()
159159

160160
int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
161161

162-
// Ignore redirects
163-
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307 || httpStatus == 308)
162+
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307 || httpStatus == 308 || httpStatus == 401) {
163+
// Ignore redirects and authentication failures.
164+
// This is handled by AbstractNetworkJob, so don't let this job error out in this case.
165+
bool ok = disconnect(reply(), &QNetworkReply::finished, this, &GETFileJob::slotReadyRead)
166+
&& disconnect(reply(), &QNetworkReply::readyRead, this, &GETFileJob::slotReadyRead);
167+
ASSERT(ok);
164168
return;
169+
}
165170

166171
// If the status code isn't 2xx, don't write the reply body to the file.
167172
// For any error: handle it when the job is finished, not here.

src/libsync/propagatedownload.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class GETFileJob : public AbstractNetworkJob
6565
virtual void start() Q_DECL_OVERRIDE;
6666
virtual bool finished() Q_DECL_OVERRIDE
6767
{
68-
if (reply()->bytesAvailable()) {
68+
if (_saveBodyToFile && reply()->bytesAvailable()) {
6969
return false;
7070
} else {
7171
if (_bandwidthManager) {

0 commit comments

Comments
 (0)