summaryrefslogtreecommitdiffstats
path: root/src/oauth/qoauthurischemereplyhandler.cpp
blob: 860c895e995680188e7c7c88d98e2376d5a2c0d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "qabstractoauthreplyhandler_p.h" // for lcReplyHandler()
#include "qoauthoobreplyhandler_p.h"
#include "qoauthurischemereplyhandler.h"

#include <QtGui/qdesktopservices.h>

#include <private/qobject_p.h>

#include <QtCore/qloggingcategory.h>
#include <QtCore/qurlquery.h>

QT_BEGIN_NAMESPACE

/*!
    \class QOAuthUriSchemeReplyHandler
    \inmodule QtNetworkAuth
    \ingroup oauth
    \since 6.8

    \brief Handles private/custom and https URI scheme redirects.

    This class serves as a reply handler for
    \l {https://datatracker.ietf.org/doc/html/rfc6749}{OAuth 2.0} authorization
    processes that use private/custom or HTTPS URI schemes for redirection.
    It manages the reception of the authorization redirection (also known as the
    callback) and the subsequent acquisition of access tokens.

    The \l {https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
    {redirection URI} is where the authorization server redirects the
    user-agent (typically, and preferably, the system browser) once
    the authorization part of the flow is complete.

    The use of specific URI schemes requires configuration at the
    operating system level to associate the URI with
    the correct application. The way to set up this association varies
    between operating systems. See \l {Platform Support and Dependencies}.

    This class complements QOAuthHttpServerReplyHandler,
    which handles \c http schemes by setting up a localhost server.

    The following code illustrates the usage. First, the needed variables:

    \snippet src_oauth_replyhandlers.cpp uri-variables

    Followed up by the OAuth setup (error handling omitted for brevity):

    \snippet src_oauth_replyhandlers.cpp uri-oauth-setup

    Finally, we then set up the URI scheme reply-handler:

    \snippet src_oauth_replyhandlers.cpp uri-handler-setup

    \section1 Private/Custom URI Schemes

    Custom URI schemes typically use reverse-domain notation followed
    by a path, or occasionally a host/host+path:
    \badcode
    // Example with path:
    com.example.myapp:/oauth2/callback
    // Example with host:
    com.example.myapp://oauth2.callback
    \endcode

    \section1 HTTPS URI Scheme

    With HTTPS URI schemes, the redirect URLs are regular https links:
    \badcode
    https://myapp.example.com/oauth2/callback
    \endcode

    These links are called
    \l {https://developer.apple.com/ios/universal-links/}{Universal Links}
    on iOS and
    \l {https://developer.android.com/training/app-links}{App Links on Android}.

    The use of https schemes is recommended as it provides additional security
    by forcing application developers to prove ownership of the URLs used. This
    proving is done by hosting an association file, which the operating system
    will consult as part of its internal URL dispatching.

    The content of this file associates the application and the used URLs.
    The association files must be publicly accessible without any HTTP
    redirects. In addition, the hosting site must have valid certificates
    and, at least with Android, the file must be served as
    \c application/json content-type (refer to your server's configuration
    guide).

    In addition, https links can provide some usability benefits:
    \list
        \li The https URL doubles as a regular https link. If the
            user hasn't installed the application (since the URL wasn't handled
            by any application), the https link may for example serve
            instructions to do so.
        \li The application selection dialogue to open the URL may be avoided,
            and instead your application may be opened automatically
    \endlist

    The tradeoff is that this requires extra setup as you need to set up this
    publicly-hosted association file.

    \section1 Platform Support and Dependencies

    Currently supported platforms are Android, iOS, and macOS.

    URI scheme listening is based on QDesktopServices::setUrlHandler()
    and QDesktopServices::unsetUrlHandler(). These are currently
    provided by Qt::Gui module and therefore QtNetworkAuth module
    depends on Qt::Gui. If QtNetworkAuth is built without Qt::Gui,
    QOAuthUriSchemeReplyHandler will not be included.

    \section2 Android

    On \l {Qt for Android}{Android} the URI schemes require:
    \list
        \li Setting up
            \l {configuring qdesktopservices url handler on android}{intent-filters}
            in the application manifest
        \li Optionally, for automatic verification with https schemes,
            hosting a site association file
            \l {configuring qdesktopservices url handler on android}{assetlinks.json}
    \endlist

    See also the
    \l {https://doc.qt.io/qt-6/android-manifest-file-configuration.html}
    {Qt Android Manifest File Configuration}.

    \section2 iOS and macOS

    On \l {Qt for iOS}{iOS} and \l {Qt for macOS}{macOS} the URI schemes require:
    \list
        \li Setting up site association
            \l {configuring qdesktopservices url handler on ios and macos}{entitlement}
        \li With https schemes, hosting a
            \l {configuring qdesktopservices url handler on ios and macos}{site association file}
            (\c apple-app-site-association)
    \endlist

    \section2 \l {Qt for Windows}{Windows}, \l {Qt for Linux/X11}{Linux}

    Currently not supported.
*/

