1"""
2oauthlib.oauth1.rfc5849
3~~~~~~~~~~~~~~
4
5This module is an implementation of various logic needed
6for signing and checking OAuth 1.0 RFC 5849 requests.
7
8It supports all three standard signature methods defined in RFC 5849:
9
10- HMAC-SHA1
11- RSA-SHA1
12- PLAINTEXT
13
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:
16
17- HMAC-SHA256
18- RSA-SHA256
19
20"""
21import base64
22import hashlib
23import logging
24import urllib.parse as urlparse
25
26from oauthlib.common import (
27 Request, generate_nonce, generate_timestamp, to_unicode, urlencode,
28)
29
30from . import parameters, signature
31
32log = logging.getLogger(__name__)
33
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.
44
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
49
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
54
55SIGNATURE_PLAINTEXT = "PLAINTEXT"
56
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)
66
67SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
68SIGNATURE_TYPE_QUERY = 'QUERY'
69SIGNATURE_TYPE_BODY = 'BODY'
70
71CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
72
73
74class Client:
75
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 }
86
87 @classmethod
88 def register_signature_method(cls, method_name, method_callback):
89 cls.SIGNATURE_METHODS[method_name] = method_callback
90
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.
102
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 def encode(x):
125 return to_unicode(x, encoding) if encoding else x
126
127 self.client_key = encode(client_key)
128 self.client_secret = encode(client_secret)
129 self.resource_owner_key = encode(resource_owner_key)
130 self.resource_owner_secret = encode(resource_owner_secret)
131 self.signature_method = encode(signature_method)
132 self.signature_type = encode(signature_type)
133 self.callback_uri = encode(callback_uri)
134 self.rsa_key = encode(rsa_key)
135 self.verifier = encode(verifier)
136 self.realm = encode(realm)
137 self.encoding = encode(encoding)
138 self.decoding = encode(decoding)
139 self.nonce = encode(nonce)
140 self.timestamp = encode(timestamp)
141
142 def __repr__(self):
143 attrs = vars(self).copy()
144 attrs['client_secret'] = '****' if attrs['client_secret'] else None
145 attrs['rsa_key'] = '****' if attrs['rsa_key'] else None
146 attrs[
147 'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
148 attribute_str = ', '.join('{}={}'.format(k, v) for k, v in attrs.items())
149 return '<{} {}>'.format(self.__class__.__name__, attribute_str)
150
151 def get_oauth_signature(self, request):
152 """Get an OAuth signature to be used in signing a request
153
154 To satisfy `section 3.4.1.2`_ item 2, if the request argument's
155 headers dict attribute contains a Host item, its value will
156 replace any netloc part of the request argument's uri attribute
157 value.
158
159 .. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
160 """
161 if self.signature_method == SIGNATURE_PLAINTEXT:
162 # fast-path
163 return signature.sign_plaintext(self.client_secret,
164 self.resource_owner_secret)
165
166 uri, headers, body = self._render(request)
167
168 collected_params = signature.collect_parameters(
169 uri_query=urlparse.urlparse(uri).query,
170 body=body,
171 headers=headers)
172 log.debug("Collected params: {}".format(collected_params))
173
174 normalized_params = signature.normalize_parameters(collected_params)
175 normalized_uri = signature.base_string_uri(uri, headers.get('Host', None))
176 log.debug("Normalized params: {}".format(normalized_params))
177 log.debug("Normalized URI: {}".format(normalized_uri))
178
179 base_string = signature.signature_base_string(request.http_method,
180 normalized_uri, normalized_params)
181
182 log.debug("Signing: signature base string: {}".format(base_string))
183
184 if self.signature_method not in self.SIGNATURE_METHODS:
185 raise ValueError('Invalid signature method.')
186
187 sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
188
189 log.debug("Signature: {}".format(sig))
190 return sig
191
192 def get_oauth_params(self, request):
193 """Get the basic OAuth parameters to be used in generating a signature.
194 """
195 nonce = (generate_nonce()
196 if self.nonce is None else self.nonce)
197 timestamp = (generate_timestamp()
198 if self.timestamp is None else self.timestamp)
199 params = [
200 ('oauth_nonce', nonce),
201 ('oauth_timestamp', timestamp),
202 ('oauth_version', '1.0'),
203 ('oauth_signature_method', self.signature_method),
204 ('oauth_consumer_key', self.client_key),
205 ]
206 if self.resource_owner_key:
207 params.append(('oauth_token', self.resource_owner_key))
208 if self.callback_uri:
209 params.append(('oauth_callback', self.callback_uri))
210 if self.verifier:
211 params.append(('oauth_verifier', self.verifier))
212
213 # providing body hash for requests other than x-www-form-urlencoded
214 # as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1
215 # 4.1.1. When to include the body hash
216 # * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
217 # * [...] SHOULD include the oauth_body_hash parameter on all other requests.
218 # Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2
219 # At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension.
220 content_type = request.headers.get('Content-Type', None)
221 content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
222 if request.body is not None and content_type_eligible:
223 params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8'))) # noqa: S324
224
225 return params
226
227 def _render(self, request, formencode=False, realm=None):
228 """Render a signed request according to signature type
229
230 Returns a 3-tuple containing the request URI, headers, and body.
231
232 If the formencode argument is True and the body contains parameters, it
233 is escaped and returned as a valid formencoded string.
234 """
235 # TODO what if there are body params on a header-type auth?
236 # TODO what if there are query params on a body-type auth?
237
238 uri, headers, body = request.uri, request.headers, request.body
239
240 # TODO: right now these prepare_* methods are very narrow in scope--they
241 # only affect their little thing. In some cases (for example, with
242 # header auth) it might be advantageous to allow these methods to touch
243 # other parts of the request, like the headers—so the prepare_headers
244 # method could also set the Content-Type header to x-www-form-urlencoded
245 # like the spec requires. This would be a fundamental change though, and
246 # I'm not sure how I feel about it.
247 if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
248 headers = parameters.prepare_headers(
249 request.oauth_params, request.headers, realm=realm)
250 elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
251 body = parameters.prepare_form_encoded_body(
252 request.oauth_params, request.decoded_body)
253 if formencode:
254 body = urlencode(body)
255 headers['Content-Type'] = 'application/x-www-form-urlencoded'
256 elif self.signature_type == SIGNATURE_TYPE_QUERY:
257 uri = parameters.prepare_request_uri_query(
258 request.oauth_params, request.uri)
259 else:
260 raise ValueError('Unknown signature type specified.')
261
262 return uri, headers, body
263
264 def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
265 """Sign a request
266
267 Signs an HTTP request with the specified parts.
268
269 Returns a 3-tuple of the signed request's URI, headers, and body.
270 Note that http_method is not returned as it is unaffected by the OAuth
271 signing process. Also worth noting is that duplicate parameters
272 will be included in the signature, regardless of where they are
273 specified (query, body).
274
275 The body argument may be a dict, a list of 2-tuples, or a formencoded
276 string. The Content-Type header must be 'application/x-www-form-urlencoded'
277 if it is present.
278
279 If the body argument is not one of the above, it will be returned
280 verbatim as it is unaffected by the OAuth signing process. Attempting to
281 sign a request with non-formencoded data using the OAuth body signature
282 type is invalid and will raise an exception.
283
284 If the body does contain parameters, it will be returned as a properly-
285 formatted formencoded string.
286
287 Body may not be included if the http_method is either GET or HEAD as
288 this changes the semantic meaning of the request.
289
290 All string data MUST be unicode or be encoded with the same encoding
291 scheme supplied to the Client constructor, default utf-8. This includes
292 strings inside body dicts, for example.
293 """
294 # normalize request data
295 request = Request(uri, http_method, body, headers,
296 encoding=self.encoding)
297
298 # sanity check
299 content_type = request.headers.get('Content-Type', None)
300 multipart = content_type and content_type.startswith('multipart/')
301 should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
302 has_params = request.decoded_body is not None
303 # 3.4.1.3.1. Parameter Sources
304 # [Parameters are collected from the HTTP request entity-body, but only
305 # if [...]:
306 # * The entity-body is single-part.
307 if multipart and has_params:
308 raise ValueError(
309 "Headers indicate a multipart body but body contains parameters.")
310 # * The entity-body follows the encoding requirements of the
311 # "application/x-www-form-urlencoded" content-type as defined by
312 # [W3C.REC-html40-19980424].
313 elif should_have_params and not has_params:
314 raise ValueError(
315 "Headers indicate a formencoded body but body was not decodable.")
316 # * The HTTP request entity-header includes the "Content-Type"
317 # header field set to "application/x-www-form-urlencoded".
318 elif not should_have_params and has_params:
319 raise ValueError(
320 "Body contains parameters but Content-Type header was {} "
321 "instead of {}".format(content_type or "not set",
322 CONTENT_TYPE_FORM_URLENCODED))
323
324 # 3.5.2. Form-Encoded Body
325 # Protocol parameters can be transmitted in the HTTP request entity-
326 # body, but only if the following REQUIRED conditions are met:
327 # o The entity-body is single-part.
328 # o The entity-body follows the encoding requirements of the
329 # "application/x-www-form-urlencoded" content-type as defined by
330 # [W3C.REC-html40-19980424].
331 # o The HTTP request entity-header includes the "Content-Type" header
332 # field set to "application/x-www-form-urlencoded".
333 elif self.signature_type == SIGNATURE_TYPE_BODY and not (
334 should_have_params and has_params and not multipart):
335 raise ValueError(
336 'Body signatures may only be used with form-urlencoded content')
337
338 # We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
339 # with the clause that parameters from body should only be included
340 # in non GET or HEAD requests. Extracting the request body parameters
341 # and including them in the signature base string would give semantic
342 # meaning to the body, which it should not have according to the
343 # HTTP 1.1 spec.
344 elif http_method.upper() in ('GET', 'HEAD') and has_params:
345 raise ValueError('GET/HEAD requests should not include body.')
346
347 # generate the basic OAuth parameters
348 request.oauth_params = self.get_oauth_params(request)
349
350 # generate the signature
351 request.oauth_params.append(
352 ('oauth_signature', self.get_oauth_signature(request)))
353
354 # render the signed request and return it
355 uri, headers, body = self._render(request, formencode=True,
356 realm=(realm or self.realm))
357
358 if self.decoding:
359 log.debug('Encoding URI, headers and body to %s.', self.decoding)
360 uri = uri.encode(self.decoding)
361 body = body.encode(self.decoding) if body else body
362 new_headers = {}
363 for k, v in headers.items():
364 new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
365 headers = new_headers
366 return uri, headers, body