1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Network Auth module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 or (at your option) any later version |
20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by |
21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
22 | ** included in the packaging of this file. Please review the following |
23 | ** information to ensure the GNU General Public License requirements will |
24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
25 | ** |
26 | ** $QT_END_LICENSE$ |
27 | ** |
28 | ****************************************************************************/ |
29 | |
30 | #include "qoauth1signature.h" |
31 | #include "qoauth1signature_p.h" |
32 | |
33 | #include <QtCore/qurlquery.h> |
34 | #include <QtCore/qloggingcategory.h> |
35 | #include <QtCore/qmessageauthenticationcode.h> |
36 | |
37 | #include <QtNetwork/qnetworkaccessmanager.h> |
38 | |
39 | #include <functional> |
40 | #include <type_traits> |
41 | |
42 | QT_BEGIN_NAMESPACE |
43 | |
44 | Q_LOGGING_CATEGORY(loggingCategory, "qt.networkauth.oauth1.signature" ) |
45 | |
46 | /*! |
47 | \class QOAuth1Signature |
48 | \inmodule QtNetworkAuth |
49 | \ingroup oauth |
50 | \brief Implements OAuth 1 signature methods. |
51 | \since 5.8 |
52 | |
53 | OAuth-authenticated requests can have two sets of credentials: |
54 | those passed via the "oauth_consumer_key" parameter and those in |
55 | the "oauth_token" parameter. In order for the server to verify |
56 | the authenticity of the request and prevent unauthorized access, |
57 | the client needs to prove that it is the rightful owner of the |
58 | credentials. This is accomplished using the shared-secret (or |
59 | RSA key) part of each set of credentials. |
60 | |
61 | OAuth specifies three methods for the client to establish its |
62 | rightful ownership of the credentials: "HMAC-SHA1", "RSA-SHA1", |
63 | and "PLAINTEXT". Each generates a "signature" with which the |
64 | request is "signed"; the first two use a digest of the data |
65 | signed in generating this, though the last does not. The |
66 | "RSA-SHA1" method is not supported here; it would use an RSA key |
67 | rather than the shared-secret associated with the client |
68 | credentials. |
69 | */ |
70 | |
71 | /*! |
72 | \enum QOAuth1Signature::HttpRequestMethod |
73 | |
74 | Indicates the HTTP request method. |
75 | |
76 | \value Head HEAD method. |
77 | \value Get GET method. |
78 | \value Put PUT method. |
79 | \value Post POST method. |
80 | \value Delete DELETE method. |
81 | \value Custom Identifies a custom method. |
82 | \value Unknown Method not set. |
83 | */ |
84 | |
85 | static_assert(static_cast<int>(QOAuth1Signature::HttpRequestMethod::Head) == |
86 | static_cast<int>(QNetworkAccessManager::HeadOperation) && |
87 | static_cast<int>(QOAuth1Signature::HttpRequestMethod::Get) == |
88 | static_cast<int>(QNetworkAccessManager::GetOperation) && |
89 | static_cast<int>(QOAuth1Signature::HttpRequestMethod::Put) == |
90 | static_cast<int>(QNetworkAccessManager::PutOperation) && |
91 | static_cast<int>(QOAuth1Signature::HttpRequestMethod::Post) == |
92 | static_cast<int>(QNetworkAccessManager::PostOperation) && |
93 | static_cast<int>(QOAuth1Signature::HttpRequestMethod::Delete) == |
94 | static_cast<int>(QNetworkAccessManager::DeleteOperation), |
95 | "Invalid QOAuth1Signature::HttpRequestMethod enumeration values" ); |
96 | |
97 | QOAuth1SignaturePrivate QOAuth1SignaturePrivate::shared_null; |
98 | |
99 | QOAuth1SignaturePrivate::QOAuth1SignaturePrivate(const QUrl &url, |
100 | QOAuth1Signature::HttpRequestMethod method, |
101 | const QVariantMap ¶meters, |
102 | const QString &clientSharedKey, |
103 | const QString &tokenSecret) : |
104 | method(method), url(url), clientSharedKey(clientSharedKey), tokenSecret(tokenSecret), |
105 | parameters(parameters) |
106 | {} |
107 | |
108 | QByteArray QOAuth1SignaturePrivate::signatureBaseString() const |
109 | { |
110 | // https://tools.ietf.org/html/rfc5849#section-3.4.1 |
111 | QByteArray base; |
112 | |
113 | switch (method) { |
114 | case QOAuth1Signature::HttpRequestMethod::Head: |
115 | base.append(s: "HEAD" ); |
116 | break; |
117 | case QOAuth1Signature::HttpRequestMethod::Get: |
118 | base.append(s: "GET" ); |
119 | break; |
120 | case QOAuth1Signature::HttpRequestMethod::Put: |
121 | base.append(s: "PUT" ); |
122 | break; |
123 | case QOAuth1Signature::HttpRequestMethod::Post: |
124 | base.append(s: "POST" ); |
125 | break; |
126 | case QOAuth1Signature::HttpRequestMethod::Delete: |
127 | base.append(s: "DELETE" ); |
128 | break; |
129 | case QOAuth1Signature::HttpRequestMethod::Custom: |
130 | if (!customVerb.isEmpty()) { |
131 | base.append(a: customVerb); |
132 | } else { |
133 | qCCritical(loggingCategory, "QOAuth1Signature: HttpRequestMethod::Custom requires " |
134 | "the verb to be set via setCustomMethodString" ); |
135 | } |
136 | break; |
137 | default: |
138 | qCCritical(loggingCategory, "QOAuth1Signature: HttpRequestMethod not supported" ); |
139 | } |
140 | base.append(c: '&'); |
141 | base.append(a: QUrl::toPercentEncoding(url.toString(options: QUrl::RemoveQuery)) + "&" ); |
142 | |
143 | QVariantMap p = parameters; |
144 | { |
145 | // replace '+' with spaces now before decoding so that '%2B' gets left as '+' |
146 | const QString query = url.query().replace(before: QLatin1Char('+'), after: QLatin1Char(' ')); |
147 | const auto queryItems = QUrlQuery(query).queryItems(encoding: QUrl::FullyDecoded); |
148 | for (auto it = queryItems.begin(), end = queryItems.end(); it != end; ++it) |
149 | p.insert(akey: it->first, avalue: it->second); |
150 | } |
151 | base.append(a: encodeHeaders(headers: p)); |
152 | return base; |
153 | } |
154 | |
155 | QByteArray QOAuth1SignaturePrivate::secret() const |
156 | { |
157 | QByteArray secret; |
158 | secret.append(a: QUrl::toPercentEncoding(clientSharedKey)); |
159 | secret.append(c: '&'); |
160 | secret.append(a: QUrl::toPercentEncoding(tokenSecret)); |
161 | return secret; |
162 | } |
163 | |
164 | QByteArray QOAuth1SignaturePrivate::parameterString(const QVariantMap ¶meters) |
165 | { |
166 | QByteArray ret; |
167 | auto previous = parameters.end(); |
168 | for (auto it = parameters.begin(), end = parameters.end(); it != end; previous = it++) { |
169 | if (previous != parameters.end()) { |
170 | if (Q_UNLIKELY(previous.key() == it.key())) |
171 | qCWarning(loggingCategory, "duplicated key %s" , qPrintable(it.key())); |
172 | ret.append(s: "&" ); |
173 | } |
174 | ret.append(a: QUrl::toPercentEncoding(it.key())); |
175 | ret.append(s: "=" ); |
176 | ret.append(a: QUrl::toPercentEncoding(it.value().toString())); |
177 | } |
178 | return ret; |
179 | } |
180 | |
181 | QByteArray QOAuth1SignaturePrivate::(const QVariantMap &) |
182 | { |
183 | return QUrl::toPercentEncoding(QString::fromLatin1(str: parameterString(parameters: headers))); |
184 | } |
185 | |
186 | /*! |
187 | Creates a QOAuth1Signature using |
188 | \list |
189 | \li \a url as the target address |
190 | \li \a method as the HTTP method used to send the request |
191 | \li and the given user \a parameters to augment the request. |
192 | \endlist |
193 | */ |
194 | QOAuth1Signature::QOAuth1Signature(const QUrl &url, QOAuth1Signature::HttpRequestMethod method, |
195 | const QVariantMap ¶meters) : |
196 | d(new QOAuth1SignaturePrivate(url, method, parameters)) |
197 | {} |
198 | |
199 | /*! |
200 | Creates a QOAuth1Signature using |
201 | \list |
202 | \li \a url as the target address |
203 | \li \a clientSharedKey as the user token used to verify the |
204 | signature |
205 | \li \a tokenSecret as the negotiated token used to verify |
206 | the signature |
207 | \li \a method as the HTTP method used to send the request |
208 | \li and the given user \a parameters to augment the request. |
209 | \endlist |
210 | */ |
211 | QOAuth1Signature::QOAuth1Signature(const QUrl &url, const QString &clientSharedKey, |
212 | const QString &tokenSecret, HttpRequestMethod method, |
213 | const QVariantMap ¶meters) : |
214 | d(new QOAuth1SignaturePrivate(url, method, parameters, clientSharedKey, tokenSecret)) |
215 | {} |
216 | |
217 | /*! |
218 | Creates a copy of \a other. |
219 | */ |
220 | QOAuth1Signature::QOAuth1Signature(const QOAuth1Signature &other) : d(other.d) |
221 | {} |
222 | |
223 | /*! |
224 | Move-constructs a QOAuth1Signature instance, taking over the |
225 | private data \a other was using. |
226 | */ |
227 | QOAuth1Signature::QOAuth1Signature(QOAuth1Signature &&other) : d(std::move(other.d)) |
228 | { |
229 | } |
230 | |
231 | /*! |
232 | Destroys the QOAuth1Signature. |
233 | */ |
234 | QOAuth1Signature::~QOAuth1Signature() |
235 | {} |
236 | |
237 | /*! |
238 | Returns the request method. |
239 | */ |
240 | QOAuth1Signature::HttpRequestMethod QOAuth1Signature::httpRequestMethod() const |
241 | { |
242 | return d->method; |
243 | } |
244 | |
245 | /*! |
246 | Sets the request \a method. |
247 | */ |
248 | void QOAuth1Signature::setHttpRequestMethod(QOAuth1Signature::HttpRequestMethod method) |
249 | { |
250 | d->method = method; |
251 | } |
252 | |
253 | /*! |
254 | \since 5.13 |
255 | |
256 | Returns the custom method string. |
257 | |
258 | \sa httpRequestMethod() |
259 | */ |
260 | QByteArray QOAuth1Signature::customMethodString() const |
261 | { |
262 | return d->customVerb; |
263 | } |
264 | |
265 | /*! |
266 | \since 5.13 |
267 | |
268 | Sets a custom request method. Will set the httpRequestMethod |
269 | to QOAuth1Signature::HttpRequestMethod::Custom and store the |
270 | \a verb to use it for the generation of the signature. |
271 | |
272 | \note Using this method is required when working with custom verbs. |
273 | Setting only the request method will fail, as the signure needs to |
274 | know the actual verb. |
275 | |
276 | \sa setHttpRequestMethod(), HttpRequestMethod |
277 | */ |
278 | void QOAuth1Signature::setCustomMethodString(const QByteArray &verb) |
279 | { |
280 | d->method = QOAuth1Signature::HttpRequestMethod::Custom; |
281 | d->customVerb = verb; |
282 | } |
283 | |
284 | /*! |
285 | Returns the URL. |
286 | */ |
287 | QUrl QOAuth1Signature::url() const |
288 | { |
289 | return d->url; |
290 | } |
291 | |
292 | /*! |
293 | Sets the URL to \a url. |
294 | */ |
295 | void QOAuth1Signature::setUrl(const QUrl &url) |
296 | { |
297 | d->url = url; |
298 | } |
299 | |
300 | /*! |
301 | Returns the parameters. |
302 | */ |
303 | QVariantMap QOAuth1Signature::parameters() const |
304 | { |
305 | return d->parameters; |
306 | } |
307 | |
308 | /*! |
309 | Sets the \a parameters. |
310 | */ |
311 | void QOAuth1Signature::setParameters(const QVariantMap ¶meters) |
312 | { |
313 | d->parameters.clear(); |
314 | for (auto it = parameters.cbegin(), end = parameters.cend(); it != end; ++it) |
315 | d->parameters.insert(akey: it.key(), avalue: it.value()); |
316 | } |
317 | |
318 | /*! |
319 | Adds the request \a body to the signature. When a POST request |
320 | body contains arguments they should be included in the signed |
321 | data. |
322 | */ |
323 | void QOAuth1Signature::addRequestBody(const QUrlQuery &body) |
324 | { |
325 | const auto list = body.queryItems(); |
326 | for (auto it = list.begin(), end = list.end(); it != end; ++it) |
327 | d->parameters.replace(key: it->first, value: it->second); |
328 | } |
329 | |
330 | /*! |
331 | Inserts a new pair \a key, \a value into the signature. When a |
332 | POST request body contains arguments they should be included in |
333 | the signed data. |
334 | */ |
335 | void QOAuth1Signature::insert(const QString &key, const QVariant &value) |
336 | { |
337 | d->parameters.replace(key, value); |
338 | } |
339 | |
340 | /*! |
341 | Retrieves the list of keys of parameters included in the signed |
342 | data. |
343 | */ |
344 | QList<QString> QOAuth1Signature::keys() const |
345 | { |
346 | return d->parameters.uniqueKeys(); |
347 | } |
348 | |
349 | /*! |
350 | Removes \a key and any associated value from the signed data. |
351 | */ |
352 | QVariant QOAuth1Signature::take(const QString &key) |
353 | { |
354 | return d->parameters.take(akey: key); |
355 | } |
356 | |
357 | /*! |
358 | Returns the value associated with \a key, if present in the |
359 | signed data, otherwise \a defaultValue. |
360 | */ |
361 | QVariant QOAuth1Signature::value(const QString &key, const QVariant &defaultValue) const |
362 | { |
363 | return d->parameters.value(akey: key, adefaultValue: defaultValue); |
364 | } |
365 | |
366 | /*! |
367 | Returns the user secret used to generate the signature. |
368 | */ |
369 | QString QOAuth1Signature::clientSharedKey() const |
370 | { |
371 | return d->clientSharedKey; |
372 | } |
373 | |
374 | /*! |
375 | Sets \a secret as the user secret used to generate the signature. |
376 | */ |
377 | void QOAuth1Signature::setClientSharedKey(const QString &secret) |
378 | { |
379 | d->clientSharedKey = secret; |
380 | } |
381 | |
382 | /*! |
383 | Returns the negotiated secret used to generate the signature. |
384 | */ |
385 | QString QOAuth1Signature::tokenSecret() const |
386 | { |
387 | return d->tokenSecret; |
388 | } |
389 | |
390 | /*! |
391 | Sets \a secret as the negotiated secret used to generate the |
392 | signature. |
393 | */ |
394 | void QOAuth1Signature::setTokenSecret(const QString &secret) |
395 | { |
396 | d->tokenSecret = secret; |
397 | } |
398 | |
399 | /*! |
400 | Generates the HMAC-SHA1 signature using the client shared secret |
401 | and, where available, token secret. |
402 | */ |
403 | QByteArray QOAuth1Signature::hmacSha1() const |
404 | { |
405 | QMessageAuthenticationCode code(QCryptographicHash::Sha1); |
406 | code.setKey(d->secret()); |
407 | code.addData(data: d->signatureBaseString()); |
408 | return code.result(); |
409 | } |
410 | |
411 | /*! |
412 | Generates the RSA-SHA1 signature. |
413 | |
414 | \note Currently this method is not supported. |
415 | */ |
416 | QByteArray QOAuth1Signature::rsaSha1() const |
417 | { |
418 | qCCritical(loggingCategory, "RSA-SHA1 signing method not supported" ); |
419 | return QByteArray(); |
420 | } |
421 | |
422 | /*! |
423 | Generates the PLAINTEXT signature. |
424 | */ |
425 | QByteArray QOAuth1Signature::plainText() const |
426 | { |
427 | return plainText(clientSharedSecret: d->clientSharedKey, tokenSecret: d->tokenSecret); |
428 | } |
429 | |
430 | /*! |
431 | Generates a PLAINTEXT signature from the client secret |
432 | \a clientSharedKey and the token secret \a tokenSecret. |
433 | */ |
434 | QByteArray QOAuth1Signature::plainText(const QString &clientSharedKey, |
435 | const QString &tokenSecret) |
436 | { |
437 | QByteArray ret; |
438 | ret += clientSharedKey.toUtf8() + '&' + tokenSecret.toUtf8(); |
439 | return ret; |
440 | } |
441 | |
442 | /*! |
443 | Swaps signature \a other with this signature. This operation is |
444 | very fast and never fails. |
445 | */ |
446 | void QOAuth1Signature::swap(QOAuth1Signature &other) |
447 | { |
448 | qSwap(value1&: d, value2&: other.d); |
449 | } |
450 | |
451 | QOAuth1Signature &QOAuth1Signature::operator=(const QOAuth1Signature &other) |
452 | { |
453 | if (d != other.d) { |
454 | QOAuth1Signature tmp(other); |
455 | tmp.swap(other&: *this); |
456 | } |
457 | return *this; |
458 | } |
459 | |
460 | /*! |
461 | Move-assignment operator. |
462 | */ |
463 | QOAuth1Signature &QOAuth1Signature::operator=(QOAuth1Signature &&other) |
464 | { |
465 | QOAuth1Signature moved(std::move(other)); |
466 | swap(other&: moved); |
467 | return *this; |
468 | } |
469 | |
470 | QT_END_NAMESPACE |
471 | |