diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 83f1504f00..c80910818e 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -189,6 +189,12 @@ void AccountManager::setAuthURL(const QUrl& authURL) { requestProfile(); } + // prepare to refresh our token if it is about to expire + if (needsToRefreshToken()) { + qCDebug(networking) << "Refreshing access token since it will be expiring soon."; + requestNewAccessToken(); + } + // tell listeners that the auth endpoint has changed emit authEndpointChanged(); } @@ -443,6 +449,12 @@ bool AccountManager::hasValidAccessToken() { return false; } else { + + if (!_isWaitingForTokenRefresh && needsToRefreshToken()) { + qCDebug(networking) << "Refreshing access token since it will be expiring soon."; + requestNewAccessToken(); + } + return true; } } @@ -458,6 +470,15 @@ bool AccountManager::checkAndSignalForAccessToken() { return hasToken; } +bool AccountManager::needsToRefreshToken() { + if (!_accountInfo.getAccessToken().token.isEmpty()) + { + qlonglong expireThreshold = QDateTime::currentDateTime().addSecs(1 * 60 * 60).toMSecsSinceEpoch(); + if (_accountInfo.getAccessToken().expiryTimestamp < expireThreshold) return true; + } + return false; +} + void AccountManager::setAccessTokenForCurrentAuthURL(const QString& accessToken) { // replace the account info access token with a new OAuthAccessToken OAuthAccessToken newOAuthToken; @@ -530,6 +551,33 @@ void AccountManager::requestAccessTokenWithSteam(QByteArray authSessionTicket) { connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } +void AccountManager::requestNewAccessToken() { + _isWaitingForTokenRefresh = true; + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest request; + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + + QUrl grantURL = _authURL; + grantURL.setPath("/oauth/token"); + + const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; + + QByteArray postData; + postData.append("grant_type=refresh_token&"); + postData.append("refresh_token=" + QUrl::toPercentEncoding(_accountInfo.getAccessToken().refreshToken) + "&"); + postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); + + request.setUrl(grantURL); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply* requestReply = networkAccessManager.post(request, postData); + connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestNewAccessTokenFinished); + connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestNewAccessTokenError(QNetworkReply::NetworkError))); +} + void AccountManager::requestAccessTokenFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); @@ -568,10 +616,54 @@ void AccountManager::requestAccessTokenFinished() { void AccountManager::requestAccessTokenError(QNetworkReply::NetworkError error) { // TODO: error handling - qCDebug(networking) << "AccountManager requestError - " << error; + qCDebug(networking) << "AccountManager: failed to fetch access token - " << error; emit loginFailed(); } +void AccountManager::requestNewAccessTokenFinished() { + QNetworkReply* requestReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (!rootObject.contains("error")) { + // construct an OAuthAccessToken from the json object + + if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") + || !rootObject.contains("token_type")) { + // TODO: error handling - malformed token response + qCDebug(networking) << "Received a response for refresh grant that is missing one or more expected values."; + } + else { + // clear the path from the response URL so we have the right root URL for this access token + QUrl rootURL = requestReply->url(); + rootURL.setPath(""); + + qCDebug(networking) << "Storing an account with access-token for" << qPrintable(rootURL.toString()); + + _accountInfo.setAccessTokenFromJSON(rootObject); + + persistAccountToFile(); + + requestProfile(); + } + } + else { + // TODO: error handling + qCDebug(networking) << "Error in response for refresh grant -" << rootObject["error_description"].toString(); + emit loginFailed(); + } + + _isWaitingForTokenRefresh = false; +} + +void AccountManager::requestNewAccessTokenError(QNetworkReply::NetworkError error) { + // TODO: error handling + qCDebug(networking) << "AccountManager: failed to refresh access token - " << error; + emit loginFailed(); + _isWaitingForTokenRefresh = false; +} + void AccountManager::requestProfile() { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index eb4d224501..d0fd507173 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -79,6 +79,7 @@ public: bool isLoggedIn() { return !_authURL.isEmpty() && hasValidAccessToken(); } bool hasValidAccessToken(); + bool needsToRefreshToken(); Q_INVOKABLE bool checkAndSignalForAccessToken(); void setAccessTokenForCurrentAuthURL(const QString& accessToken); @@ -97,10 +98,13 @@ public: public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); + void requestNewAccessToken(); void requestAccessTokenFinished(); + void requestNewAccessTokenFinished(); void requestProfileFinished(); void requestAccessTokenError(QNetworkReply::NetworkError error); + void requestNewAccessTokenError(QNetworkReply::NetworkError error); void requestProfileError(QNetworkReply::NetworkError error); void logout(); void generateNewUserKeypair() { generateNewKeypair(); } @@ -141,6 +145,7 @@ private: QMap _pendingCallbackMap; DataServerAccountInfo _accountInfo; + bool _isWaitingForTokenRefresh { false }; bool _isAgent { false }; bool _isWaitingForKeypairResponse { false };