Files
QSfera/Desktop/test/testoauth/testoauth.cpp
T
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

766 lines
34 KiB
C++

/*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*
*/
#include <QDesktopServices>
#include <QtTest/QtTest>
#include "common/asserts.h"
#include "libsync/creds/oauth.h"
#include "testutils/syncenginetestutils.h"
#include "theme.h"
using namespace std::chrono_literals;
using namespace OCC;
namespace {
class DesktopServiceHook : public QObject
{
Q_OBJECT
Q_SIGNALS:
void hooked(const QUrl &);
public:
DesktopServiceHook() { QDesktopServices::setUrlHandler(QStringLiteral("oauthtest"), this, "hooked"); }
~DesktopServiceHook() { QDesktopServices::unsetUrlHandler(QStringLiteral("oauthtest")); }
};
const QUrl sOAuthTestServer(QStringLiteral("oauthtest://someserver/opencloud"));
const QString localHost{QStringLiteral("127.0.0.1")};
}
class FakePostReply : public QNetworkReply
{
Q_OBJECT
public:
std::unique_ptr<QIODevice> payload;
bool aborted = false;
FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, std::unique_ptr<QIODevice> payload_, QObject *parent)
: QNetworkReply{parent}
, payload{std::move(payload_)}
{
setRequest(request);
setUrl(request.url());
setOperation(op);
open(QIODevice::ReadOnly);
payload->open(QIODevice::ReadOnly);
QMetaObject::invokeMethod(this, &FakePostReply::respond, Qt::QueuedConnection);
}
Q_INVOKABLE virtual void respond()
{
if (aborted) {
setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
Q_EMIT metaDataChanged();
checkedFinished();
return;
}
setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
Q_EMIT metaDataChanged();
if (bytesAvailable())
Q_EMIT readyRead();
checkedFinished();
}
void abort() override { aborted = true; }
void checkedFinished()
{
if (!isFinished()) {
setFinished(true);
Q_EMIT finished();
}
}
qint64 bytesAvailable() const override
{
if (aborted)
return 0;
return payload->bytesAvailable();
}
qint64 readData(char *data, qint64 maxlen) override { return payload->read(data, maxlen); }
};
// Reply with a small delay
class SlowFakePostReply : public FakePostReply
{
Q_OBJECT
public:
using FakePostReply::FakePostReply;
void respond() override
{
// override of FakePostReply::respond, will call the real one with a delay.
QTimer::singleShot(100ms, this, [this] { this->FakePostReply::respond(); });
}
};
class OAuthTestCase : public QObject
{
Q_OBJECT
DesktopServiceHook desktopServiceHook;
protected:
QString _expectedClientId = Theme::instance()->oauthClientId();
public:
enum State { StartState, StatusPhpState, BrowserOpened, TokenAsked, CustomState } state = StartState;
Q_ENUM(State);
bool replyToBrowserOk = false;
bool gotAuthOk = false;
virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
FakeAM *fakeAm = nullptr;
QNetworkAccessManager realQNAM;
QPointer<QNetworkReply> browserReply = nullptr;
QString code = generateEtag();
OCC::AccountPtr account;
std::unique_ptr<AccountBasedOAuth> oauth;
virtual std::unique_ptr<AccountBasedOAuth> prepareOauth()
{
fakeAm = new FakeAM({}, nullptr);
account = Account::create(QUuid::createUuid());
account->setUrl(sOAuthTestServer);
// the account seizes ownership over the qnam in account->setCredentials(...) by keeping a shared pointer on it
// therefore, we should never call fakeAm->setThis(...)
account->setCredentials(new FakeCredentials{fakeAm});
fakeAm->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
if (req.url().path().endsWith(QLatin1String(".well-known/openid-configuration"))) {
return this->wellKnownReply(op, req);
} else if (req.url().path().endsWith(QLatin1String("status.php"))) {
return this->statusPhpReply(op, req);
} else if (req.url().path().endsWith(QLatin1String("clients-registrations"))) {
return this->clientRegistrationReply(op, req);
}
OC_ASSERT(device);
OC_ASSERT(device && device->bytesAvailable() > 0); // OAuth2 always sends around POST data.
return this->tokenReply(op, req, device);
});
QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked, this, &OAuthTestCase::openBrowserHook);
auto out = std::make_unique<AccountBasedOAuth>(account);
QObject::connect(out.get(), &OAuth::result, this, &OAuthTestCase::oauthResult);
return out;
}
virtual void test()
{
oauth = prepareOauth();
oauth->startAuthentication();
QSignalSpy spy(oauth.get(), &OCC::OAuth::authorisationLinkChanged);
if (spy.wait()) {
oauth->openBrowser();
}
QTRY_VERIFY(done());
}
virtual void openBrowserHook(const QUrl &url)
{
QCOMPARE(state, StatusPhpState);
state = BrowserOpened;
QCOMPARE(url.path(), sOAuthTestServer.path() + QStringLiteral("/index.php/apps/oauth2/authorize"));
QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
QUrlQuery query(url);
QCOMPARE(query.queryItemValue(QStringLiteral("response_type")), QLatin1String("code"));
QCOMPARE(query.queryItemValue(QStringLiteral("client_id")), _expectedClientId);
QUrl redirectUri(query.queryItemValue(QStringLiteral("redirect_uri")));
QCOMPARE(redirectUri.host(), localHost);
redirectUri.setQuery(QStringLiteral("code=%1&state=%2").arg(code, query.queryItemValue(QStringLiteral("state"))));
createBrowserReply(QNetworkRequest(redirectUri));
}
virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request)
{
auto r = request;
// don't follow the redirect to qsfera://success
r.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy);
browserReply = realQNAM.get(r);
QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
return browserReply;
}
virtual void browserReplyFinished()
{
QCOMPARE(sender(), browserReply.data());
QCOMPARE(state, TokenAsked);
browserReply->deleteLater();
QCOMPARE(QNetworkReply::NoError, browserReply->error());
QCOMPARE(browserReply->rawHeader("Location"), QByteArray("qsfera://success"));
replyToBrowserOk = true;
}
virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req, [[maybe_unused]] QIODevice *device)
{
OC_ASSERT(state == BrowserOpened);
state = TokenAsked;
OC_ASSERT(op == QNetworkAccessManager::PostOperation);
OC_ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
OC_ASSERT(req.url().path() == sOAuthTestServer.path() + QStringLiteral("/token_endpoint"));
auto payload = std::make_unique<QBuffer>();
payload->setData(tokenReplyPayload());
return new FakePostReply(op, req, std::move(payload), fakeAm);
}
virtual QNetworkReply *statusPhpReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
{
OC_ASSERT(state == StartState);
state = StatusPhpState;
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
OC_ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
OC_ASSERT(req.url().path() == sOAuthTestServer.path() + QStringLiteral("/status.php"));
auto payload = std::make_unique<QBuffer>();
payload->setData(statusPhpPayload());
return new FakePostReply(op, req, std::move(payload), fakeAm);
}
virtual QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
QJsonDocument jsondata(QJsonObject{
{QStringLiteral("authorization_endpoint"),
QJsonValue(Utility::concatUrlPath(sOAuthTestServer, QStringLiteral("/index.php/apps/oauth2/authorize")).toString())},
{QStringLiteral("token_endpoint"), Utility::concatUrlPath(sOAuthTestServer, QStringLiteral("token_endpoint")).toString()},
{QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_post")}},
});
return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm);
}
virtual QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
{
return new FakeErrorReply(op, req, fakeAm, 404, {});
}
virtual QByteArray tokenReplyPayload() const
{
// the dummy server provides the user admin
QJsonDocument jsondata(QJsonObject{{QStringLiteral("access_token"), QStringLiteral("123")}, {QStringLiteral("refresh_token"), QStringLiteral("456")},
{QStringLiteral("message_url"), QStringLiteral("qsfera://success")}, {QStringLiteral("id_token"), idToken()},
{QStringLiteral("token_type"), QStringLiteral("Bearer")}});
return jsondata.toJson();
}
virtual QByteArray statusPhpPayload() const
{
QJsonDocument jsondata(
QJsonObject{{QStringLiteral("installed"), true}, {QStringLiteral("maintenance"), false}, {QStringLiteral("needsDbUpgrade"), false},
{QStringLiteral("version"), QStringLiteral("10.5.0.10")}, {QStringLiteral("versionstring"), QStringLiteral("10.5.0")},
{QStringLiteral("edition"), QStringLiteral("Enterprise")}, {QStringLiteral("productname"), QStringLiteral("OpenCloud")}});
return jsondata.toJson();
}
virtual QString idToken() const
{
/* https://10015.io/tools/jwt-encoder-decoder with sample key
{
"amr": [
"pwd",
"pop",
"hwk",
"user",
"pin",
"mfa"
],
"at_hash": "jEL4ptHeYx4eQa847tOVoQ",
"aud": [
"QsferaDesktop"
],
"auth_time": 1737560752,
"azp": "QsferaDesktop",
"client_id": "QsferaDesktop",
"email": "admin@admin.admin",
"email_verified": true,
"exp": 1739884152,
"iat": 1739880552,
"iss": "oauthtest://someserver/opencloud",
"jti": "e2db5f2d-6bcc-42d7-a20f-46955d7ab6b4",
"name": "Admin",
"preferred_username": "admin",
"sub": "f4a04b62-e17a-4a98-bcc6-63345ded5a25"
}
*/
return QStringLiteral(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJhbXIiOlsicHdkIiwicG9wIiwiaHdrIiwidXNlciIsInBpbiIsIm1mYSJdLCJhdF9oYXNoIjoiakVMNHB0SGVZeDRlUWE4NDd0T1ZvUSIsImF1ZCI6WyJRc2ZlcmFEZX"
"NrdG9wIl0sImF1dGhfdGltZSI6MTczNzU2MDc1MiwiYXpwIjoiUXNmZXJhRGVza3RvcCIsImNsaWVudF9pZCI6IlFzZmVyYURlc2t0b3AiLCJlbWFpbCI6ImFkbWluQGFk"
"bWluLmFkbWluIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTczOTg4NDE1MiwiaWF0IjoxNzM5ODgwNTUyLCJpc3MiOiJvYXV0aHRlc3Q6Ly9zb21lc2VydmVyL2"
"9wZW5jbG91ZCIsImp0aSI6ImUyZGI1ZjJkLTZiY2MtNDJkNy1hMjBmLTQ2OTU1ZDdhYjZiNCIsIm5hbWUiOiJBZG1pbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWlu"
"Iiwic3ViIjoiZjRhMDRiNjItZTE3YS00YTk4LWJjYzYtNjMzNDVkZWQ1YTI1In0."
"wj3NyKWaDhWWwui6lxGdmJEGUyqCsNYCRJFTbgIUeC4");
}
virtual void oauthResult(OAuth::Result result, const QString &token, const QString &refreshToken)
{
QCOMPARE(result, OAuth::LoggedIn);
QCOMPARE(state, TokenAsked);
QCOMPARE(token, QStringLiteral("123"));
QCOMPARE(refreshToken, QStringLiteral("456"));
gotAuthOk = true;
}
};
class TestOAuth : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testBasic()
{
OAuthTestCase test;
test.test();
}
void testWrongUser()
{
struct Test : OAuthTestCase
{
QByteArray tokenReplyPayload() const override
{
// the dummy server provides the user admin
QJsonDocument jsondata(QJsonObject{{QStringLiteral("access_token"), QStringLiteral("123")},
{QStringLiteral("refresh_token"), QStringLiteral("456")}, {QStringLiteral("message_url"), QStringLiteral("QSfera://success")},
{QStringLiteral("user_id"), QStringLiteral("wrong_user")}, {QStringLiteral("token_type"), QStringLiteral("Bearer")}});
return jsondata.toJson();
}
void browserReplyFinished() override
{
QCOMPARE(sender(), browserReply.data());
QCOMPARE(state, TokenAsked);
browserReply->deleteLater();
QCOMPARE(QNetworkReply::AuthenticationRequiredError, browserReply->error());
}
bool done() const override { return true; }
};
Test test;
test.test();
}
// Test for https://github.com/owncloud/client/pull/6057
void testCloseBrowserDontCrash()
{
struct Test : OAuthTestCase
{
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req, [[maybe_unused]] QIODevice *device) override
{
OC_ASSERT(browserReply);
// simulate the fact that the browser is closing the connection
browserReply->abort();
OC_ASSERT(state == BrowserOpened);
state = TokenAsked;
auto payload = std::make_unique<QBuffer>();
payload->setData(tokenReplyPayload());
return new SlowFakePostReply(op, req, std::move(payload), fakeAm);
}
void browserReplyFinished() override
{
QCOMPARE(sender(), browserReply.data());
QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
replyToBrowserOk = true;
}
} test;
test.test();
}
void testRandomConnections()
{
// Test that we can send random garbage to the litening socket and it does not prevent the connection
struct Test : OAuthTestCase
{
QNetworkReply *createBrowserReply(const QNetworkRequest &request) override
{
QTimer::singleShot(0, this, [this, request] {
auto port = request.url().port();
state = CustomState;
const QVector<QByteArray> payloads = {
"GET FOFOFO HTTP 1/1\n\n",
"GET /?code=invalie HTTP 1/1\n\n",
"GET /?code=xxxxx&bar=fff",
QByteArray("\0\0\0", 3),
QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
QByteArray("\n\n\n\n"),
};
for (const auto &x : payloads) {
auto socket = new QTcpSocket(this);
socket->connectToHost(QStringLiteral("localhost"), port);
QVERIFY(socket->waitForConnected());
socket->write(x);
}
// Do the actual request a bit later
QTimer::singleShot(100ms, this, [this, request] {
QCOMPARE(state, CustomState);
state = BrowserOpened;
this->OAuthTestCase::createBrowserReply(request);
});
});
return nullptr;
}
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) override
{
if (state == CustomState)
return new FakeErrorReply{op, req, this, 500};
return OAuthTestCase::tokenReply(op, req, device);
}
void oauthResult(OAuth::Result result, const QString &token, const QString &refreshToken) override
{
if (state != CustomState) {
return OAuthTestCase::oauthResult(result, token, refreshToken);
}
QCOMPARE(result, OAuth::Error);
}
} test;
test.test();
}
void testTimeout()
{
struct Test : OAuthTestCase
{
QScopedValueRollback<decltype(AbstractNetworkJob::httpTimeout)> rollback;
Test()
: rollback(AbstractNetworkJob::httpTimeout, 50ms)
{
}
QNetworkReply *statusPhpReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
return new FakeHangingReply(op, req, fakeAm);
}
void oauthResult(OAuth::Result result, const QString &token, const QString &refreshToken) override
{
Q_UNUSED(token);
Q_UNUSED(refreshToken);
QCOMPARE(state, StartState);
QCOMPARE(result, OAuth::Error);
gotAuthOk = true;
replyToBrowserOk = true;
}
} test;
test.test();
}
void testDynamicRegistrationFailFallback()
{
// similar to testWellKnown but the server announces dynamic client registration
// when this fails we fall back to the default client id and secret
struct Test : OAuthTestCase
{
Test() { }
QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
const QJsonDocument jsondata(QJsonObject{
{QStringLiteral("authorization_endpoint"),
QJsonValue(QStringLiteral("oauthtest://openidserver") + sOAuthTestServer.path() + QStringLiteral("/index.php/apps/oauth2/authorize"))},
{QStringLiteral("token_endpoint"), QStringLiteral("oauthtest://openidserver/token_endpoint")},
{QStringLiteral("registration_endpoint"), QStringLiteral("%1/clients-registrations").arg(localHost)},
{QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_basic")}},
});
return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm);
}
void openBrowserHook(const QUrl &url) override
{
OC_ASSERT(url.host() == QStringLiteral("openidserver"));
QUrl url2 = url;
url2.setHost(sOAuthTestServer.host());
OAuthTestCase::openBrowserHook(url2);
}
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *device) override
{
OC_ASSERT(browserReply);
OC_ASSERT(request.url().toString().startsWith(QStringLiteral("oauthtest://openidserver/token_endpoint")));
auto req = request;
qDebug() << request.url() << request.url().query();
req.setUrl(QUrl(request.url().toString().replace(
QLatin1String("oauthtest://openidserver/token_endpoint"), sOAuthTestServer.toString() + QStringLiteral("/token_endpoint"))));
return OAuthTestCase::tokenReply(op, req, device);
}
} test;
test.test();
}
void testDynamicRegistrationFailFallback2()
{
// similar to testWellKnown but the server announces dynamic client registration
// when this fails we fall back to the default client id and secret
struct Test : OAuthTestCase
{
Test() { }
QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
const QJsonDocument jsondata(QJsonObject{
{QStringLiteral("authorization_endpoint"),
QJsonValue(QStringLiteral("oauthtest://openidserver") + sOAuthTestServer.path() + QStringLiteral("/index.php/apps/oauth2/authorize"))},
{QStringLiteral("token_endpoint"), QStringLiteral("oauthtest://openidserver/token_endpoint")},
{QStringLiteral("registration_endpoint"), QStringLiteral("%1/clients-registrations").arg(localHost)},
{QStringLiteral("token_endpoint_auth_methods_supported"),
QJsonArray{QStringLiteral("client_secret_basic"), QStringLiteral("client_secret_post")}},
});
return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm);
}
void openBrowserHook(const QUrl &url) override
{
OC_ASSERT(url.host() == QStringLiteral("openidserver"));
QUrl url2 = url;
url2.setHost(sOAuthTestServer.host());
OAuthTestCase::openBrowserHook(url2);
}
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *device) override
{
OC_ASSERT(browserReply);
OC_ASSERT(request.url().toString().startsWith(QStringLiteral("oauthtest://openidserver/token_endpoint")));
auto req = request;
qDebug() << request.url() << request.url().query();
req.setUrl(QUrl(request.url().toString().replace(
QLatin1String("oauthtest://openidserver/token_endpoint"), sOAuthTestServer.toString() + QStringLiteral("/token_endpoint"))));
return OAuthTestCase::tokenReply(op, req, device);
}
QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override
{
return new FakePayloadReply(op, request, {}, fakeAm);
}
} test;
test.test();
}
void testDynamicRegistration()
{
// similar to testWellKnown but the server announces dynamic client registration
// this means that the client id and secret are provided by the server
struct Test : OAuthTestCase
{
Test() { _expectedClientId = QStringLiteral("3e4ea0f3-59ea-434a-92f2-b0d3b54443e9"); }
QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
const QJsonDocument jsondata(QJsonObject{
{QStringLiteral("authorization_endpoint"),
QJsonValue(QStringLiteral("oauthtest://openidserver") + sOAuthTestServer.path() + QStringLiteral("/index.php/apps/oauth2/authorize"))},
{QStringLiteral("token_endpoint"), QStringLiteral("oauthtest://openidserver/token_endpoint")},
{QStringLiteral("registration_endpoint"), QStringLiteral("%1/clients-registrations").arg(localHost)},
{QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_basic")}},
});
return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm);
}
void openBrowserHook(const QUrl &url) override
{
OC_ASSERT(url.host() == QStringLiteral("openidserver"));
QUrl url2 = url;
url2.setHost(sOAuthTestServer.host());
OAuthTestCase::openBrowserHook(url2);
}
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *device) override
{
OC_ASSERT(browserReply);
OC_ASSERT(request.url().toString().startsWith(QStringLiteral("oauthtest://openidserver/token_endpoint")));
auto req = request;
qDebug() << request.url() << request.url().query();
req.setUrl(QUrl(request.url().toString().replace(
QStringLiteral("oauthtest://openidserver/token_endpoint"), sOAuthTestServer.toString() + QStringLiteral("/token_endpoint"))));
return OAuthTestCase::tokenReply(op, req, device);
}
QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override
{
const QByteArray payload(QByteArrayLiteral(
"{\"redirect_uris\":[\"http://"
"127.0.0.1\"],\"token_endpoint_auth_method\":\"client_secret_basic\",\"grant_types\":[\"authorization_code\",\"refresh_token\"],\"response_"
"types\":[\"code\",\"none\"],\"client_id\":\"3e4ea0f3-59ea-434a-92f2-b0d3b54443e9\",\"client_secret\":\"rmoEXFc1Z5tGTApxanBW7STlWODqRTYx\","
"\"client_name\":\"QSfera 3.0.0.0\",\"scope\":\"web-origins address phone offline_access "
"microprofile-jwt\",\"subject_type\":\"public\",\"request_uris\":[],\"tls_client_certificate_bound_access_tokens\":false,\"client_id_"
"issued_at\":1663074650,\"client_secret_expires_at\":0,\"registration_client_uri\":\"https://someserver.de/auth/realms/opencloud/"
"clients-registrations/openid-connect/"
"3e4ea0f3-59ea-434a-92f2-b0d3b54443e9\",\"registration_access_token\":"
"\"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIzYjQ2YWVkYi00Y2I3LTRiMGItODA5Ny1lNjRmOGQ5ZWY2YjQifQ."
"eyJleHAiOjAsImlhdCI6MTY2MzA3NDY1MCwianRpIjoiNTlkZWIzNTktNTBmZS00YTUyLWFmNTItZjFjNDg3ZTFlOWRmIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5vd25jbG91ZC"
"5jbG91ZHNwZWljaGVyLWJheWVybi5kZS9hdXRoL3JlYWxtcy9vY2lzIiwiYXVkIjoiaHR0cHM6Ly9rZXljbG9hay5vd25jbG91ZC5jbG91ZHNwZWljaGVyLWJheWVybi5kZS9hdXRo"
"L3JlYWxtcy9vY2lzIiwidHlwIjoiUmVnaXN0cmF0aW9uQWNjZXNzVG9rZW4iLCJyZWdpc3RyYXRpb25fYXV0aCI6ImFub255bW91cyJ9."
"v1giSvpnKw1hTtBYZaqdp3JqnZ5mvCKYhQDKkT7x8Us\",\"backchannel_logout_session_required\":false,\"require_pushed_authorization_requests\":"
"false}"));
auto *out = new FakePayloadReply(op, request, payload, fakeAm);
out->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
return out;
}
QString idToken() const override
{
// same as the parent implementation but with the current client id
return QStringLiteral(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJhbXIiOlsicHdkIiwicG9wIiwiaHdrIiwidXNlciIsInBpbiIsIm1mYSJdLCJhdF9oYXNoIjoiakVMNHB0SGVZeDRlUWE4NDd0T1ZvUSIsImF1ZCI6WyIzZTRlYTBmMy01OWVhLT"
"QzNGEtOTJmMi1iMGQzYjU0NDQzZTkiXSwiYXV0aF90aW1lIjoxNzM3NTYwNzUyLCJhenAiOiJPcGVuQ2xvdWREZXNrdG9wIiwiY2xpZW50X2lkIjoiT3BlbkNsb3VkRGVza3RvcCIs"
"ImVtYWlsIjoiYWRtaW5AYWRtaW4uYWRtaW4iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZXhwIjoxNzM5ODg0MTUyLCJpYXQiOjE3Mzk4ODA1NTIsImlzcyI6Im9hdXRodGVzdDovL3"
"NvbWVzZXJ2ZXIvb3BlbmNsb3VkIiwianRpIjoiZTJkYjVmMmQtNmJjYy00MmQ3LWEyMGYtNDY5NTVkN2FiNmI0IiwibmFtZSI6IkFkbWluIiwicHJlZmVycmVkX3VzZXJuYW1lIjoi"
"YWRtaW4iLCJzdWIiOiJmNGEwNGI2Mi1lMTdhLTRhOTgtYmNjNi02MzM0NWRlZDVhMjUifQ.UVjqXnuHFiu2iIPOW8qXze_a8tVMk03kuxoN4FKxhoY");
}
} test;
test.test();
}
void testDynamicTokenRefresh()
{
// simulate a token refresh with dynamic registration
struct Test : OAuthTestCase
{
QString _expectedClientSecret = QStringLiteral("rmoEXFc1Z5tGTApxanBW7STlWODqRTYx");
Test() { _expectedClientId = QStringLiteral("3e4ea0f3-59ea-434a-92f2-b0d3b54443e9"); }
QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
const QJsonDocument jsondata(QJsonObject{
{QStringLiteral("authorization_endpoint"),
QJsonValue(QStringLiteral("oauthtest://openidserver") + sOAuthTestServer.path() + QStringLiteral("/index.php/apps/oauth2/authorize"))},
{QStringLiteral("token_endpoint"), QStringLiteral("oauthtest://openidserver/token_endpoint")},
{QStringLiteral("registration_endpoint"), QStringLiteral("%1/clients-registrations").arg(localHost)},
// this test explicitly check for the client secret in the post body
{QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_post")}},
});
return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm);
}
void openBrowserHook(const QUrl &) override { Q_UNREACHABLE(); }
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *device) override
{
OC_ASSERT(request.url().toString().startsWith(QStringLiteral("oauthtest://openidserver/token_endpoint")));
auto req = request;
const auto query = QUrlQuery(QString::fromUtf8(device->peek(device->size())));
OC_ASSERT(query.queryItemValue(QStringLiteral("refresh_token")) == QLatin1String("foo"));
qDebug() << request.url() << request.url().query() << device->peek(device->size());
req.setUrl(QUrl(request.url().toString().replace(
QStringLiteral("oauthtest://openidserver/token_endpoint"), sOAuthTestServer.toString() + QStringLiteral("/token_endpoint"))));
// OAuthTestCase::tokenReply expects BrowserOpened
state = BrowserOpened;
return OAuthTestCase::tokenReply(op, req, device);
}
QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override
{
auto *out = new FakePayloadReply(op, request, TestUtils::getPayload("testDynamicTokenRefresh/clientRegistrationReply.json"), fakeAm);
out->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
return out;
}
virtual void test() override
{
oauth = prepareOauth();
QSignalSpy spy(oauth.get(), &OCC::AccountBasedOAuth::refreshFinished);
oauth->refreshAuthentication(QStringLiteral("foo"));
QVERIFY(spy.wait());
QCOMPARE(oauth->clientId(), QStringLiteral("d8a5d1f4-dabc-4e6a-a9a9-da729cff8ab0"));
QCOMPARE(oauth->clientSecret(), QString());
}
} test;
test.test();
}
void testDynamicTokenRefreshFailingRegistration()
{
// similar to testDynamicTokenRefresh but the dynamic registration fails and we fall back to the defaul
// client id and secret
struct Test : OAuthTestCase
{
Test() { }
QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
{
OC_ASSERT(op == QNetworkAccessManager::GetOperation);
const QJsonDocument jsondata(QJsonObject{
{QStringLiteral("authorization_endpoint"),
QJsonValue(QStringLiteral("oauthtest://openidserver") + sOAuthTestServer.path() + QStringLiteral("/index.php/apps/oauth2/authorize"))},
{QStringLiteral("token_endpoint"), QStringLiteral("oauthtest://openidserver/token_endpoint")},
{QStringLiteral("registration_endpoint"), QStringLiteral("%1/clients-registrations").arg(localHost)},
// this test explicitly check for the client secret in the post body
{QStringLiteral("token_endpoint_auth_methods_supported"), QJsonArray{QStringLiteral("client_secret_post")}},
});
return new FakePayloadReply(op, req, jsondata.toJson(), fakeAm);
}
void openBrowserHook(const QUrl &) override { Q_UNREACHABLE(); }
QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *device) override
{
OC_ASSERT(request.url().toString().startsWith(QStringLiteral("oauthtest://openidserver/token_endpoint")));
auto req = request;
const auto query = QUrlQuery(QString::fromUtf8(device->peek(device->size())));
OC_ASSERT(query.queryItemValue(QStringLiteral("refresh_token")) == QLatin1String("foo"));
OC_ASSERT(query.queryItemValue(QStringLiteral("client_id")) == _expectedClientId);
OC_ASSERT(query.queryItemValue(QStringLiteral("client_secret")) == Theme::instance()->oauthClientSecret());
req.setUrl(QUrl(request.url().toString().replace(
QStringLiteral("oauthtest://openidserver/token_endpoint"), sOAuthTestServer.toString() + QStringLiteral("/token_endpoint"))));
// OAuthTestCase::tokenReply expects BrowserOpened
state = BrowserOpened;
return OAuthTestCase::tokenReply(op, req, device);
}
QNetworkReply *clientRegistrationReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request) override
{
return new FakePayloadReply(op, request, {}, fakeAm);
}
virtual void test() override
{
oauth = prepareOauth();
QSignalSpy spy(oauth.get(), &OCC::AccountBasedOAuth::refreshFinished);
oauth->refreshAuthentication(QStringLiteral("foo"));
QVERIFY(spy.wait());
}
} test;
test.test();
}
};
QTEST_MAIN(TestOAuth)
#include "testoauth.moc"