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

Benjamin Mako Hill || Want to submit a patch?