1
2
3
4
5
6
7 import urllib
8 import time
9 import random
10 import urlparse
11 import hmac
12 import binascii
13
14 try:
15 from urlparse import parse_qs, parse_qsl
16 except ImportError:
17 from cgi import parse_qs, parse_qsl
18
19 from restkit.util import to_bytestring
20
21
22 VERSION = '1.0'
23 HTTP_METHOD = 'GET'
24 SIGNATURE_METHOD = 'PLAINTEXT'
25
26
27 -class Error(RuntimeError):
28 """Generic exception class."""
29
30 - def __init__(self, message='OAuth error occurred.'):
32
33 @property
35 """A hack to get around the deprecation errors in 2.6."""
36 return self._message
37
40
44
47 """Optional WWW-Authenticate header (401 error)"""
48 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
49
52 """Build an XOAUTH string for use in SMTP/IMPA authentication."""
53 request = Request.from_consumer_and_token(consumer, token,
54 "GET", url)
55
56 signing_method = SignatureMethod_HMAC_SHA1()
57 request.sign_request(signing_method, consumer, token)
58
59 params = []
60 for k, v in sorted(request.iteritems()):
61 if v is not None:
62 params.append('%s="%s"' % (k, escape(v)))
63
64 return "%s %s %s" % ("GET", url, ','.join(params))
65
68 """Escape a URL including any /."""
69 return urllib.quote(s, safe='~')
70
73 """Get seconds since epoch (UTC)."""
74 return int(time.time())
75
78 """Generate pseudorandom number."""
79 return ''.join([str(random.randint(0, 9)) for i in range(length)])
80
83 """Generate pseudorandom number."""
84 return ''.join([str(random.randint(0, 9)) for i in range(length)])
85
88 """A consumer of OAuth-protected services.
89
90 The OAuth consumer is a "third-party" service that wants to access
91 protected resources from an OAuth service provider on behalf of an end
92 user. It's kind of the OAuth client.
93
94 Usually a consumer must be registered with the service provider by the
95 developer of the consumer software. As part of that process, the service
96 provider gives the consumer a *key* and a *secret* with which the consumer
97 software can identify itself to the service. The consumer will include its
98 key in each request to identify itself, but will use its secret only when
99 signing requests, to prove that the request is from that particular
100 registered consumer.
101
102 Once registered, the consumer can then use its consumer credentials to ask
103 the service provider for a request token, kicking off the OAuth
104 authorization process.
105 """
106
107 key = None
108 secret = None
109
111 self.key = key
112 self.secret = secret
113
114 if self.key is None or self.secret is None:
115 raise ValueError("Key and secret must be set.")
116
118 data = {'oauth_consumer_key': self.key,
119 'oauth_consumer_secret': self.secret}
120
121 return urllib.urlencode(data)
122
125 """An OAuth credential used to request authorization or a protected
126 resource.
127
128 Tokens in OAuth comprise a *key* and a *secret*. The key is included in
129 requests to identify the token being used, but the secret is used only in
130 the signature, to prove that the requester is who the server gave the
131 token to.
132
133 When first negotiating the authorization, the consumer asks for a *request
134 token* that the live user authorizes with the service provider. The
135 consumer then exchanges the request token for an *access token* that can
136 be used to access protected resources.
137 """
138
139 key = None
140 secret = None
141 callback = None
142 callback_confirmed = None
143 verifier = None
144
146 self.key = key
147 self.secret = secret
148
149 if self.key is None or self.secret is None:
150 raise ValueError("Key and secret must be set.")
151
155
161
163 if self.callback and self.verifier:
164
165 parts = urlparse.urlparse(self.callback)
166 scheme, netloc, path, params, query, fragment = parts[:6]
167 if query:
168 query = '%s&oauth_verifier=%s' % (query, self.verifier)
169 else:
170 query = 'oauth_verifier=%s' % self.verifier
171 return urlparse.urlunparse((scheme, netloc, path, params,
172 query, fragment))
173 return self.callback
174
176 """Returns this token as a plain string, suitable for storage.
177
178 The resulting string includes the token's secret, so you should never
179 send or store this string where a third party can read it.
180 """
181
182 data = {
183 'oauth_token': self.key,
184 'oauth_token_secret': self.secret,
185 }
186
187 if self.callback_confirmed is not None:
188 data['oauth_callback_confirmed'] = self.callback_confirmed
189 return urllib.urlencode(data)
190
191 @staticmethod
193 """Deserializes a token from a string like one returned by
194 `to_string()`."""
195
196 if not len(s):
197 raise ValueError("Invalid parameter string.")
198
199 params = parse_qs(s, keep_blank_values=False)
200 if not len(params):
201 raise ValueError("Invalid parameter string.")
202
203 try:
204 key = params['oauth_token'][0]
205 except Exception:
206 raise ValueError("'oauth_token' not found in OAuth request.")
207
208 try:
209 secret = params['oauth_token_secret'][0]
210 except Exception:
211 raise ValueError("'oauth_token_secret' not found in "
212 "OAuth request.")
213
214 token = Token(key, secret)
215 try:
216 token.callback_confirmed = params['oauth_callback_confirmed'][0]
217 except KeyError:
218 pass
219 return token
220
223
226 name = attr.__name__
227
228 def getter(self):
229 try:
230 return self.__dict__[name]
231 except KeyError:
232 raise AttributeError(name)
233
234 def deleter(self):
235 del self.__dict__[name]
236
237 return property(getter, attr, deleter)
238
241
242 """The parameters and information for an HTTP request, suitable for
243 authorizing with OAuth credentials.
244
245 When a consumer wants to access a service's protected resources, it does
246 so using a signed HTTP request identifying itself (the consumer) with its
247 key, and providing an access token authorized by the end user to access
248 those resources.
249
250 """
251
252 version = VERSION
253
255 self.method = method
256 self.url = url
257 if parameters is not None:
258 self.update(parameters)
259
260 @setter
261 - def url(self, value):
262 self.__dict__['url'] = value
263 if value is not None:
264 scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
265
266
267 if scheme == 'http' and netloc[-3:] == ':80':
268 netloc = netloc[:-3]
269 elif scheme == 'https' and netloc[-4:] == ':443':
270 netloc = netloc[:-4]
271 if scheme not in ('http', 'https'):
272 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
273
274
275 self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
276 else:
277 self.normalized_url = None
278 self.__dict__['url'] = None
279
280 @setter
282 self.__dict__['method'] = value.upper()
283
285 return self['oauth_timestamp'], self['oauth_nonce']
286
288 """Get any non-OAuth parameters."""
289 return dict([(k, v) for k, v in self.iteritems()
290 if not k.startswith('oauth_')])
291
293 """Serialize as a header for an HTTPAuth request."""
294 oauth_params = ((k, v) for k, v in self.items()
295 if k.startswith('oauth_'))
296 stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
297 header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
298 params_header = ', '.join(header_params)
299
300 auth_header = 'OAuth realm="%s"' % realm
301 if params_header:
302 auth_header = "%s, %s" % (auth_header, params_header)
303
304 return {'Authorization': auth_header}
305
306 - def to_postdata(self):
307 """Serialize as post data for a POST request."""
308
309
310
311 return urllib.urlencode(self, True).replace('+', '%20')
312
314 """Serialize as a URL for a GET request."""
315 base_url = urlparse.urlparse(self.url)
316 try:
317 query = base_url.query
318 except AttributeError:
319
320 query = base_url[4]
321 query = parse_qs(query)
322 for k, v in self.items():
323 query.setdefault(k, []).append(v)
324
325 try:
326 scheme = base_url.scheme
327 netloc = base_url.netloc
328 path = base_url.path
329 params = base_url.params
330 fragment = base_url.fragment
331 except AttributeError:
332
333 scheme = base_url[0]
334 netloc = base_url[1]
335 path = base_url[2]
336 params = base_url[3]
337 fragment = base_url[5]
338
339 url = (scheme, netloc, path, params,
340 urllib.urlencode(query, True), fragment)
341 return urlparse.urlunparse(url)
342
344 ret = self.get(parameter)
345 if ret is None:
346 raise Error('Parameter not found: %s' % parameter)
347
348 return ret
349
351 """Return a string that contains the parameters that must be signed."""
352 items = []
353 for key, value in self.iteritems():
354 if key == 'oauth_signature':
355 continue
356
357
358 if hasattr(value, '__iter__'):
359 items.extend((key, item) for item in value)
360 else:
361 items.append((key, value))
362
363
364 query = urlparse.urlparse(self.url)[4]
365
366 url_items = self._split_url_string(query).items()
367 non_oauth_url_items = list([(k, v) for k, v in url_items if not k.startswith('oauth_')])
368 items.extend(non_oauth_url_items)
369
370 encoded_str = urllib.urlencode(sorted(items))
371
372
373
374
375 return encoded_str.replace('+', '%20').replace('%7E', '~')
376
378 """Set the signature parameter to the result of sign."""
379
380 if 'oauth_consumer_key' not in self:
381 self['oauth_consumer_key'] = consumer.key
382
383 if token and 'oauth_token' not in self:
384 self['oauth_token'] = token.key
385
386 self['oauth_signature_method'] = signature_method.name
387 self['oauth_signature'] = signature_method.sign(self, consumer, token)
388
389 @classmethod
391 """Get seconds since epoch (UTC)."""
392 return str(int(time.time()))
393
394 @classmethod
396 """Generate pseudorandom number."""
397 return str(random.randint(0, 100000000))
398
399 @classmethod
400 - def from_request(cls, http_method, http_url, headers=None, parameters=None,
401 query_string=None):
402 """Combines multiple parameter sources."""
403 if parameters is None:
404 parameters = {}
405
406
407 if headers and 'Authorization' in headers:
408 auth_header = headers['Authorization']
409
410 if auth_header[:6] == 'OAuth ':
411 auth_header = auth_header[6:]
412 try:
413
414 header_params = cls._split_header(auth_header)
415 parameters.update(header_params)
416 except:
417 raise Error('Unable to parse OAuth parameters from '
418 'Authorization header.')
419
420
421 if query_string:
422 query_params = cls._split_url_string(query_string)
423 parameters.update(query_params)
424
425
426 param_str = urlparse.urlparse(http_url)[4]
427 url_params = cls._split_url_string(param_str)
428 parameters.update(url_params)
429
430 if parameters:
431 return cls(http_method, http_url, parameters)
432
433 return None
434
435 @classmethod
438 if not parameters:
439 parameters = {}
440
441 defaults = {
442 'oauth_consumer_key': consumer.key,
443 'oauth_timestamp': cls.make_timestamp(),
444 'oauth_nonce': cls.make_nonce(),
445 'oauth_version': cls.version,
446 }
447
448 defaults.update(parameters)
449 parameters = defaults
450
451 if token:
452 parameters['oauth_token'] = token.key
453 if token.verifier:
454 parameters['oauth_verifier'] = token.verifier
455
456 return Request(http_method, http_url, parameters)
457
458 @classmethod
461
462 if not parameters:
463 parameters = {}
464
465 parameters['oauth_token'] = token.key
466
467 if callback:
468 parameters['oauth_callback'] = callback
469
470 return cls(http_method, http_url, parameters)
471
472 @staticmethod
474 """Turn Authorization: header into parameters."""
475 params = {}
476 parts = header.split(',')
477 for param in parts:
478
479 if param.find('realm') > -1:
480 continue
481
482 param = param.strip()
483
484 param_parts = param.split('=', 1)
485
486 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
487 return params
488
489 @staticmethod
491 """Turn URL string into parameters."""
492 parameters = parse_qs(param_str, keep_blank_values=False)
493 for k, v in parameters.iteritems():
494 parameters[k] = urllib.unquote(v[0])
495 return parameters
496
498 """A skeletal implementation of a service provider, providing protected
499 resources to requests from authorized consumers.
500
501 This class implements the logic to check requests for authorization. You
502 can use it with your web server or web framework to protect certain
503 resources with OAuth.
504 """
505
506 timestamp_threshold = 300
507 version = VERSION
508 signature_methods = None
509
510 - def __init__(self, signature_methods=None):
512
516
524
526 """Optional support for the authenticate header."""
527 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
528
540
542 """Figure out the signature with some defaults."""
543 try:
544 signature_method = request.get_parameter('oauth_signature_method')
545 except:
546 signature_method = SIGNATURE_METHOD
547
548 try:
549
550 signature_method = self.signature_methods[signature_method]
551 except:
552 signature_method_names = ', '.join(self.signature_methods.keys())
553 raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
554
555 return signature_method
556
559
580
582 """Verify that timestamp is recentish."""
583 timestamp = int(timestamp)
584 now = int(time.time())
585 lapsed = now - timestamp
586 if lapsed > self.timestamp_threshold:
587 raise Error('Expired timestamp: given %d and now %s has a '
588 'greater difference than threshold %d' % (timestamp, now,
589 self.timestamp_threshold))
590
593 """A way of signing requests.
594
595 The OAuth protocol lets consumers and service providers pick a way to sign
596 requests. This interface shows the methods expected by the other `oauth`
597 modules for signing requests. Subclass it and implement its methods to
598 provide a new way to sign requests.
599 """
600
602 """Calculates the string that needs to be signed.
603
604 This method returns a 2-tuple containing the starting key for the
605 signing and the message to be signed. The latter may be used in error
606 messages to help clients debug their software.
607
608 """
609 raise NotImplementedError
610
611 - def sign(self, request, consumer, token):
612 """Returns the signature for the given request, based on the consumer
613 and token also provided.
614
615 You should use your implementation of `signing_base()` to build the
616 message to sign. Otherwise it may be less useful for debugging.
617
618 """
619 raise NotImplementedError
620
621 - def check(self, request, consumer, token, signature):
622 """Returns whether the given signature is the correct signature for
623 the given consumer and token signing the given request."""
624 built = self.sign(request, consumer, token)
625 return built == signature
626
629 name = 'HMAC-SHA1'
630
646
647 - def sign(self, request, consumer, token):
648 """Builds the base signature string."""
649 key, raw = self.signing_base(request, consumer, token)
650
651
652 try:
653 from hashlib import sha1 as sha
654 except ImportError:
655 import sha
656
657 hashed = hmac.new(key, raw, sha)
658
659
660 return binascii.b2a_base64(hashed.digest())[:-1]
661
662
663 -class SignatureMethod_PLAINTEXT(SignatureMethod):
664
665 name = 'PLAINTEXT'
666
667 - def signing_base(self, request, consumer, token):
668 """Concatenates the consumer key and secret with the token's
669 secret."""
670 sig = '%s&' % escape(consumer.secret)
671 if token:
672 sig = sig + escape(token.secret)
673 return sig, sig
674
675 - def sign(self, request, consumer, token):
676 key, raw = self.signing_base(request, consumer, token)
677 return raw
678