1 /*
2  *    This software is in the public domain, furnished "as is", without technical
3  *    support, and with no warranty, express or implied, as to its usefulness for
4  *    any purpose.
5  *
6  */
7 
8 #include <QtTest/QtTest>
9 #include <QDesktopServices>
10 
11 #include "libsync/creds/oauth.h"
12 #include "syncenginetestutils.h"
13 #include "theme.h"
14 #include "common/asserts.h"
15 
16 using namespace OCC;
17 
18 class DesktopServiceHook : public QObject
19 {
20     Q_OBJECT
21 signals:
22     void hooked(const QUrl &);
23 public:
DesktopServiceHook()24     DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); }
25 };
26 
27 static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud");
28 
29 
30 class FakePostReply : public QNetworkReply
31 {
32     Q_OBJECT
33 public:
34     std::unique_ptr<QIODevice> payload;
35     bool aborted = false;
36     bool redirectToPolicy = false;
37     bool redirectToToken = false;
38 
FakePostReply(QNetworkAccessManager::Operation op,const QNetworkRequest & request,std::unique_ptr<QIODevice> payload_,QObject * parent)39     FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
40                   std::unique_ptr<QIODevice> payload_, QObject *parent)
41         : QNetworkReply{parent}, payload{std::move(payload_)}
42     {
43         setRequest(request);
44         setUrl(request.url());
45         setOperation(op);
46         open(QIODevice::ReadOnly);
47         payload->open(QIODevice::ReadOnly);
48         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
49     }
50 
respond()51     Q_INVOKABLE virtual void respond() {
52         if (aborted) {
53             setError(OperationCanceledError, "Operation Canceled");
54             emit metaDataChanged();
55             emit finished();
56             return;
57         } else if (redirectToPolicy) {
58             setHeader(QNetworkRequest::LocationHeader, "/my.policy");
59             setAttribute(QNetworkRequest::RedirectionTargetAttribute, "/my.policy");
60             setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 302); // 302 might or might not lose POST data in rfc
61             setHeader(QNetworkRequest::ContentLengthHeader, 0);
62             emit metaDataChanged();
63             emit finished();
64             return;
65         } else if (redirectToToken) {
66             // Redirect to self
67             QVariant destination = QVariant(sOAuthTestServer.toString()+QLatin1String("/index.php/apps/oauth2/api/v1/token"));
68             setHeader(QNetworkRequest::LocationHeader, destination);
69             setAttribute(QNetworkRequest::RedirectionTargetAttribute, destination);
70             setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 307); // 307 explicitly in rfc says to not lose POST data
71             setHeader(QNetworkRequest::ContentLengthHeader, 0);
72             emit metaDataChanged();
73             emit finished();
74             return;
75         }
76         setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
77         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
78         emit metaDataChanged();
79         if (bytesAvailable())
80             emit readyRead();
81         emit finished();
82     }
83 
abort()84     void abort() override {
85         aborted = true;
86     }
bytesAvailable() const87     qint64 bytesAvailable() const override {
88         if (aborted)
89             return 0;
90         return payload->bytesAvailable();
91     }
92 
readData(char * data,qint64 maxlen)93     qint64 readData(char *data, qint64 maxlen) override {
94         return payload->read(data, maxlen);
95     }
96 };
97 
98 // Reply with a small delay
99 class SlowFakePostReply : public FakePostReply {
100     Q_OBJECT
101 public:
102     using FakePostReply::FakePostReply;
respond()103     void respond() override {
104         // override of FakePostReply::respond, will call the real one with a delay.
105         QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
106     }
107 };
108 
109 
110 class OAuthTestCase : public QObject
111 {
112     Q_OBJECT
113     DesktopServiceHook desktopServiceHook;
114 public:
115     enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
116     Q_ENUM(State);
117     bool replyToBrowserOk = false;
118     bool gotAuthOk = false;
done() const119     virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
120 
121     FakeQNAM *fakeQnam = nullptr;
122     QNetworkAccessManager realQNAM;
123     QPointer<QNetworkReply> browserReply = nullptr;
124     QString code = generateEtag();
125     OCC::AccountPtr account;
126 
127     QScopedPointer<OAuth> oauth;
128 
test()129     virtual void test() {
130         fakeQnam = new FakeQNAM({});
131         account = OCC::Account::create();
132         account->setUrl(sOAuthTestServer);
133         account->setCredentials(new FakeCredentials{fakeQnam});
134         fakeQnam->setParent(this);
135         fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device)  {
136             if (req.url().path().endsWith(".well-known/openid-configuration"))
137                 return this->wellKnownReply(op, req);
138             OC_ASSERT(device);
139             OC_ASSERT(device->bytesAvailable() > 0); // OAuth2 always sends around POST data.
140             return this->tokenReply(op, req);
141         });
142 
143         QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
144                          this, &OAuthTestCase::openBrowserHook);
145 
146         oauth.reset(new OAuth(account.data(), nullptr));
147         QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
148         oauth->startAuthentication();
149         QTRY_VERIFY(done());
150     }
151 
openBrowserHook(const QUrl & url)152     virtual void openBrowserHook(const QUrl &url) {
153         QCOMPARE(state, StartState);
154         state = BrowserOpened;
155         QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
156         QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
157         QUrlQuery query(url);
158         QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
159         QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
160         QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
161         QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
162         redirectUri.setQuery(QStringLiteral("code=%1&state=%2").arg(code, query.queryItemValue(QStringLiteral("state"))));
163         createBrowserReply(QNetworkRequest(redirectUri));
164     }
165 
createBrowserReply(const QNetworkRequest & request)166     virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
167         browserReply = realQNAM.get(request);
168         QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
169         return browserReply;
170     }
171 
browserReplyFinished()172     virtual void browserReplyFinished() {
173         QCOMPARE(sender(), browserReply.data());
174         QCOMPARE(state, TokenAsked);
175         browserReply->deleteLater();
176         QCOMPARE(QNetworkReply::NoError, browserReply->error());
177         QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
178         replyToBrowserOk = true;
179     }
180 
tokenReply(QNetworkAccessManager::Operation op,const QNetworkRequest & req)181     virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
182     {
183         OC_ASSERT(state == BrowserOpened);
184         state = TokenAsked;
185         OC_ASSERT(op == QNetworkAccessManager::PostOperation);
186         OC_ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
187         OC_ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
188         std::unique_ptr<QBuffer> payload(new QBuffer());
189         payload->setData(tokenReplyPayload());
190         return new FakePostReply(op, req, std::move(payload), fakeQnam);
191     }
192 
wellKnownReply(QNetworkAccessManager::Operation op,const QNetworkRequest & req)193     virtual QNetworkReply *wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
194     {
195         return new FakeErrorReply(op, req, fakeQnam, 404);
196     }
197 
tokenReplyPayload() const198     virtual QByteArray tokenReplyPayload() const {
199         // the dummy server provides the user admin
200         QJsonDocument jsondata(QJsonObject{
201                 { "access_token", "123" },
202                 { "refresh_token" , "456" },
203                 { "message_url",  "owncloud://success"},
204                 { "user_id", "admin" },
205                 { "token_type", "Bearer" }
206         });
207         return jsondata.toJson();
208     }
209 
oauthResult(OAuth::Result result,const QString & user,const QString & token,const QString & refreshToken)210     virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
211         QCOMPARE(state, TokenAsked);
212         QCOMPARE(result, OAuth::LoggedIn);
213         QCOMPARE(user, QString("admin"));
214         QCOMPARE(token, QString("123"));
215         QCOMPARE(refreshToken, QString("456"));
216         gotAuthOk = true;
217     }
218 };
219 
220 class TestOAuth: public QObject
221 {
222     Q_OBJECT
223 
224 private slots:
testBasic()225     void testBasic()
226     {
227         OAuthTestCase test;
228         test.test();
229     }
230 
231 
testWrongUser()232     void testWrongUser()
233     {
234         struct Test : OAuthTestCase {
235             QByteArray tokenReplyPayload() const override {
236                 // the dummy server provides the user admin
237                 QJsonDocument jsondata(QJsonObject{
238                     { "access_token", "123" },
239                     { "refresh_token" , "456" },
240                     { "message_url",  "owncloud://success"},
241                     { "user_id", "wrong_user" },
242                     { "token_type", "Bearer" }
243                 });
244                 return jsondata.toJson();
245             }
246 
247             void browserReplyFinished() override {
248                 QCOMPARE(sender(), browserReply.data());
249                 QCOMPARE(state, TokenAsked);
250                 browserReply->deleteLater();
251                 QCOMPARE(QNetworkReply::AuthenticationRequiredError, browserReply->error());
252             }
253 
254             bool done() const override{
255                 return true;
256             }
257         };
258         Test test;
259         test.test();
260     }
261 
262     // Test for https://github.com/owncloud/client/pull/6057
testCloseBrowserDontCrash()263     void testCloseBrowserDontCrash()
264     {
265         struct Test : OAuthTestCase {
266             QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
267             {
268                 OC_ASSERT(browserReply);
269                 // simulate the fact that the browser is closing the connection
270                 browserReply->abort();
271                 // don't process network events, as it messes up the execution order and
272                 // causes an Qt internal crash
273                 QCoreApplication::processEvents(QEventLoop::ExcludeSocketNotifiers);
274 
275                 OC_ASSERT(state == BrowserOpened);
276                 state = TokenAsked;
277 
278                 std::unique_ptr<QBuffer> payload(new QBuffer);
279                 payload->setData(tokenReplyPayload());
280                 return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
281             }
282 
283             void browserReplyFinished() override
284             {
285                 QCOMPARE(sender(), browserReply.data());
286                 QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
287                 replyToBrowserOk = true;
288             }
289         } test;
290         test.test();
291     }
292 
testRandomConnections()293     void testRandomConnections()
294     {
295         // Test that we can send random garbage to the litening socket and it does not prevent the connection
296         struct Test : OAuthTestCase {
297             QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
298                 QTimer::singleShot(0, this, [this, request] {
299                     auto port = request.url().port();
300                     state = CustomState;
301                     QVector<QByteArray> payloads = {
302                         "GET FOFOFO HTTP 1/1\n\n",
303                         "GET /?code=invalie HTTP 1/1\n\n",
304                         "GET /?code=xxxxx&bar=fff",
305                         QByteArray("\0\0\0", 3),
306                         QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
307                         QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
308                         QByteArray("\n\n\n\n"),
309                     };
310                     foreach (const auto &x, payloads) {
311                         auto socket = new QTcpSocket(this);
312                         socket->connectToHost("localhost", port);
313                         QVERIFY(socket->waitForConnected());
314                         socket->write(x);
315                     }
316 
317                     // Do the actual request a bit later
318                     QTimer::singleShot(100, this, [this, request] {
319                         QCOMPARE(state, CustomState);
320                         state = BrowserOpened;
321                         this->OAuthTestCase::createBrowserReply(request);
322                     });
323                });
324                return nullptr;
325             }
326 
327             QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
328             {
329                 if (state == CustomState)
330                     return new FakeErrorReply{op, req, this, 500};
331                 return OAuthTestCase::tokenReply(op, req);
332             }
333 
334             void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
335                              const QString &refreshToken) override {
336                 if (state != CustomState)
337                     return OAuthTestCase::oauthResult(result, user, token, refreshToken);
338                 QCOMPARE(result, OAuth::Error);
339             }
340         } test;
341         test.test();
342     }
343 
testTokenUrlHasRedirect()344     void testTokenUrlHasRedirect()
345     {
346         struct Test : OAuthTestCase {
347             int redirectsDone = 0;
348             QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & request) override
349             {
350                 OC_ASSERT(browserReply);
351                 // Kind of reproduces what we had in https://github.com/owncloud/enterprise/issues/2951 (not 1:1)
352                 if (redirectsDone == 0) {
353                     std::unique_ptr<QBuffer> payload(new QBuffer());
354                     payload->setData("");
355                     SlowFakePostReply *reply = new SlowFakePostReply(op, request, std::move(payload), this);
356                     reply->redirectToPolicy = true;
357                     redirectsDone++;
358                     return reply;
359                 } else if  (redirectsDone == 1) {
360                     std::unique_ptr<QBuffer> payload(new QBuffer());
361                     payload->setData("");
362                     SlowFakePostReply *reply = new SlowFakePostReply(op, request, std::move(payload), this);
363                     reply->redirectToToken = true;
364                     redirectsDone++;
365                     return reply;
366                 } else {
367                     // ^^ This is with a custom reply and not actually HTTP, so we're testing the HTTP redirect code
368                     // we have in AbstractNetworkJob::slotFinished()
369                     redirectsDone++;
370                     return OAuthTestCase::tokenReply(op, request);
371                 }
372             }
373         } test;
374         test.test();
375     }
376 
testWellKnown()377     void testWellKnown() {
378         struct Test : OAuthTestCase {
379             int redirectsDone = 0;
380             QNetworkReply * wellKnownReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override {
381                 OC_ASSERT(op == QNetworkAccessManager::GetOperation);
382                 QJsonDocument jsondata(QJsonObject{
383                     { "authorization_endpoint", QJsonValue(
384                             "oauthtest://openidserver" + sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize") },
385                     { "token_endpoint" , "oauthtest://openidserver/token_endpoint" }
386                 });
387                 return new FakePayloadReply(op, req, jsondata.toJson(), fakeQnam);
388             }
389 
390             void openBrowserHook(const QUrl & url) override {
391                 OC_ASSERT(url.host() == "openidserver");
392                 QUrl url2 = url;
393                 url2.setHost(sOAuthTestServer.host());
394                 OAuthTestCase::openBrowserHook(url2);
395             }
396 
397             QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & request) override
398             {
399                 OC_ASSERT(browserReply);
400                 OC_ASSERT(request.url().toString().startsWith("oauthtest://openidserver/token_endpoint"));
401                 auto req = request;
402                 req.setUrl(request.url().toString().replace("oauthtest://openidserver/token_endpoint",
403                         sOAuthTestServer.toString() + "/index.php/apps/oauth2/api/v1/token"));
404                 return OAuthTestCase::tokenReply(op, req);
405             }
406         } test;
407         test.test();
408     }
409 };
410 
411 
412 QTEST_GUILESS_MAIN(TestOAuth)
413 #include "testoauth.moc"
414