1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:authorization-protocol
4
5#include "qabstractoauthreplyhandler_p.h" // for lcReplyHandler()
6#include "qoauthoobreplyhandler_p.h"
7#include "qoauthurischemereplyhandler.h"
8
9#include <QtGui/qdesktopservices.h>
10
11#include <private/qobject_p.h>
12
13#include <QtCore/qloggingcategory.h>
14#include <QtCore/qurlquery.h>
15
16QT_BEGIN_NAMESPACE
17
18/*!
19 \class QOAuthUriSchemeReplyHandler
20 \inmodule QtNetworkAuth
21 \ingroup oauth
22 \since 6.8
23
24 \brief Handles private/custom and https URI scheme redirects.
25
26 This class serves as a reply handler for
27 \l {https://datatracker.ietf.org/doc/html/rfc6749}{OAuth 2.0} authorization
28 processes that use private/custom or HTTPS URI schemes for redirection.
29 It manages the reception of the authorization redirection (also known as the
30 callback) and the subsequent acquisition of access tokens.
31
32 The \l {https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
33 {redirection URI} is where the authorization server redirects the
34 user-agent (typically, and preferably, the system browser) once
35 the authorization part of the flow is complete.
36
37 The use of specific URI schemes requires configuration at the
38 operating system level to associate the URI with
39 the correct application. The way to set up this association varies
40 between operating systems. See \l {Platform Support and Dependencies}.
41
42 This class complements QOAuthHttpServerReplyHandler,
43 which handles \c http schemes by setting up a localhost server.
44
45 The following code illustrates the usage. First, the needed variables:
46
47 \snippet src_oauth_replyhandlers_p.h uri-variables
48
49 Followed up by the OAuth setup (error handling omitted for brevity):
50
51 \snippet src_oauth_replyhandlers.cpp uri-service-configuration
52 \codeline
53 \snippet src_oauth_replyhandlers.cpp uri-oauth-setup
54
55 Finally, we then set up the URI scheme reply-handler:
56
57 \snippet src_oauth_replyhandlers.cpp uri-handler-setup
58
59 \section1 Private/Custom URI Schemes
60
61 Custom URI schemes typically use reverse-domain notation followed
62 by a path, or occasionally a host/host+path:
63 \badcode
64 // Example with path:
65 com.example.myapp:/oauth2/callback
66 // Example with host:
67 com.example.myapp://oauth2.callback
68 \endcode
69
70 \section1 HTTPS URI Scheme
71
72 With HTTPS URI schemes, the redirect URLs are regular https links:
73 \badcode
74 https://myapp.example.com/oauth2/callback
75 \endcode
76
77 These links are called
78 \l {https://developer.apple.com/ios/universal-links/}{Universal Links}
79 on iOS and
80 \l {https://developer.android.com/training/app-links}{App Links on Android}.
81
82 The use of https schemes is recommended as it provides additional security
83 by forcing application developers to prove ownership of the URLs used. This
84 proving is done by hosting an association file, which the operating system
85 will consult as part of its internal URL dispatching.
86
87 The content of this file associates the application and the used URLs.
88 The association files must be publicly accessible without any HTTP
89 redirects. In addition, the hosting site must have valid certificates
90 and, at least with Android, the file must be served as
91 \c application/json content-type (refer to your server's configuration
92 guide).
93
94 In addition, https links can provide some usability benefits:
95 \list
96 \li The https URL doubles as a regular https link. If the
97 user hasn't installed the application (since the URL wasn't handled
98 by any application), the https link may for example serve
99 instructions to do so.
100 \li The application selection dialogue to open the URL may be avoided,
101 and instead your application may be opened automatically
102 \endlist
103
104 The tradeoff is that this requires extra setup as you need to set up this
105 publicly-hosted association file.
106
107 \section1 Platform Support and Dependencies
108
109 Currently supported platforms are Android, iOS, and macOS.
110
111 URI scheme listening is based on QDesktopServices::setUrlHandler()
112 and QDesktopServices::unsetUrlHandler(). These are currently
113 provided by Qt::Gui module and therefore QtNetworkAuth module
114 depends on Qt::Gui. If QtNetworkAuth is built without Qt::Gui,
115 QOAuthUriSchemeReplyHandler will not be included.
116
117 \section2 Android
118
119 On \l {Qt for Android}{Android} the URI schemes require:
120 \list
121 \li Setting up
122 \l {configuring qdesktopservices url handler on android}{intent-filters}
123 in the application manifest
124 \li Optionally, for automatic verification with https schemes,
125 hosting a site association file
126 \l {configuring qdesktopservices url handler on android}{assetlinks.json}
127 \endlist
128
129 See also the
130 \l {https://doc.qt.io/qt-6/android-manifest-file-configuration.html}
131 {Qt Android Manifest File Configuration}.
132
133 \section2 iOS and macOS
134
135 On \l {Qt for iOS}{iOS} and \l {Qt for macOS}{macOS} the URI schemes require:
136 \list
137 \li Setting up site association
138 \l {configuring qdesktopservices url handler on ios and macos}{entitlement}
139 \li With https schemes, hosting a
140 \l {configuring qdesktopservices url handler on ios and macos}{site association file}
141 (\c apple-app-site-association)
142 \endlist
143
144 \section2 \l {Qt for Windows}{Windows}, \l {Qt for Linux/X11}{Linux}
145
146 Currently not supported. However platforms and use cases supporting
147 \l {Qt WebEngine Platform Notes}{Qt WebEngine} can still use this reply
148 handler - see \l {Qt OAuth2 Browser Support} for details.
149*/
150
151class QOAuthUriSchemeReplyHandlerPrivate : public QOAuthOobReplyHandlerPrivate
152{
153 Q_DECLARE_PUBLIC(QOAuthUriSchemeReplyHandler)
154
155public:
156 bool hasValidRedirectUrl() const
157 {
158 // RFC 6749 Section 3.1.2
159 return redirectUrl.isValid()
160 && !redirectUrl.scheme().isEmpty()
161 && redirectUrl.fragment().isEmpty();
162 }
163
164 bool _q_handleRedirectUrl(const QUrl &url)
165 {
166 Q_Q(QOAuthUriSchemeReplyHandler);
167 // Remove the query parameters from comparison, and compare them manually (the parameters
168 // of interest like 'code' and 'state' are received as query parameters and comparison
169 // would always fail). Fragments are removed as some servers (eg. Reddit) seem to add some,
170 // possibly for some implementation consistency with other OAuth flows where fragments
171 // are actually used.
172 bool urlMatch = url.matches(url: redirectUrl, options: QUrl::RemoveQuery | QUrl::RemoveFragment);
173
174 const QUrlQuery responseQuery{url};
175 if (urlMatch) {
176 // Verify that query parameters that are part of redirect URL are present in redirection
177 const auto registeredItems = QUrlQuery{redirectUrl}.queryItems();
178 for (const auto &item: registeredItems) {
179 if (!responseQuery.hasQueryItem(key: item.first)
180 || responseQuery.queryItemValue(key: item.first) != item.second) {
181 urlMatch = false;
182 break;
183 }
184 }
185 }
186
187 if (!urlMatch) {
188 qCDebug(lcReplyHandler(), "Url ignored");
189 if (forwardUnhandledUrls) {
190 // The URLs received here might be unrelated. Further, in case of "https" scheme,
191 // the first request issued to the authorization server comes through here
192 // (if this handler is listening)
193 QDesktopServices::openUrl(url);
194 }
195 return false;
196 }
197
198 qCDebug(lcReplyHandler(), "Url handled");
199 emit q->callbackDataReceived(data: url.toEncoded());
200
201 QVariantMap resultParameters;
202 const auto responseItems = responseQuery.queryItems(encoding: QUrl::FullyDecoded);
203 for (const auto &item : responseItems)
204 resultParameters.insert(key: item.first, value: item.second);
205
206 emit q->callbackReceived(values: resultParameters);
207 return true;
208 }
209
210public:
211 QUrl redirectUrl;
212 bool forwardUnhandledUrls = true;
213 bool listening = false;
214};
215
216/*!
217 \fn QOAuthUriSchemeReplyHandler::QOAuthUriSchemeReplyHandler()
218
219 Constructs a QOAuthUriSchemeReplyHandler object with empty callback()/
220 redirectUrl() and no parent. The constructed object does not automatically
221 listen.
222*/
223
224/*!
225 Constructs a QOAuthUriSchemeReplyHandler object with \a parent and empty
226 callback()/redirectUrl(). The constructed object does not automatically listen.
227*/
228QOAuthUriSchemeReplyHandler::QOAuthUriSchemeReplyHandler(QObject *parent) :
229 QOAuthOobReplyHandler(*new QOAuthUriSchemeReplyHandlerPrivate(), parent)
230{
231}
232
233/*!
234 Constructs a QOAuthUriSchemeReplyHandler object and sets \a parent as the
235 parent object and \a redirectUrl as the redirect URL. The constructed
236 object attempts automatically to listen.
237
238 \sa redirectUrl(), setRedirectUrl(), listen(), isListening()
239*/
240QOAuthUriSchemeReplyHandler::QOAuthUriSchemeReplyHandler(const QUrl &redirectUrl, QObject *parent)
241 : QOAuthUriSchemeReplyHandler(parent)
242{
243 Q_D(QOAuthUriSchemeReplyHandler);
244 d->redirectUrl = redirectUrl;
245 listen();
246}
247
248/*!
249 Destroys the QOAuthUriSchemeReplyHandler object. Closes
250 this handler.
251
252 \sa close()
253*/
254QOAuthUriSchemeReplyHandler::~QOAuthUriSchemeReplyHandler()
255{
256 close();
257}
258
259QString QOAuthUriSchemeReplyHandler::callback() const
260{
261 Q_D(const QOAuthUriSchemeReplyHandler);
262 return d->redirectUrl.toString();
263}
264
265/*!
266 \property QOAuthUriSchemeReplyHandler::redirectUrl
267 \brief The URL used to receive authorization redirection/response.
268
269 This property is used as the
270 \l{https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
271 {OAuth2 redirect_uri parameter}, which is sent as part of the
272 authorization request. The \c redirect_uri is acquired by
273 calling QUrl::toString() with default options.
274
275 The URL must match the one registered at the authorization server,
276 as the authorization servers likely reject any mismatching redirect_uris.
277
278 Similarly, when this handler receives the redirection,
279 the redirection URL must match the URL set here. The handler
280 compares the scheme, host, port, path, and any
281 query items that were part of the URL set by this method.
282
283 The URL is handled only if all of these match. The comparison of query
284 parameters excludes any additional query parameters that may have been set
285 at server-side, as these contain the actual data of interest.
286*/
287void QOAuthUriSchemeReplyHandler::setRedirectUrl(const QUrl &url)
288{
289 Q_D(QOAuthUriSchemeReplyHandler);
290 if (url == d->redirectUrl)
291 return;
292
293 if (d->listening) {
294 close(); // close previous url listening first
295 d->redirectUrl = url;
296 listen();
297 } else {
298 d->redirectUrl = url;
299 }
300 emit redirectUrlChanged();
301}
302
303QUrl QOAuthUriSchemeReplyHandler::redirectUrl() const
304{
305 Q_D(const QOAuthUriSchemeReplyHandler);
306 return d->redirectUrl;
307}
308
309/*!
310 \since 6.9
311 This function is used to supply the redirect URL the Authorization
312 Server provides at the end of the authorization stage. The provided
313 \a url undergoes the same URL matching as described in \l {redirectUrl}.
314
315 Suppyling the URL can be useful for scenarios where this redirect URL
316 is captured by some other means, for example with \l {Qt WebEngine} or
317 through some other custom arrangement. This way such agent usage can be
318 integrated with rest of the OAuth2 flow.
319
320 This handler does not need to be listening, and therefore it is
321 recommended to \l close() the handler to avoid unnecessary listening.
322
323 Returns \c true if the URL matched and was handled, \c false otherwise.
324
325 See also \l {Redirect URI Schemes}{Redirect URI Schemes with Qt WebEngine}.
326*/
327bool QOAuthUriSchemeReplyHandler::handleAuthorizationRedirect(const QUrl &url)
328{
329 Q_D(QOAuthUriSchemeReplyHandler);
330 d->forwardUnhandledUrls = false;
331 const bool handled = d->_q_handleRedirectUrl(url);
332 d->forwardUnhandledUrls = true;
333 return handled;
334}
335
336/*!
337 Tells this handler to listen for incoming URLs. Returns
338 \c true if listening is successful, and \c false otherwise.
339
340 The handler will match URLs to redirectUrl().
341 If the received URL does not match, it will be forwarded to
342 QDesktopServices::openURL().
343
344 Active listening is only required when performing the initial
345 authorization phase, typically initiated by a
346 QOAuth2AuthorizationCodeFlow::grant() call.
347
348 It is recommended to close the listener after successful authorization.
349 Listening is not needed for
350 \l {QOAuth2AuthorizationCodeFlow::requestAccessToken()}{acquiring access tokens}.
351*/
352bool QOAuthUriSchemeReplyHandler::listen()
353{
354 Q_D(QOAuthUriSchemeReplyHandler);
355 if (d->listening)
356 return true;
357
358 if (!d->hasValidRedirectUrl()) {
359 qCWarning(lcReplyHandler(), "listen(): callback url not valid");
360 return false;
361 }
362 qCDebug(lcReplyHandler(), "listen() URL listener");
363 QDesktopServices::setUrlHandler(scheme: d->redirectUrl.scheme(), receiver: this, method: "_q_handleRedirectUrl");
364
365 d->listening = true;
366 return true;
367}
368
369/*!
370 Tells this handler to stop listening for incoming URLs.
371
372 \sa listen(), isListening()
373*/
374void QOAuthUriSchemeReplyHandler::close()
375{
376 Q_D(QOAuthUriSchemeReplyHandler);
377 if (!d->listening)
378 return;
379
380 qCDebug(lcReplyHandler(), "close() URL listener");
381 QDesktopServices::unsetUrlHandler(scheme: d->redirectUrl.scheme());
382 d->listening = false;
383}
384
385/*!
386 Returns \c true if this handler is currently listening,
387 and \c false otherwise.
388
389 \sa listen(), close()
390*/
391bool QOAuthUriSchemeReplyHandler::isListening() const noexcept
392{
393 Q_D(const QOAuthUriSchemeReplyHandler);
394 return d->listening;
395}
396
397QT_END_NAMESPACE
398
399#include "moc_qoauthurischemereplyhandler.cpp"
400

source code of qtnetworkauth/src/oauth/qoauthurischemereplyhandler.cpp