Updated packages and code to python3. Won't work with python 2
[twitter-api-cdsw-solutions] / oauthlib / oauth1 / rfc5849 / signature.py
1 # -*- coding: utf-8 -*-
2 """
3 oauthlib.oauth1.rfc5849.signature
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 This module represents a direct implementation of `section 3.4`_ of the spec.
7
8 Terminology:
9  * Client: software interfacing with an OAuth API
10  * Server: the API provider
11  * Resource Owner: the user who is granting authorization to the client
12
13 Steps for signing a request:
14
15 1. Collect parameters from the uri query, auth header, & body
16 2. Normalize those parameters
17 3. Normalize the uri
18 4. Pass the normalized uri, normalized parameters, and http method to
19    construct the base string
20 5. Pass the base string and any keys needed to a signing function
21
22 .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
23 """
24 from __future__ import absolute_import, unicode_literals
25
26 import binascii
27 import hashlib
28 import hmac
29 try:
30     import urlparse
31 except ImportError:
32     import urllib.parse as urlparse
33 from . import utils
34 from oauthlib.common import urldecode, extract_params, safe_string_equals
35 from oauthlib.common import bytes_type, unicode_type
36
37
38 def construct_base_string(http_method, base_string_uri,
39                           normalized_encoded_request_parameters):
40     """**String Construction**
41     Per `section 3.4.1.1`_ of the spec.
42
43     For example, the HTTP request::
44
45         POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
46         Host: example.com
47         Content-Type: application/x-www-form-urlencoded
48         Authorization: OAuth realm="Example",
49             oauth_consumer_key="9djdj82h48djs9d2",
50             oauth_token="kkk9d7dh3k39sjv7",
51             oauth_signature_method="HMAC-SHA1",
52             oauth_timestamp="137131201",
53             oauth_nonce="7d8f3e4a",
54             oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
55
56         c2&a3=2+q
57
58     is represented by the following signature base string (line breaks
59     are for display purposes only)::
60
61         POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
62         %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
63         key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
64         ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
65         9d7dh3k39sjv7
66
67     .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
68     """
69
70     # The signature base string is constructed by concatenating together,
71     # in order, the following HTTP request elements:
72
73     # 1.  The HTTP request method in uppercase.  For example: "HEAD",
74     #     "GET", "POST", etc.  If the request uses a custom HTTP method, it
75     #     MUST be encoded (`Section 3.6`_).
76     #
77     # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
78     base_string = utils.escape(http_method.upper())
79
80     # 2.  An "&" character (ASCII code 38).
81     base_string += '&'
82
83     # 3.  The base string URI from `Section 3.4.1.2`_, after being encoded
84     #     (`Section 3.6`_).
85     #
86     # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
87     # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
88     base_string += utils.escape(base_string_uri)
89
90     # 4.  An "&" character (ASCII code 38).
91     base_string += '&'
92
93     # 5.  The request parameters as normalized in `Section 3.4.1.3.2`_, after
94     #     being encoded (`Section 3.6`).
95     #
96     # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
97     # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
98     base_string += utils.escape(normalized_encoded_request_parameters)
99
100     return base_string
101
102
103 def normalize_base_string_uri(uri, host=None):
104     """**Base String URI**
105     Per `section 3.4.1.2`_ of the spec.
106
107     For example, the HTTP request::
108
109         GET /r%20v/X?id=123 HTTP/1.1
110         Host: EXAMPLE.COM:80
111
112     is represented by the base string URI: "http://example.com/r%20v/X".
113
114     In another example, the HTTPS request::
115
116         GET /?q=1 HTTP/1.1
117         Host: www.example.net:8080
118
119     is represented by the base string URI: "https://www.example.net:8080/".
120
121     .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
122
123     The host argument overrides the netloc part of the uri argument.
124     """
125     if not isinstance(uri, unicode_type):
126         raise ValueError('uri must be a unicode object.')
127
128     # FIXME: urlparse does not support unicode
129     scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
130
131     # The scheme, authority, and path of the request resource URI `RFC3986`
132     # are included by constructing an "http" or "https" URI representing
133     # the request resource (without the query or fragment) as follows:
134     #
135     # .. _`RFC3986`: http://tools.ietf.org/html/rfc3986
136
137     if not scheme or not netloc:
138         raise ValueError('uri must include a scheme and netloc')
139
140     # Per `RFC 2616 section 5.1.2`_:
141     #
142     # Note that the absolute path cannot be empty; if none is present in
143     # the original URI, it MUST be given as "/" (the server root).
144     #
145     # .. _`RFC 2616 section 5.1.2`: http://tools.ietf.org/html/rfc2616#section-5.1.2
146     if not path:
147         path = '/'
148
149     # 1.  The scheme and host MUST be in lowercase.
150     scheme = scheme.lower()
151     netloc = netloc.lower()
152
153     # 2.  The host and port values MUST match the content of the HTTP
154     #     request "Host" header field.
155     if host is not None:
156         netloc = host.lower()
157
158     # 3.  The port MUST be included if it is not the default port for the
159     #     scheme, and MUST be excluded if it is the default.  Specifically,
160     #     the port MUST be excluded when making an HTTP request `RFC2616`_
161     #     to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
162     #     All other non-default port numbers MUST be included.
163     #
164     # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616
165     # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818
166     default_ports = (
167         ('http', '80'),
168         ('https', '443'),
169     )
170     if ':' in netloc:
171         host, port = netloc.split(':', 1)
172         if (scheme, port) in default_ports:
173             netloc = host
174
175     return urlparse.urlunparse((scheme, netloc, path, params, '', ''))
176
177
178 # ** Request Parameters **
179 #
180 #    Per `section 3.4.1.3`_ of the spec.
181 #
182 #    In order to guarantee a consistent and reproducible representation of
183 #    the request parameters, the parameters are collected and decoded to
184 #    their original decoded form.  They are then sorted and encoded in a
185 #    particular manner that is often different from their original
186 #    encoding scheme, and concatenated into a single string.
187 #
188 # .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
189
190 def collect_parameters(uri_query='', body=[], headers=None,
191                        exclude_oauth_signature=True, with_realm=False):
192     """**Parameter Sources**
193
194     Parameters starting with `oauth_` will be unescaped.
195
196     Body parameters must be supplied as a dict, a list of 2-tuples, or a
197     formencoded query string.
198
199     Headers must be supplied as a dict.
200
201     Per `section 3.4.1.3.1`_ of the spec.
202
203     For example, the HTTP request::
204
205         POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
206         Host: example.com
207         Content-Type: application/x-www-form-urlencoded
208         Authorization: OAuth realm="Example",
209             oauth_consumer_key="9djdj82h48djs9d2",
210             oauth_token="kkk9d7dh3k39sjv7",
211             oauth_signature_method="HMAC-SHA1",
212             oauth_timestamp="137131201",
213             oauth_nonce="7d8f3e4a",
214             oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"
215
216         c2&a3=2+q
217
218     contains the following (fully decoded) parameters used in the
219     signature base sting::
220
221         +------------------------+------------------+
222         |          Name          |       Value      |
223         +------------------------+------------------+
224         |           b5           |       =%3D       |
225         |           a3           |         a        |
226         |           c@           |                  |
227         |           a2           |        r b       |
228         |   oauth_consumer_key   | 9djdj82h48djs9d2 |
229         |       oauth_token      | kkk9d7dh3k39sjv7 |
230         | oauth_signature_method |     HMAC-SHA1    |
231         |     oauth_timestamp    |     137131201    |
232         |       oauth_nonce      |     7d8f3e4a     |
233         |           c2           |                  |
234         |           a3           |        2 q       |
235         +------------------------+------------------+
236
237     Note that the value of "b5" is "=%3D" and not "==".  Both "c@" and
238     "c2" have empty values.  While the encoding rules specified in this
239     specification for the purpose of constructing the signature base
240     string exclude the use of a "+" character (ASCII code 43) to
241     represent an encoded space character (ASCII code 32), this practice
242     is widely used in "application/x-www-form-urlencoded" encoded values,
243     and MUST be properly decoded, as demonstrated by one of the "a3"
244     parameter instances (the "a3" parameter is used twice in this
245     request).
246
247     .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
248     """
249     headers = headers or {}
250     params = []
251
252     # The parameters from the following sources are collected into a single
253     # list of name/value pairs:
254
255     # *  The query component of the HTTP request URI as defined by
256     #    `RFC3986, Section 3.4`_.  The query component is parsed into a list
257     #    of name/value pairs by treating it as an
258     #    "application/x-www-form-urlencoded" string, separating the names
259     #    and values and decoding them as defined by
260     #    `W3C.REC-html40-19980424`_, Section 17.13.4.
261     #
262     # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
263     # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
264     if uri_query:
265         params.extend(urldecode(uri_query))
266
267     # *  The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
268     #    present.  The header's content is parsed into a list of name/value
269     #    pairs excluding the "realm" parameter if present.  The parameter
270     #    values are decoded as defined by `Section 3.5.1`_.
271     #
272     # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
273     if headers:
274         headers_lower = dict((k.lower(), v) for k, v in headers.items())
275         authorization_header = headers_lower.get('authorization')
276         if authorization_header is not None:
277             params.extend([i for i in utils.parse_authorization_header(
278                 authorization_header) if with_realm or i[0] != 'realm'])
279
280     # *  The HTTP request entity-body, but only if all of the following
281     #    conditions are met:
282     #     *  The entity-body is single-part.
283     #
284     #     *  The entity-body follows the encoding requirements of the
285     #        "application/x-www-form-urlencoded" content-type as defined by
286     #        `W3C.REC-html40-19980424`_.
287
288     #     *  The HTTP request entity-header includes the "Content-Type"
289     #        header field set to "application/x-www-form-urlencoded".
290     #
291     # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
292
293     # TODO: enforce header param inclusion conditions
294     bodyparams = extract_params(body) or []
295     params.extend(bodyparams)
296
297     # ensure all oauth params are unescaped
298     unescaped_params = []
299     for k, v in params:
300         if k.startswith('oauth_'):
301             v = utils.unescape(v)
302         unescaped_params.append((k, v))
303
304     # The "oauth_signature" parameter MUST be excluded from the signature
305     # base string if present.
306     if exclude_oauth_signature:
307         unescaped_params = list(filter(lambda i: i[0] != 'oauth_signature',
308                                        unescaped_params))
309
310     return unescaped_params
311
312
313 def normalize_parameters(params):
314     """**Parameters Normalization**
315     Per `section 3.4.1.3.2`_ of the spec.
316
317     For example, the list of parameters from the previous section would
318     be normalized as follows:
319
320     Encoded::
321
322     +------------------------+------------------+
323     |          Name          |       Value      |
324     +------------------------+------------------+
325     |           b5           |     %3D%253D     |
326     |           a3           |         a        |
327     |          c%40          |                  |
328     |           a2           |       r%20b      |
329     |   oauth_consumer_key   | 9djdj82h48djs9d2 |
330     |       oauth_token      | kkk9d7dh3k39sjv7 |
331     | oauth_signature_method |     HMAC-SHA1    |
332     |     oauth_timestamp    |     137131201    |
333     |       oauth_nonce      |     7d8f3e4a     |
334     |           c2           |                  |
335     |           a3           |       2%20q      |
336     +------------------------+------------------+
337
338     Sorted::
339
340     +------------------------+------------------+
341     |          Name          |       Value      |
342     +------------------------+------------------+
343     |           a2           |       r%20b      |
344     |           a3           |       2%20q      |
345     |           a3           |         a        |
346     |           b5           |     %3D%253D     |
347     |          c%40          |                  |
348     |           c2           |                  |
349     |   oauth_consumer_key   | 9djdj82h48djs9d2 |
350     |       oauth_nonce      |     7d8f3e4a     |
351     | oauth_signature_method |     HMAC-SHA1    |
352     |     oauth_timestamp    |     137131201    |
353     |       oauth_token      | kkk9d7dh3k39sjv7 |
354     +------------------------+------------------+
355
356     Concatenated Pairs::
357
358     +-------------------------------------+
359     |              Name=Value             |
360     +-------------------------------------+
361     |               a2=r%20b              |
362     |               a3=2%20q              |
363     |                 a3=a                |
364     |             b5=%3D%253D             |
365     |                c%40=                |
366     |                 c2=                 |
367     | oauth_consumer_key=9djdj82h48djs9d2 |
368     |         oauth_nonce=7d8f3e4a        |
369     |   oauth_signature_method=HMAC-SHA1  |
370     |      oauth_timestamp=137131201      |
371     |     oauth_token=kkk9d7dh3k39sjv7    |
372     +-------------------------------------+
373
374     and concatenated together into a single string (line breaks are for
375     display purposes only)::
376
377         a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
378         dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
379         &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
380
381     .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
382     """
383
384     # The parameters collected in `Section 3.4.1.3`_ are normalized into a
385     # single string as follows:
386     #
387     # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
388
389     # 1.  First, the name and value of each parameter are encoded
390     #     (`Section 3.6`_).
391     #
392     # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
393     key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
394
395     # 2.  The parameters are sorted by name, using ascending byte value
396     #     ordering.  If two or more parameters share the same name, they
397     #     are sorted by their value.
398     key_values.sort()
399
400     # 3.  The name of each parameter is concatenated to its corresponding
401     #     value using an "=" character (ASCII code 61) as a separator, even
402     #     if the value is empty.
403     parameter_parts = ['{0}={1}'.format(k, v) for k, v in key_values]
404
405     # 4.  The sorted name/value pairs are concatenated together into a
406     #     single string by using an "&" character (ASCII code 38) as
407     #     separator.
408     return '&'.join(parameter_parts)
409
410
411 def sign_hmac_sha1_with_client(base_string, client):
412     return sign_hmac_sha1(base_string,
413                           client.client_secret,
414                           client.resource_owner_secret
415                           )
416
417
418 def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
419     """**HMAC-SHA1**
420
421     The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
422     algorithm as defined in `RFC2104`_::
423
424         digest = HMAC-SHA1 (key, text)
425
426     Per `section 3.4.2`_ of the spec.
427
428     .. _`RFC2104`: http://tools.ietf.org/html/rfc2104
429     .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
430     """
431
432     # The HMAC-SHA1 function variables are used in following way:
433
434     # text is set to the value of the signature base string from
435     # `Section 3.4.1.1`_.
436     #
437     # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
438     text = base_string
439
440     # key is set to the concatenated values of:
441     # 1.  The client shared-secret, after being encoded (`Section 3.6`_).
442     #
443     # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
444     key = utils.escape(client_secret or '')
445
446     # 2.  An "&" character (ASCII code 38), which MUST be included
447     #     even when either secret is empty.
448     key += '&'
449
450     # 3.  The token shared-secret, after being encoded (`Section 3.6`_).
451     #
452     # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
453     key += utils.escape(resource_owner_secret or '')
454
455     # FIXME: HMAC does not support unicode!
456     key_utf8 = key.encode('utf-8')
457     text_utf8 = text.encode('utf-8')
458     signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
459
460     # digest  is used to set the value of the "oauth_signature" protocol
461     #         parameter, after the result octet string is base64-encoded
462     #         per `RFC2045, Section 6.8`.
463     #
464     # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8
465     return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
466
467 _jwtrs1 = None
468
469 #jwt has some nice pycrypto/cryptography abstractions
470 def _jwt_rs1_signing_algorithm():
471     global _jwtrs1
472     if _jwtrs1 is None:
473         import jwt.algorithms as jwtalgo
474         _jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1)
475     return _jwtrs1
476
477 def sign_rsa_sha1(base_string, rsa_private_key):
478     """**RSA-SHA1**
479
480     Per `section 3.4.3`_ of the spec.
481
482     The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
483     algorithm as defined in `RFC3447, Section 8.2`_ (also known as
484     PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5.  To
485     use this method, the client MUST have established client credentials
486     with the server that included its RSA public key (in a manner that is
487     beyond the scope of this specification).
488
489     .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
490     .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
491
492     """
493     if isinstance(base_string, unicode_type):
494         base_string = base_string.encode('utf-8')
495     # TODO: finish RSA documentation
496     alg = _jwt_rs1_signing_algorithm()
497     key = _prepare_key_plus(alg, rsa_private_key)
498     s=alg.sign(base_string, key)
499     return binascii.b2a_base64(s)[:-1].decode('utf-8')
500
501
502 def sign_rsa_sha1_with_client(base_string, client):
503     return sign_rsa_sha1(base_string, client.rsa_key)
504
505
506 def sign_plaintext(client_secret, resource_owner_secret):
507     """Sign a request using plaintext.
508
509     Per `section 3.4.4`_ of the spec.
510
511     The "PLAINTEXT" method does not employ a signature algorithm.  It
512     MUST be used with a transport-layer mechanism such as TLS or SSL (or
513     sent over a secure channel with equivalent protections).  It does not
514     utilize the signature base string or the "oauth_timestamp" and
515     "oauth_nonce" parameters.
516
517     .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4
518
519     """
520
521     # The "oauth_signature" protocol parameter is set to the concatenated
522     # value of:
523
524     # 1.  The client shared-secret, after being encoded (`Section 3.6`_).
525     #
526     # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
527     signature = utils.escape(client_secret or '')
528
529     # 2.  An "&" character (ASCII code 38), which MUST be included even
530     #     when either secret is empty.
531     signature += '&'
532
533     # 3.  The token shared-secret, after being encoded (`Section 3.6`_).
534     #
535     # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
536     signature += utils.escape(resource_owner_secret or '')
537
538     return signature
539
540
541 def sign_plaintext_with_client(base_string, client):
542     return sign_plaintext(client.client_secret, client.resource_owner_secret)
543
544
545 def verify_hmac_sha1(request, client_secret=None,
546                      resource_owner_secret=None):
547     """Verify a HMAC-SHA1 signature.
548
549     Per `section 3.4`_ of the spec.
550
551     .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
552
553     To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
554     attribute MUST be an absolute URI whose netloc part identifies the
555     origin server or gateway on which the resource resides. Any Host
556     item of the request argument's headers dict attribute will be
557     ignored.
558
559     .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
560
561     """
562     norm_params = normalize_parameters(request.params)
563     uri = normalize_base_string_uri(request.uri)
564     base_string = construct_base_string(request.http_method, uri, norm_params)
565     signature = sign_hmac_sha1(base_string, client_secret,
566                                resource_owner_secret)
567     return safe_string_equals(signature, request.signature)
568
569 def _prepare_key_plus(alg, keystr):
570     if isinstance(keystr, bytes_type):
571         keystr = keystr.decode('utf-8')
572     return alg.prepare_key(keystr)
573
574 def verify_rsa_sha1(request, rsa_public_key):
575     """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.
576
577     Per `section 3.4.3`_ of the spec.
578
579     Note this method requires the jwt and cryptography libraries.
580
581     .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
582
583     To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
584     attribute MUST be an absolute URI whose netloc part identifies the
585     origin server or gateway on which the resource resides. Any Host
586     item of the request argument's headers dict attribute will be
587     ignored.
588
589     .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
590     """
591     norm_params = normalize_parameters(request.params)
592     uri = normalize_base_string_uri(request.uri)
593     message = construct_base_string(request.http_method, uri, norm_params).encode('utf-8')
594     sig = binascii.a2b_base64(request.signature.encode('utf-8'))
595
596     alg = _jwt_rs1_signing_algorithm()
597     key = _prepare_key_plus(alg, rsa_public_key)
598     return alg.verify(message, key, sig)
599
600
601 def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
602     """Verify a PLAINTEXT signature.
603
604     Per `section 3.4`_ of the spec.
605
606     .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
607     """
608     signature = sign_plaintext(client_secret, resource_owner_secret)
609     return safe_string_equals(signature, request.signature)

Benjamin Mako Hill || Want to submit a patch?