1 # -*- coding: utf-8 -*-
6 This module provides data structures and utilities common
7 to all implementations of OAuth.
9 from __future__ import absolute_import, unicode_literals
20 from urllib import quote as _quote
21 from urllib import unquote as _unquote
22 from urllib import urlencode as _urlencode
24 from urllib.parse import quote as _quote
25 from urllib.parse import unquote as _unquote
26 from urllib.parse import urlencode as _urlencode
30 import urllib.parse as urlparse
32 UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
33 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
36 CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
37 'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
40 always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
41 'abcdefghijklmnopqrstuvwxyz'
44 log = logging.getLogger('oauthlib')
46 PY3 = sys.version_info[0] == 3
52 unicode_type = unicode
56 # 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
57 def quote(s, safe=b'/'):
58 s = s.encode('utf-8') if isinstance(s, unicode_type) else s
60 # PY3 always returns unicode. PY2 may return either, depending on whether
61 # it had to modify the string.
62 if isinstance(s, bytes_type):
69 # PY3 always returns unicode. PY2 seems to always return what you give it,
70 # which differs from quote's behavior. Just to be safe, make sure it is
71 # unicode before we return.
72 if isinstance(s, bytes_type):
77 def urlencode(params):
78 utf8_params = encode_params_utf8(params)
79 urlencoded = _urlencode(utf8_params)
80 if isinstance(urlencoded, unicode_type): # PY3 returns unicode
83 return urlencoded.decode("utf-8")
86 def encode_params_utf8(params):
87 """Ensures that all parameters in a list of 2-element tuples are encoded to
88 bytestrings using UTF-8
93 k.encode('utf-8') if isinstance(k, unicode_type) else k,
94 v.encode('utf-8') if isinstance(v, unicode_type) else v))
98 def decode_params_utf8(params):
99 """Ensures that all parameters in a list of 2-element tuples are decoded to
105 k.decode('utf-8') if isinstance(k, bytes_type) else k,
106 v.decode('utf-8') if isinstance(v, bytes_type) else v))
110 urlencoded = set(always_safe) | set('=&;%+~,*@')
113 def urldecode(query):
114 """Decode a query string in x-www-form-urlencoded format into a sequence
115 of two-element tuples.
117 Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
118 correct formatting of the query string by validation. If validation fails
119 a ValueError will be raised. urllib.parse_qsl will only raise errors if
120 any of name-value pairs omits the equals sign.
122 # Check if query contains invalid characters
123 if query and not set(query) <= urlencoded:
124 error = ("Error trying to decode a non urlencoded string. "
125 "Found invalid characters: %s "
126 "in the string: '%s'. "
127 "Please ensure the request/response body is "
128 "x-www-form-urlencoded.")
129 raise ValueError(error % (set(query) - urlencoded, query))
131 # Check for correctly hex encoded values using a regular expression
132 # All encoded values begin with % followed by two hex characters
133 # correct = %00, %A0, %0A, %FF
134 # invalid = %G0, %5H, %PO
135 invalid_hex = '%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]'
136 if len(re.findall(invalid_hex, query)):
137 raise ValueError('Invalid hex encoding in query string.')
139 # We encode to utf-8 prior to parsing because parse_qsl behaves
140 # differently on unicode input in python 2 and 3.
142 # >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
143 # u'\xe5\x95\xa6\xe5\x95\xa6'
144 # Python 2.7, non unicode input gives the same
145 # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
146 # '\xe5\x95\xa6\xe5\x95\xa6'
147 # but now we can decode it to unicode
148 # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
151 # >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
153 query = query.encode(
154 'utf-8') if not PY3 and isinstance(query, unicode_type) else query
155 # We want to allow queries such as "c2" whereas urlparse.parse_qsl
156 # with the strict_parsing flag will not.
157 params = urlparse.parse_qsl(query, keep_blank_values=True)
159 # unicode all the things
160 return decode_params_utf8(params)
163 def extract_params(raw):
164 """Extract parameters and return them as a list of 2-tuples.
166 Will successfully extract parameters from urlencoded query strings,
167 dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
168 empty list of parameters. Any other input will result in a return
171 if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
173 params = urldecode(raw)
176 elif hasattr(raw, '__iter__'):
184 params = list(raw.items() if isinstance(raw, dict) else raw)
185 params = decode_params_utf8(params)
192 def generate_nonce():
193 """Generate pseudorandom nonce that is unlikely to repeat.
195 Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
196 Per `section 3.2.1`_ of the MAC Access Authentication spec.
198 A random 64-bit number is appended to the epoch timestamp for both
199 randomness and to decrease the likelihood of collisions.
201 .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
202 .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
204 return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
207 def generate_timestamp():
208 """Get seconds since epoch (UTC).
210 Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
211 Per `section 3.2.1`_ of the MAC Access Authentication spec.
213 .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
214 .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
216 return unicode_type(int(time.time()))
219 def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
220 """Generates a non-guessable OAuth token
222 OAuth (1 and 2) does not specify the format of tokens except that they
223 should be strings of random characters. Tokens should not be guessable
224 and entropy when generating the random characters is important. Which is
225 why SystemRandom is used instead of the default random.choice method.
227 rand = random.SystemRandom()
228 return ''.join(rand.choice(chars) for x in range(length))
231 def generate_signed_token(private_pem, request):
234 now = datetime.datetime.utcnow()
237 'scope': request.scope,
238 'exp': now + datetime.timedelta(seconds=request.expires_in)
241 claims.update(request.claims)
243 token = jwt.encode(claims, private_pem, 'RS256')
244 token = to_unicode(token, "UTF-8")
249 def verify_signed_token(public_pem, token):
252 return jwt.decode(token, public_pem, algorithms=['RS256'])
255 def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
256 """Generates an OAuth client_id
258 OAuth 2 specify the format of client_id in
259 http://tools.ietf.org/html/rfc6749#appendix-A.
261 return generate_token(length, chars)
264 def add_params_to_qs(query, params):
265 """Extend a query with a list of two-tuples."""
266 if isinstance(params, dict):
267 params = params.items()
268 queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
269 queryparams.extend(params)
270 return urlencode(queryparams)
273 def add_params_to_uri(uri, params, fragment=False):
274 """Add a list of two-tuples to the uri query components."""
275 sch, net, path, par, query, fra = urlparse.urlparse(uri)
277 fra = add_params_to_qs(fra, params)
279 query = add_params_to_qs(query, params)
280 return urlparse.urlunparse((sch, net, path, par, query, fra))
283 def safe_string_equals(a, b):
284 """ Near-constant time string comparison.
286 Used in order to avoid timing attacks on sensitive information such
287 as secret keys during request verification (`rootLabs`_).
289 .. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
296 for x, y in zip(a, b):
297 result |= ord(x) ^ ord(y)
301 def to_unicode(data, encoding='UTF-8'):
302 """Convert a number of different types of objects to unicode."""
303 if isinstance(data, unicode_type):
306 if isinstance(data, bytes_type):
307 return unicode_type(data, encoding=encoding)
309 if hasattr(data, '__iter__'):
315 # Assume it's a one dimensional data structure
316 return (to_unicode(i, encoding) for i in data)
318 # We support 2.6 which lacks dict comprehensions
319 if hasattr(data, 'items'):
321 return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
326 class CaseInsensitiveDict(dict):
328 """Basic case insensitive dict with strings only keys."""
332 def __init__(self, data):
333 self.proxy = dict((k.lower(), k) for k in data)
337 def __contains__(self, k):
338 return k.lower() in self.proxy
340 def __delitem__(self, k):
341 key = self.proxy[k.lower()]
342 super(CaseInsensitiveDict, self).__delitem__(key)
343 del self.proxy[k.lower()]
345 def __getitem__(self, k):
346 key = self.proxy[k.lower()]
347 return super(CaseInsensitiveDict, self).__getitem__(key)
349 def get(self, k, default=None):
350 return self[k] if k in self else default
352 def __setitem__(self, k, v):
353 super(CaseInsensitiveDict, self).__setitem__(k, v)
354 self.proxy[k.lower()] = k
357 class Request(object):
359 """A malleable representation of a signable HTTP request.
361 Body argument may contain any data, but parameters will only be decoded if
364 * urlencoded query string
368 Anything else will be treated as raw body data to be passed through
372 def __init__(self, uri, http_method='GET', body=None, headers=None,
374 # Convert to unicode using encoding if given, else assume unicode
375 encode = lambda x: to_unicode(x, encoding) if encoding else x
377 self.uri = encode(uri)
378 self.http_method = encode(http_method)
379 self.headers = CaseInsensitiveDict(encode(headers or {}))
380 self.body = encode(body)
381 self.decoded_body = extract_params(encode(body))
382 self.oauth_params = []
385 self._params.update(dict(urldecode(self.uri_query)))
386 self._params.update(dict(self.decoded_body or []))
387 self._params.update(self.headers)
389 def __getattr__(self, name):
390 return self._params.get(name, None)
393 return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
394 self.uri, self.http_method, self.headers, self.body)
398 return urlparse.urlparse(self.uri).query
401 def uri_query_params(self):
402 if not self.uri_query:
404 return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
408 def duplicate_params(self):
409 seen_keys = collections.defaultdict(int)
411 for p in (self.decoded_body or []) + self.uri_query_params)
414 return [k for k, c in seen_keys.items() if c > 1]