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
12 log = logging.getLogger(__name__)
18 import urllib.parse as urlparse
20 if sys.version_info[0] == 3:
25 from oauthlib.common import Request, urlencode, generate_nonce
26 from oauthlib.common import generate_timestamp, to_unicode
27 from . import parameters, signature
29 SIGNATURE_HMAC = "HMAC-SHA1"
30 SIGNATURE_RSA = "RSA-SHA1"
31 SIGNATURE_PLAINTEXT = "PLAINTEXT"
32 SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
34 SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
35 SIGNATURE_TYPE_QUERY = 'QUERY'
36 SIGNATURE_TYPE_BODY = 'BODY'
38 CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
43 """A client used to sign OAuth 1.0 RFC 5849 requests."""
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
51 def register_signature_method(cls, method_name, method_callback):
52 cls.SIGNATURE_METHODS[method_name] = method_callback
54 def __init__(self, client_key,
56 resource_owner_key=None,
57 resource_owner_secret=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.
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
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)
86 # Convert to unicode using encoding if given, else assume unicode
87 encode = lambda x: to_unicode(x, encoding) if encoding else x
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)
104 if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
106 'rsa_key is required when using RSA signature method.')
109 attrs = vars(self).copy()
110 attrs['client_secret'] = '****' if attrs['client_secret'] else None
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)
116 def get_oauth_signature(self, request):
117 """Get an OAuth signature to be used in signing a request
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
124 .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
126 if self.signature_method == SIGNATURE_PLAINTEXT:
128 return signature.sign_plaintext(self.client_secret,
129 self.resource_owner_secret)
131 uri, headers, body = self._render(request)
133 collected_params = signature.collect_parameters(
134 uri_query=urlparse.urlparse(uri).query,
137 log.debug("Collected params: {0}".format(collected_params))
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))
145 base_string = signature.construct_base_string(request.http_method,
146 normalized_uri, normalized_params)
148 log.debug("Base signing string: {0}".format(base_string))
150 if self.signature_method not in self.SIGNATURE_METHODS:
151 raise ValueError('Invalid signature method.')
153 sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
155 log.debug("Signature: {0}".format(sig))
158 def get_oauth_params(self, request):
159 """Get the basic OAuth parameters to be used in generating a signature.
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)
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),
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))
177 params.append(('oauth_verifier', self.verifier))
181 def _render(self, request, formencode=False, realm=None):
182 """Render a signed request according to signature type
184 Returns a 3-tuple containing the request URI, headers, and body.
186 If the formencode argument is True and the body contains parameters, it
187 is escaped and returned as a valid formencoded string.
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?
192 uri, headers, body = request.uri, request.headers, request.body
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)
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)
214 raise ValueError('Unknown signature type specified.')
216 return uri, headers, body
218 def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
221 Signs an HTTP request with the specified parts.
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).
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'
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.
238 If the body does contain parameters, it will be returned as a properly-
239 formatted formencoded string.
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.
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.
248 # normalize request data
249 request = Request(uri, http_method, body, headers,
250 encoding=self.encoding)
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
260 # * The entity-body is single-part.
261 if multipart and has_params:
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:
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:
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))
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):
290 'Body signatures may only be used with form-urlencoded content')
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
298 elif http_method.upper() in ('GET', 'HEAD') and has_params:
299 raise ValueError('GET/HEAD requests should not include body.')
301 # generate the basic OAuth parameters
302 request.oauth_params = self.get_oauth_params(request)
304 # generate the signature
305 request.oauth_params.append(
306 ('oauth_signature', self.get_oauth_signature(request)))
308 # render the signed request and return it
309 uri, headers, body = self._render(request, formencode=True,
310 realm=(realm or self.realm))
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
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