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

Benjamin Mako Hill || Want to submit a patch?