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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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