__init__.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. """
  2. oauthlib.oauth1.rfc5849
  3. ~~~~~~~~~~~~~~
  4. This module is an implementation of various logic needed
  5. for signing and checking OAuth 1.0 RFC 5849 requests.
  6. It supports all three standard signature methods defined in RFC 5849:
  7. - HMAC-SHA1
  8. - RSA-SHA1
  9. - PLAINTEXT
  10. It also supports signature methods that are not defined in RFC 5849. These are
  11. based on the standard ones but replace SHA-1 with the more secure SHA-256:
  12. - HMAC-SHA256
  13. - RSA-SHA256
  14. """
  15. import base64
  16. import hashlib
  17. import logging
  18. import urllib.parse as urlparse
  19. from oauthlib.common import (
  20. Request, generate_nonce, generate_timestamp, to_unicode, urlencode,
  21. )
  22. from . import parameters, signature
  23. log = logging.getLogger(__name__)
  24. # Available signature methods
  25. #
  26. # Note: SIGNATURE_HMAC and SIGNATURE_RSA are kept for backward compatibility
  27. # with previous versions of this library, when it the only HMAC-based and
  28. # RSA-based signature methods were HMAC-SHA1 and RSA-SHA1. But now that it
  29. # supports other hashing algorithms besides SHA1, explicitly identifying which
  30. # hashing algorithm is being used is recommended.
  31. #
  32. # Note: if additional values are defined here, don't forget to update the
  33. # imports in "../__init__.py" so they are available outside this module.
  34. SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
  35. SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
  36. SIGNATURE_HMAC_SHA512 = "HMAC-SHA512"
  37. SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 # deprecated variable for HMAC-SHA1
  38. SIGNATURE_RSA_SHA1 = "RSA-SHA1"
  39. SIGNATURE_RSA_SHA256 = "RSA-SHA256"
  40. SIGNATURE_RSA_SHA512 = "RSA-SHA512"
  41. SIGNATURE_RSA = SIGNATURE_RSA_SHA1 # deprecated variable for RSA-SHA1
  42. SIGNATURE_PLAINTEXT = "PLAINTEXT"
  43. SIGNATURE_METHODS = (
  44. SIGNATURE_HMAC_SHA1,
  45. SIGNATURE_HMAC_SHA256,
  46. SIGNATURE_HMAC_SHA512,
  47. SIGNATURE_RSA_SHA1,
  48. SIGNATURE_RSA_SHA256,
  49. SIGNATURE_RSA_SHA512,
  50. SIGNATURE_PLAINTEXT
  51. )
  52. SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
  53. SIGNATURE_TYPE_QUERY = 'QUERY'
  54. SIGNATURE_TYPE_BODY = 'BODY'
  55. CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
  56. class Client:
  57. """A client used to sign OAuth 1.0 RFC 5849 requests."""
  58. SIGNATURE_METHODS = {
  59. SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client,
  60. SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client,
  61. SIGNATURE_HMAC_SHA512: signature.sign_hmac_sha512_with_client,
  62. SIGNATURE_RSA_SHA1: signature.sign_rsa_sha1_with_client,
  63. SIGNATURE_RSA_SHA256: signature.sign_rsa_sha256_with_client,
  64. SIGNATURE_RSA_SHA512: signature.sign_rsa_sha512_with_client,
  65. SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
  66. }
  67. @classmethod
  68. def register_signature_method(cls, method_name, method_callback):
  69. cls.SIGNATURE_METHODS[method_name] = method_callback
  70. def __init__(self, client_key,
  71. client_secret=None,
  72. resource_owner_key=None,
  73. resource_owner_secret=None,
  74. callback_uri=None,
  75. signature_method=SIGNATURE_HMAC_SHA1,
  76. signature_type=SIGNATURE_TYPE_AUTH_HEADER,
  77. rsa_key=None, verifier=None, realm=None,
  78. encoding='utf-8', decoding=None,
  79. nonce=None, timestamp=None):
  80. """Create an OAuth 1 client.
  81. :param client_key: Client key (consumer key), mandatory.
  82. :param resource_owner_key: Resource owner key (oauth token).
  83. :param resource_owner_secret: Resource owner secret (oauth token secret).
  84. :param callback_uri: Callback used when obtaining request token.
  85. :param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
  86. :param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
  87. SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
  88. depending on where you want to embed the oauth
  89. credentials.
  90. :param rsa_key: RSA key used with SIGNATURE_RSA.
  91. :param verifier: Verifier used when obtaining an access token.
  92. :param realm: Realm (scope) to which access is being requested.
  93. :param encoding: If you provide non-unicode input you may use this
  94. to have oauthlib automatically convert.
  95. :param decoding: If you wish that the returned uri, headers and body
  96. from sign be encoded back from unicode, then set
  97. decoding to your preferred encoding, i.e. utf-8.
  98. :param nonce: Use this nonce instead of generating one. (Mainly for testing)
  99. :param timestamp: Use this timestamp instead of using current. (Mainly for testing)
  100. """
  101. # Convert to unicode using encoding if given, else assume unicode
  102. encode = lambda x: to_unicode(x, encoding) if encoding else x
  103. self.client_key = encode(client_key)
  104. self.client_secret = encode(client_secret)
  105. self.resource_owner_key = encode(resource_owner_key)
  106. self.resource_owner_secret = encode(resource_owner_secret)
  107. self.signature_method = encode(signature_method)
  108. self.signature_type = encode(signature_type)
  109. self.callback_uri = encode(callback_uri)
  110. self.rsa_key = encode(rsa_key)
  111. self.verifier = encode(verifier)
  112. self.realm = encode(realm)
  113. self.encoding = encode(encoding)
  114. self.decoding = encode(decoding)
  115. self.nonce = encode(nonce)
  116. self.timestamp = encode(timestamp)
  117. def __repr__(self):
  118. attrs = vars(self).copy()
  119. attrs['client_secret'] = '****' if attrs['client_secret'] else None
  120. attrs['rsa_key'] = '****' if attrs['rsa_key'] else None
  121. attrs[
  122. 'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
  123. attribute_str = ', '.join('{}={}'.format(k, v) for k, v in attrs.items())
  124. return '<{} {}>'.format(self.__class__.__name__, attribute_str)
  125. def get_oauth_signature(self, request):
  126. """Get an OAuth signature to be used in signing a request
  127. To satisfy `section 3.4.1.2`_ item 2, if the request argument's
  128. headers dict attribute contains a Host item, its value will
  129. replace any netloc part of the request argument's uri attribute
  130. value.
  131. .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
  132. """
  133. if self.signature_method == SIGNATURE_PLAINTEXT:
  134. # fast-path
  135. return signature.sign_plaintext(self.client_secret,
  136. self.resource_owner_secret)
  137. uri, headers, body = self._render(request)
  138. collected_params = signature.collect_parameters(
  139. uri_query=urlparse.urlparse(uri).query,
  140. body=body,
  141. headers=headers)
  142. log.debug("Collected params: {}".format(collected_params))
  143. normalized_params = signature.normalize_parameters(collected_params)
  144. normalized_uri = signature.base_string_uri(uri, headers.get('Host', None))
  145. log.debug("Normalized params: {}".format(normalized_params))
  146. log.debug("Normalized URI: {}".format(normalized_uri))
  147. base_string = signature.signature_base_string(request.http_method,
  148. normalized_uri, normalized_params)
  149. log.debug("Signing: signature base string: {}".format(base_string))
  150. if self.signature_method not in self.SIGNATURE_METHODS:
  151. raise ValueError('Invalid signature method.')
  152. sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
  153. log.debug("Signature: {}".format(sig))
  154. return sig
  155. def get_oauth_params(self, request):
  156. """Get the basic OAuth parameters to be used in generating a signature.
  157. """
  158. nonce = (generate_nonce()
  159. if self.nonce is None else self.nonce)
  160. timestamp = (generate_timestamp()
  161. if self.timestamp is None else self.timestamp)
  162. params = [
  163. ('oauth_nonce', nonce),
  164. ('oauth_timestamp', timestamp),
  165. ('oauth_version', '1.0'),
  166. ('oauth_signature_method', self.signature_method),
  167. ('oauth_consumer_key', self.client_key),
  168. ]
  169. if self.resource_owner_key:
  170. params.append(('oauth_token', self.resource_owner_key))
  171. if self.callback_uri:
  172. params.append(('oauth_callback', self.callback_uri))
  173. if self.verifier:
  174. params.append(('oauth_verifier', self.verifier))
  175. # providing body hash for requests other than x-www-form-urlencoded
  176. # as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1
  177. # 4.1.1. When to include the body hash
  178. # * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
  179. # * [...] SHOULD include the oauth_body_hash parameter on all other requests.
  180. # Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2
  181. # At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension.
  182. content_type = request.headers.get('Content-Type', None)
  183. content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
  184. if request.body is not None and content_type_eligible:
  185. params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))
  186. return params
  187. def _render(self, request, formencode=False, realm=None):
  188. """Render a signed request according to signature type
  189. Returns a 3-tuple containing the request URI, headers, and body.
  190. If the formencode argument is True and the body contains parameters, it
  191. is escaped and returned as a valid formencoded string.
  192. """
  193. # TODO what if there are body params on a header-type auth?
  194. # TODO what if there are query params on a body-type auth?
  195. uri, headers, body = request.uri, request.headers, request.body
  196. # TODO: right now these prepare_* methods are very narrow in scope--they
  197. # only affect their little thing. In some cases (for example, with
  198. # header auth) it might be advantageous to allow these methods to touch
  199. # other parts of the request, like the headers—so the prepare_headers
  200. # method could also set the Content-Type header to x-www-form-urlencoded
  201. # like the spec requires. This would be a fundamental change though, and
  202. # I'm not sure how I feel about it.
  203. if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
  204. headers = parameters.prepare_headers(
  205. request.oauth_params, request.headers, realm=realm)
  206. elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
  207. body = parameters.prepare_form_encoded_body(
  208. request.oauth_params, request.decoded_body)
  209. if formencode:
  210. body = urlencode(body)
  211. headers['Content-Type'] = 'application/x-www-form-urlencoded'
  212. elif self.signature_type == SIGNATURE_TYPE_QUERY:
  213. uri = parameters.prepare_request_uri_query(
  214. request.oauth_params, request.uri)
  215. else:
  216. raise ValueError('Unknown signature type specified.')
  217. return uri, headers, body
  218. def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
  219. """Sign a request
  220. Signs an HTTP request with the specified parts.
  221. Returns a 3-tuple of the signed request's URI, headers, and body.
  222. Note that http_method is not returned as it is unaffected by the OAuth
  223. signing process. Also worth noting is that duplicate parameters
  224. will be included in the signature, regardless of where they are
  225. specified (query, body).
  226. The body argument may be a dict, a list of 2-tuples, or a formencoded
  227. string. The Content-Type header must be 'application/x-www-form-urlencoded'
  228. if it is present.
  229. If the body argument is not one of the above, it will be returned
  230. verbatim as it is unaffected by the OAuth signing process. Attempting to
  231. sign a request with non-formencoded data using the OAuth body signature
  232. type is invalid and will raise an exception.
  233. If the body does contain parameters, it will be returned as a properly-
  234. formatted formencoded string.
  235. Body may not be included if the http_method is either GET or HEAD as
  236. this changes the semantic meaning of the request.
  237. All string data MUST be unicode or be encoded with the same encoding
  238. scheme supplied to the Client constructor, default utf-8. This includes
  239. strings inside body dicts, for example.
  240. """
  241. # normalize request data
  242. request = Request(uri, http_method, body, headers,
  243. encoding=self.encoding)
  244. # sanity check
  245. content_type = request.headers.get('Content-Type', None)
  246. multipart = content_type and content_type.startswith('multipart/')
  247. should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
  248. has_params = request.decoded_body is not None
  249. # 3.4.1.3.1. Parameter Sources
  250. # [Parameters are collected from the HTTP request entity-body, but only
  251. # if [...]:
  252. # * The entity-body is single-part.
  253. if multipart and has_params:
  254. raise ValueError(
  255. "Headers indicate a multipart body but body contains parameters.")
  256. # * The entity-body follows the encoding requirements of the
  257. # "application/x-www-form-urlencoded" content-type as defined by
  258. # [W3C.REC-html40-19980424].
  259. elif should_have_params and not has_params:
  260. raise ValueError(
  261. "Headers indicate a formencoded body but body was not decodable.")
  262. # * The HTTP request entity-header includes the "Content-Type"
  263. # header field set to "application/x-www-form-urlencoded".
  264. elif not should_have_params and has_params:
  265. raise ValueError(
  266. "Body contains parameters but Content-Type header was {} "
  267. "instead of {}".format(content_type or "not set",
  268. CONTENT_TYPE_FORM_URLENCODED))
  269. # 3.5.2. Form-Encoded Body
  270. # Protocol parameters can be transmitted in the HTTP request entity-
  271. # body, but only if the following REQUIRED conditions are met:
  272. # o The entity-body is single-part.
  273. # o The entity-body follows the encoding requirements of the
  274. # "application/x-www-form-urlencoded" content-type as defined by
  275. # [W3C.REC-html40-19980424].
  276. # o The HTTP request entity-header includes the "Content-Type" header
  277. # field set to "application/x-www-form-urlencoded".
  278. elif self.signature_type == SIGNATURE_TYPE_BODY and not (
  279. should_have_params and has_params and not multipart):
  280. raise ValueError(
  281. 'Body signatures may only be used with form-urlencoded content')
  282. # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
  283. # with the clause that parameters from body should only be included
  284. # in non GET or HEAD requests. Extracting the request body parameters
  285. # and including them in the signature base string would give semantic
  286. # meaning to the body, which it should not have according to the
  287. # HTTP 1.1 spec.
  288. elif http_method.upper() in ('GET', 'HEAD') and has_params:
  289. raise ValueError('GET/HEAD requests should not include body.')
  290. # generate the basic OAuth parameters
  291. request.oauth_params = self.get_oauth_params(request)
  292. # generate the signature
  293. request.oauth_params.append(
  294. ('oauth_signature', self.get_oauth_signature(request)))
  295. # render the signed request and return it
  296. uri, headers, body = self._render(request, formencode=True,
  297. realm=(realm or self.realm))
  298. if self.decoding:
  299. log.debug('Encoding URI, headers and body to %s.', self.decoding)
  300. uri = uri.encode(self.decoding)
  301. body = body.encode(self.decoding) if body else body
  302. new_headers = {}
  303. for k, v in headers.items():
  304. new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
  305. headers = new_headers
  306. return uri, headers, body