1 # -*- coding: utf-8 -*-
3 oauthlib.oauth1.rfc5849
6 This module is an implementation of various logic needed
7 for signing and checking OAuth 1.0 RFC 5849 requests.
9 from __future__ import absolute_import, unicode_literals
13 log = logging.getLogger(__name__)
19 import urllib.parse as urlparse
21 if sys.version_info[0] == 3:
26 from oauthlib.common import Request, urlencode, generate_nonce
27 from oauthlib.common import generate_timestamp, to_unicode
28 from . import parameters, signature
30 SIGNATURE_HMAC = "HMAC-SHA1"
31 SIGNATURE_RSA = "RSA-SHA1"
32 SIGNATURE_PLAINTEXT = "PLAINTEXT"
33 SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
35 SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
36 SIGNATURE_TYPE_QUERY = 'QUERY'
37 SIGNATURE_TYPE_BODY = 'BODY'
39 CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
44 """A client used to sign OAuth 1.0 RFC 5849 requests."""
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
52 def register_signature_method(cls, method_name, method_callback):
53 cls.SIGNATURE_METHODS[method_name] = method_callback
55 def __init__(self, client_key,
57 resource_owner_key=None,
58 resource_owner_secret=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.
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
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)
87 # Convert to unicode using encoding if given, else assume unicode
88 encode = lambda x: to_unicode(x, encoding) if encoding else x
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)
106 attrs = vars(self).copy()
107 attrs['client_secret'] = '****' if attrs['client_secret'] else None
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)
113 def get_oauth_signature(self, request):
114 """Get an OAuth signature to be used in signing a request
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
121 .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
123 if self.signature_method == SIGNATURE_PLAINTEXT:
125 return signature.sign_plaintext(self.client_secret,
126 self.resource_owner_secret)
128 uri, headers, body = self._render(request)
130 collected_params = signature.collect_parameters(
131 uri_query=urlparse.urlparse(uri).query,
134 log.debug("Collected params: {0}".format(collected_params))
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))
142 base_string = signature.construct_base_string(request.http_method,
143 normalized_uri, normalized_params)
145 log.debug("Base signing string: {0}".format(base_string))
147 if self.signature_method not in self.SIGNATURE_METHODS:
148 raise ValueError('Invalid signature method.')
150 sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
152 log.debug("Signature: {0}".format(sig))
155 def get_oauth_params(self, request):
156 """Get the basic OAuth parameters to be used in generating a signature.
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)
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),
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))
174 params.append(('oauth_verifier', self.verifier))
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')))
188 def _render(self, request, formencode=False, realm=None):
189 """Render a signed request according to signature type
191 Returns a 3-tuple containing the request URI, headers, and body.
193 If the formencode argument is True and the body contains parameters, it
194 is escaped and returned as a valid formencoded string.
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?
199 uri, headers, body = request.uri, request.headers, request.body
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)
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)
221 raise ValueError('Unknown signature type specified.')
223 return uri, headers, body
225 def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
228 Signs an HTTP request with the specified parts.
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).
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'
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.
245 If the body does contain parameters, it will be returned as a properly-
246 formatted formencoded string.
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.
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.
255 # normalize request data
256 request = Request(uri, http_method, body, headers,
257 encoding=self.encoding)
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
267 # * The entity-body is single-part.
268 if multipart and has_params:
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:
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:
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))
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):
297 'Body signatures may only be used with form-urlencoded content')
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
305 elif http_method.upper() in ('GET', 'HEAD') and has_params:
306 raise ValueError('GET/HEAD requests should not include body.')
308 # generate the basic OAuth parameters
309 request.oauth_params = self.get_oauth_params(request)
311 # generate the signature
312 request.oauth_params.append(
313 ('oauth_signature', self.get_oauth_signature(request)))
315 # render the signed request and return it
316 uri, headers, body = self._render(request, formencode=True,
317 realm=(realm or self.realm))
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
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