class QOAuthUriSchemeReplyHandlerPrivate : public QOAuthOobReplyHandlerPrivate
{
    Q_DECLARE_PUBLIC(QOAuthUriSchemeReplyHandler)

public:
    bool hasValidRedirectUrl() const
    {
        // RFC 6749 Section 3.1.2
        return redirectUrl.isValid()
               && !redirectUrl.scheme().isEmpty()
               && redirectUrl.fragment().isEmpty();
    }

    void _q_handleRedirectUrl(const QUrl &url)
    {
        Q_Q(QOAuthUriSchemeReplyHandler);
        // Remove the query parameters from comparison, and compare them manually (the parameters
        // of interest like 'code' and 'state' are received as query parameters and comparison
        // would always fail). Fragments are removed as some servers (eg. Reddit) seem to add some,
        // possibly for some implementation consistency with other OAuth flows where fragments
        // are actually used.
        bool urlMatch = url.matches(redirectUrl, QUrl::RemoveQuery | QUrl::RemoveFragment);

        const QUrlQuery responseQuery{url};
        if (urlMatch) {
            // Verify that query parameters that are part of redirect URL are present in redirection
            const auto registeredItems = QUrlQuery{redirectUrl}.queryItems();
            for (const auto &item: registeredItems) {
                if (!responseQuery.hasQueryItem(item.first)
                    || responseQuery.queryItemValue(item.first) != item.second) {
                    urlMatch = false;
                    break;
                }
            }
        }

        if (!urlMatch) {
            qCDebug(lcReplyHandler(), "Url ignored");
            // The URLs received here might be unrelated. Further, in case of "https" scheme,
            // the first request issued to the authorization server comes through here
            // (if this handler is listening)
            QDesktopServices::openUrl(url);
            return;
        }

        qCDebug(lcReplyHandler(), "Url handled");

        QVariantMap resultParameters;
        const auto responseItems = responseQuery.queryItems(QUrl::FullyDecoded);
        for (const auto &item : responseItems)
            resultParameters.insert(item.first, item.second);

        emit q->callbackReceived(resultParameters);
    }

public:
    QUrl redirectUrl;
    bool listening = false;
};

/*!
    \fn QOAuthUriSchemeReplyHandler::QOAuthUriSchemeReplyHandler()

    Constructs a QOAuthUriSchemeReplyHandler object with empty callback()/
    redirectUrl() and no parent. The constructed object does not automatically
    listen.
*/

/*!
    Constructs a QOAuthUriSchemeReplyHandler object with \a parent and empty
    callback()/redirectUrl(). The constructed object does not automatically listen.
*/
QOAuthUriSchemeReplyHandler::QOAuthUriSchemeReplyHandler(QObject *parent) :
    QOAuthOobReplyHandler(*new QOAuthUriSchemeReplyHandlerPrivate(), parent)
{
}

