summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorJuha Vuolle <juha.vuolle@qt.io>2023-04-23 20:08:32 +0300
committerJuha Vuolle <juha.vuolle@qt.io>2023-05-10 09:28:22 +0300
commit32f29d3e227da206f262efa055d1cac895855a98 (patch)
tree95e6154205e37fd133e6229c2930e1f73f272783 /tests
parentc7812a421f3ad250d7d0bf79580296eee697bf25 (diff)
Improve error handling and reporting in OAuth2
The OAuth2 authorization and access token requests can fail for a number of reasons, both network and authorization server related. These errors are reported as a log output, leaving the application unaware. In addition since the refresh token errors were not handled, a failed refresh attempt left the OAuth2 class in a "refershing token" status without proper means for application to recover. This commit harnesses the pre-existing QAbstractOAuth::requestFailed() signal for reporting these issues. It's used by OAuth1 implementation for similar purpose. This consists of: - Document the requestFailed() signal - Add new QAbstractOAuthReplyHandler::tokenRequestError() signal, which reply handlers can emit upon error - Connect AuthorizationCodeFlow class to that signal and handle it - Implement error emission in OobReplyHandler, which is used by the examples (via HTTPReplyHandler) - Autotests [ChangeLog][QAbstractOAuth] Add token request error signal and improve related error handling Fixes: QTBUG-102279 Fixes: QTBUG-106821 Change-Id: I4dc14aa237d92bd1a2ba830c349cae4121be2e57 Reviewed-by: Ivan Solovev <ivan.solovev@qt.io> Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
Diffstat (limited to 'tests')
-rw-r--r--tests/auto/oauth2/tst_oauth2.cpp214
1 files changed, 213 insertions, 1 deletions
diff --git a/tests/auto/oauth2/tst_oauth2.cpp b/tests/auto/oauth2/tst_oauth2.cpp
index 4aa4f66..992817c 100644
--- a/tests/auto/oauth2/tst_oauth2.cpp
+++ b/tests/auto/oauth2/tst_oauth2.cpp
@@ -13,6 +13,8 @@
#include "webserver.h"
#include "tlswebserver.h"
+using namespace Qt::StringLiterals;
+
class tst_OAuth2 : public QObject
{
Q_OBJECT
@@ -22,6 +24,8 @@ private Q_SLOTS:
void getToken();
void refreshToken();
void getAndRefreshToken();
+ void tokenRequestErrors();
+ void authorizationErrors();
void prepareRequest();
#ifndef QT_NO_SSL
void setSslConfig();
@@ -39,13 +43,19 @@ struct ReplyHandler : QAbstractOAuthReplyHandler
return QLatin1String("test");
}
+ QAbstractOAuth::Error aTokenRequestError = QAbstractOAuth::Error::NoError;
+
void networkReplyFinished(QNetworkReply *reply) override
{
QVariantMap data;
const auto items = QUrlQuery(reply->readAll()).queryItems();
for (const auto &pair : items)
data.insert(pair.first, pair.second);
- Q_EMIT tokensReceived(data);
+
+ if (aTokenRequestError == QAbstractOAuth::Error::NoError)
+ emit tokensReceived(data);
+ else
+ emit tokenRequestError(aTokenRequestError, "a token request error");
}
void emitCallbackReceived(const QVariantMap &data)
@@ -56,6 +66,7 @@ struct ReplyHandler : QAbstractOAuthReplyHandler
void tst_OAuth2::initTestCase()
{
+ // QLoggingCategory::setFilterRules(QStringLiteral("qt.networkauth* = true"));
testDataDir = QFileInfo(QFINDTESTDATA("certs")).absolutePath();
if (testDataDir.isEmpty())
testDataDir = QCoreApplication::applicationDirPath();
@@ -63,6 +74,91 @@ void tst_OAuth2::initTestCase()
testDataDir += QLatin1String("/");
}
+void tst_OAuth2::authorizationErrors()
+{
+ // This tests failures in authorization stage. For this test we don't need a web server
+ // as we emit the final (failing) callbackReceived directly.
+ // Helper to catch the expected warning messages:
+ constexpr auto expectWarning = [](){
+ static const QRegularExpression authStageWarning{"Authorization stage:.*"};
+ QTest::ignoreMessage(QtWarningMsg, authStageWarning);
+ };
+
+ QOAuth2AuthorizationCodeFlow oauth2;
+ oauth2.setAuthorizationUrl(QUrl{"authorization"_L1});
+ oauth2.setAccessTokenUrl(QUrl{"accessToken"_L1});
+ ReplyHandler replyHandler;
+ oauth2.setReplyHandler(&replyHandler);
+
+ QVariantMap callbackParameters;
+ connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser,
+ &oauth2, [&](const QUrl& /* url */) {
+ replyHandler.emitCallbackReceived(callbackParameters);
+ });
+
+ QSignalSpy requestFailedSpy(&oauth2, &QAbstractOAuth::requestFailed);
+ QSignalSpy errorSpy(&oauth2, &QAbstractOAuth2::error);
+ QSignalSpy statusSpy(&oauth2, &QAbstractOAuth2::statusChanged);
+ auto clearSpies = [&](){
+ requestFailedSpy.clear();
+ errorSpy.clear();
+ statusSpy.clear();
+ };
+
+ // Test error response from the authorization server (RFC 6749 section 5.2)
+ callbackParameters = {{"error"_L1, "invalid_grant"_L1},
+ {"error_description"_L1, "The error description"_L1},
+ {"error_uri"_L1, "The error URI"_L1}};
+ expectWarning();
+ oauth2.grant();
+ QTRY_COMPARE(errorSpy.count(), 1);
+ QTRY_COMPARE(requestFailedSpy.count(), 1);
+ QCOMPARE(errorSpy.first().at(0).toString(), "invalid_grant"_L1);
+ QCOMPARE(errorSpy.first().at(1).toString(), "The error description"_L1);
+ QCOMPARE(errorSpy.first().at(2).toString(), "The error URI"_L1);
+ QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(),
+ QAbstractOAuth::Error::ServerError);
+ QVERIFY(statusSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated);
+
+ // Test not providing authorization code
+ clearSpies();
+ callbackParameters = {{"state"_L1, "thestate"_L1}};
+ expectWarning();
+ oauth2.grant();
+ QTRY_COMPARE(requestFailedSpy.count(), 1);
+ QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(),
+ QAbstractOAuth::Error::OAuthTokenNotFoundError);
+ QCOMPARE(errorSpy.count(), 0);
+ QVERIFY(statusSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated);
+
+ // Test not providing a state
+ clearSpies();
+ callbackParameters = {{"code"_L1, "thecode"_L1}};
+ expectWarning();
+ oauth2.grant();
+ QTRY_COMPARE(requestFailedSpy.count(), 1);
+ QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(),
+ QAbstractOAuth::Error::ServerError);
+ QCOMPARE(errorSpy.count(), 0);
+ QVERIFY(statusSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated);
+
+ // Test state mismatch (here we use "thestate" while the actual, expected, state is a
+ // random generated string varying each run
+ clearSpies();
+ callbackParameters = {{"code"_L1, "thecode"_L1}, {"state"_L1, "thestate"_L1}};
+ expectWarning();
+ oauth2.grant();
+ QTRY_COMPARE(requestFailedSpy.count(), 1);
+ QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(),
+ QAbstractOAuth::Error::ServerError);
+ QCOMPARE(errorSpy.count(), 0);
+ QVERIFY(statusSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated);
+}
+
void tst_OAuth2::getToken()
{
WebServer webServer([](const WebServer::HttpRequest &request, QTcpSocket *socket) {
@@ -163,6 +259,122 @@ void tst_OAuth2::getAndRefreshToken()
QCOMPARE(oauth2.token(), QLatin1String("refresh_token"));
}
+void tst_OAuth2::tokenRequestErrors()
+{
+ // This test tests the token acquisition and refreshing errors.
+ // Helper to catch the expected warning messages:
+ constexpr auto expectWarning = [](){
+ static const QRegularExpression tokenWarning{"Token request failed:.*"};
+ QTest::ignoreMessage(QtWarningMsg, tokenWarning);
+ };
+
+ QByteArray accessTokenResponse; // Varying reply for the auth server
+ WebServer authServer([&](const WebServer::HttpRequest &request, QTcpSocket *socket) {
+ if (request.url.path() == QLatin1String("/accessToken"))
+ socket->write(accessTokenResponse);
+ });
+
+ QOAuth2AuthorizationCodeFlow oauth2;
+ oauth2.setAuthorizationUrl(authServer.url(QLatin1String("authorization")));
+ oauth2.setAccessTokenUrl(authServer.url(QLatin1String("accessToken")));
+
+ ReplyHandler replyHandler;
+ oauth2.setReplyHandler(&replyHandler);
+
+ QSignalSpy requestFailedSpy(&oauth2, &QAbstractOAuth::requestFailed);
+ QSignalSpy grantedSpy(&oauth2, &QOAuth2AuthorizationCodeFlow::granted);
+ QSignalSpy statusSpy(&oauth2, &QAbstractOAuth2::statusChanged);
+ auto clearSpies = [&](){
+ requestFailedSpy.clear();
+ grantedSpy.clear();
+ statusSpy.clear();
+ };
+
+ connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser,
+ &oauth2, [&](const QUrl &url) {
+ // Successful authorization stage, after which we can test token requests.
+ // For clarity: in these tests we omit browser interaction by directly triggering
+ // the emission of replyhandler::callbackReceived() signal
+ const QUrlQuery query(url.query());
+ replyHandler.emitCallbackReceived(QVariantMap {
+ { QLatin1String("code"), QLatin1String("test") },
+ { QLatin1String("state"),
+ query.queryItemValue(QLatin1String("state")) }
+ });
+ });
+
+ // Check the initial state
+ QVERIFY(requestFailedSpy.isEmpty());
+ QVERIFY(grantedSpy.isEmpty());
+ QVERIFY(statusSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated);
+
+ // Try to get an access token with an invalid response
+ accessTokenResponse = "an invalid response"_ba;
+ expectWarning();
+ oauth2.grant();
+ QTRY_COMPARE(requestFailedSpy.size(), 1);
+ QVERIFY(grantedSpy.isEmpty());
+ QCOMPARE(statusSpy.size(), 1); // Authorization was successful so we get one signal
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived);
+
+ // Try to get an access token, but replyhandler indicates an error
+ clearSpies();
+ replyHandler.aTokenRequestError = QAbstractOAuth::Error::NetworkError;
+ expectWarning();
+ oauth2.grant();
+ QTRY_COMPARE(requestFailedSpy.size(), 1);
+ QVERIFY(grantedSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived);
+
+ // Make a successful access & refresh token acquisition
+ replyHandler.aTokenRequestError = QAbstractOAuth::Error::NoError;
+ clearSpies();
+ accessTokenResponse =
+ "HTTP/1.0 200 OK\r\n"
+ "Content-Type: application/x-www-form-urlencoded; charset=\"utf-8\"\r\n"
+ "\r\n"
+ "access_token=the_access_token&token_type=bearer&refresh_token=the_refresh_token"_ba;
+ oauth2.grant();
+ QTRY_COMPARE(grantedSpy.size(), 1);
+ QCOMPARE(statusSpy.size(), 3);
+ // First status change is going from TempCred back to NotAuthenticated
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::NotAuthenticated);
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::TemporaryCredentialsReceived);
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::Granted);
+ QVERIFY(requestFailedSpy.isEmpty());
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::Granted);
+ QCOMPARE(oauth2.token(), u"the_access_token"_s);
+ QCOMPARE(oauth2.refreshToken(), u"the_refresh_token"_s);
+
+ // Successfully refresh access token
+ clearSpies();
+ oauth2.refreshAccessToken();
+ QTRY_COMPARE(statusSpy.size(), 2);
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::RefreshingToken);
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::Granted);
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::Granted);
+ QVERIFY(requestFailedSpy.isEmpty());
+
+ // Failed access token refresh
+ clearSpies();
+ replyHandler.aTokenRequestError = QAbstractOAuth::Error::ServerError;
+ expectWarning();
+ oauth2.refreshAccessToken();
+ QTRY_COMPARE(statusSpy.size(), 2);
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::RefreshingToken);
+ QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(),
+ QAbstractOAuth::Status::Granted); // back to granted since we have an access token
+ QCOMPARE(requestFailedSpy.size(), 1);
+ QCOMPARE(oauth2.status(), QAbstractOAuth::Status::Granted);
+}
+
void tst_OAuth2::prepareRequest()
{
QOAuth2AuthorizationCodeFlow oauth2;