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{|}')
39 PASSWORD_PATTERN = re.compile(r'password=[^&]+')
40 INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
42 always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
43 'abcdefghijklmnopqrstuvwxyz'
46 log = logging.getLogger('oauthlib')
48 PY3 = sys.version_info[0] == 3
54 unicode_type = unicode
58 # 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
59 def quote(s, safe=b'/'):
60 s = s.encode('utf-8') if isinstance(s, unicode_type) else s
62 # PY3 always returns unicode. PY2 may return either, depending on whether
63 # it had to modify the string.
64 if isinstance(s, bytes_type):
71 # PY3 always returns unicode. PY2 seems to always return what you give it,
72 # which differs from quote's behavior. Just to be safe, make sure it is
73 # unicode before we return.
74 if isinstance(s, bytes_type):
79 def urlencode(params):
80 utf8_params = encode_params_utf8(params)
81 urlencoded = _urlencode(utf8_params)
82 if isinstance(urlencoded, unicode_type): # PY3 returns unicode
85 return urlencoded.decode("utf-8")
88 def encode_params_utf8(params):
89 """Ensures that all parameters in a list of 2-element tuples are encoded to
90 bytestrings using UTF-8
95 k.encode('utf-8') if isinstance(k, unicode_type) else k,
96 v.encode('utf-8') if isinstance(v, unicode_type) else v))
100 def decode_params_utf8(params):
101 """Ensures that all parameters in a list of 2-element tuples are decoded to
107 k.decode('utf-8') if isinstance(k, bytes_type) else k,
108 v.decode('utf-8') if isinstance(v, bytes_type) else v))
112 urlencoded = set(always_safe) | set('=&;%+~,*@!')
115 def urldecode(query):
116 """Decode a query string in x-www-form-urlencoded format into a sequence
117 of two-element tuples.
119 Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
120 correct formatting of the query string by validation. If validation fails
121 a ValueError will be raised. urllib.parse_qsl will only raise errors if
122 any of name-value pairs omits the equals sign.
124 # Check if query contains invalid characters
125 if query and not set(query) <= urlencoded:
126 error = ("Error trying to decode a non urlencoded string. "
127 "Found invalid characters: %s "
128 "in the string: '%s'. "
129 "Please ensure the request/response body is "
130 "x-www-form-urlencoded.")
131 raise ValueError(error % (set(query) - urlencoded, query))
133 # Check for correctly hex encoded values using a regular expression
134 # All encoded values begin with % followed by two hex characters
135 # correct = %00, %A0, %0A, %FF
136 # invalid = %G0, %5H, %PO
137 if INVALID_HEX_PATTERN.search(query):
138 raise ValueError('Invalid hex encoding in query string.')
140 # We encode to utf-8 prior to parsing because parse_qsl behaves
141 # differently on unicode input in python 2 and 3.
143 # >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
144 # u'\xe5\x95\xa6\xe5\x95\xa6'
145 # Python 2.7, non unicode input gives the same
146 # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
147 # '\xe5\x95\xa6\xe5\x95\xa6'
148 # but now we can decode it to unicode
149 # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
152 # >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
154 query = query.encode(
155 'utf-8') if not PY3 and isinstance(query, unicode_type) else query
156 # We want to allow queries such as "c2" whereas urlparse.parse_qsl
157 # with the strict_parsing flag will not.
158 params = urlparse.parse_qsl(query, keep_blank_values=True)
160 # unicode all the things
161 return decode_params_utf8(params)
164 def extract_params(raw):
165 """Extract parameters and return them as a list of 2-tuples.
167 Will successfully extract parameters from urlencoded query strings,
168 dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
169 empty list of parameters. Any other input will result in a return
172 if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
174 params = urldecode(raw)
177 elif hasattr(raw, '__iter__'):
185 params = list(raw.items() if isinstance(raw, dict) else raw)
186 params = decode_params_utf8(params)
193 def generate_nonce():
194 """Generate pseudorandom nonce that is unlikely to repeat.
196 Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
197 Per `section 3.2.1`_ of the MAC Access Authentication spec.
199 A random 64-bit number is appended to the epoch timestamp for both
200 randomness and to decrease the likelihood of collisions.
202 .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
203 .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
205 return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
208 def generate_timestamp():
209 """Get seconds since epoch (UTC).
211 Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
212 Per `section 3.2.1`_ of the MAC Access Authentication spec.
214 .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
215 .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
217 return unicode_type(int(time.time()))
220 def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
221 """Generates a non-guessable OAuth token
223 OAuth (1 and 2) does not specify the format of tokens except that they
224 should be strings of random characters. Tokens should not be guessable
225 and entropy when generating the random characters is important. Which is
226 why SystemRandom is used instead of the default random.choice method.
228 rand = random.SystemRandom()
229 return ''.join(rand.choice(chars) for x in range(length))
232 def generate_signed_token(private_pem, request):
235 now = datetime.datetime.utcnow()
238 'scope': request.scope,
239 'exp': now + datetime.timedelta(seconds=request.expires_in)
242 claims.update(request.claims)
244 token = jwt.encode(claims, private_pem, 'RS256')
245 token = to_unicode(token, "UTF-8")
250 def verify_signed_token(public_pem, token):
253 return jwt.decode(token, public_pem, algorithms=['RS256'])
256 def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
257 """Generates an OAuth client_id
259 OAuth 2 specify the format of client_id in
260 http://tools.ietf.org/html/rfc6749#appendix-A.
262 return generate_token(length, chars)
265 def add_params_to_qs(query, params):
266 """Extend a query with a list of two-tuples."""
267 if isinstance(params, dict):
268 params = params.items()
269 queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
270 queryparams.extend(params)
271 return urlencode(queryparams)
274 def add_params_to_uri(uri, params, fragment=False):
275 """Add a list of two-tuples to the uri query components."""
276 sch, net, path, par, query, fra = urlparse.urlparse(uri)
278 fra = add_params_to_qs(fra, params)
280 query = add_params_to_qs(query, params)
281 return urlparse.urlunparse((sch, net, path, par, query, fra))
284 def safe_string_equals(a, b):
285 """ Near-constant time string comparison.
287 Used in order to avoid timing attacks on sensitive information such
288 as secret keys during request verification (`rootLabs`_).
290 .. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
297 for x, y in zip(a, b):
298 result |= ord(x) ^ ord(y)
302 def to_unicode(data, encoding='UTF-8'):
303 """Convert a number of different types of objects to unicode."""
304 if isinstance(data, unicode_type):
307 if isinstance(data, bytes_type):
308 return unicode_type(data, encoding=encoding)
310 if hasattr(data, '__iter__'):
316 # Assume it's a one dimensional data structure
317 return (to_unicode(i, encoding) for i in data)
319 # We support 2.6 which lacks dict comprehensions
320 if hasattr(data, 'items'):
322 return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
327 class CaseInsensitiveDict(dict):
329 """Basic case insensitive dict with strings only keys."""
333 def __init__(self, data):
334 self.proxy = dict((k.lower(), k) for k in data)
338 def __contains__(self, k):
339 return k.lower() in self.proxy
341 def __delitem__(self, k):
342 key = self.proxy[k.lower()]
343 super(CaseInsensitiveDict, self).__delitem__(key)
344 del self.proxy[k.lower()]
346 def __getitem__(self, k):
347 key = self.proxy[k.lower()]
348 return super(CaseInsensitiveDict, self).__getitem__(key)
350 def get(self, k, default=None):
351 return self[k] if k in self else default
353 def __setitem__(self, k, v):
354 super(CaseInsensitiveDict, self).__setitem__(k, v)
355 self.proxy[k.lower()] = k
358 class Request(object):
360 """A malleable representation of a signable HTTP request.
362 Body argument may contain any data, but parameters will only be decoded if
365 * urlencoded query string
369 Anything else will be treated as raw body data to be passed through
373 def __init__(self, uri, http_method='GET', body=None, headers=None,
375 # Convert to unicode using encoding if given, else assume unicode
376 encode = lambda x: to_unicode(x, encoding) if encoding else x
378 self.uri = encode(uri)
379 self.http_method = encode(http_method)
380 self.headers = CaseInsensitiveDict(encode(headers or {}))
381 self.body = encode(body)
382 self.decoded_body = extract_params(self.body)
383 self.oauth_params = []
384 self.validator_log = {}
387 "access_token": None,
390 "client_secret": None,
392 "extra_credentials": None,
394 "redirect_uri": None,
395 "refresh_token": None,
396 "response_type": None,
402 "token_type_hint": None,
404 self._params.update(dict(urldecode(self.uri_query)))
405 self._params.update(dict(self.decoded_body or []))
406 self._params.update(self.headers)
408 def __getattr__(self, name):
409 if name in self._params:
410 return self._params[name]
412 raise AttributeError(name)
416 if body and 'password=' in body:
417 body = PASSWORD_PATTERN.sub('password=***', body)
418 return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
419 self.uri, self.http_method, self.headers, body)
423 return urlparse.urlparse(self.uri).query
426 def uri_query_params(self):
427 if not self.uri_query:
429 return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
433 def duplicate_params(self):
434 seen_keys = collections.defaultdict(int)
436 for p in (self.decoded_body or []) + self.uri_query_params)
439 return [k for k, c in seen_keys.items() if c > 1]