/*!
    Constructs a QOAuthUriSchemeReplyHandler object and sets \a parent as the
    parent object and \a redirectUrl as the redirect URL. The constructed
    object attempts automatically to listen.

    \sa redirectUrl(), setRedirectUrl(), listen(), isListening()
*/
QOAuthUriSchemeReplyHandler::QOAuthUriSchemeReplyHandler(const QUrl &redirectUrl, QObject *parent)
    : QOAuthUriSchemeReplyHandler(parent)
{
    Q_D(QOAuthUriSchemeReplyHandler);
    d->redirectUrl = redirectUrl;
    listen();
}

/*!
    Destroys the QOAuthUriSchemeReplyHandler object. Closes
    this handler.

    \sa close()
*/
QOAuthUriSchemeReplyHandler::~QOAuthUriSchemeReplyHandler()
{
    close();
}

QString QOAuthUriSchemeReplyHandler::callback() const
{
    Q_D(const QOAuthUriSchemeReplyHandler);
    return d->redirectUrl.toString();
}

/*!
    \property QOAuthUriSchemeReplyHandler::redirectUrl
    \brief The URL used to receive authorization redirection/response.

    This property is used as the
    \l{https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2}
    {OAuth2 redirect_uri parameter}, which is sent as part of the
    authorization request. The \c redirect_uri is acquired by
    calling QUrl::toString() with default options.

    The URL must match the one registered at the authorization server,
    as the authorization servers likely reject any mismatching redirect_uris.

    Similarly, when this handler receives the redirection,
    the redirection URL must match the URL set here. The handler
    compares the scheme, host, port, path, and any
    query items that were part of the URL set by this method.

    The URL is handled only if all of these match. The comparison of query
    parameters excludes any additional query parameters that may have been set
    at server-side, as these contain the actual data of interest.
*/
void QOAuthUriSchemeReplyHandler::setRedirectUrl(const QUrl &url)
{
    Q_D(QOAuthUriSchemeReplyHandler);
    if (url == d->redirectUrl)
        return;

    if (d->listening) {
        close(); // close previous url listening first
        d->redirectUrl = url;
        listen();
    } else {
        d->redirectUrl = url;
    }
    emit redirectUrlChanged();
}

QUrl QOAuthUriSchemeReplyHandler::redirectUrl() const
{
    Q_D(const QOAuthUriSchemeReplyHandler);
    return d->redirectUrl;
}

/*!
    Tells this handler to listen for incoming URLs. Returns
    \c true if listening is successful, and \c false otherwise.

    The handler will match URLs to redirectUrl().
    If the received URL does not match, it will be forwarded to
    QDesktopServices::openURL().

    Active listening is only required when performing the initial
    authorization phase, typically initiated by a
    QOAuth2AuthorizationCodeFlow::grant() call.

    It is recommended to close the listener after successful authorization.
    Listening is not needed for
    \l {QOAuth2AuthorizationCodeFlow::requestAccessToken()}{acquiring access tokens}.
*/
bool QOAuthUriSchemeReplyHandler::listen()
{
    Q_D(QOAuthUriSchemeReplyHandler);
    if (d->listening)
        return true;

    if (!d->hasValidRedirectUrl()) {
        qCWarning(lcReplyHandler(), "listen(): callback url not valid");
        return false;
    }
    qCDebug(lcReplyHandler(), "listen() URL listener");
    QDesktopServices::setUrlHandler(d->redirectUrl.scheme(), this, "_q_handleRedirectUrl");

    d->listening = true;
    return true;
}

/*!
    Tells this handler to stop listening for incoming URLs.

    \sa listen(), isListening()
*/
void QOAuthUriSchemeReplyHandler::close()
{
    Q_D(QOAuthUriSchemeReplyHandler);
    if (!d->listening)
        return;

    qCDebug(lcReplyHandler(), "close() URL listener");
    QDesktopServices::unsetUrlHandler(d->redirectUrl.scheme());
    d->listening = false;
}

/*!
    Returns \c true if this handler is currently listening,
    and \c false otherwise.

    \sa listen(), close()
*/
bool QOAuthUriSchemeReplyHandler::isListening() const noexcept
{
    Q_D(const QOAuthUriSchemeReplyHandler);
    return d->listening;
}

QT_END_NAMESPACE

#include "moc_qoauthurischemereplyhandler.cpp"