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