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